3

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

问题是当@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()
    }
}
4

2 回答 2

0

我发现了这个问题。在 中updateUIViewUIViewRepresentable我还需要将绑定传递给 的新实例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调用。一个新的 View 结构有一个新的var value: Binding<Double>,所以我们将它分配给我们的协调器

于 2020-10-26T14:54:48.923 回答
0

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

但是当这种情况发生时,它会启动MyModalView两次,并使用两个单独TempViewModel.

首先MyModalViewSwiftUISlider创建视图层次结构。这是Coordinator创建 并TempViewModel存储绑定(绑定到 的第一个实例)的位置。

在第二个MyModelView上,视图层次结构是相同的,因此它不调用makeUIView(仅在视图首次出现时调用),仅updateUIView被调用。正如您正确指出的那样,将绑定更新为现在的第二个实例TempViewModel可以解决它。

因此,一个解决方案是您在另一个答案中所做的 - 基本上将绑定重新分配给新对象的属性(顺便说一句,它也会释放旧对象)。无论如何,这个解决方案对我来说实际上是正确的做法。

但是为了完整起见,另一种方法是不创建多个实例TempViewModel,例如,通过使用 a@StateObject来存储视图模型实例。这可以在 parent 内部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()

   // ...
}
于 2020-10-26T19:30:56.893 回答