SwiftUI - распространение уведомлений об изменениях через вложенные ссылочные типы

Я хотел бы расширить поведение ObservableObject в SwiftUI на вложенные классы, и я ищу правильный способ сделать это. Это можно сделать «вручную» с помощью Combine, но я полагаю, что есть более чистый способ сделать это с помощью SwiftUI, и я надеюсь, что вы укажете мне правильное направление. Вот что я имею в виду ...

Ниже приведено типичное приложение ObservableObject, чтобы представление динамически реагировало на изменения ссылочного типа. Нажатие на кнопку переключает значение showText, в результате чего текст появляется / исчезает на экране:

import SwiftUI

class MyClass: ObservableObject {
    @Published var showText = false
}


struct ContentView: View {

    @ObservedObject var instance = MyClass()

    var body: some View {
        VStack(spacing: 10) {
            Button(action: {
                print(self.instance.showText)
                self.instance.showText.toggle()
            }) {
                Text("BUTTON").bold().padding()
                    .foregroundColor(.white)
                    .background(Color.red)
            }
            if instance.showText {
                Text("Hello, World!")
            }
        }
    }
}

Это прекрасно работает.

Но как насчет модификации ниже, где класс, содержащий showText, является InnerClass, сам содержится в OuterClass? Кнопка переключает showText нормально, но уведомление об изменении значения больше не распространяется через экземпляр OuterClass на представление, поэтому в представлении больше не отображается текст.

import SwiftUI

class OuterClass: ObservableObject {
    @Published var innerInstance = InnerClass()
}

class InnerClass: ObservableObject {
    @Published var showText = false
}

struct ContentView: View {

    @ObservedObject var outerInstance = OuterClass()

    var body: some View {
        VStack(spacing: 10) {
            Button(action: {
                self.outerInstance.innerInstance.showText.toggle()
            }) {
                Text("BUTTON").bold().padding()
                    .foregroundColor(.white)
                    .background(Color.red)
            }
            if outerInstance.innerInstance.showText {
                Text("Hello, World!")
            }
        }
    }
}

Какое изящное исправление для этого?


person Anton    schedule 07.02.2020    source источник


Ответы (2)


это может быть сделано в вашей модели

import Combine // required for AnyCancelable

class OuterClass: ObservableObject {
    private let _inner: InnerClass
    var innerInstance: InnerClass {
        return _inner
    }
    var store = Set<AnyCancellable>()
    init(_ inner: InnerClass) {
        _inner = inner
        inner.$showText.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &store)
    }
}

и как использовать в вашем примере

import SwiftUI
import Combine

class OuterClass: ObservableObject {
    private let _inner: InnerClass
    var innerInstance: InnerClass {
        return _inner
    }
    var store = Set<AnyCancellable>()
    init(_ inner: InnerClass) {
        _inner = inner
        inner.$showText.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &store)
    }
}

class InnerClass: ObservableObject {
    @Published var showText = false
}

let inner = InnerClass()
let outer = OuterClass(inner)

struct ContentView: View {

    @ObservedObject var outerInstance = outer

    var body: some View {
        VStack(spacing: 10) {
            Button(action: {
                self.outerInstance.innerInstance.showText.toggle()
            }) {
                Text("BUTTON").bold().padding()
                    .foregroundColor(.white)
                    .background(Color.red)
            }
            if outerInstance.innerInstance.showText {
                Text("Hello, World!")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

если вам нравится наблюдать какие-либо изменения в своем внутреннем объекте, просто сделайте это!

class OuterClass: ObservableObject {
    private let _inner: InnerClass
    var innerInstance: InnerClass {
        return _inner
    }
    var store = Set<AnyCancellable>()
    init(_ inner: InnerClass) {
        _inner = inner
        inner.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &store)
    }
}

ОБНОВЛЕНИЕ: на основе обсуждения ниже

class OuterClass: Combine.ObservableObject {
    private let _inner: InnerClass
    var innerInstance: InnerClass {
        return _inner
    }
    var store = Set<AnyCancellable>()
    init(_ inner: InnerClass) {
        _inner = inner
        inner.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &store)
    }
}
person user3441734    schedule 08.02.2020
comment
Хотя это, безусловно, хороший ответ, OP явно попросил сделать это без объединения. - person nine stones; 08.02.2020
comment
@ninestones без объединения? все издатели определены там, ObservableObject является его частью ... import Combine - это только потому, что AnyCancelable - person user3441734; 08.02.2020
comment
Будьте уверены, я знаю, что ObservableObject использует Combine для достижения своих средств (тем не менее, технически ObservableObject определен в SwiftUI, который, в свою очередь, импортирует Combine). Я просто цитирую OP, который заявил, что знает, как это сделать, используя комбинацию напрямую, ища простой способ обновить свое состояние. И лично я - в зависимости от сложности дела - наверное, часто выбирал предложенный вами путь. - person nine stones; 08.02.2020
comment
observableObject является частью Combine, ObservedObject - частью SwiftUI. Это неважные детали (неправильные заголовки SDK), ваш ответ мне кажется нормальным. Я просто не понимаю, в чем заключается идея использовать оболочку @Published для ссылочного типа :-). Это действительно избыточно, и из-за этого возникает путаница, как это работает в фоновом режиме. - person user3441734; 08.02.2020

Позвоните издателю явно, и вы получите уведомление:

struct ContentView: View {   
  @ObservedObject var outerInstance = OuterClass()
  var body: some View {
    VStack(spacing: 10) {
      Button(action: {
        self.outerInstance.innerInstance.showText.toggle()
        // Call the publisher
        self.outerInstance.objectWillChange.send()
      }) {
        Text("BUTTON").bold().padding()
          .foregroundColor(.white)
          .background(Color.red)
      }
      if outerInstance.innerInstance.showText {
        Text("Hello, World!")
      }
    }
  }
}
person nine stones    schedule 08.02.2020
comment
Спасибо, @ девять-камни - это определенно работает! Хотя мне бы очень понравилось, если бы существовал способ Swifty для настройки самих классов (или их свойств), чтобы сообщения об изменениях распространялись без необходимости отправлять их вручную, когда это необходимо. - person Anton; 08.02.2020
comment
Согласовано. то же самое относится к изменению содержимого массива наблюдаемого объекта или опубликованного массива ivar. - person nine stones; 08.02.2020
comment
это могло быть контрпродуктивным. innerInstance является ссылкой и в вашей модели, скорее всего, должен быть постоянным. так зачем использовать для этого оболочку свойств @Published? Смотрите мой ответ ... - person user3441734; 08.02.2020
comment
@ user3441734 Я согласен с тем, что в моем втором примере, строго говоря, нет смысла помещать оболочку Published вокруг innerInstance, поскольку это ссылочный тип. Но я написал этот пример не для того, чтобы на самом деле работать (я знал, что это не так), а для демонстрации намерения, и я намеревался изменить свойства innerInstance, чтобы пометить изменение во externalInstance, поэтому я добавил это в духе псевдокода. Не обращайте внимания, если это вас беспокоит. :) - person Anton; 09.02.2020