简体   繁体   中英

ObservedObject inside ObservableObject not refreshing View

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM