Update UI on Singleton property change without sharing dependency between Views - swift

I want to make my UI update itself on singleton's class property change without passing the singleton object between views as .environmentObject or any another.
I have NetworkManager class, which is singleton:
final class NetworkManager: ObservableObject {
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "NetworkManager")
#Published var isConnected = true
static let shared: NetworkManager = {
NetworkManager()
}()
private init() {
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
self.isConnected = path.status == .satisfied
}
}
monitor.start(queue: queue)
}
}
Whenever #Published var isConnected property of this class changes, I want to update the UI for example with this piece of code: SOME_VIEW.disabled(!NetworkManager.shared.isConnected)
Unfortunately, SOME_VIEW is disabled only when I exit and entry this view once again. I want it to update itself in real time. Is it possible without #EnvironmentObject and only having my NetworkManager class as Singleton?

The ObservableObject updates view only via ObservedObject/StateObject/EnvironmentObject (which are actually have same nature). That wrapper (one of them) actually observers changes in published properties and refreshes view.
So your case can be solved like
struct ParentView: View {
#StateObject private var networkNanager = NetworkManager.shared // << observer !!
var body: some View {
SOME_VIEW.disabled(!networkNanager.isConnected) // injects dependency !!
}
}

Related

Clean Swift architecture for SwiftUI application: Assign values from Interactor to AppState class

I am building a small application whose purpose is to fetch real time data from an api and display them in a SwiftUI view. I thought to base it on the clean Swift architecture and to store the values in an AppState class, while including the business logic in an interactor. The workflow is the following:
The view calls the interactor function to fetch and update the data > the interactor saves the data in the AppState class > The view shows the data from AppState.
The first issue I have encountered is how the interactor updates AppState.
So far I could solve the issue just by making AppState a singleton. So I wanted to ask you if there are better ways to do it and if the way I have structured my code is good practice. Here is an example:
My View
struct MyView: View {
var appState = AppState.shared
var interactor = Interactor()
var body: some View {
VStack{
Text(appState.temperature)
}
.onAppear {
interactor.updateData()
}
}
}
My AppState
public class AppState: ObservableObject {
static let shared = AppState()
#Published var temperature: Double = 0
}
My Interactor
public class Interactor {
var appState = AppState.shared
func updateData() {
// some code to fetch data
self.appState.temperature = data.temperature ?? 0
}
}

In SwiftUI / Combine, how can two ObservableObjects coordinate?

My app has two ObservableObjects:
#main
struct SomeApp: App {
#StateObject private var foo = Foo()
#StateObject private var bar = Bar()
var body: some Scene {
WindowGroup {
AppView()
.environmentObject(foo)
.environmentObject(bar)
}
}
}
class Foo: ObservableObject {
#Published var operationResult: Bool?
private let operationQueue = OperationQueue()
func start() {
operationQueue.addOperation(SomeOperation())
// SomeOperation will set operationResult
}
}
class Bar: ObservableObject {
#Published var resultsAvailable = false
func updateResults(operationResult: Bool) {
if operationResult && someOtherCondition {
resultsAvailable = true
}
}
}
At some point, Foo is doing some work on a background thread. When it completes, state in Bar needs to be updated, perhaps by calling updateResults(). In a traditional UIKit app, I might coordinate this by setting a callback handler or posting an NSNotification.
Instead of passing operationResult around via a notification or handler, is there a way for Bar to subscribe to changes to Foo's operationResult property? In examples I've found so far, it seems like the subscriptions may only be available inside View structs, but I haven't been able to confirm or deny that.

Singleton publisher with binding to multiple views

Overview
My app has the feature of favorit-ing objects. There are multiple views that require access to [Favorite] to render UI as well as adding and removing them.
I would like to have a single source of [Favorite] where:
all views render UI based on it
updating this source signals all views subscribed to it and rerender based on the updated value
on each update, the source is persisted in UserDefaults
updating favorites from UI also updates the Singleton's source, therefore signally other views to update
Attempt 1
I attempted to use #Binding to link the the source but it does not update UI when the source is changed.
class Singleton {
static let shared = Singleton()
var favorites = CurrentValueSubject<[Favorite], Never>(someFavorites)
}
class ViewModel: ObservableObject {
#Binding var favorites: [Favorite]
init() {
_favorites = Binding<[Favorite]>(get: { () -> [Favorite] in
Singleton.shared.favorites.value
}, set: { newValue in
Singleton.shared.favorites.send(newValue)
})
}
}
Attempt 2
I've also attempted creating the binding using Publishers and Subscribers but that ends up in an infinite loop.
Thanks in advance
Here is possible approach. Tested with Xcode 11.5b2.
class Singleton {
static let shared = Singleton()
// configure set initial value as needed, [] used for testing
var favorites = CurrentValueSubject<[Favorite], Never>([])
}
class ViewModel: ObservableObject {
#Published var favorites: [Favorite] = []
private var cancellables = Set<AnyCancellable>()
init() {
Singleton.shared.favorites
.receive(on: DispatchQueue.main)
.sink { [weak self] values in
self?.favorites = values
}
.store(in: &cancellables)
}
}

Changing a property of my ObservableObject instance doesn't update View. Code:

//in ContentView.swift:
struct ContentView: View {
#EnvironmentObject var app_state_instance : AppState //this is set to a default of false.
var body: some View {
if self.app_state_instance.complete == true {
Image("AppIcon")
}}
public class AppState : ObservableObject {
#Published var inProgress: Bool = false
#Published var complete : Bool = false
public static var app_state_instance: AppState? //I init this instance in SceneDelegate & reference it throughout the app via AppState.app_state_instance
...
}
in AnotherFile.swift:
AppState.app_state_instance?.complete = true
in SceneDelegate.swift:
app_state_instance = AppState( UserDefaults.standard.string(forKey: "username"), UserDefaults.standard.string(forKey: "password") )
AppState.app_state_instance = app_state_instance
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(app_state_instance!))
This ^ does not trigger an update of my view.Anyone know why?
Also, is it possible to make a static var # published?
Also, I'm kind somewhat new to class-based + structs-as-values paradigm, if you have any tips on improving this please do share.
I've considered using an 'inout' ContentView struct reference to update struct state from other files (as going through app_state_instance isn't working with this code).

Updating a #Published variable based on changes in an observed variable

I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}