Observing multiple published variable changes inside ObservableObject - ios

I have an ObservableObject that contains multiple published variables to handle my app state.
Whenever one of those published variables change, I want to call a function inside my ObservableObject. What's the best way to do that?
class AppModelController: ObservableObject {
#Published var a: String = "R"
#Published var b: CGFloat = 0.0
#Published var c: CGFloat = 0.9
// Call this function whenever a, b or c change
func check() -> Bool {
}
}

You can use didSet, like this code:
class AppModelController: ObservableObject {
#Published var a: String = "R" { didSet(oldValue) { if (a != oldValue) { check() } } }
#Published var b: CGFloat = 0.0 { didSet(oldValue) { if (b != oldValue) { check() } } }
#Published var c: CGFloat = 0.9 { didSet(oldValue) { if (c != oldValue) { check() } } }
func check() {
// Some Work!
}
}

The simplest thing you can do is listen to objectWillChange. The catch is that it gets called before the object updates. You can use .receive(on: RunLoop.main) to get the updates on the next loop, which will reflect the changed values:
import Combine
class AppModelController: ObservableObject {
#Published var a: String = "R"
#Published var b: CGFloat = 0.0
#Published var c: CGFloat = 0.9
private var cancellable : AnyCancellable?
init() {
cancellable = self.objectWillChange
.receive(on: RunLoop.main)
.sink { newValue in
let _ = self.check()
}
}
// Call this function whenever a, b or c change
func check() -> Bool {
return true
}
}

Related

Why lazy property and didSet will end up with recursion call?

The below code works just fine
class A {
var s: MyStruct! {
didSet {
print("didSet")
print(n)
}
}
lazy var n: Int = s.x + 1
func viewDidLoad() {
s = MyStruct()
}
}
struct MyStruct {
var x = 1
init() { print("MyStruct init") }
}
let a = A()
a.viewDidLoad()
with output :
MyStruct init
didSet
2
However, if we have lazy properties as follow
class A {
var s: MyStruct! {
didSet {
print("didSet")
print(n)
}
}
lazy var n: Int = s.x + 1
func viewDidLoad() {
s = MyStruct()
}
}
struct MyStruct {
lazy var x = 1
init() { print("MyStruct init") }
}
let a = A()
a.viewDidLoad()
It will end up with endless recursion call
MyStruct init
didSet
didSet
didSet
...
Why lazy property and didSet will end up with recursion call?
You are declaring a lazy stored property. When the struct is initialized with MyStruct(), there's no value stored in MyStruct.x.
It will only be populated when it is accessed first time. When a property is changed, a value type like struct MyStruct is considered to be changed as well - so it's didSet is invoked (again) upon first access of x.
Here's how it becomes infinite loop.
viewDidLoad() > A.s.setter > A.s.didset [Expected]
First access of A.n.getter for the print(n) part.
s.x is lazy stored and upon first value population (update), it triggers - A.s.modify > A.s.didset & we land again at A.n.getter.
It loops indefinitely between 2 & 3 after this.
See screenshot -

Swift - toggle model to readonly momentarily

I have a phone number model which looks like this:
import UIKit
import Foundation
struct PhoneValidation : OptionSet {
let rawValue: Int
static let phoneInValid = PhoneValidation(rawValue: 1 << 0)
static let phoneValid = PhoneValidation(rawValue: 1 << 1)
static let smsValidationAttempted = PhoneValidation(rawValue: 1 << 2)
static let smsValidationFailed = PhoneValidation(rawValue: 1 << 3)
static let smsValidationSuccessful = PhoneValidation(rawValue: 1 << 4) // OTP is successfully validated in backend. The field should be non-editable in this duration
static let smsValidationOTPTriggered = PhoneValidation(rawValue: 1 << 5) // OTP validation triggered. The field should be non-editable in this duration
}
class PhonesViewModel: NSCopying {
public var phoneType: PhoneNumberType = PhoneNumberType.mobile
public var phone: String?
public var code: String?
public var countryCode: String?
public var isValid : PhoneValidation?
func copy(with zone: NSZone? = nil) -> Any {
let copy = PhonesViewModel()
copy.phoneType = phoneType
copy.phone = phone
copy.code = code
copy.countryCode = countryCode
copy.isValid = isValid
return copy
}
}
As you can see above the phone model can transition between different states. The SMS validation is available for few countries and for few it is not applicable. So, I plan on setting smsValidationOTPTriggered state when SMS validation is applicable for a country and while the validation is in progress.
What I need here is, while the states smsValidationOTPTriggered or smsValidationSuccessful are set I would not want any module of the application to modify the values(phoneType, phone, code, countryCode) of the model. In other words, I would like the model to switch to a read-only mode while these 2 states are set in model and would like the module to be informed with an error or exception when a modification is attempted.
Is there a best practice already available for what I am trying to achieve here? I have searched before raising this question but did not find any. How can I achieve this?
Thanks,
Raj Pawan Gumdal
How about something like this, I think its better to use property wrappers for your case! The below is not an exact solution but can modify/change to accommodate your need
import UIKit
enum PhoneNumberType {
case mobile
}
enum PhoneValidation {
case phoneInValid
case phoneValid
case smsValidationAttempted
case smsValidationFailed
case smsValidationSuccessful
case smsValidationOTPTriggered
}
struct PhonesViewModel {
public var phoneType: PhoneNumberType = PhoneNumberType.mobile
public var phone: String?
public var code: String?
public var countryCode: String?
public var phoneValidation : PhoneValidation?
func validate(value: [PhoneValidation]) -> Bool {
//add proper check here
return false
}
}
#propertyWrapper
struct Wrapper {
private(set) var value: PhonesViewModel? = nil
var validators: [PhoneValidation] = []
var wrappedValue: PhonesViewModel? {
get { value }
set {
if let model = newValue, model.validate(value: validators) {
value = newValue
print("Value assigned")
} else {
print("Value not assigned")
}
}
}
}
struct SomeOtherClass {
#Wrapper(validators: [PhoneValidation.phoneInValid])
var model: PhonesViewModel?
}
var a = SomeOtherClass()
a.model = PhonesViewModel()
a.model = PhonesViewModel()
You can use a technique with the name "popsicle immutability". An object is initially mutable, but can be "frozen". Modifications for frozen objects are forbidden. In your case PhonesViewModel become frozen when isValid property have value smsValidationOTPTriggered or smsValidationSuccessful.
Let's add Freezable protocol for requirements to objects that can become immutable and conforming for PhonesViewModel:
protocol Freezable: class {
var isFrozen: Bool { get }
}
extension PhonesViewModel: Freezable {
var isFrozen: Bool {
isValid == .smsValidationOTPTriggered || isValid == .smsValidationSuccessful
}
}
Now we must add validation for isFrozen value when a property is assigned. It can be added in property observers like:
...
public var phone: String? {
didSet {
validate()
}
}
...
private func validate() {
assert(!isFrozen)
}
Or using property wrapper:
#propertyWrapper
struct Guarded<Value> {
private var value: Value
init(wrappedValue: Value) {
value = wrappedValue
}
#available(*, unavailable)
var wrappedValue: Value {
get { fatalError("only works on instance properties of classes that conforms to Freezable protocol") }
set { fatalError("only works on instance properties of classes that conforms to Freezable protocol") }
}
static subscript<EnclosingSelf: Freezable>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value {
get {
object[keyPath: storageKeyPath].value
}
set {
precondition(!object.isFrozen, "Object \(object) is frozen! Modifications are forbidden")
object[keyPath: storageKeyPath].value = newValue
}
}
}
So your class will look like:
class PhonesViewModel: NSCopying {
#Guarded
public var phoneType: PhoneNumberType = PhoneNumberType.mobile
#Guarded
public var phone: String?
#Guarded
public var code: String?
#Guarded
public var countryCode: String?
#Guarded
public var isValid : PhoneValidation?
func copy(with zone: NSZone? = nil) -> Any {
let copy = PhonesViewModel()
copy.phoneType = phoneType
copy.phone = phone
copy.code = code
copy.countryCode = countryCode
copy.isValid = isValid
return copy
}
}

Combine - subscribe to a publisher of PassthroughSubject twice

I'm trying to subscribe to a Publisher created from a PassthroughSubject twice and only one of them is executed when PassthroughSubject fires a value.
Here is what I tried:
class Worker {
let stringGeneratorResultSubject: PassthroughSubject<String, Error>
init(stringGeneratorResultSubject: PassthroughSubject<String, Error>) {
self.stringGeneratorResultSubject = stringGeneratorResultSubject
}
func generateString() {
stringGeneratorResultSubject.send("someValue")
}
}
class A {
let workerObj: Worker
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObj: Worker,
workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObj = workerObj
self.workerObjPublisher = workerObjPublisher
super.init()
getString()
}
func getString() {
workerObjPublisher.sink { result in
// do something with result for
}.store(in: &cancellable)
workerObj.generateString()
}
}
class B {
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObjPublisher = workerObjPublisher
super.init()
loadString()
}
func loadString() {
workerObjPublisher.sink { result in
// do something with result
}.store(in: &cancellable)
}
}
class Parent {
lazy var stringGeneratorResultSubject: PassthroughSubject<String, Swift.Error> = .init()
lazy var workerObj: Worker = .init(stringGeneratorResultSubject: stringGeneratorResultSubject)
lazy var aObj: A = .init(workerObj: workerObj,
workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
lazy var bObj: B = .init(workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
_ = bObj
aObj.getString()
}
Only class A's subscription block in getString() is called. class B's subscription block inside loadString() is not executed. Am I missing something?

Testing BehaviorSubject/Relay in RxSwift

I'm having trouble testing a BehaviorRelay. The following minimal code binds an observable to a BehaviorRelay, but when testing, the tests won't end and are hung - the observable keeps emitting events, because it starts with Observable.timer. When getting rid of the bindRx method and testing the observable only, it works without a problem. But I'd like to test the class properly - does that make sense?
How do I go about making this work?
import XCTest
import RxSwift
import RxCocoa
struct TestObject: Codable {
var a: Int?
var b: Int?
private enum CodingKeys: String, CodingKey {
case a = "test"
case b
}
}
extension TestObject: Equatable {
static func == (lhs: TestObject, rhs: TestObject) -> Bool {
return lhs.a == rhs.a && lhs.b == rhs.b
}
}
class TestObjectFetcher {
private let scheduler: SchedulerType
private let disposeBag = DisposeBag()
let testObject = BehaviorRelay<TestObject?>(value: nil)
var syncInterval = 30.0
init(scheduler: SchedulerType) {
self.scheduler = scheduler
self.bindRx()
}
var fetchTestObjectObservable: Observable<TestObject?> {
return Observable<Int>.timer(0, period: self.syncInterval, scheduler: self.scheduler)
.map { _ -> TestObject? in
TestObject(a: 1, b: 2)
}
}
private func bindRx() {
self.fetchTestObjectObservable
.bind(to: self.testObject)
.disposed(by: self.disposeBag)
}
}
class TestObjectFetcherTests: XCTestCase {
let testScheduler = TestScheduler(initialClock: 0)
func testTestObjectFetcher() {
let testObjectFetcher = TestObjectFetcher(scheduler: testScheduler)
let events: [Recorded<Event<TestObject?>>] = [
Recorded.next(1, TestObject(a: 1, b: 2)),
Recorded.next(31, TestObject(a: 1, b: 2)),
Recorded.next(61, TestObject(a: 1, b: 2))
]
let res = testScheduler.start(created: 0, subscribed: 0, disposed: 90) { () -> Observable<TestObject?> in
return testObjectFetcher.testObject.asObservable()
}
XCTAssertEqual(res.events, events)
}
}
The problem was that the observable never completed. Adding scheduler.subscribeAt(100) {}
and setting the fetcher to nil solved the problem.

How to observe change of value and show it? In Swift

I have a variable that takes the number of objects in the array in a different class, how can I keep track of the change of this variable in the current class? I did a lot of different attempts but failed.
var digitIndex: Int! {
set {
self.digitIndex = newValue
}
get {
return firstClass.indexesOfSelectedNodes().count
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &digitIndex {
if let newValue = change?[NSKeyValueChangeNewKey] {
print("newValue")
infoLabel.text = "\(newValue)"
}
}
}
1) in your code
var digitIndex: Int! {
set {
self.digitIndex = newValue
}
get {
return firstClass.indexesOfSelectedNodes().count
}
}
digitIndex is computed property! You are not able to set self.digitIndex within its own setter!
this code, even though compiled, will run forever :-)
var i: Int {
get {
return 10
}
set {
i = newValue
}
}
i = 100
print(i)
2) How to use willSet and didSet (for STORED properties)?
class C1 {}
class C2 {}
class C {
var objects: [AnyObject] = [C1(),C1(),C2()] {
willSet {
print(objects, objects.count, newValue)
}
didSet {
print(objects, objects.count)
}
}
func add(object: AnyObject) {
objects.append(object)
}
}
let c = C()
c.add(C1())
/*
[C1, C1, C2] 3 [C1, C1, C2, C1]
[C1, C1, C2, C1] 4
*/
var i: Int = 0 {
willSet {
print(newValue, "will replace", i)
}
didSet {
print(oldValue, "was replaced by", i)
}
}
i = 100
/*
100 will replace 0
0 was replaced by 100
*/
you could combine the computed and stored properties for your advantage
// 'private' storage
var _j:Int = 0
var j: Int {
get {
return _j
}
set {
print(newValue)
if newValue < 300 {
_j = newValue
} else {
print(newValue, "refused")
}
}
}
print(j) // prints 0
j = 200 // prints 200
print(j) // prints 200
j = 500 // prints 500 refused
print(j) // prints 200
Try this :
var digitIndex: Int! {
set {
self.digitIndex = newValue
}
get {
return firstClass.indexesOfSelectedNodes().count
}
didSet {
//you will get new value here
}
}
No need to add observer for our class properties, you just required to add observer for the properties provided by super class.
You can make use of a delegate handling the communication between your two classes.
An example follows below, using a custom delegate MyDelegate (protocol). A delegate instance is initialized in MyOtherClass (e.g., a view controller class), which makes a delegate callback whenever the array myArr is updated in this class. This, in turn, updates the value of digitIndex in MyCurrentClass (e.g., some custom control), which conforms to MyDelegate by implementing the blueprinted arrUpdated(..) method. Finally, the didSet property observer on digitIndex in MyCurrentClass tells us via console print-out that its value has been updated.
protocol MyDelegate: class {
func arrUpdated(arr: [Int])
}
class MyDifferentClass {
private var myArr : [Int] = [] {
didSet {
// Call delegate.
delegate?.arrUpdated(myArr)
}
}
weak var delegate: MyDelegate?
}
class MyCurrentClass: MyDelegate {
var myDifferentClass : MyDifferentClass
var digitIndex: Int = 0 {
didSet {
print("digitIndex updated: \(digitIndex)")
}
}
init(a: MyDifferentClass) {
myDifferentClass = a
myDifferentClass.delegate = self
}
// MyDelegate
func arrUpdated(arr: [Int]) {
digitIndex = arr.count
}
}
Test:
var a = MyDifferentClass()
var b = MyCurrentClass(a: a)
a.myArr.append(1) // prints 'digitIndex updated: 1'

Resources