SwiftUI 页面作为一级页面数据被重置问题分析

一. 引言

SwiftUI 作为 Apple 官方推出的响应式 UI 框架,安全、快速、声明式的特点确实非常吸引人。更重要的是,它可以无缝接入现有的 UIKit 项目,甚至是 Objective-C 项目,这使它在许多老项目中也逐渐"站稳脚跟"。

但当我们对 SwiftUI 的运行机制还不够熟悉时,就难免会遇到一些让人摸不着头脑的问题。

今天要讨论的就是这样一个真实场景:

在一个 UIKit 项目中,我们用 UIViewController 包了一层 SwiftUI 页面,并将它放到 UITabBarController 的一级页面中。

乍一看没什么问题,但当我们切换 Tab 时,却发现一个特别诡异的现象:

  • SwiftUI 页面会被重建
  • 页面内部的数据会被重置
  • UI 重新回到加载态
  • 而一些标记变量却保持了原来的状态
  • 整体呈现出一种"状态错乱"的感觉

这让人非常疑惑,因为在传统的 UIKit 中,TabBar 的一级页面通常只在第一次创建时进行一次布局和数据请求,之后切换 Tab 都是常驻的,不会被重置。

那么问题来了:

在这种 SwiftUI 嵌套 UIKit 的混编结构里,我们该如何实现"一级页面只加载一次"的效果?

又为什么会出现这样奇怪的行为?

本文将结合一个实际的例子,把问题复现出来并彻底分析其原因,同时给出真正可靠的解决方案。

二. 问题场景回顾

这是一个比较典型的场景,在UIKit的UIViewController中使用UIHostingController嵌套了一个SwiftUI的页面。

Swift 复制代码
class ZMNewsViewController: ZMBaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setSwiftUI()
    }

    private func setSwiftUI() {
        let newsPage = ZMNewsPage()
        let hostingController = UIHostingController(rootView: newsPage)
        self.addChild(hostingController)
        self.view.addSubview(hostingController.view)
        hostingController.view.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
            make.top.equalToSuperview()
        }
    }
}

而ZMNewsPage 中包含多个 ZMNewsSubPage;我们只聚焦 ZMNewsSubPage:

Swift 复制代码
struct ZMNewsSubPage: View {
    var category: ZMNewsCategoryItemModel?
    @ObservedObject private var presenter = ZMNewsPresenter()
    @State private var page = 1
    @State private var hasRequest = false

    var body: some View {
        // ... List / Banner / loading 省略
        .onAppear {
            if hasRequest { return }
            requestListData()
            if category == nil { requestBannerData() }
        }
    }

    private func requestListData() {
        hasRequest = true
        presenter.requestNewsList(page: page, categoryId: category?.id)
    }

    private func requestBannerData() {
        presenter.requestNewsBanner()
    }
}

然后出现的现象就是:从其它 tab 切回后,界面回到 loading,但 hasRequest 仍为 true,presenter(以及数据)却被重置为初始状态,导致页面卡在 loading。

三. 问题分析(深入生命周期与修饰符差异)

要把问题分析清楚,需要把几个关键点拼起来看:UIKit ↔ SwiftUI 的桥接View 的重建各类属性修饰符的生命周期

3.1 UIHostingController 与 tab 切换的交互

  • 在 UIKit 中,把 UIHostingController 作为 UITabBarController 的子控制器通常看起来「常驻」------但实际细节取决于如何创建、持有和内存回收。
  • 当切换 tab 时,UIKit 可能会卸载某些视图层级(尤其是在内存紧张或 viewDidUnload 场景中),并在切回时重新创建视图层级。
  • 更关键的是:SwiftUI 的 view 树是"值语义"的,SwiftUI 负责根据 identity 决定哪些 @State/@StateObject 等需要保留或重建;而我们在 View 中直接写 @ObservedObject var presenter = ...,这会在 View 被重新创建时重新 init 出一个新的 presenter。

3.2 @ObservedObject vs @StateObject vs @State 行为对比(最容易混淆的点)

  • @State:属于 View 的内部状态。只要 View 的 identity 被 SwiftUI 认为是同一个,@State 会被保留(不会每次 body 重建而置零)。因此 hasRequest 有可能在视图重建后仍保留旧值。
  • @ObservedObject:不能保证实例生命周期。当你用 @ObservedObject private var presenter = ... 这样的写法时,实例是在 View 初始化时创建、并由 View 持有;但 View 每次被重新实例化时会再次创建新的 presenter。换句话说:@ObservedObject 只是"观察一个对象",并不负责持有/稳定这个对象的创建周期。
  • @StateObject(iOS14+):SwiftUI 提供来只创建一次并保持实例的修饰符。把 presenter 放到 @StateObject,SwiftUI 会负责持久化它的实例,即便 view 被重新构建(在大多数场景里)也不会新建。

3.3 为什么会出现"hasRequest 保留但 presenter 重置"的错位

  • 我们的 hasRequest 是 @State,当 SwiftUI 决定复用 view identity 时,它保留了 hasRequest = true。
  • 但 presenter 是以 @ObservedObject 并在声明处 new 的------视图重建时这个 presenter 被重新初始化,数据为空。
  • 结果:页面逻辑判断 if hasRequest { return } 导致不再发请求,但数据又丢失,UI 始终停在 loading(presenter.newsList == nil)。

3.4 其他相关因素(会影响表现的细节)

  • 如果我们的 UIHostingController 每次创建不是同一个实例(例如在 setSwiftUI 每次都 new,并且频繁调用setSwiftUI方法),那问题会更明显。确保你把 hostingController 保持为持久变量,而非每次都创建。
  • 如果 presenter 的初始化有副作用(例如自动触发网络请求),那么频繁重建会造成重复请求或数据混乱。
  • SwiftUI 的更新不是"每帧都重建",但 body 会频繁执行。关键是 view identity(例如 id 修改、父 view 重建)会决定哪些状态被保留。

四. 解决方案

其实解决方案有很多,主要看我们想要实现那种效果,对于这个示例项目而言呢,我们只需要保证 presenter 对象别被重置就是最理想的选择。

4.1 把 presenter 用 @StateObject并把 hasRequest 移到 Presenter

这是最稳妥、与 SwiftUI 生命周期契合的做法:让 Presenter 成为"状态对象"(State),并把请求状态 hasRequested 作为数据状态管理,而不是 UI 状态。

  • Presenter 的生命周期由 SwiftUI 管理且稳定(只创建一次)
  • hasRequest 与数据同属 Presenter,避免状态分裂
  • 清晰:所有数据状态都在 Presenter 中,View 只负责渲染与交互

示例

Swift 复制代码
final class ZMNewsPresenter: ObservableObject {
    @Published var newsList: [NewsItem]? = nil
    @Published var bannerList: [BannerItem] = []
    @Published var hasMore: Bool = true
    @Published var hasRequested: Bool = false

    func requestNewsList(page: Int, categoryId: String?) {
        guard !hasRequested else { return } // 可根据需要更加细化
        hasRequested = true
        // 发请求并在完成后更新 newsList、hasMore 等
    }

    func requestNewsBanner() {
        // 请求 banner
    }
}

使用:

Swift 复制代码
struct ZMNewsSubPage: View {
    @StateObject private var presenter = ZMNewsPresenter()
    @State private var page = 1
    var category: ZMNewsCategoryItemModel?

    var body: some View {
        // ... 同上
        .onAppear {
            if presenter.newsList == nil {
                presenter.requestNewsList(page: page, categoryId: category?.id)
            }
            if category == nil && presenter.bannerList.isEmpty {
                presenter.requestNewsBanner()
            }
        }
    }
}

4.2 由上层或容器创建 Presenter 并注入(依赖注入 )

如果你的多个子页面需要共享同一个 presenter,或者你想显式管理 presenter 的生命周期,可以在 ZMNewsViewController 里创建 presenter 并把它注入进 SwiftUI:

Swift 复制代码
let presenter = ZMNewsPresenter()
let newsPage = ZMNewsPage().environmentObject(presenter)
let hostingController = UIHostingController(rootView: newsPage)

View 中使用:

Swift 复制代码
@EnvironmentObject var presenter: ZMNewsPresenter
  • 明确生命周期(由 UIKit 持有)
  • 便于多个子 page 共享

4.3 单利式

或者直接将presenter设置为单利,自己来管理生命周期,这个方案其实和4.2 类似,只是生命周期更长,但是也增加了管理的难度。

  • @StateObject 是为了解决这种"只创建一次"的需求设计的:它把对象的创建与持久化交给 SwiftUI 管理,避免 View 重建带来的重复 init。
  • 把 hasRequest(请求状态)放到 Presenter 中则把"数据状态"集中管理,避免把数据状态放在 UI 状态(@State)上引起的不同步问题。
  • 结合上层注入(方案 B)可以在更复杂的场景下获得更明确的生命周期控制,但对于大多数 Tab 下一级页面场景,@StateObject + Presenter 内管理状态已经足够且优雅。

五. 结语

SwiftUI 和 UIKit 的混编虽然灵活,但也带来了不少"生命周期不在一个频道上"的微妙问题。这次的例子就是一个典型场景:View 被重建、@ObservedObject 被重置、@State 又被保留,最终导致状态错位,页面卡在 Loading ------ 但理解了每个修饰符的生命周期后,解决方案其实非常清晰。

在实际开发中,你可以根据场景选择不同的处理方式,但最通用、最稳定的方案仍然是:

使用 @StateObject 持有 Presenter,把请求状态也放到 Presenter 中统一管理。

这样既能保证 Presenter 生命周期稳定,也能避免 UI 状态和数据状态分裂。

当然,SwiftUI 的生命机制本身很灵活,在不同的项目结构里可能会遇到更多有趣(或者诡异 😂)的边界问题。如果你在混编项目里遇到类似的生命周期、状态同步、页面恢复等问题,也欢迎在评论区一起讨论交流,说不定下一个坑我们一起填!

相关推荐
guangzan7 小时前
AI 结队编程:解决 SwiftUI 窗口点击关闭按钮崩溃问题
swiftui·tca
健了个平_247 小时前
【iOS】如何在 iOS 26 的UITabBarController中使用自定义TabBar
ios·swift·wwdc
Digitally10 小时前
无需 iTunes 将文件从 PC 传输到 iPhone
ios·iphone
1024小神13 小时前
xcode 配置了AppIcon 但是不显示icon图标
ios·swiftui·swift
奶糖 肥晨13 小时前
架构深度解析|基于亚马逊云科技与Swift Alliance Cloud构建高可用金融报文交换架构
科技·架构·swift
2501_9159184115 小时前
iOS 项目中证书管理常见的协作问题
android·ios·小程序·https·uni-app·iphone·webview
ITKEY_15 小时前
iOS网页应用无地址栏无工具栏
ios
2501_9159184115 小时前
提升 iOS 应用安全审核通过率的一种思路,把容易被拒的点先处理
android·安全·ios·小程序·uni-app·iphone·webview
RollingPin15 小时前
iOS探究使用Block方式实现一对多回调能力
ios·block·runtime·数据分发·解耦·动态绑定·一对多回调