Combine: Listen to internal collection changes - ios

I have a data manager that encapsulates a collection of objects. I want to listen to changes in that manager, as well as changes in collection objects. I came up with the solution using PassthroughSubject and sink, but I am pretty new to Combine and wondering is it correct and is there a better way to do that.
import Combine
class Item {
var data = false {
didSet {
self.subject.send()
}
}
let subject = PassthroughSubject<Void, Never>()
}
class DataManager {
private(set) var items = [Item]() {
didSet {
self.subject.send()
}
}
let subject = PassthroughSubject<Void, Never>()
func addItem(_ item: Item) {
self.items.append(item)
item.subject.sink { [weak self] in
self?.subject.send()
}
}
}
var item = Item()
var manager = DataManager()
manager.subject.sink {
print("Received Update")
}
manager.addItem(item) // Received Update
item.data = false // Received Update
item.data = true // Received Update

If you have control over the stored items, making them all structures should work. Arrays are structures, so will trigger the didSet when changed. Structures inside of arrays should change the value of the array and cause didSet to trigger for the array. Classes will not because the reference value of the class never changes. The current stance is that you should use structures over classes unless you have a good reason to use a class. Swift documentation for more info.
The other option is to do what you are already doing and make all of the classes conform to some protocol like BindableObject, then monitor didChange for each object.
Currently though you are not handling cancelation when an item is removed from the array. You should subscribe the didChange of DataManager to the didChange of every element. Then take the resultant AnyCancellable and add it to a dictionary keyed under the the item. Then once that item is removed from the array you should remove the associated AnyCancellable which will cancel the subscription.

For the newest version of SwiftUI, I will pass down the objectWillChange.send function to each item in the #Published array. Then, for each property of each item, I will call the update handler in the willSet property change handler.
Here's an example:
import Combine
final class User {
let prepareForUpdate: (() -> Void)?
var name: String {
willSet {
prepareForUpdate?()
}
}
init(prepareForUpdate: (() -> Void)? = nil, name: String) {
self.prepareForUpdate = prepareForUpdate
self.name = name
}
}
final class UserStore: ObservableObject {
#Published var users: [User]
init(_ users: [User] = []) {
self.users = users
}
func addUser(name: String) {
// Pass in our objectWillChange.send to the User object to listen for updates
let user = User(prepareForUpdate: objectWillChange.send, name: name)
users.append(user)
return user
}
}
Using this method, the view will be updated whenever a User in the users array is changed.

Related

How Model notifies its change to ViewModel in iOS MVVM pattern

Most articles about MVVM describe when the model is changed, for example when new data is made available and we need to update the UI, the Model notifies the View Model.
But I don’t get how Model communicate with View Model to notify about its change.
In the code below, I used property observer to bind View and ViewModel. And I know that I can change my Model by assigning new value like self.person.value.name = name in ViewModel.
I read a lot of articles about mvvm and think I wrote an appropriate example as follows, but even in this code I cannot get the concept that Model notifies its change to ViewModel. In my code below, does model notifies viewmodel about its change? Can you explain with examples?
class Observable<T> {
var value: T {
didSet {
self.listener?(value)
}
}
var listener: ((T) -> Void)?
init(_ value: T) {
self.value = value
}
func subscribe(listener: #escaping (T) -> Void) {
listener(value)
self.listener = listener
}
}
struct Person {
var name: String
var age: Int
}
struct MyViewModel {
var person: Observable<Person>
init(person: Person) {
self.person = Observable(person)
}
func changePersonName(with name: String) {
person.value.name = name
}
}
class ViewController: UIViewController {
#IBOutlet weak var infoLabel: UILabel!
let viewModel = MyViewModel(person: Person(name: “Mike“, age: 100))
override func viewDidLoad() {
viewModel.person.subscribe { [weak self] person in
self?.infoLabel.text = person.name + “& " + "\(person.age)"
}
}
}

RxSwift - Determining whether an Observable has been disposed

I'm trying to get Publisher which vends Observables to its clients Consumer, to determine when one of its consumers has disposed of its Observable.
Annoyingly. my code was working fine, until I removed an RxSwift .debug from within the Consumer code.
Is there some alternative way I might get this working?
private class Subscriber {
var ids: [Int]
// This property exists so I can watch whether the observable has
// gone nil (which I though would happen when its been disposed, but it
// seems to happen immediately)
weak var observable: Observable<[Updates]>?
}
class Publisher {
private let relay: BehaviorRelay<[Int: Updates]>
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.asObservable()
let subscriber = Subscriber(ids: ids, observable: observable)
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
let updates: [Updates] = ....
// I need to be able to determine at this point whether the subscriber's
// observable has been disposed of, so I can remove it from the list of
// subscribers. However `subscriber.observable` is always nil here.
// PS: I am happy for this to happen before the `repeatTimer` func fires
subscribers.remove(where: { subscriber in
return subscriber.observable == nil
})
relay.accept(updates)
}
}
class Client {
private var disposeBag: DisposeBag?
private let publisher = Publisher()
func startWatching() {
let disposeBag = DisposeBag()
self.disposeBag = disposeBag
publisher
// with the `.debug` below things work OK, without it the
///`Publisher.Subscriber.observable` immediately becomes nil
//.debug("Consumer")
.updatesStream(for: [1, 2, 3])
.subscribe(onNext: { values in
print(values)
})
.disposed(by: disposeBag)
}
func stopWatching() {
disposeBag = nil
}
}
I think this is a very bad idea, but it solves the requested problem... If I had to put this code in one of my projects, I would be very worried about race conditions...
struct Subscriber {
let ids: [Int]
var subscribeCount: Int = 0
let lock = NSRecursiveLock()
}
class Publisher {
private let relay = BehaviorRelay<[Int: Updates]>(value: [:])
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
var subscriber = Subscriber(ids: ids)
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.do(
onSubscribe: {
subscriber.lock.lock()
subscriber.subscribeCount += 1
subscriber.lock.unlock()
},
onDispose: {
subscriber.lock.lock()
subscriber.subscribeCount -= 1
subscriber.lock.unlock()
})
.asObservable()
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
subscribers.removeAll(where: { subscriber in
subscriber.subscribeCount == 0
})
}
}

UserDefaults implementation always has null in extension

Im trying so save persisting data to my Userdefault storage so I can use it inside my extension.
Question
How do I implement this so I can update my view(update value of toggle) when another target is run, in my case an extension. I created the same app group. For my userdefaults
App is structured like this first my UserDefaults implementation
extension UserDefaults {
static let group = UserDefaults(suiteName: "group.com.carlpalsson.superapp")
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.group?.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.group?.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}
}
class UserSettings : ObservableObject {
let test = FamilyActivitySelection()
#Published var discouragedAppsCategoryTokens : Set<ActivityCategoryToken> {
didSet {
UserDefaults.group?.save(discouragedAppsCategoryTokens, forKey:"DiscourageAppsCategoryTokens")
}
}
init() {
self.discouragedAppsCategoryTokens =
(UserDefaults.group?.getObject(forKey: "DiscourageAppsCategoryTokens")) ?? appcategorytokens
}
static var shared: UserSettings {
return _userSettings
}
}
In my extension
class MyDeviceActivityMonitor: DeviceActivityMonitor {
let store = ManagedSettingsStore()
let userSettings = UserSettings.shared
let korven = UserSettings()
override func intervalDidStart(for activity: DeviceActivityName) {
do{
//Im trying to my values here but it´s always null
var family = userSettings.DiscouragedAppsFamilyActivitySelection
var familys: FamilyActivitySelection? = UserDefaults.group?.getObject(forKey: "DiscouragedAppsFamilyActivitySelection")
var iii = korven.DiscouragedAppsFamilyActivitySelection
}
}
Inside my #main
#StateObject var userSettings = UserSettings.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
.environmentObject(store)
.environmentObject(userSettings)
}
}
And in my view
struct ContentView: View {
#EnvironmentObject var userSettings : UserSettings
VStack {
Button("Select Apps to Discourage") {
isDiscouragedPresented = true
}
.familyActivityPicker(isPresented: $isDiscouragedPresented, selection: $userSettings.DiscouragedAppsFamilyActivitySelection)
.onChange(of: userSettings.DiscouragedAppsFamilyActivitySelection) { newSelection in
UserSettings.shared.DiscouragedAppsFamilyActivitySelection = newSelection
MyModel.shared.startDiscourageApps()
// MySchedule.setSchedule()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyModel())
.environmentObject(UserSettings())
}
}
If I understand you correctly you want to update your UI in your extension when the user defaults value changes in your app. This certainly is possible, but requires some more code on your side.
In your UserSettings class you currently read the user defaults in the initializer and then leave it as is. To get your UI to update you also need to update your property in there when the actual user defaults change. To do this you need to observe the user defaults. The easy way would be using the Notification Center with the NSUserDefaultsDidChangeNotification. In your case this won’t work as this notification only is sent for changes made in the same process.
Changes from a different process can be observed using Key-Value-Observing (KVO) though. Unfortunately this seems to be impossible using the nice Swift KeyPath API. Instead you have to do that using the Objective-C version. To do this you would make your UserSettings class inherit NSObject and implement observeValue(forKeyPath:of:change:context:). In there you can read the new data from the user defaults. Then you can add your object as an observer on the user defaults using addObserver(_:forKeyPath:options:context:). Options can stay empty and context can be nil.

How do I make an Rx subject react to new elements added to the array?

I would like my subject to observe changes on myArray so every time a new element is appended to myArray the subscription fires.
var subject = PublishSubject<[String]>()
var myArray = ["One", "Two"]
subject.flatMap{Observable.from($0)}.subscribe(onNext: {
print($0)
}).disposed(by: disposeBag)
subject.onNext(myArray)
myArray.append("Three")
subject.onNext(myArray)
it works by adding myArray to onNext every time it changes, but is there a way for the subscription to trigger automatically when myArray gets a new element without using onNext? Perhaps by making myArray into an observable?
This is a great time to take advantage of property wrappers. Be careful that you don't use this in a struct though.
#propertyWrapper
class RxPublished<Element> {
private let subject: BehaviorSubject<Element>
init(wrapped: Element) {
subject = BehaviorSubject(value: wrapped)
}
deinit {
subject.onCompleted()
}
var wrappedValue: Element {
get { try! subject.value() }
set { subject.onNext(newValue) }
}
var projectedValue: Observable<Element> {
subject.asObservable()
}
}
class MyClass {
#RxPublished(wrapped: ["One", "Two"]) var myArray: [String]
}
func example() {
let foo = MyClass()
_ = foo.$myArray
.debug("foo.myArray")
.subscribe()
foo.myArray.append("Three")
}

Create an app lifetime object (not possible to be deallocated) in Swift

I want to create an object from a class. And I want it life time to be the app life time. I mean I want it not be deallocated since the app is running.
the class that I want to make an instance from:
extension Post {
#NSManaged var userId: Int
#NSManaged var id: Int
#NSManaged var title: String
#NSManaged var body: String
}
class Post: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
override func awakeFromInsert() {
super.awakeFromInsert()
title = ""
userId = 0
id = 0
body = ""
}
}
An object that keeps a strong pointer to itself cannot be deallocated:
class Permanent {
private var ref: Permanent?
init() {
ref = self
}
deinit {
// This will never be called
print("Permanent deinit")
}
}
func test() {
var a = [Permanent(), Permanent()]
print(a.count)
print("free the items")
a = []
print("end of test")
}
test()
Output:
2
free the items
end of test
If you comment out the ref = self line:
Output:
2
free the items
Permanent deinit
Permanent deinit
end of test

Resources