Removing #AppStorage objects using UserDefaults does not update SwiftUI view - ios

I have multiple #AppStorage-wrapped variables:
#AppStorage(Keys.test1.rawValue) var test1 = false
#AppStorage(Keys.test2.rawValue) var test2 = 0
#AppStorage(Keys.test3.rawValue) var test3 = 1.0
// ... Many more
And I wanted to reset them all to their default values without having to set them again one by one.
I added an extension to UserDefaults to reset the keys:
enum Keys: String, CaseIterable {
case test1 = "TestKey1"
case test2 = "TestKey2"
case test3 = "TestKey3"
}
extension UserDefaults {
func resetKeys(){
Keys.allCases.forEach {
removeObject(forKey: $0.rawValue)
}
}
}
But after resetting with UserDefauls.standard.resetKeys() it does reset but doesn't update the SwiftUI view at all, unless I close the app and relaunch.
Having a function that does it manually, updates the view instantly:
func resetKeys() {
test1 = false
test2 = 0
test3 = 1.0
}
Entire Sample:
enum Keys: String, CaseIterable {
case test1 = "TestKey1"
case test2 = "TestKey2"
case test3 = "TestKey3"
}
class SomeClass: ObservableObject {
#AppStorage(Keys.test1.rawValue) var test1 = false
#AppStorage(Keys.test2.rawValue) var test2 = 0
#AppStorage(Keys.test3.rawValue) var test3 = 1.0
// ... Many more
}
struct ContentView: View {
#EnvironmentObject var storage: SomeClass
var body: some View {
...
}
}
extension UserDefaults {
func resetKeys(){
Keys.allCases.forEach {
removeObject(forKey: $0.rawValue)
}
}
}

Related

Mock presenter for unit tests

My presenter class MyItemsPresenter will look something like so:
class MyItemsPresenter {
internal var deliveryType: ServiceType = .typeA
var myItemSections: [MySections] = [.sectionA, .sectionB, .sectionC]
var didMoveDeals = false
private(set) var childViewControllers = [UIViewController]()
var segmentTitles: [String] {
myItemSections.map { $0.title }
}
var selectedOption: String?
func createChildViewControllers() -> [UIViewController] {
var childViewControllers = [UIViewController]()
for item in myItemSections {
switch item {
case . sectionA:
childViewControllers.append(MyListWireframe.assembleModuleA())
case . sectionB:
childViewControllers.append(MyListWireframe.assembleModuleB())
case . sectionC:
childViewControllers.append(MyListWireframe.assembleModuleC())
default:
break
}
}
self.childViewControllers = childViewControllers
return childViewControllers
}
private func resetSections() {
myItemSections = getOriginalSections()
}
private func getOriginalSections() -> [MySections] {
[.list, .buyItAgain, .clippedDeals, .lastOrder]
}
func showFirstTwoMenuItems() {
if myItemSections.count > 2 {
let upperRange = myItemSections.count-1
myItemSections.removeSubrange(ClosedRange(uncheckedBounds: (lower: 2, upper: upperRange)))
childViewControllers.removeSubrange(ClosedRange(uncheckedBounds: (lower: 2, upper: upperRange)))
}
}
}
For writing test cases, I would like to write a Mock Presenter class for the above class say MyItemsPresenterMock. So how will my mock presenter class look? I'm using Quick and Nimble for test cases.

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
}
}

What am I doing wrong on passing data through protocol

I'm trying to pass data between viewControllers, but something seems wrong.
The first viewController I want to set the "Bool" to the protocol function to be able to recover in the other screen. What am I doing wrong, I always used protocols but at this time I got in trouble.
That's how I'm doing that:
//
// ComboBoxNode.swift
//
import Foundation
import SWXMLHash
protocol ComboBoxNodeDelegate {
func getCustomOption(data:Bool)
}
class ComboBoxNode: FormControlNode, IFormControlDataSource {
var listType: String?
var dataSource: String?
var dataSourceValue: String?
var dataSourceText: String?
var hasCustomOption:Bool?
var customOptionText: String?
var ctrlDataSourceType: String?
var parameters = [ParameterNode]()
var staticList: FormControlStaticListNode?
var delegate:ComboBoxNodeDelegate?
override init(indexer: XMLIndexer) {
super.init(indexer: indexer)
guard let element = indexer.element else {
preconditionFailure("Error")
}
let isCustomOption = element.bool(by: .hasCustomOption) ?? hasCustomOption
if isCustomOption == true {
self.delegate?.getCustomOption(data: hasCustomOption!)
}
self.readFormControlDataSource(indexer: indexer)
}
override func accept<T, E: IViewVisitor>(visitor: E) -> T where E.T == T {
return visitor.visit(node: self)
}
}
That's how I'm trying to recover on next screen:
// FormPickerViewDelegate.swift
import Foundation
import ViewLib
import RxSwift
class FormPickerViewDelegate: NSObject {
var items = Variable([(value: AnyHashable, text: String)]()) {
didSet {
PickerNodeDelegate = self
self.setDefaultValues()
}
}
private var controlViewModel: FormControlViewModel
private var customText:Bool?
private var PickerNodeDelegate:ComboBoxNodeDelegate?
init(controlViewModel: FormControlViewModel) {
self.controlViewModel = controlViewModel
}
func getItemByValue(_ value: Any) -> (AnyHashable, String)? {
if value is AnyHashable {
let found = items.value.filter {$0.value == value as! AnyHashable}
if found.count >= 1 {
return found[0]
}
}
return nil
}
}
extension FormPickerViewDelegate:ComboBoxNodeDelegate {
func getCustomOption(data: Bool) {
customText = data
}
}
Instead of setting PickerNodeDelegate = self in didSet {} closure
var items = Variable([(value: AnyHashable, text: String)]()) {
didSet {
PickerNodeDelegate = self
self.setDefaultValues()
}
}
Assign it in your init() function instead
init(controlViewModel: FormControlViewModel) {
self.controlViewModel = controlViewModel
PickerNodeDelegate = self
}
Note, your should declare your delegate to be weak also, since it's a delegate, your protocol should conform to be a class type in order to be weakified.
protocol ComboBoxNodeDelegate: class
...
weak var delegate: ComboBoxNodeDelegate?
Here is an example, hope it helps!
protocol ComboBoxNodeDelegate {
func getCustomOption(data:Bool) -> String
}
class ViewOne:ComboBoxNodeDelegate {
var foo:Bool = false
var bar:String = "it works!"
/** Return: String */
func getCustomOption(data:Bool) -> String { //conform here to protocol
// do whatever you wanna do here ...example
self.foo = data // you can set
return bar // even return what you want
}
//initialize
func initalizeViewTwo() {
let v2 = ViewTwo()
v2.delegate = self //since `self` conforms to the ComboBoxNodeDelegate protcol you are allowed to set
}
}
class ViewTwo {
var delegate:ComboBoxNodeDelegate?
func getCustomOption_forV1() {
let view2_foo = delegate.getCustomOption(data:true)
print(view2_foo) // should print "it works!"
}
}
All parameters passed around in Swift are constants -- so you cannot change them.
If you want to change them in a function, you must declare your protocol to pass by reference with inout:
protocol ComboBoxNodeDelegate {
func getCustomOption(data: inout Bool)
}
Note: you cannot pass a constant (let) to this function. It must be a variable -- which I see you are doing!

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