#swiftui #combine #observableobject
#swiftui #объединить #observableobject
Вопрос:
чтобы настроить a UISlider
, я использую его в a UIViewRepresentable
. Он предоставляет a @Binding var value: Double
, чтобы мое представление view model ( ObservableObject
) могло наблюдать изменения и обновлять a 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: amp;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()
}
}
Ответ №1:
Я нашел проблему. В в updateUIView
из 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
Может быть воссоздан в любое время, и когда это произойдет updateUIView
, вызывается. Новая структура представления имеет новую var value: Binding<Double>
, поэтому мы назначаем ее нашему координатору
Ответ №2:
Здесь происходит то, что включение @Environment(.presentationMode)
приводит ContentView
к тому, что тело пересчитывается, как только представлена модель. (Я точно не знаю, почему это происходит; возможно, потому, что при отображении изменяется режим представления sheet
).
Но когда это происходит, он инициирует MyModalView
дважды и с двумя отдельными экземплярами TempViewModel
.
На первом MyModalView
создается иерархия представлений с SwiftUISlider
помощью. Именно здесь Coordinator
создается и сохраняется привязка (привязанная к первому экземпляру TempViewModel
).
Во втором MyModelView
случае иерархия представлений такая же, поэтому она не вызывается makeUIView
(вызывается только при первом появлении представления), а только updateUIView
вызывается. Как вы правильно заметили, обновление привязки ко второму экземпляру TempViewModel
решает эту проблему.
Итак, одно из решений — это то, что вы сделали в своем другом ответе — в основном переназначить привязку к свойству нового объекта (которое, кстати, также освобождает старый объект). Это решение на самом деле кажется мне правильным в любом случае.
Но для полноты картины другой подход заключается в том, чтобы не создавать несколько экземпляров TempViewModel
, например, используя a @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()
// ...
}
Комментарии:
1. да, ваше решение также работает, но предполагает использование
@StateObject
, которое недоступно в iOS 13.2. @Jan, использование a
@StateObject
не является обязательным. Моя более широкая точка зрения заключалась в том, чтобы указать, чтоTempViewModel
экземпляр создается дважды, что и стало основной причиной возникшей у вас проблемы. Ваше решение устраняет последствия этой проблемы, но комплексный подход заключался бы в том, чтобы также устранить проблему для начала