You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

@ObservedObject的@Published属性作为UIViewControllerRepresentable内@Binding输入时动画失效的解决咨询

@ObservedObject的@Published属性作为UIViewControllerRepresentable内@Binding输入时动画失效的解决咨询

我之前搞定了一个相关问题后,现在又卡在UIViewControllerRepresentable的另一个坑上了,想请教下大家怎么解决:

我写了个PickerView,用@Binding var value: String来展示和更新值,值变化时会触发自定义的上下滑+透明度的过渡动画。在普通SwiftUI视图里用的时候完全正常,但要是把@ObservedObject的@Published属性作为@Binding传入UIViewControllerRepresentable内部的PickerView,值变化的动画就直接失效了。反而用本地的@State属性当输入时,动画从头到尾都正常。

具体现象就是:黄色容器区域里,绑定@ObservedObject的那个PickerView更新值时完全没动画,而绑定@State的就正常;但在容器外面的普通视图里,不管是绑定@ObservedObject还是@State,动画都没问题。

注:因为咱们App要兼容iOS16+,所以暂时没法用新的@Observable宏。

下面是我写的完整代码:

struct PickerView: View {
    @Binding var value: String
    
    var body: some View {
        VStack {
            Text(value)
                .transition(textTransition)
                .id(value)
            
            Button("Change Value") {
                withAnimation {
                    value = String(UUID().uuidString.prefix(8))
                }
            }
        }
    }
    
    private var textTransition: AnyTransition {
        .asymmetric(
            insertion: .move(edge: .bottom).combined(with: .opacity),
            removal: .move(edge: .top).combined(with: .opacity)
        )
    }
}

class PickerViewModel: ObservableObject {
    @Published var value: String = "viewModelValue"
}

struct PickerContainer: View {
    @ObservedObject var viewModel: PickerViewModel
    @State var stateValue: String = "stateValue"
    
    var body: some View {
        VStack {
            ContainerView{
                VStack(spacing: 20) {
                    Text("Container: ViewModel Fails")
                        .font(.headline)
                    PickerView(value: $viewModel.value) // 这里更新时无动画
                    PickerView(value: $stateValue)
                }
            }
            
            VStack(spacing: 20) {
                Text("Both OK")
                    .font(.headline)
                PickerView(value: $viewModel.value)
                PickerView(value: $stateValue)
            }
        }
    }
}

struct PickerPreview: View {
    @StateObject var viewModel = PickerViewModel()
    
    var body: some View {
        PickerContainer(viewModel: viewModel)
    }
}

struct ContainerView<Content: View>: UIViewControllerRepresentable {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        let containerVC = UIViewController()
        containerVC.view.backgroundColor = .yellow
        let contentVC = UIHostingController(rootView: self.content)
        contentVC.view.backgroundColor = .clear
        context.coordinator.contentVC = contentVC
        
        contentVC.willMove(toParent: containerVC)
        containerVC.addChild(contentVC)
        contentVC.view.translatesAutoresizingMaskIntoConstraints = false
        containerVC.view.addSubview(contentVC.view)
        
        NSLayoutConstraint.activate([
            contentVC.view.topAnchor.constraint(equalTo: containerVC.view.topAnchor),
            contentVC.view.bottomAnchor.constraint(equalTo: containerVC.view.bottomAnchor),
            contentVC.view.leadingAnchor.constraint(equalTo: containerVC.view.leadingAnchor),
            contentVC.view.trailingAnchor.constraint(equalTo: containerVC.view.trailingAnchor)
        ])
        
        contentVC.didMove(toParent: containerVC)
        return containerVC
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        context.coordinator.contentVC?.rootView = content
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator: NSObject, UIScrollViewDelegate {
        var contentVC: UIHostingController<Content>?
    }
}

#Preview {
    PickerPreview()
}

我找到的解决办法

这个问题的根子在ContainerView的updateUIViewController方法里——我之前直接写了context.coordinator.contentVC?.rootView = content,这会直接把原来的rootView换掉。当@ObservedObject的@Published属性更新时,updateUIViewController会被调用,这时候直接替换rootView相当于把原来的视图树整个重建了一遍,动画上下文直接被打断,自然就看不到动画了。

解决的核心就是不要直接替换rootView,让UIHostingController自己处理rootView的动态更新,保持视图树的连续性。修改后的ContainerView代码如下:

struct ContainerView<Content: View>: UIViewControllerRepresentable {
    @ViewBuilder let content: () -> Content

    func makeUIViewController(context: Context) -> UIViewController {
        let containerVC = UIViewController()
        containerVC.view.backgroundColor = .yellow
        // 直接用content()初始化,因为@ViewBuilder会处理动态内容
        let contentVC = UIHostingController(rootView: content())
        contentVC.view.backgroundColor = .clear
        context.coordinator.contentVC = contentVC
        
        contentVC.willMove(toParent: containerVC)
        containerVC.addChild(contentVC)
        contentVC.view.translatesAutoresizingMaskIntoConstraints = false
        containerVC.view.addSubview(contentVC.view)
        
        NSLayoutConstraint.activate([
            contentVC.view.topAnchor.constraint(equalTo: containerVC.view.topAnchor),
            contentVC.view.bottomAnchor.constraint(equalTo: containerVC.view.bottomAnchor),
            contentVC.view.leadingAnchor.constraint(equalTo: containerVC.view.leadingAnchor),
            contentVC.view.trailingAnchor.constraint(equalTo: containerVC.view.trailingAnchor)
        ])
        
        contentVC.didMove(toParent: containerVC)
        return containerVC
    }

    // 关键:删掉原来替换rootView的代码!
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // 啥也不用做,SwiftUI的绑定系统会自动更新content里的视图内容
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    class Coordinator: NSObject {
        var contentVC: UIHostingController<Content>?
    }
}

为啥这样能行?

  • 原来的写法直接替换rootView,等于把之前的视图全删了重建,动画的上下文直接就没了,withAnimation自然触发不了。
  • 删掉替换rootView的代码后,UIHostingController会保留原来的rootView引用,SwiftUI的绑定系统会自动把@ObservedObject的更新传递给子视图,视图树不会被重建,动画上下文得以保留,自定义的过渡动画就能正常触发了。

亲测这样修改后,不管是绑定@ObservedObject还是@State的属性,在容器里的动画都能正常工作了。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:29:33