如何从SwiftUI视图调用GlobeViewController中的函数?
解决方案
一、最简方式:持有控制器引用避免重复回调
你碰到的闭包重复触发问题,核心是SwiftUI视图更新时会重新生成闭包,导致每次都执行粒子系统添加逻辑。解决办法是让UIViewControllerRepresentable只在控制器初始化时传递一次引用,而非每次视图更新都绑定回调。
示例代码
- 实现
GlobeView(UIViewControllerRepresentable):
struct GlobeView: UIViewControllerRepresentable { let onControllerReady: (GlobeViewController) -> Void func makeUIViewController(context: Context) -> GlobeViewController { let vc = GlobeViewController() // 仅在控制器创建时执行一次,把VC实例传出去 onControllerReady(vc) return vc } func updateUIViewController(_ uiViewController: GlobeViewController, context: Context) { // 这里别放粒子系统相关逻辑,不然视图更新就会重复执行 } }
- 在SwiftUI父视图中调用:
struct ParentView: View { @State private var globeVC: GlobeViewController? var body: some View { GlobeView(onControllerReady: { vc in globeVC = vc }) .onTapGesture { // 直接调用VC里的函数 globeVC?.startParticleSystem() } } }
这样onControllerReady只会触发一次,不会因为SwiftUI视图刷新重复执行,自然就不会无限添加粒子系统了。
二、通过ViewModel监听变量变化(更贴合MVVM)
如果想用ViewModel驱动逻辑,让GlobeViewController感知ViewModel的特定变量变化,可以用以下两种方式:
方式1:用@Published + Combine订阅
这种方式利用SwiftUI的响应式特性,让VC自动监听ViewModel的状态变化:
- 定义ViewModel:
class GlobeViewModel: ObservableObject { // 标记需要监听的变量 @Published var shouldStartParticles = false }
- 修改
GlobeViewController,订阅ViewModel的变化:
class GlobeViewController: UIViewController { var viewModel: GlobeViewModel! private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // 只在shouldStartParticles变为true时触发 viewModel.$shouldStartParticles .filter { $0 } .sink { [weak self] _ in self?.startParticleSystem() // 执行完重置状态,防止重复触发 self?.viewModel.shouldStartParticles = false } .store(in: &cancellables) } func startParticleSystem() { // 先判断粒子系统是否已存在,避免重复添加 guard let scene = sceneView.scene, !scene.particleSystems?.contains(particleSystem) ?? false else { return } scene.addParticleSystem(particleSystem) } }
- 在
GlobeView中传递ViewModel:
struct GlobeView: UIViewControllerRepresentable { @ObservedObject var viewModel: GlobeViewModel func makeUIViewController(context: Context) -> GlobeViewController { let vc = GlobeViewController() vc.viewModel = viewModel return vc } func updateUIViewController(_ uiViewController: GlobeViewController, context: Context) {} }
- SwiftUI父视图使用:
struct ParentView: View { @StateObject private var viewModel = GlobeViewModel() var body: some View { GlobeView(viewModel: viewModel) Button("启动粒子效果") { viewModel.shouldStartParticles = true } } }
方式2:用didSet触发回调(轻量无Combine)
如果不想用Combine,可以给ViewModel的变量加didSet,通过回调通知VC:
class GlobeViewModel: ObservableObject { var shouldStartParticles = false { didSet { if shouldStartParticles { startParticlesCallback?() shouldStartParticles = false } } } // 定义回调闭包 var startParticlesCallback: (() -> Void)? }
然后在GlobeViewController中绑定回调:
class GlobeViewController: UIViewController { var viewModel: GlobeViewModel! override func viewDidLoad() { super.viewDidLoad() viewModel.startParticlesCallback = { [weak self] in self?.startParticleSystem() } } }
核心注意事项
- 绝对不要在
updateUIViewController里执行添加粒子系统的逻辑,SwiftUI会频繁调用这个方法,必然导致重复操作。 - 不管用哪种方法,都要在VC的函数里先判断粒子系统是否已存在,从根源避免重复添加。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

