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()
}
}
我发现了问题。在 UIViewRepresentable
的 updateUIView
中,我还需要将绑定传递给 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()
// ...
}
为了定制一个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()
}
}
我发现了问题。在 UIViewRepresentable
的 updateUIView
中,我还需要将绑定传递给 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()
// ...
}