UIViewRepresentable 无法在模态呈现的 SwiftUI 视图中工作

UIViewRepresentable not working in a modally presented SwiftUI View

为了定制一个UISlider,我用在了一个UIViewRepresentable中。它公开了一个 @Binding var value: Double 以便我的视图模型 (ObservableObject) 视图可以观察更改并相应地更新一个 View

问题是 @Binding 值更改时视图未更新。在下面的示例中,我有两个滑块。一位本地 Slider 和一位自定义 SwiftUISlider.

两者都将绑定值传递给应该更新视图的视图模型。本机 Slider 会更新视图但不会更新自定义视图。在日志中,我可以看到 $sliderValue.sink { ... 被正确调用,但视图未更新。

我注意到当呈现视图具有 @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> 属性 时会发生这种情况。如果我将其注释掉,它会按预期工作。

重现此内容的完整示例代码是

import SwiftUI
import Combine

struct ContentView: View {
    @State var isPresentingModal = false

    // comment this out
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Button("Show modal") {
                isPresentingModal = true
            }
            .padding()
        }
        .sheet(isPresented: $isPresentingModal) {
            MyModalView(viewModel: TempViewModel())
        }
    }
}

class TempViewModel: ObservableObject {
    @Published var sliderText = ""
    @Published var sliderValue: Double = 0
    private var cancellable = Set<AnyCancellable>()

        init() {
            $sliderValue
                .print("view model")
                .sink { [weak self] value in
                    guard let self = self else { return }
                    print("updating view  \(value)")
                    self.sliderText = "\(value) C = \(String(format: "%.2f" ,value * 9 / 5 + 32)) F"
                }
                .store(in: &cancellable)
        }
}

struct MyModalView: View {
    @ObservedObject var viewModel: TempViewModel

    var body: some View {
        VStack(alignment: .leading) {
            Text("SwiftUI Slider")
            Slider(value: $viewModel.sliderValue, in: -100...100, step: 0.5)
                .padding(.bottom)

            Text("UIViewRepresentable Slider")
            SwiftUISlider(minValue: -100, maxValue: 100, value: $viewModel.sliderValue)
            Text(viewModel.sliderText)
        }
        .padding()
    }
}

struct SwiftUISlider: UIViewRepresentable {
    final class Coordinator: NSObject {
        var value: Binding<Double>
        init(value: Binding<Double>) {
            self.value = value
        }

        @objc func valueChanged(_ sender: UISlider) {
            let index = Int(sender.value + 0.5)
            sender.value = Float(index)
            print("value changed \(sender.value)")
            self.value.wrappedValue = Double(sender.value)
        }
    }

    var minValue: Int = 0
    var maxValue: Int = 0

    @Binding var value: Double

    func makeUIView(context: Context) -> UISlider {
        let slider = UISlider(frame: .zero)
        slider.minimumTrackTintColor = .systemRed
        slider.maximumTrackTintColor = .systemRed
        slider.maximumValue = Float(maxValue)
        slider.minimumValue = Float(minValue)

        slider.addTarget(
            context.coordinator,
            action: #selector(Coordinator.valueChanged(_:)),
            for: .valueChanged
        )

        adapt(slider, context: context)
        return slider
    }

    func updateUIView(_ uiView: UISlider, context: Context) {
        adapt(uiView, context: context)
    }

    func makeCoordinator() -> SwiftUISlider.Coordinator {
        Coordinator(value: $value)
    }

    private func adapt(_ slider: UISlider, context: Context) {
        slider.value = Float(value)
    }
}

struct PresentationMode_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

我发现了问题。在 UIViewRepresentableupdateUIView 中,我还需要将绑定传递给 SwiftUISlider 的新实例:

func updateUIView(_ uiView: UISlider, context: Context) {
    uiView.value = Float(value)

    // the _value is the Binding<Double> of the new View struct, we pass it to the coordinator 
    context.coordinator.value = _value
}

可以随时重新创建 SwiftUI.View,当它发生时调用 updateUIView。一个新的视图结构有一个新的 var value: Binding<Double> 所以我们将它分配给我们的协调器

这里发生的是,包含 @Environment(\.presentationMode) 会导致 ContentView 的主体在呈现模型后立即重新计算。 (我不确切地知道 为什么 会发生这种情况;可能是因为显示 sheet 时演示模式发生了变化)。

但是当发生这种情况时,它会启动 MyModalView 两次,并且 两个单独的实例 TempViewModel

在第一个 MyModalView 上,创建了具有 SwiftUISlider 的视图层次结构。这是创建 Coordinator 和存储绑定(绑定到 TempViewModel 的第一个实例)的地方。

在第二个MyModelView上,视图层级是一样的,所以它不调用makeUIView(只有视图第一次出现时才调用),只有updateUIView是叫。正如您正确指出的那样,将绑定更新到 TempViewModel 的 now-the-second 实例可以解决它。

所以,一个解决方案就是您在另一个答案中所做的 - 基本上 re-assign 绑定到新对象的 属性(顺便说一句,这也释放了旧对象)。 我觉得这个解决方案无论如何都是正确的。

但是对于completeness-sake,另一种方法是不创建TempViewModel的多个实例,例如,通过使用@StateObject来存储视图模型实例。这可以在父 ContentView 内部,也可以在 MyModalView:

内部
// option 1
struct ContentView: View {
    @State var isPresentingModal = false
    @StateObject var tempViewModel = TempViewModel()

    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        // ...
        
        .sheet(isPresented: $isPresentingModal) {
            MyModalView(viewModel: tempViewModel)
        }
    }
}
// option 2
struct ContentView: View {
    @State var isPresentingModal = false
    @StateObject var tempViewModel = TempViewModel()

    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        // ...
    
        .sheet(isPresented: $isPresentingModal) {
            MyModalView()
        }
    }
}

struct MyModalView: View {
   @StateObject var viewModel = TempViewModel()

   // ...
}