Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - propagating change notifications through nested reference types

I'd like to extend ObservableObject behavior in SwiftUI to nested classes, and I'm looking for the proper way to do it. It can be done "manually" with Combine, but I imagine there's a much cleaner way to do it using SwiftUI, and I'm hoping you can point me in the right direction. Here's what I mean…

Below is a typical application of ObservableObject to make a View dynamically respond to changes to a reference type. Tapping the button toggles the showText value, which makes the text appear/disappear on the screen:

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!")
            }
        }
    }
}

This works fine.

But what about the modification below, where the class containing showText is an InnerClass, itself contained in an OuterClass? The button toggles showText just fine, but the notification of the value change no longer propagates through the OuterClass instance to the View, so the View no longer displays the Text at all.

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!")
            }
        }
    }
}

What is the elegant fix for this?

like image 548
Anton Avatar asked Feb 07 '20 18:02

Anton


2 Answers

Call the publisher explicitly and you'll get notified:

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!")
      }
    }
  }
}
like image 198
nine stones Avatar answered Nov 03 '22 00:11

nine stones


it could be done in your model

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)
    }
}

and how to use in in your example

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()
    }
}

if you like to observe any change in your inner object, just do it!

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)
    }
}

UPDATE: based on the discussion below

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)
    }
}
like image 33
user3441734 Avatar answered Nov 03 '22 00:11

user3441734