UIViewRepresentable не работает в модально представленном представлении SwiftUI

#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 экземпляр создается дважды, что и стало основной причиной возникшей у вас проблемы. Ваше решение устраняет последствия этой проблемы, но комплексный подход заключался бы в том, чтобы также устранить проблему для начала