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

时间:2020-10-26 08:48:06

标签: swiftui combine observableobject

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

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

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

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

enter image description here

再现此示例的完整示例代码是

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()
    }
}

2 个答案:

答案 0 :(得分:0)

我发现了问题。在updateUIView的{​​{1}}中,我还需要将绑定传递给UIViewRepresentable的新实例:

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时会重新创建。新的View结构具有新的updateUIView,因此我们将其分配给协调器

答案 1 :(得分:0)

这里发生的是,@Environment(\.presentationMode)的包含会导致ContentView的主体在模型出现后立即重新计算。 (我不完全知道为什么会发生 ;也许是因为显示sheet时演示模式发生了变化)。

但是,发生这种情况时,它将两次启动MyModalView,并使用{strong>两个单独的实例 TempViewModel

在第一个MyModalView上,创建带有SwiftUISlider的视图层次结构。在此处创建Coordinator,并存储绑定(绑定到TempViewModel的第一个实例)。

在第二个MyModelView上,视图层次结构是相同的,因此它不会调用makeUIView(仅在视图第一次出现时才调用),并且仅调用updateUIView。如您所正确指出的,更新到TempViewModel的第二个实例的绑定即可解决此问题。

因此,一种解决方案是您在其他答案中所做的工作-基本上将绑定重新分配给新对象的属性(顺便说一句,它还会释放旧对象)。 这个解决方案实际上让我觉得无论如何都应该做正确的事。

但是出于完整性考虑,另一种方法是不制作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()

   // ...
}