programing

스위프트에게 말하는 방법중첩된 관찰 가능 개체에 바인딩할 UI 보기

codeshow 2023. 8. 20. 12:58
반응형

스위프트에게 말하는 방법중첩된 관찰 가능 개체에 바인딩할 UI 보기

스위프트를 가지고 있습니다.의 UI appModel그런 다음 값을 읽습니다.appModel.submodel.count 그안에.body방법.이것이 제 견해를 부동산에 구속할 것으로 기대합니다.countsubmodel속성이 업데이트될 때 다시 표시되도록 하지만 이러한 현상은 발생하지 않는 것 같습니다.

이거 벌레야?그렇지 않은 경우 Swift에서 보기를 환경 개체의 중첩된 속성에 바인딩하는 관용적인 방법은 무엇입니까?UI?

구체적으로, 제 모델은 이렇게 생겼습니다.

class Submodel: ObservableObject {
  @Published var count = 0
}

class AppModel: ObservableObject {
  @Published var submodel: Submodel = Submodel()
}

제 견해는 이렇습니다.

struct ContentView: View {
  @EnvironmentObject var appModel: AppModel

  var body: some View {
    Text("Count: \(appModel.submodel.count)")
      .onTapGesture {
        self.appModel.submodel.count += 1
      }
  }
}

하면 앱을실행클릭면하라벨을고하,면▁the▁when,count속성은 증가하지만 레이블은 업데이트되지 않습니다.

이 문제는 제가 해결할 수 있습니다.appModel.submodel의 재산으로서ContentView하지만 가능하다면 피하고 싶습니다.

중첩된 모델이 Swift에서 아직 작동하지 않음UI, 하지만 당신은 다음과 같은 것을 할 수 있습니다.

class SubModel: ObservableObject {
    @Published var count = 0
}

class AppModel: ObservableObject {
    @Published var submodel: SubModel = SubModel()
    
    var anyCancellable: AnyCancellable? = nil
    
    init() {
        anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
            self?.objectWillChange.send()
        }
    } 
}

으로 당신의 기적으당신의.AppModel는 서에이를포다에서 합니다.SubModel그리고 그것을 더 멀리 보내십시오.View.

편집:

필요 없는 경우SubModel클래스가 되려면 다음과 같은 것을 시도할 수 있습니다.

struct SubModel{
    var count = 0
}

class AppModel: ObservableObject {
    @Published var submodel: SubModel = SubModel()
}

Sorin Lica의 솔루션은 문제를 해결할 수 있지만 복잡한 뷰를 처리할 때 코드 냄새가 발생합니다.

더 나은 조언으로 보이는 것은 당신의 견해를 자세히 보고, 더 많은, 더 표적적인 견해를 만들기 위해 그것들을 수정하는 것입니다.구조의 의▁that▁to▁classes▁structure▁to▁conform▁your다▁structure,▁views▁so▁a▁of▁object▁displays▁the각니▁matching와 일치하는 클래스와 일치하도록 뷰를 구성합니다.ObservableObject위의 경우 표시할 보기를 만들 수 있습니다.Submodel표시할 속성을 표시합니다.속성 요소를 해당 보기에 전달하고 게시자 체인을 추적할 수 있도록 합니다.

struct ContentView: View {
  @EnvironmentObject var appModel: AppModel

  var body: some View {
    SubView(submodel: appModel.submodel)
  }
}

struct SubView: View {
  @ObservedObject var submodel: Submodel

  var body: some View {
      Text("Count: \(submodel.count)")
      .onTapGesture {
        self.submodel.count += 1
      }
  }
}

이 패턴은 더 크고, 더 작고, 집중적인 뷰를 만드는 것을 의미하며, 엔진을 Swift 내부에 배치합니다.UI는 관련 추적을 수행합니다.그러면 부기를 다룰 필요가 없고, 당신의 관점도 잠재적으로 상당히 단순해집니다.

자세한 내용은 https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/ 에서 확인할 수 있습니다.

저는 최근에 제 블로그에 이것에 대해 썼습니다.중첩된 관찰 가능 개체입니다.솔루션의 요점은 관찰 가능한 개체의 계층 구조를 원하는 경우 관찰 가능한 개체 프로토콜에 맞게 자신만의 최상위 제목 결합을 만든 다음 업데이트를 트리거할 논리를 해당 제목을 업데이트하는 명령 코드로 캡슐화하는 것입니다.

예를 들어, 다음과 같은 두 개의 "내포" 클래스가 있는 경우

class MainThing : ObservableObject {
    @Published var element : SomeElement
    init(element : SomeElement) {
        self.element = element
    }
}
class SomeElement : ObservableObject {
    @Published var value : String
    init(value : String) {
        self.value = value
    }
}

할 수 .MainThing다음으로 이동합니다.

class MainThing : ObservableObject {
    @Published var element : SomeElement
    var cancellable : AnyCancellable?
    init(element : SomeElement) {
        self.element = element
        self.cancellable = self.element.$value.sink(
            receiveValue: { [weak self] _ in
                self?.objectWillChange.send()
            }
        )
    }
}

임베드기서출기잡것아은는내를판사에서 를 가져옵니다.ObservableObject그리고 속성이 등록될 때 로컬 게시로 업데이트를 보냅니다.valueSomeElement클래스가 수정되었습니다.이를 확장하여 여러 속성의 스트림 또는 테마의 변형 수를 게시하는 데 CombineLatest를 사용할 수 있습니다.

하지만 이것은 "그냥 하는" 해결책이 아닙니다. 이 패턴의 논리적 결론은 뷰 계층을 성장시킨 후에 해당 게시자에 구독된 엄청난 수의 View가 무효화되고 다시 그려져서 잠재적으로 과도한 결과를 초래할 수 있기 때문입니다.전면적인 재도약과 업데이트에 대한 상대적으로 저조한 성능.스위프트의 "폭발 반경"을 유지하기 위해 당신의 견해를 클래스에 특정하고 그 클래스와 일치시킬 수 있는지 확인하는 것이 좋습니다.UI의 보기 무효화를 최소화했습니다.

@Published참조 유형을 위해 설계되지 않았기 때문에 이를 추가하는 것은 프로그래밍 오류입니다.AppModel속성, 컴파일러나 런타임이 불만을 제기하지 않습니다.직관적으로 이해할 수 있었던 것은 다음과 같습니다.@ObservedObject아래와 같이 하지만 슬프게도 이것은 아무 것도 하지 않습니다.

class AppModel: ObservableObject {
    @ObservedObject var submodel: SubModel = SubModel()
}

중첩을 허용하지 않는지 확실하지 않습니다.ObservableObjects스위프트의 의도적인 행동이었습니다.나중에 채워야 할 UI 또는 공백입니다.다른 답변에서 제안한 대로 부모 및 자식 개체를 연결하는 것은 매우 지저분하고 유지 관리하기 어렵습니다.요?UI는 보기를 더 작은 보기로 분할하고 하위 개체를 하위 보기로 전달합니다.

struct ContentView: View {
    @EnvironmentObject var appModel: AppModel

    var body: some View {
        SubView(model: appModel.submodel)
    }
}

struct SubView: View {
    @ObservedObject var model: SubModel

    var body: some View {
        Text("Count: \(model.count)")
            .onTapGesture {
                model.count += 1
            }
    }
}

class SubModel: ObservableObject {
    @Published var count = 0
}

class AppModel: ObservableObject {
    var submodel: SubModel = SubModel()
}

하위 모델 돌연변이는 하위 뷰로 전달될 때 실제로 전파됩니다!

하지만, 다른 개발자가 전화하는 것을 막을 수 있는 것은 없습니다.appModel.submodel.count부모의 관점에서 보면 컴파일러 경고나 심지어 이것을 수행하지 않는 일부 Swift 방식도 없습니다.

출처: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/

저는 소린리카의 해결책을 좋아했습니다.이를 바탕으로 사용자 정의를 구현하기로 결정했습니다.Property Wrapper(이 놀라운 기사 포함) 이름이 지정되었습니다.NestedObservableObject개발자 친화적인 솔루션을 만들 수 있습니다.

이를 통해 다음과 같은 방법으로 모델을 작성할 수 있습니다.

class Submodel: ObservableObject {
  @Published var count = 0
}

class AppModel: ObservableObject {
  @NestedObservableObject var submodel: Submodel = Submodel()
}

속성 래퍼 구현

@propertyWrapper
struct NestedObservableObject<Value : ObservableObject> {
    
    static subscript<T: ObservableObject>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        
        get {
            if instance[keyPath: storageKeyPath].cancellable == nil, let publisher = instance.objectWillChange as? ObservableObjectPublisher   {
                instance[keyPath: storageKeyPath].cancellable =
                    instance[keyPath: storageKeyPath].storage.objectWillChange.sink { _ in
                            publisher.send()
                    }
            }
            
            return instance[keyPath: storageKeyPath].storage
         }
         set {
             
             if let cancellable = instance[keyPath: storageKeyPath].cancellable {
                 cancellable.cancel()
             }
             if let publisher = instance.objectWillChange as? ObservableObjectPublisher   {
                 instance[keyPath: storageKeyPath].cancellable =
                     newValue.objectWillChange.sink { _ in
                             publisher.send()
                     }
             }
             instance[keyPath: storageKeyPath].storage = newValue
         }
    }
    
    @available(*, unavailable,
        message: "This property wrapper can only be applied to classes"
    )
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    
    private var cancellable: AnyCancellable?
    private var storage: Value

    init(wrappedValue: Value) {
        storage = wrappedValue
    }
}

코드를 게시했습니다.

만약 여러분이 관찰 가능한 물체를 둥지로 만들어야 한다면, 여기가 제가 찾을 수 있는 가장 좋은 방법입니다.

class ChildModel: ObservableObject {
    
    @Published
    var count = 0
    
}

class ParentModel: ObservableObject {
    
    @Published
    private var childWillChange: Void = ()
    
    let child = ChildModel()
    
    init() {
        child.objectWillChange.assign(to: &$childWillChange)
    }
    
}

하위 개체 WillChange 게시자에 가입하고 상위 개체 WillChange 게시자를 실행하는 대신 게시된 속성과 상위 개체 WillChange 트리거에 값을 자동으로 할당합니다.

세 가지 View 모델 모두 통신 및 업데이트 가능

// First ViewModel
class FirstViewModel: ObservableObject {
var facadeViewModel: FacadeViewModels

facadeViewModel.firstViewModelUpdateSecondViewModel()
}

// Second ViewModel
class SecondViewModel: ObservableObject {

}

// FacadeViewModels Combine Both 

import Combine // so you can update thru nested Observable Objects

class FacadeViewModels: ObservableObject { 
lazy var firstViewModel: FirstViewModel = FirstViewModel(facadeViewModel: self)
  @Published var secondViewModel = secondViewModel()
}

var anyCancellable = Set<AnyCancellable>()

init() {
firstViewModel.objectWillChange.sink {
            self.objectWillChange.send()
        }.store(in: &anyCancellable)

secondViewModel.objectWillChange.sink {
            self.objectWillChange.send()
        }.store(in: &anyCancellable)
}

func firstViewModelUpdateSecondViewModel() {
     //Change something on secondViewModel
secondViewModel
}

콤바인 솔루션의 Sorin에게 감사드립니다.

어린이(뷰) 모델을 구독하는 것보다 더 우아하다고 생각하는 솔루션이 있습니다.그것은 이상하고 나는 그것이 왜 작동하는지에 대한 설명이 없습니다.

해결책

에서 되는 기본 합니다.ObservableObject 방법을 합니다.notifyWillChange() 단히말하면라고 부르는objectWillChange.send()는 파된클는재정니다됩의래스생다▁then니▁any를 재정의합니다.notifyWillChange()그리고 부모님의 집에 전화를 합니다.notifyWillChange()방법. 장포objectWillChange.send()해야 합니다. 그렇지 " 이방필니다합요법다니"로 됩니다. 그렇지 않으면 변경됩니다.@Published any any any 를 .View업데이트할 s.어쩌면 그 방법과 관련이 있을 수도 있습니다.@Published변경사항이 탐지되었습니다.스위프트를 믿습니다.UI/후드 아래의 사용 반사 결합...

OP의 코드에 약간의 추가 사항을 추가했습니다.

  • count 메소드 호출로 호출됩니다.notifyWillChange()카운터가 증가하기 전에.이것은 변경사항을 전파하는 데 필요합니다.
  • AppModel 더 된 하더 포@Published 소물유,title탐색 모음의 제목에 사용됩니다.이는 다음과 같은 것을 보여줍니다.@Published상위 개체와 하위 개체 모두에 대해 작동합니다(아래 예제에서는 모델이 초기화된 후 2초 후에 업데이트됨).

코드

기본 모델

class BaseViewModel: ObservableObject {
    func notifyWillUpdate() {
        objectWillChange.send()
    }
}

모델

class Submodel: BaseViewModel {
    @Published var count = 0
}


class AppModel: BaseViewModel {
    @Published var title: String = "Hello"
    @Published var submodel: Submodel = Submodel()

    override init() {
        super.init()
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            guard let self = self else { return }
            self.notifyWillChange() // XXX: objectWillChange.send() doesn't work!
            self.title = "Hello, World"
        }
    }

    func increment() {
        notifyWillChange() // XXX: objectWillChange.send() doesn't work!
        submodel.count += 1
    }

    override func notifyWillChange() {
        super.notifyWillChange()
        objectWillChange.send()
    }
}

더 뷰

struct ContentView: View {
    @EnvironmentObject var appModel: AppModel
    var body: some View {
        NavigationView {
            Text("Count: \(appModel.submodel.count)")
                .onTapGesture {
                    self.appModel.increment()
            }.navigationBarTitle(appModel.title)
        }
    }
}

저는 이렇게 합니다.

import Combine

extension ObservableObject {
    func propagateWeakly<InputObservableObject>(
        to inputObservableObject: InputObservableObject
    ) -> AnyCancellable where
        InputObservableObject: ObservableObject,
        InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
    {
        objectWillChange.propagateWeakly(to: inputObservableObject)
    }
}

extension Publisher where Failure == Never {
    public func propagateWeakly<InputObservableObject>(
        to inputObservableObject: InputObservableObject
    ) -> AnyCancellable where
        InputObservableObject: ObservableObject,
        InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
    {
        sink { [weak inputObservableObject] _ in
            inputObservableObject?.objectWillChange.send()
        }
    }
}

따라서 상담 측면에서는:

class TrackViewModel {
    private let playbackViewModel: PlaybackViewModel
    
    private var propagation: Any?
    
    init(playbackViewModel: PlaybackViewModel) {
        self.playbackViewModel = playbackViewModel
        
        propagation = playbackViewModel.propagateWeakly(to: self)
    }
    
    ...
}

여기 요점이 있습니다.

해결 방법은 다음 게시물을 참조하십시오. [arthurhammer.de/2020/03/combine-optional-flatmap ][1]. $ 게시자와 결합 방식으로 문제를 해결합니다.

정다하추로 합니다.class Foto에는 주석 구조체와 주석 게시자가 있으며 주석 구조체를 게시합니다.Foto.sample(표시: .Priotal) 주석 구조는 주석 게시자를 통해 비동기적으로 "로드"됩니다.일반 바닐라 혼합... 그러나 이를 View & View Model(보기 & 보기 모델)로 가져오려면 다음을 사용합니다.

class DataController: ObservableObject {
    @Published var foto: Foto
    @Published var annotation: LCPointAnnotation
    @Published var annotationFromFoto: LCPointAnnotation

    private var cancellables: Set<AnyCancellable> = []

        
    init() {
      self.foto = Foto.sample(orientation: .Portrait)
      self.annotation = LCPointAnnotation()
      self.annotationFromFoto = LCPointAnnotation()
    
      self.foto.annotationPublisher
        .replaceError(with: LCPointAnnotation.emptyAnnotation)
        .assign(to: \.annotation, on: self)
        .store(in: &cancellables)
    
      $foto
        .flatMap { $0.$annotation }
        .replaceError(with: LCPointAnnotation.emptyAnnotation)
        .assign(to: \.annotationFromFoto, on: self)
        .store(in: &cancellables)
    
    }
 }

참고: [1]: https://arthurhammer.de/2020/03/combine-optional-flatmap/

플랫맵 내의 위의 $ 주석에 주목하십시오. 게시자입니다!

 public class Foto: ObservableObject, FotoProperties, FotoPublishers {
   /// use class not struct to update asnyc properties!
   /// Source image data
   @Published public var data: Data
   @Published public var annotation = LCPointAnnotation.defaultAnnotation
   ......
   public init(data: Data)  {
      guard let _ = UIImage(data: data),
            let _ = CIImage(data: data) else {
           fatalError("Foto - init(data) - invalid Data to generate          CIImage or UIImage")
       }
      self.data = data
      self.annotationPublisher
        .replaceError(with: LCPointAnnotation.emptyAnnotation)
        .sink {resultAnnotation in
            self.annotation = resultAnnotation
            print("Foto - init annotation = \(self.annotation)")
        }
        .store(in: &cancellables)
    }

최상위 보기에서 함수 또는 최상위 클래스에 게시된 변수와 동일한 변수를 만들 수 있습니다.그런 다음 전달하고 모든 하위 보기에 바인딩합니다.하위 보기에서 변경되면 상위 보기가 업데이트됩니다.

코드 구조:

struct Expense : Identifiable {
    var id = UUID()
    var name: String
    var type: String
    var cost: Double
    var isDeletable: Bool
}

class Expenses: ObservableObject{ 
    @Published var name: String
    @Published var items: [Expense] 

    init() {
        name = "John Smith"
        items = [
            Expense(name: "Lunch", type: "Business", cost: 25.47, isDeletable: true),
            Expense(name: "Taxi", type: "Business", cost: 17.0, isDeletable: true),
            Expense(name: "Sports Tickets", type: "Personal", cost: 75.0, isDeletable: false)
        ]
    }
    
    func totalExpenses() -> Double { }      
}

class ExpenseTracker: ObservableObject {
    @Published var name: String
    @Published var expenses: Expenses
    
    init() {
        name = "My name"
        expenses = Expenses()
    }    

    func getTotalExpenses() -> Double { }
}

보기:

struct MainView: View {
    @ObservedObject var myTracker: ExpenseTracker
    @State var totalExpenses: Double = 0.0
    
    var body: some View {
        NavigationView {
            Form {
                Section (header: Text("Main")) {
                    HStack {
                        Text("name:")
                        Spacer()
                        TextField("", text: $myTracker.name)
                            .multilineTextAlignment(.trailing)
                            .keyboardType(.default)
                    }                         
                    NavigationLink(destination: ContentView(myExpenses: myTracker.expenses, totalExpenses: $totalExpenses),
                                   label: {
                                       Text("View Expenses")
                                   })
                }                
                Section (header: Text("Results")) {
                    }
                    HStack {
                        Text("Total Expenses")
                        Spacer()
                        Text("\(totalExpenses, specifier: "%.2f")")
                    }
                }
            }
            .navigationTitle("My Expense Tracker")
            .font(.subheadline)
        }      
        .onAppear{
            totalExpenses = myTracker.getTotalExpenses()
        }
    }
}

struct ContentView: View {
    @ObservedObject var myExpenses:Expenses
    @Binding var totalExpenses: Double
    @State var selectedExpenseItem:Expense? = nil
    
    var body: some View {
        NavigationView{
            Form {
                List {
                    ForEach(myExpenses.items) { item in
                        HStack {
                            Text("\(item.name)")
                            Spacer()
                            Button(action: {
                                self.selectedExpenseItem = item
                            } ) {
                                Text("View")
                            }
                        }
                        .deleteDisabled(item.isDeletable)
                    }
                    .onDelete(perform: removeItem)
                }
                HStack {
                    Text("Total Expenses:")
                    Spacer()
                    Text("\(myExpenses.totalExpenses(), specifier: "%.2f")")
                }
            }
            .navigationTitle("Expenses")
            .toolbar {
                Button {
                    let newExpense = Expense(name: "Enter name", type: "Expense item", cost: 10.00, isDeletable: false)
                    self.myExpenses.items.append(newExpense)
                    self.totalExpenses = myExpenses.totalExpenses()
                } label: {
                    Image(systemName: "plus")
                }
            }
            }
        .fullScreenCover(item: $selectedExpenseItem) { myItem in
            ItemDetailView(item: myItem, myExpenses: myExpenses, totalExpenses: $totalExpenses)
        }
    }
    func removeItem(at offsets: IndexSet){
        self.myExpenses.items.remove(atOffsets: offsets)
        self.totalExpenses = myExpenses.totalExpenses()
    }
}

내가 사용하고 있다는 것만 주목하면 됩니다.NestedObservableObject내 최신 앱의 @bsorrentino에서 접근합니다.

일반적으로 이 문제는 피할 수 있지만 문제의 중첩된 개체는 실제로 CoreData 모델이기 때문에 이와 관련하여 더 작은 보기로 항목을 분류하는 것은 실제로 효과가 없습니다.

이 솔루션은 NSManagedObjects를 (대부분이) 관찰 가능한 개체로 취급하기 때문에 가장 적합한 솔루션인 것 같습니다. CodeData 개체 모델이 변경되면 업데이트를 트리거해야 합니다.

AppModel의 var 하위 모델에는 속성 래퍼 @Published가 필요하지 않습니다.@Published의 목적은 새 값과 개체 WillChange를 내보내는 것입니다.그러나 변수는 변경되지 않고 한 번만 시작됩니다.

하위 모델의 변경 사항은 sink-objectWillChange 구성을 통해 가입자에 의해 뷰에 전달되며 뷰가 다시 그려집니다.

class SubModel: ObservableObject {
    @Published var count = 0
}

class AppModel: ObservableObject {
    let submodel = SubModel()
    
    var anyCancellable: AnyCancellable? = nil
    
    init() {
        anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
            self?.objectWillChange.send()
        }
    } 
}

중됨ObservableObject모델이 아직 작동하지 않습니다.

그러나 각 모델을 수동으로 구독하여 작동할 수 있습니다.그 대답은 이것의 간단한 예를 보여주었습니다.

이 수동 프로세스를 확장을 통해 좀 더 간소화하고 읽기 쉽게 만들 수 있다는 점을 추가하고 싶습니다.

class Submodel: ObservableObject {
  @Published var count = 0
}

class AppModel: ObservableObject {
  @Published var submodel = Submodel()
  @Published var submodel2 = Submodel2() // the code for this is not defined and is for example only
  private var cancellables: Set<AnyCancellable> = []

  init() {
    // subscribe to changes in `Submodel`
    submodel
      .subscribe(self)
      .store(in: &cancellables)

    // you can also subscribe to other models easily (this solution scales well):
    submodel2
      .subscribe(self)
      .store(in: &cancellables)
  }
}

확장자는 다음과 같습니다.

extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher  {

  func subscribe<T: ObservableObject>(
    _ observableObject: T
  ) -> AnyCancellable where T.ObjectWillChangePublisher == ObservableObjectPublisher {
    return objectWillChange
      // Publishing changes from background threads is not allowed.
      .receive(on: DispatchQueue.main)
      .sink { [weak observableObject] (_) in
        observableObject?.objectWillChange.send()
      }
  }
}

그것은 벌레처럼 보입니다.xcode를 최신 버전으로 업데이트하면 중첩된 Objects에 바인딩할 때 올바르게 작동함

언급URL : https://stackoverflow.com/questions/58406287/how-to-tell-swiftui-views-to-bind-to-nested-observableobjects

반응형