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)
}
}
Related
I use Combine to track changes in the View Model and react to these changes in the View in the UIKit Application. The thing is that every time the change occurs sink is getting called one more time. I store subscriptions in the Set() and basically sink as called as many times as there are cancellables. If I remove and add items to the cart 5 times the sink would be called 10 times. Is this correct behavior or I'm doing something wrong?
My View Model Protocol:
protocol CartTrackable {
var addedToCart: Bool { get set }
var addedToCartPublisher: Published<Bool>.Publisher { get }
}
My View Model:
final class CartViewModel: ObservableObject, CartTrackable {
#Published var addedToCart = false
var addedToCartPublisher: Published<Bool>.Publisher { $addedToCart }
func addToCart() {
addedToCart = true
}
func removeFromCart() {
addedToCart = false
}
}
And here is the relevant code in my View:
private var cancellables = Set<AnyCancellable>()
func setObserver() {
viewModel?.addedToCartPublisher
.dropFirst()
.receive(on: RunLoop.main)
.sink { [weak self] cartStatus in
self?.addToCartButton.showAnimation(for: cartStatus)
}
.store(in: &cancellables)
}
Declaring cancellable as one object helps - .sink is always called only once, but I keep track of several things, and having separate cancellables for them is just a lot of repeated code. Would love to hear your opinions! Cheers!
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 !!
}
}
I have a Complex class which I pass around as an EnvironmentObject through my SwiftUI views. Complex contains several CurrentValueSubjects. I don't want to add the Published attribute to the publishers on class Complex, since Complex is used a lot around the views and that will force the views to reload on every published value.
Instead, I want a mechanism which can subscribe to specific publisher which Complex holds. That way, Views can choose on which publisher the view should re-render itself.
The code below works, but I was wondering if there was an easier solution, it feels like a lot of work just to listen to the updates CurrentValueSubject gives me:
import SwiftUI
import Combine
struct ContentView: View {
let complex = Complex()
var body: some View {
PublisherView(boolPublisher: .init(publisher: complex.boolPublisher))
.environmentObject(complex)
}
}
struct PublisherView: View {
#EnvironmentObject var complex: Complex
#ObservedObject var boolPublisher: BoolPublisher
var body: some View {
Text("\(String(describing: boolPublisher.publisher))")
}
}
class Complex: ObservableObject {
let boolPublisher: CurrentValueSubject<Bool, Never> = .init(true)
// A lot more...
init() {
startToggling()
}
func startToggling() {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [unowned self] in
let newValue = !boolPublisher.value
print("toggling to \(newValue)")
boolPublisher.send(newValue)
startToggling()
}
}
}
class BoolPublisher: ObservableObject {
private var cancellableBag: AnyCancellable? = nil
#Published var publisher: Bool
init(publisher: CurrentValueSubject<Bool, Never>) {
self.publisher = publisher.value
cancellableBag = publisher
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
self?.publisher = value
}
}
}
Let me give you a simplified example of my problem.
My application structure is,
View <- Viewmodel <- UserRepository <- UserStorageService
in the above view and view model are pretty self-explanatory. UserRespository is an ObservableObject, which works as a single data source for the view model.
UserStorageService is another ObervableObject which handles read/write data from CoreData.
With this approach I can easily change the method I fetch data in my application. as an example if I want to fetch data from an api I have to replace the StorageService with an APIService.
Let me show you my sample code,
UserStorageService.swift
class UserStorageService: NSObject, ObservableObject {
#Published var users: [User] = []
private let userController: NSFetchedResultsController<User>
private let dataController: DataController
init(controller: DataController) {
dataController = controller
let fetchRequest = User.fetchRequest()
userController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: dataController.container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
super.init()
userController.delegate = self
}
func add(data: User) throws {
// Add implementation
}
func fetch() throws {
try userController.performFetch()
users = userController.fetchedObjects ?? []
}
}
extension UserStorageService: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let users = userController.fetchedObjects else {
return
}
self.users = users
}
}
This class will fetch data from CoreData and keeps the users property updated when it changes.
On my UserRepository.swift class I have created a #StateObject instance of UserStorageService and also a #Published property users which will be used in my view model as a source of truth for users.
class UserRepository: RepositoryProtocol, ObservableObject {
#Published var users: [User] = []
#StateObject var userStorageService: UserStorageService
init(dataController: DataController) {
let storageService = UserStorageService(controller: dataController)
_userStorageService = StateObject(wrappedValue: storageService)
}
func fetch() {
try? userStorageService.fetch()
users = userStorageService.users
}
func add(data: User) throws {
try? userStorageService.add(data: data)
}
}
My question is, whenever the storage changes it updates the users in UserStorageService class. When that happens, how do I update the users property in my UserRepository class which will eventually update the view model and updates the UI.
I am newbie, any help will be very much appreciated.
You can use Singleton pattern. Create one in your main class and use users variable to update it inside another class
static var singleton: UserStorageService = UserStorageService()
After declaration you can use it in UserRepository
#Published var users: [User] = UserStorageService.singleton.users
Also , this can be achieved with Combine framework, which implements Observer Design Pattern, where you have one Publisher and Subscribers.
Instantiate a published UserStorageService in your UserRepository then subscribe to it in the init() to pass any changes along to your users array:
#Published var userStorageService = UserStorageService()
var cancellables = Set<AnyCancellable>()
init() {
userStorageService.$users
.assign(to: \.users, on: self)
.store(in: &cancellables)
}
This requires you to import the Combine framework.
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")
}
}
}
}