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 love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With