iOS App 从 0 到 1

程序与视图生命周期

程序级 scenePhase + 视图级 onAppear/task:时机、顺序、流程图;并对照 HarmonyOS UIAbility / 组件钩子。

1先分清两层(别混)

iOS 里日常说的「生命周期」其实是两套:程序/场景级管前后台;视图级管某一屏出现与消失。混用会导致「回前台请求挂在错误地方」。

层iOS(SwiftUI 主线)≈ HarmonyOS典型用途
程序/Ability@main App + scenePhaseUIAbility 回调停定位、刷 Token、持久化
窗口/页面容器WindowGroup / SceneonWindowStageCreate + 页面栈加载根界面
视图/组件onAppear / .taskaboutToAppear / onPageShow拉列表、订通知
口诀:进程还活着 ≠ 某个 View 还在屏幕上。进后台时页面可能仍在导航栈里;反之,换 Tab 会触发视图 disappear,但 App 仍是 .active。

2程序生命周期总流程

冷启动 → 前台交互 →(可多次)进后台再回前台 → 最终被系统回收。SwiftUI 里你主要观察 ScenePhase;底层仍是 UIKit 的 Scene/App 在驱动。

点图标启动 → Inactive → Active → Home 键/上滑 → Background

3关键 API:ScenePhase(SwiftUI)

@Environment(\.scenePhase) 是现代 SwiftUI App 观察前后台的主入口。三个值,调用时机如下。

值含义常见触发你该做什么
.active前台且可交互启动完成、从后台回前台、关掉控制中心恢复刷新、重连 WebSocket、开定位
.inactive过渡:在前台但暂不可交互来电、下拉通知中心、App 切换动画中暂停游戏循环;一般别在这里狂发网络
.background已进后台回主屏、切到别的 App停传感器、存草稿、取消非必要任务
import SwiftUI

@main
struct MyApp: App {
  @Environment(\.scenePhase) private var scenePhase

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .onChange(of: scenePhase) { oldPhase, newPhase in
      print("scenePhase: \(oldPhase) → \(newPhase)")
      switch newPhase {
      case .active:
        // 回前台:可静默刷新未读数
        break
      case .inactive:
        // 过渡态:短暂停动画
        break
      case .background:
        // 进后台:立即落盘
        TodoStore.shared.save()
      @unknown default:
        break
      }
    }
  }
}
冷启动时常见顺序:先经历 inactive,再进 active。从后台回来也常是 background → inactive → active。用 print 自己跑一遍最踏实。

4程序侧:典型调用顺序

冷启动(用户点图标)

  1. 进程创建,执行 @main App 初始化
  2. 创建 WindowGroup,构建根 View
  3. scenePhase → .inactive
  4. 根视图出现(触发视图级 onAppear / .task)
  5. scenePhase → .active

进后台再回前台

  1. .active → .inactive(开始离开)
  2. .inactive → .background
  3. (系统可能 Suspend;你收不到单独回调)
  4. 用户再点图标:.background → .inactive → .active
  5. 注意:栈里的 View 不一定再走一遍 onAppear

5对照 HarmonyOS:UIAbility

你会的 Stage 模型里,程序级几乎全在 UIAbility。和 iOS scenePhase 不是一一改名,但职责同构。

时机iOS(SwiftUI)HarmonyOS UIAbility
Ability/进程创建@main App initonCreate
窗口就绪、加载 UIWindowGroup { RootView() }onWindowStageCreate → loadContent
到前台scenePhase == .activeonForeground
到后台.backgroundonBackground
窗口销毁Scene 结束(较少手写)onWindowStageDestroy
Ability 销毁进程被杀(无稳定对称回调)onDestroy
再入(single)通常仍是同一 SceneonNewWant
SwiftUI
@main
struct MyApp: App {
  @Environment(\.scenePhase) private var phase
  var body: some Scene {
    WindowGroup { ContentView() }
      .onChange(of: phase) { _, p in
        if p == .background { save() }
        if p == .active { refresh() }
      }
  }
}
ArkTS
export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) { /* 初始化一次 */ }
  onWindowStageCreate(windowStage) {
    windowStage.loadContent('pages/Index')
  }
  onForeground() { /* ≈ active */ }
  onBackground() { /* ≈ background */ }
  onDestroy() { /* 释放 */ }
}
鸿蒙把「创建窗口」和「前后台」拆成多个明确回调;iOS SwiftUI 把前后台收成 scenePhase,窗口创建则藏在 WindowGroup 声明里。

6视图生命周期:关键方法

SwiftUI 没有 UIKit 那么多 viewWillAppear 回调;日常就这几只「钩子」。理解时机比背名字重要。

API调用时机会不会多次适合做什么
.onAppear视图出现在层级中(即将/已经显示)会;导航返回、Tab 切回常再进轻量逻辑、埋点;请求建议用 .task
.onDisappear视图从层级移除/离开屏幕会取消订阅、停动画(别在这里改关键导航状态)
.task { }出现时启动异步任务随身份/参数变化可重跑首选拉数;视图消失时自动取消
.task(id:) { }id 变化时取消旧任务并重跑按 id详情页随 itemId 换数据
.onChange(of:)某状态值变化每次变化响应式副作用(慎防连环触发)
struct DetailView: View {
  let itemId: String
  @State private var title = "加载中…"

  var body: some View {
    Text(title)
      .onAppear { print("appear \(itemId)") }
      .onDisappear { print("disappear \(itemId)") }
      .task(id: itemId) {
        // itemId 变了会取消上一次 fetch
        title = await api.fetchTitle(itemId)
      }
  }
}

7视图生命周期流程图与调用顺序

父子同屏时:大体是父先 appear,再子 appear;消失时常见子先 disappear,再父(以实际 print 为准,导航动画可能导致额外次数)。

push 详情 → Detail.onAppear → .task 拉数 → pop → onDisappear + 取消
验证方法:在父子 View 里 print(#function) / 自定义标签,真机或模拟器走一遍导航。Preview 也会触发 appear,调试时别被吓到。

8对照 HarmonyOS:组件 / 页面钩子

时机SwiftUIArkUI 组件ArkUI 页面
创建后、首帧前(无对等;用 init / 状态默认值)aboutToAppear(在首帧 build 前)同左
显示时onAppear配合首次 buildonPageShow(每次显示)
隐藏时onDisappearaboutToDisappearonPageHide
异步加载.task在 aboutToAppear 里异步(勿写进 build)同左
SwiftUI
struct ListPage: View {
  @State private var items: [Item] = []
  var body: some View {
    List(items) { Text($0.name) }
      .task { items = await api.list() }
      .onDisappear { /* 可选清理 */ }
  }
}
ArkTS
@ComponentV2
struct ListPage {
  @Local items: Item[] = []
  aboutToAppear(): void {
    this.load() // 禁止在 build() 里打网络
  }
  aboutToDisappear(): void {
    // 取消监听;勿乱改 @Param 链路状态
  }
  async load() {
    this.items = await api.list()
  }
  build() {
    List() { ForEach(this.items, (it: Item) => {
      ListItem() { Text(it.name) }
    }, (it: Item) => it.id) }
  }
}
鸿蒙官方顺序记忆:Parent aboutToAppear → Parent build → Child aboutToAppear → Child build → onPageShow。iOS 没有对称的「build 回调」,但「父先于子出现」的直觉可迁移。

9什么逻辑放哪一层

scenePhase == .background

全局落盘、停定位/音频、断开长连接。

scenePhase == .active

静默刷新会话、检查版本、恢复连接。

View .task

本页数据加载;随离开自动取消。

View onDisappear

页面级订阅、计时器、NotificationCenter。

需求放这里别放这里
进待办页拉列表该页 .taskApp.onChange(scenePhase)
回前台刷新未读角标scenePhase → .active每个子 View 的 onAppear 都打一遍
杀进程前尽量存盘.background + 关键删时即时 save只靠 onDisappear(后台不一定再走)

10补充:UIKit 视图控制器(知道即可)

纯 SwiftUI 项目很少手写;但调试、混排、看崩溃栈时会撞到。和鸿蒙「页面钩子」更像这一层。

loadView → viewDidLoad → viewWillAppear → viewDidAppear → Will/Did Disappear
viewDidLoad 只来一次;viewWillAppear 每次显示都可能来。SwiftUI 的 onAppear 更接近「appear 系」,不要幻想有一次性格的 viewDidLoad 等价物——用 Store 里的「已加载」标记代替。

11真实坑

onAppear 里请求发了两次 / 多次
原因:Preview + 真机、动画、身份变化。处理:改用 .task;或 Store 内做 in-flight / 缓存;调试时先关掉 Canvas Preview。
进后台后 onDisappear 没走,数据没存
页面还在栈上,只是场景进了 background。关键数据在 scenePhase == .background 存,平时改完也即时 save。
回前台想刷新,但写在 onAppear 不触发
因为 View 没被移除。听 scenePhase → .active,或用 scenePhase 变化去通知 Store。
和鸿蒙一样在「build/body」里打网络
两端都禁止。iOS 放 .task;鸿蒙放 aboutToAppear / 页面级异步,结果写入状态再驱动 UI。

12一张速查表收工

你想…iOSHarmonyOS
监听前后台scenePhaseonForeground / onBackground
进页拉数.taskaboutToAppear + async
离页取消.task 自动取消 / onDisappearaboutToDisappear
每次页面显示不一定有;用 scenePhase 或自己标记onPageShow
窗口加载根页WindowGrouponWindowStageCreate
先画清「程序层还是视图层」,再选钩子;用流程图 + 一次 print 验证,比死记更稳。

iOS App 从 0 到 1 · 主讲 SwiftUI / iOS