为了自定义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()
}
}
答案 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()
// ...
}