ObservedObject inside ObservableObject not refreshing View - ios

I'm trying to display an activity indicator when performing an async request.
What I did is creating an ActivityTracker object that will track life cycle of a publisher.
This ActivityTracker is an ObservableObject and will be stored in the view model which also is an ObservableObject.
It seems that this kind of setup isn't refreshing the View. Here's my code:
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack(spacing: 16) {
Text("Counter: \(viewModel.tracker.count)\nPerforming: \(viewModel.tracker.isPerformingActivity ? "true" : "false")")
Button(action: {
_ = request().trackActivity(self.viewModel.tracker).sink { }
}) {
Text("Request")
}
}
}
}
class ContentViewModel: ObservableObject {
#Published var tracker = Publishers.ActivityTracker()
}
private func request() -> AnyPublisher<Void, Never> {
return Just(()).delay(for: 2.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
extension Publishers {
final class ActivityTracker: ObservableObject {
// MARK: Properties
#Published var count: Int = 0
var isPerformingActivity: Bool {
return count > 0
}
private var cancellables: [AnyCancellable] = []
private let counterSubject = CurrentValueSubject<Int, Never>(0)
private let lock: NSRecursiveLock = .init()
init() {
counterSubject.removeDuplicates()
.receive(on: RunLoop.main)
.print()
.sink { [weak self] counter in
self?.count = counter
}
.store(in: &cancellables)
}
// MARK: Private methods
fileprivate func trackActivity<Value, Error: Swift.Error>(
ofPublisher publisher: AnyPublisher<Value, Error>
) {
publisher
.receive(on: RunLoop.main)
.handleEvents(
receiveSubscription: { _ in self.increment() },
receiveOutput: nil,
receiveCompletion: { _ in self.decrement() },
receiveCancel: { self.decrement() },
receiveRequest: nil
)
.print()
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
.store(in: &cancellables)
}
private func increment() {
lock.lock()
defer { lock.unlock() }
counterSubject.value += 1
}
private func decrement() {
lock.lock()
defer { lock.unlock() }
counterSubject.value -= 1
}
}
}
extension AnyPublisher {
func trackActivity(_ activityTracker: Publishers.ActivityTracker) -> AnyPublisher {
activityTracker.trackActivity(ofPublisher: self)
return self
}
}
I also tried to declare my ActivityTracker as #Published but same result, my text is not updated.
Note that storing the activity tracker directly in the view will work but this is not what I'm looking for.
Did I miss something here ?

Nested ObservableObjects is not supported yet.
When you want to use these nested objects, you need to notify the objects by yourself when data got changed.
I hope the following code can help you with your problem.
First of all use: import Combine
Then declare your model and submodels, they all need to use the #ObservableObject property to work. (Do not forget the #Published property aswel)
I made a parent model named Model and two submodels Submodel1 & Submodel2. When you use the parent model when changing data e.x: model.submodel1.count, you need to use a notifier in order to let the View update itself.
The AnyCancellables notifies the parent model itself, in that case the View will be updated automatically.
Copy the code and use it by yourself, then try to remake your code while using this. Hope this helps, goodluck!
class Submodel1: ObservableObject {
#Published var count = 0
}
class Submodel2: ObservableObject {
#Published var count = 0
}
class Model: ObservableObject {
#Published var submodel1 = Submodel1()
#Published var submodel2 = Submodel2()
var anyCancellable: AnyCancellable? = nil
var anyCancellable2: AnyCancellable? = nil
init() {
anyCancellable = submodel1.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
anyCancellable2 = submodel2.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
When you want to use this Model, just use it like normal usage of the ObservedObjects.
struct Example: View {
#ObservedObject var obj: Model
var body: some View {
Button(action: {
self.obj.submodel1.count = 123
// If you've build a complex layout and it still won't work, you can always notify the modal by the following line of code:
// self.obj.objectWillChange.send()
}) {
Text("Change me")
}
}

If you have a collection of stuff you can do this:
import Foundation
import Combine
class Submodel1: ObservableObject {
#Published var count = 0
}
class Submodel2: ObservableObject {
var anyCancellable: [AnyCancellable] = []
#Published var submodels: [Submodel1] = []
init() {
submodels.forEach({ submodel in
anyCancellable.append(submodel.objectWillChange.sink{ [weak self] (_) in
self?.objectWillChange.send()
})
})
}
}

Related

Reset TextField value using Combine and Swiftui

I try to reset a TextField value when a certain condition is met (.count == 4), but it does not work, what am I missing?
class ViewModel: ObservableObject {
#Published var code = ""
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = $code.sink { (newVal) in
if newVal.count == 4 {
self.code = ""
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField("My code", text: $viewModel.code)
}
}
This is a case where you don't need any Combine. Just use the normal didSet to observe changes to the property:
class ViewModel: ObservableObject {
#Published var code = "" {
didSet {
if code.count == 4 {
self.code = ""
}
}
}
}
Adding .receive(on: DispatchQueue.main) seems to fix this issue, however, I am not entirly sure why it is needed.
On a side note, make sure you capture [weak self] in a sink block to avoid memory leaks:
anyCancellable = $code
.receive(on: DispatchQueue.main) // <--
.sink { [weak self] newVal in
if newVal.count == 4 {
self?.code = ""
}

Binding a SwiftUI Button to AnySubscriber like RxCocoa's button tap

I use the following UIViewController and RxSwift/RxCocoa based piece of code to write a very simply MVVM pattern to bind a UIButton tap event to trigger some Observable work and listen for the result:
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var someButton: UIButton!
var viewModel: ViewModel!
private var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel()
setupBindings()
}
private func setupBindings() {
someButton.rx.tap
.bind(to: self.viewModel.input.trigger)
.disposed(by: disposeBag)
viewModel.output.result
.subscribe(onNext: { element in
print("element is \(element)")
}).disposed(by: disposeBag)
}
}
class ViewModel {
struct Input {
let trigger: AnyObserver<Void>
}
struct Output {
let result: Observable<String>
}
let input: Input
let output: Output
private let triggerSubject = PublishSubject<Void>()
init() {
self.input = Input(trigger: triggerSubject.asObserver())
let resultObservable = triggerSubject.flatMap { Observable.just("TEST") }
self.output = Output(result: resultObservable)
}
}
It compiles and runs well. However, I need to Combinify this pattern with SwiftUI, so I converted that code into the following:
import SwiftUI
import Combine
struct ContentView: View {
var viewModel: ViewModel
var subscriptions = Set<AnyCancellable>()
init(viewModel: ViewModel) {
self.viewModel = viewModel
setupBindings()
}
var body: some View {
Button(action: {
// <---- how to trigger viewModel's trigger from here
}, label: {
Text("Click Me")
})
}
private func setupBindings() {
self.viewModel.output.result.sink(receiveValue: { value in
print("value is \(value)")
})
.store(in: &subscriptions) // <--- doesn't compile due to immutability of ContentView
}
}
class ViewModel {
struct Input {
let trigger: AnySubscriber<Void, Never>
}
struct Output {
let result: AnyPublisher<String, Never>
}
let input: Input
let output: Output
private let triggerSubject = PassthroughSubject<Void, Never>()
init() {
self.input = Input(trigger: AnySubscriber(triggerSubject))
let resultPublisher = triggerSubject
.flatMap { Just("TEST") }
.eraseToAnyPublisher()
self.output = Output(result: resultPublisher)
}
}
This sample doesn't compile due to two errors (commented in code):
(1) Problem 1: How to trigger the publisher's work from the button's action closure like the case of RxSwift above ?
(2) Problem 2 is related somehow to architectural design rather than a compile error:
the error says: ... Cannot pass immutable value as inout argument: 'self' is immutable ..., that's because SwiftUI views are structs, they are designed to be changed only through sorts of bindings (#State, #ObservedObject, etc ...), I have two sub-questions related to problem 2:
[A]: is it considered a bad practice to sink a publisher in a SwiftUI View ? which may need some workaround to store the cancellable at the View's struct scope ?
[B]: which one is better for SwiftUI/Combine projects in terms of MVVM architectural pattern: using a ViewModel with [ Input[Subscribers], Output[AnyPublishers] ] pattern, or a
ObservableObject ViewModel with [ #Published properties] ?
I had same problem understanding best mvvm approach.
Recommend also look into this thread Best data-binding practice in Combine + SwiftUI?
Will post my working example. Should be easy to convert to what you want.
SwiftUI View:
struct ContentView: View {
#State private var dataPublisher: String = "ggg"
#State private var sliderValue: String = "0"
#State private var buttonOutput: String = "Empty"
let viewModel: SwiftUIViewModel
let output: SwiftUIViewModel.Output
init(viewModel: SwiftUIViewModel) {
self.viewModel = viewModel
self.output = viewModel.bind(())
}
var body: some View {
VStack {
Text(self.dataPublisher)
Text(self.sliderValue)
Slider(value: viewModel.$sliderBinding, in: 0...100, step: 1)
Button(action: {
self.viewModel.buttonBinding = ()
}, label: {
Text("Click Me")
})
Text(self.buttonOutput)
}
.onReceive(output.dataPublisher) { value in
self.dataPublisher = value
}
.onReceive(output.slider) { (value) in
self.sliderValue = "\(value)"
}
.onReceive(output.resultPublisher) { (value) in
self.buttonOutput = value
}
}
}
AbstractViewModel:
protocol ViewModelProtocol {
associatedtype Output
associatedtype Input
func bind(_ input: Input) -> Output
}
ViewModel:
final class SwiftUIViewModel: ViewModelProtocol {
struct Output {
let dataPublisher: AnyPublisher<String, Never>
let slider: AnyPublisher<Double, Never>
let resultPublisher: AnyPublisher<String, Never>
}
typealias Input = Void
#SubjectBinding var sliderBinding: Double = 0.0
#SubjectBinding var buttonBinding: Void = ()
func bind(_ input: Void) -> Output {
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.delay(for: 5.0, scheduler: DispatchQueue.main)
.map{ "Just for testing - \($0)"}
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.share()
.eraseToAnyPublisher()
let resultPublisher = _buttonBinding.anyPublisher()
.dropFirst()
.flatMap { Just("TEST") }
.share()
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher,
slider: _sliderBinding.anyPublisher(),
resultPublisher: resultPublisher)
}
}
SubjectBinding property wrapper:
#propertyWrapper
struct SubjectBinding<Value> {
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue: Value) {
subject = CurrentValueSubject<Value, Never>(wrappedValue)
}
func anyPublisher() -> AnyPublisher<Value, Never> {
return subject.eraseToAnyPublisher()
}
var wrappedValue: Value {
get {
return subject.value
}
set {
subject.value = newValue
}
}
var projectedValue: Binding<Value> {
return Binding<Value>(get: { () -> Value in
return self.subject.value
}) { (value) in
self.subject.value = value
}
}
}
So I recently was also wondering how I would do this since we are not starting to write out views in SwiftUI.
I made a helper object the encapsulates the transition from a function call to a Publisher. I called it a Relay.
#available(iOS 13.0, *)
struct Relay<Element> {
var call: (Element) -> Void { didCall.send }
var publisher: AnyPublisher<Element, Never> {
didCall.eraseToAnyPublisher()
}
// MARK: Private
private let didCall = PassthroughSubject<Element, Never>()
}
In your case specifically, you would be able to declare a private Relay and use it like so;
Button(action: relay.call,
label: {
Text("Click Me")
})
And then you can do whatever you like with.
relay.publisher

#Published property in singleton not emitting events after the first one

I have a #Published property in a singleton class which is observed in a view model class and it only received the first initial value and not after that. Why?
View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel(singletonService: SingletonService.shared)
var body: some View {
NavigationView {
Form {
Text("Hello World")
}.navigationBarItems(trailing: Button("Increase Number", action: {
SingletonService.shared.increaseNumber()
}))
}
}
}
ViewModel
import Combine
class ContentViewModel: ObservableObject {
init(
singletonService: SingletonService
) {
let cencellable = singletonService.$number.sink(receiveValue: { print($0) })
// Never called after the first time
}
}
Singleton
import Combine
class SingletonService {
static let shared = SingletonService()
#Published var number = 0
func increaseNumber() {
number += 1
}
}
The subscription lives until subscriber is alive, but in this your case
let cencellable = singletonService.$number.sink(receiveValue: { print($0)
subscriber is destroyed right out of context. It should be like
private var cancellables = Set<AnyCancellable>()
init(singletonService: SingletonService) {
singletonService.$number
.sink(receiveValue: { print($0) })
.store(in: &cancellables)
}

How to tell SwiftUI views to bind to nested ObservableObjects

I have a SwiftUI view that takes in an EnvironmentObject called appModel. It then reads the value appModel.submodel.count in its body method. I expect this to bind my view to the property count on submodel so that it re-renders when the property updates, but this does not seem to happen.
Is this a bug? And if not, what is the idiomatic way to have views bind to nested properties of environment objects in SwiftUI?
Specifically, my model looks like this...
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: Submodel = Submodel()
}
And my view looks like this...
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
Text("Count: \(appModel.submodel.count)")
.onTapGesture {
self.appModel.submodel.count += 1
}
}
}
When I run the app and click on the label, the count property does increase but the label does not update.
I can fix this by passing in appModel.submodel as a property to ContentView, but I'd like to avoid doing so if possible.
Nested models does not work yet in SwiftUI, but you could do something like this
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()
}
}
}
Basically your AppModel catches the event from SubModel and send it further to the View.
Edit:
If you do not need SubModel to be class, then you could try something like this either:
struct SubModel{
var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: SubModel = SubModel()
}
Sorin Lica's solution can solve the problem but this will result in code smell when dealing with complicated views.
What seems to better advice is to look closely at your views, and revise them to make more, and more targeted views. Structure your views so that each view displays a single level of the object structure, matching views to the classes that conform to ObservableObject. In the case above, you could make a view for displaying Submodel (or even several views) that display's the property from it that you want show. Pass the property element to that view, and let it track the publisher chain for you.
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
}
}
}
This pattern implies making more, smaller, and focused views, and lets the engine inside SwiftUI do the relevant tracking. Then you don't have to deal with the book keeping, and your views potentially get quite a bit simpler as well.
You can check for more detail in this post: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
I wrote about this recently on my blog: Nested Observable Objects. The gist of the solution, if you really want a hierarchy of ObservableObjects, is to create your own top-level Combine Subject to conform to the ObservableObject protocol, and then encapsulate any logic of what you want to trigger updates into imperative code that updates that subject.
For example, if you had two "nested" classes, such as
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
}
}
Then you could expand the top-level class (MainThing in this case) to:
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()
}
)
}
}
Which grabs a publisher from the embedded ObservableObject, and sends an update into the local published when the property value on SomeElement class is modified. You can extend this to use CombineLatest for publishing streams from multiple properties, or any number of variations on the theme.
This isn't a "just do it" solution though, because the logical conclusion of this pattern is after you've grown that hierarchy of views, you're going to end up with potentially huge swatches of a View subscribed to that publisher that will invalidate and redraw, potentially causing excessive, sweeping redraws and relatively poor performance on updates. I would advise seeing if you can refactor your views to be specific to a class, and match it to just that class, to keep the "blast radius" of SwiftUI's view invalidation minimized.
#Published is not designed for reference types so it's a programming error to add it on the AppModel property, even though the compiler or runtime doesn't complain. What would've been intuitive is adding #ObservedObject like below but sadly this silently does nothing:
class AppModel: ObservableObject {
#ObservedObject var submodel: SubModel = SubModel()
}
I'm not sure if disallowing nested ObservableObjects was intentional by SwiftUI or a gap to be filled in the future. Wiring up the parent and child objects as suggested in the other answers is very messy and hard to maintain. What seems to be the idea of SwiftUI is to split up the views into smaller ones and pass the child object to the subview:
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()
}
The submodel mutations actually propagate when passing into a subview!
However, there's nothing stopping another dev from calling appModel.submodel.count from the parent view which is annoying there's no compiler warning or even some Swift way to enforce not doing this.
Source: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
If you need to nest observable objects here is the best way to do it that I could find.
class ChildModel: ObservableObject {
#Published
var count = 0
}
class ParentModel: ObservableObject {
#Published
private var childWillChange: Void = ()
let child = ChildModel()
init() {
child.objectWillChange.assign(to: &$childWillChange)
}
}
Instead of subscribing to child's objectWillChange publisher and firing parent's publisher, you assign values to published property and parent's objectWillChange triggers automatically.
All three ViewModels can communicate and update
// 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
}
Thank you Sorin for Combine solution.
I have a solution that I believe is more ellegant than subscribing to the child (view)models. It's weird and I don't have an explanation for why it works.
Solution
Define a base class that inherits from ObservableObject, and defines a method notifyWillChange() that simply calls objectWillChange.send(). Any derived class then overrides notifyWillChange() and calls the parent's notifyWillChange() method.
Wrapping objectWillChange.send() in a method is required, otherwise the changes to #Published properties do not cause the any Views to update. It may have something to do with how #Published changes are detected. I believe SwiftUI/Combine use reflection under the hood...
I have made some slight additions to OP's code:
count is wrapped in a method call which calls notifyWillChange() before the counter is incremented. This is required for the propagation of the changes.
AppModel contains one more #Published property, title, which is used for the navigation bar's title. This showcases that #Published works for both the parent object and the child (in the example below, updated 2 seconds after the model is initialized).
Code
Base Model
class BaseViewModel: ObservableObject {
func notifyWillUpdate() {
objectWillChange.send()
}
}
Models
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()
}
}
The View
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
NavigationView {
Text("Count: \(appModel.submodel.count)")
.onTapGesture {
self.appModel.increment()
}.navigationBarTitle(appModel.title)
}
}
}
I liked solution by sorin-lica. Based upon that I've decided to implement a custom Property Wrapper (following this amazing article) named NestedObservableObject to make that solution more developer friendly.
This allow to write your model in the following way
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#NestedObservableObject var submodel: Submodel = Submodel()
}
Property Wrapper implementation
#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
}
}
I've published code on gist
I do it like this:
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()
}
}
}
So on the call side:
class TrackViewModel {
private let playbackViewModel: PlaybackViewModel
private var propagation: Any?
init(playbackViewModel: PlaybackViewModel) {
self.playbackViewModel = playbackViewModel
propagation = playbackViewModel.propagateWeakly(to: self)
}
...
}
Here's a gist.
See following post for a solution: [arthurhammer.de/2020/03/combine-optional-flatmap][1] . This is solving the question in a Combine-Way with the $ publisher.
Assume class Foto has an annotation struct and and annotation publisher, which publish an annotation struct. Within Foto.sample(orientation: .Portrait) the annotation struct gets "loaded" through the annotation publisher asynchroniously. Plain vanilla combine.... but to get that into a View & ViewModel, use this:
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)
}
}
Note: [1]: https://arthurhammer.de/2020/03/combine-optional-flatmap/
Pay attention the $annotation above within the flatMap, it's a publisher!
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)
}
You can create a var in your top view that is equal to a function or published var in your top class. Then pass it and bind it to every sub view. If it changes in any sub view then the top view will be updated.
Code Structure:
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 { }
}
Views:
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()
}
}
Just noting that I'm using the NestedObservableObject approach from #bsorrentino in my latest app.
Normally I'd avoid this but the nested object in question is actually a CoreData model so breaking things out into smaller views doesn't really work in this regard.
This solution seemed best since the world treats NSManagedObjects as (mostly) ObservableObjects and I really, really need to trigger an update if the CodeData object model is changed down the line.
The var submodel in AppModel doesn't need the property wrapper #Published.
The purpose of #Published is to emit new values and objectWillChange.
But the variable is never changed but only initiated once.
Changes in submodel are propagated to the view by the subscriber anyCancellable and ObservableObject-protocol via the sink-objectWillChange construction and causes a View to redraw.
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()
}
}
}
Nested ObservableObject models do not work yet.
However, you can make it work by manually subscribing each model. The answer gave a simple example of this.
I wanted to add that you can make this manual process a bit more streamlined & readable via extensions:
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)
}
}
Here is the extension:
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()
}
}
}
It looks like bug. When I update the xcode to the latest version, it work correctly when binding to nested ObservableObjects

Best data-binding practice in Combine + SwiftUI?

In RxSwift it's pretty easy to bind a Driver or an Observable in a View Model to some observer in a ViewController (i.e. a UILabel).
I usually prefer to build a pipeline, with observables created from other observables, instead of "imperatively" pushing values, say via a PublishSubject).
Let's use this example: update a UILabel after fetching some data from the network
RxSwift + RxCocoa example
final class RxViewModel {
private var dataObservable: Observable<Data>
let stringDriver: Driver<String>
init() {
let request = URLRequest(url: URL(string:"https://www.google.com")!)
self.dataObservable = URLSession.shared
.rx.data(request: request).asObservable()
self.stringDriver = dataObservable
.asDriver(onErrorJustReturn: Data())
.map { _ in return "Network data received!" }
}
}
final class RxViewController: UIViewController {
private let disposeBag = DisposeBag()
let rxViewModel = RxViewModel()
#IBOutlet weak var rxLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
rxViewModel.stringDriver.drive(rxLabel.rx.text).disposed(by: disposeBag)
}
}
Combine + UIKit example
In a UIKit-based project it seems like you can keep the same pattern:
view model exposes publishers
view controller binds its UI elements to those publishers
final class CombineViewModel: ObservableObject {
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
var stringPublisher: AnyPublisher<String, Never>
init() {
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
self.stringPublisher = dataPublisher
.map { (_, _) in return "Network data received!" }
.replaceError(with: "Oh no, error!")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
final class CombineViewController: UIViewController {
private var cancellableBag = Set<AnyCancellable>()
let combineViewModel = CombineViewModel()
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
combineViewModel.stringPublisher
.flatMap { Just($0) }
.assign(to: \.text, on: self.label)
.store(in: &cancellableBag)
}
}
What about SwiftUI?
SwiftUI relies on property wrappers like #Published and protocols like ObservableObject, ObservedObject to automagically take care of bindings (As of Xcode 11b7).
Since (AFAIK) property wrappers cannot be "created on the fly", there's no way you can re-create the example above using to the same pattern.
The following does not compile
final class WrongViewModel: ObservableObject {
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
#Published var stringValue: String
init() {
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
self.stringValue = dataPublisher.map { ... }. ??? <--- WRONG!
}
}
The closest I could come up with is subscribing in your view model (UGH!) and imperatively update your property, which does not feel right and reactive at all.
final class SwiftUIViewModel: ObservableObject {
private var cancellableBag = Set<AnyCancellable>()
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
#Published var stringValue: String = ""
init() {
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
dataPublisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in }) { (_, _) in
self.stringValue = "Network data received!"
}.store(in: &cancellableBag)
}
}
struct ContentView: View {
#ObservedObject var viewModel = SwiftUIViewModel()
var body: some View {
Text(viewModel.stringValue)
}
}
Is the "old way of doing bindings" to be forgotten and replaced, in this new UIViewController-less world?
An elegant way I found is to replace the error on the publisher with Never and to then use assign (assign only works if Failure == Never).
In your case...
dataPublisher
.receive(on: DispatchQueue.main)
.map { _ in "Data received" } //for the sake of the demo
.replaceError(with: "An error occurred") //this sets Failure to Never
.assign(to: \.stringValue, on: self)
.store(in: &cancellableBag)
I think the missing piece here is that you are forgetting that your SwiftUI code is functional. In the MVVM paradigm, we split the functional part into the view model and keep the side effects in the view controller. With SwiftUI, the side effects are pushed even higher into the UI engine itself.
I haven't messed much with SwiftUI yet so I can't say I understand all the ramifications yet, but unlike UIKit, SwiftUI code doesn't directly manipulate screen objects, instead it creates a structure that will do the manipulation when passed to the UI engine.
After posting previous answer read this article: https://nalexn.github.io/swiftui-observableobject/
and decide to do same way. Use #State and don't use #Published
General ViewModel protocol:
protocol ViewModelProtocol {
associatedtype Output
associatedtype Input
func bind(_ input: Input) -> Output
}
ViewModel class:
final class SwiftUIViewModel: ViewModelProtocol {
struct Output {
var dataPublisher: AnyPublisher<String, Never>
}
typealias Input = Void
func bind(_ input: Void) -> Output {
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.map{ "Just for testing - \($0)"}
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher)
}
}
SwiftUI View:
struct ContentView: View {
#State private var dataPublisher: String = "ggg"
let viewModel: SwiftUIViewModel
let output: SwiftUIViewModel.Output
init(viewModel: SwiftUIViewModel) {
self.viewModel = viewModel
self.output = viewModel.bind(())
}
var body: some View {
VStack {
Text(self.dataPublisher)
}
.onReceive(output.dataPublisher) { value in
self.dataPublisher = value
}
}
}
I ended up with some compromise. Using #Published in viewModel but subscribing in SwiftUI View.
Something like this:
final class SwiftUIViewModel: ObservableObject {
struct Output {
var dataPublisher: AnyPublisher<String, Never>
}
#Published var dataPublisher : String = "ggg"
func bind() -> Output {
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.map{ "Just for testing - \($0)"}
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher)
}
}
and SwiftUI:
struct ContentView: View {
private var cancellableBag = Set<AnyCancellable>()
#ObservedObject var viewModel: SwiftUIViewModel
init(viewModel: SwiftUIViewModel) {
self.viewModel = viewModel
let bindStruct = viewModel.bind()
bindStruct.dataPublisher
.assign(to: \.dataPublisher, on: viewModel)
.store(in: &cancellableBag)
}
var body: some View {
VStack {
Text(self.viewModel.dataPublisher)
}
}
}
You can also extend CurrentValueSubject to expose a Binding as demonstrated in this Gist. Namely thus:
extension CurrentValueSubject {
var binding: Binding<Output> {
Binding(get: {
self.value
}, set: {
self.send($0)
})
}
}

Resources