Convert computed property for #State for Swiftui Views - swift

I'm new with SwiftUI and i want to convert basically my computed property for being used in SwiftUI views with combine and all that. I couldn’t use it like that because "get set" doesn't work for my SwiftUI views and I kinda struggled here.
Maybe someone has a good solution how can i convert it with with combine to use in swift ui.
Storage service saves the Authdata into userdefaults.
var currentAuthData: AuthData? {
get {
return self.storageService.get(AuthData.self, forKey: authDataStorageKey)
}
set {
if let value = newValue {
self.storageService.store(value, forKey: authDataStorageKey)
}
}
}

This is how you would turn a computed property in SwiftUI, by making it a #propertyWrapper. I have added a solution to read the data using Combine if you need it. I also make you property optional.
I assume this is the model you want to save.
struct AuthData {
var name: String
var email: String
}
Prepare the protocol for your property wrapper to be able to be set to nil using Optional.
public protocol AnyOptional {
var isNil: Bool { get }
}
extension Optional: AnyOptional {
public var isNil: Bool { self == nil }
}
Extend UserDefaults to conform to this protocol and be optional.
extension UserDefault where Value: ExpressibleByNilLiteral {
init(key: String, _ container: UserDefaults = .standard) {
self.init(key: key, defaultValue: nil, container: container)
}
}
Create the property wrapper that is the same thing as a getter and setter for SwiftUI views. This is a generic one and can be use to any type. You can set them in the UserDefaults extension after this bloc of code.
import Combine
#propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
var container: UserDefaults = .standard
// Set a Combine publisher for your new value to
// always read its changes when using Combine.
private let publisher = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get {
// Get the new value or nil if any.
container.object(forKey: key) as? Value ?? defaultValue
}
set {
// Check if the value is nil and remove it from your object.
if let optional = newValue as? AnyOptional, optional.isNil {
container.removeObject(forKey: key)
}
else {
// Set your new value inside UserDefaults.
container.set(newValue, forKey: key)
}
// Add the newValue to your combine publisher
publisher.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
}
Create the extension in UserDefaults to use your property wrapper in your code.
extension UserDefaults {
#UserDefault(key: "authDataStorageKey", defaultValue: nil)
static var savedAuthData: AuthData?
// You can create as many #propertyWrapper as you want that fits your need
}
examples of use
// Set a new value
UserDefaults.savedAuthData = AuthData(name: "Muli", email: "muli#stackoverflow.com")
// Read the saved value
print(UserDefaults.savedAuthData as Any)
// When using combine
var subscriptions = Set<AnyCancellable>()
UserDefaults.$savedAuthData
.sink { savedValue in
print(savedValue as Any) } // Yours saved value that changes over time.
.store(in: &subscriptions)

Related

Userdefaults with published enum

try to save user setting, but UserDefaults is not working, Xcode 12.3, swiftui 2.0, when I am reload my app, my setting not updating for new value)
class PrayerTimeViewModel: ObservableObject {
#Published var lm = LocationManager()
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults.standard.set(method.params, forKey: "method")
self.getPrayerTime()
}
}
func getPrayerTime() {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: lm.location?.latitude ?? 0.0, longitude: lm.location?.longitude ?? 0.0)
var par = method.params
par.madhab = mashab
self.times = PrayerTimes(coordinates: coordinates, date: date, calculationParameters: par)
}
and view.. update with AppStorage
struct MethodView: View {
#ObservedObject var model: PrayerTimeViewModel
#Environment(\.presentationMode) var presentationMode
#AppStorage("method", store: UserDefaults(suiteName: "method")) var method: CalculationMethod = .dubai
var body: some View {
List(CalculationMethod.allCases, id: \.self) { item in
Button(action: {
self.model.objectWillChange.send()
self.presentationMode.wrappedValue.dismiss()
self.model.method = item
method = item
}) {
HStack {
Text("\(item.rawValue)")
if model.method == item {
Image(systemName: "checkmark")
.foregroundColor(.black)
}
}
}
}
}
}
You have two issues.
First, as I mentioned in my comment above that you are using two different suites for UserDefaults. This means that you are storing and retrieving from two different locations. Either use UserDefaults.standard or use the one with your chosen suite UserDefaults(suitName: "method") - you don't have to use a suite unless you plan on sharing your defaults with other extensions then it would be prudent to do so.
Secondly you are storing the wrong item in UserDefaults. You are storing a computed property params rather than the actual enum value. When you try to retrieve the value it fails as it is not getting what it expects and uses the default value that you have set.
Here is a simple example that shows what you could do. There is a simple enum that has a raw value (String) and conforms to Codable, it also has a computed property. This matches your enum.
I have added an initialiser to my ObservableObject. This serves the purpose to populate my published Place from UserDefaults when the Race object is constructed.
Then in my ContentView I update the place depending on a button press. This updates the UI and it updates the value in UserDefaults.
This should be enough for you to understand how it works.
enum Place: String, Codable {
case first
case second
case third
case notPlaced
var someComputedProperty: String {
"Value stored: \(self.rawValue)"
}
}
class Race: ObservableObject {
#Published var place: Place = .notPlaced {
didSet {
// Store the rawValue of the enum into UserDefaults
// We can store the actual enum but that requires more code
UserDefaults.standard.setValue(place.rawValue, forKey: "method")
// Using a custom suite
// UserDefaults(suiteName: "method").setValue(place.rawValue, forKey: "method")
}
}
init() {
// Load the value from UserDefaults if it exists
if let rawValue = UserDefaults.standard.string(forKey: "method") {
// We need to nil-coalesce here as this is a failable initializer
self.place = Place(rawValue: rawValue) ?? .notPlaced
}
// Using a custom suite
// if let rawValue = UserDefaults(suiteName: "method")?.string(forKey: "method") {
// self.place = Place(rawValue: rawValue) ?? .notPlaced
// }
}
}
struct ContentView: View {
#StateObject var race: Race = Race()
var body: some View {
VStack(spacing: 20) {
Text(race.place.someComputedProperty)
.padding(.bottom, 20)
Button("Set First") {
race.place = .first
}
Button("Set Second") {
race.place = .second
}
Button("Set Third") {
race.place = .third
}
}
}
}
Addendum:
Because the enum conforms to Codable it would be possible to use AppStorage to read and write the property. However, that won't update the value in your ObservableObject so they could easily get out of sync. It is best to have one place where you control a value. In this case your ObservableObject should be the source of truth, and all updates (reading and writing to UserDefaults) should take place through there.
You write in one UserDefaults domain but read from the different. Assuming your intention is to use suite only UserDefaults, you should change one in model, like
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults(suiteName: "method").set(method.params, forKey: "method")
self.getPrayerTime()
}
}
or if you want to use standard then just use AppStorage with default constructor, like
// use UserDefaults.standard by default
#AppStorage("method") var method: CalculationMethod = .dubai

State variables in Property Wrappers

I would like to use a variable to cache user defaults which are fetched by a propert wrapper. I used the boilerplate from this quite good tutorial regarding property wrappers as a basis https://www.avanderlee.com/swift/property-wrappers/
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
#State var cacheValue: T?
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
if (self.cacheValue == nil){
self.cacheValue = UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
return self.cacheValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
During debugging I found out that my cacheValue always returns nil, even if there is a corresponding user property value that is returned.
So the question is: Are #State variables supposed to work in property wrapppers? And if not, is there a workaround, that does not mean coding it in the class/struct that uses the property wrapper construct?
#State is a property wrapper declared inside SwiftUI and it should only be used inside Views.

Detect change in NSMutableOrderedSet with Swift Combine

I'm trying to observe change of an NSMutableOrderedSet in my ViewModel with combine.
I want to know when some element is added or removed of NSMutableOrderedSet
Some code of my ViewModel :
class TrainingAddExerciceViewModel: ObservableObject {
#Published var exercice: Exercice?
#Published var serieHistories = NSMutableOrderedSet()
...
init(...) {
...
//Where i'm trying to observe
$serieHistories
.sink { (value) in
print(value)
}
.store(in: &self.cancellables)
}
}
This is the function I use in my ViewModel to add element to NSMutableOrderedSet :
func add(managedObjectContext: NSManagedObjectContext) {
let newSerieHistory = ExerciceSerieHistory(context: managedObjectContext)
self.serieHistories.add(newSerieHistory)
self.updateView()
}
I have some other publisher working well with an other type (custom class).
Did I miss something ?
If I correctly understood logic of your code try the following (that init not needed)
variant 1 - add force update
func updateView() {
// ... other code
self.objectWillChange.send()
}
variant 2 - recreate storage
func add(managedObjectContext: NSManagedObjectContext) {
let newSerieHistory = ExerciceSerieHistory(context: managedObjectContext)
let newStorage = NSMutableOrderedSet(orderedSet: self.serieHistories)
newStorage.add(newSerieHistory)
self.serieHistories = newStorage // << fires publisher
self.updateView()
}

How do I use UserDefaults with SwiftUI?

struct ContentView: View {
#State var settingsConfiguration: Settings
struct Settings {
var passwordLength: Double = 20
var moreSpecialCharacters: Bool = false
var specialCharacters: Bool = false
var lowercaseLetters: Bool = true
var uppercaseLetters: Bool = true
var numbers: Bool = true
var space: Bool = false
}
var body: some View {
VStack {
HStack {
Text("Password Length: \(Int(settingsConfiguration.passwordLength))")
Spacer()
Slider(value: $settingsConfiguration.passwordLength, from: 1, through: 512)
}
Toggle(isOn: $settingsConfiguration.moreSpecialCharacters) {
Text("More Special Characters")
}
Toggle(isOn: $settingsConfiguration.specialCharacters) {
Text("Special Characters")
}
Toggle(isOn: $settingsConfiguration.space) {
Text("Spaces")
}
Toggle(isOn: $settingsConfiguration.lowercaseLetters) {
Text("Lowercase Letters")
}
Toggle(isOn: $settingsConfiguration.uppercaseLetters) {
Text("Uppercase Letters")
}
Toggle(isOn: $settingsConfiguration.numbers) {
Text("Numbers")
}
Spacer()
}
.padding(.all)
.frame(width: 500, height: 500)
}
}
So I have all this code here and I want to use UserDefaults to save settings whenever a switch is changed or a slider is slid and to retrieve all this data when the app launches but I have no idea how I would go about using UserDefaults with SwiftUI (Or UserDefaults in general, I've just started looking into it so I could use it for my SwiftUI app but all the examples I see are for UIKit and when I try implementing them in SwiftUI I just run into a ton of errors).
The approach from caram is in general ok but there are so many problems with the code that SmushyTaco did not get it work. Below you will find an "Out of the Box" working solution.
1. UserDefaults propertyWrapper
import Foundation
import Combine
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
2. UserSettings class
final class UserSettings: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#UserDefault("ShowOnStart", defaultValue: true)
var showOnStart: Bool {
willSet {
objectWillChange.send()
}
}
}
3. SwiftUI view
struct ContentView: View {
#ObservedObject var settings = UserSettings()
var body: some View {
VStack {
Toggle(isOn: $settings.showOnStart) {
Text("Show welcome text")
}
if settings.showOnStart{
Text("Welcome")
}
}
}
Starting from Xcode 12.0 (iOS 14.0) you can use #AppStorage property wrapper for such types: Bool, Int, Double, String, URL and Data.
Here is example of usage for storing String value:
struct ContentView: View {
static let userNameKey = "user_name"
#AppStorage(Self.userNameKey) var userName: String = "Unnamed"
var body: some View {
VStack {
Text(userName)
Button("Change automatically ") {
userName = "Ivor"
}
Button("Change manually") {
UserDefaults.standard.setValue("John", forKey: Self.userNameKey)
}
}
}
}
Here you are declaring userName property with default value which isn't going to the UserDefaults itself. When you first mutate it, application will write that value into the UserDefaults and automatically update the view with the new value.
Also there is possibility to set custom UserDefaults provider if needed via store parameter like this:
#AppStorage(Self.userNameKey, store: UserDefaults.shared) var userName: String = "Mike"
and
extension UserDefaults {
static var shared: UserDefaults {
let combined = UserDefaults.standard
combined.addSuite(named: "group.myapp.app")
return combined
}
}
Notice: ff that value will change outside of the Application (let's say manually opening the plist file and changing value), View will not receive that update.
P.S. Also there is new Extension on View which adds func defaultAppStorage(_ store: UserDefaults) -> some View which allows to change the storage used for the View. This can be helpful if there are a lot of #AppStorage properties and setting custom storage to each of them is cumbersome to do.
The code below adapts Mohammad Azam's excellent solution in this video:
import SwiftUI
struct ContentView: View {
#ObservedObject var userDefaultsManager = UserDefaultsManager()
var body: some View {
VStack {
Toggle(isOn: self.$userDefaultsManager.firstToggle) {
Text("First Toggle")
}
Toggle(isOn: self.$userDefaultsManager.secondToggle) {
Text("Second Toggle")
}
}
}
}
class UserDefaultsManager: ObservableObject {
#Published var firstToggle: Bool = UserDefaults.standard.bool(forKey: "firstToggle") {
didSet { UserDefaults.standard.set(self.firstToggle, forKey: "firstToggle") }
}
#Published var secondToggle: Bool = UserDefaults.standard.bool(forKey: "secondToggle") {
didSet { UserDefaults.standard.set(self.secondToggle, forKey: "secondToggle") }
}
}
First, create a property wrapper that will allow us to easily make the link between your Settings class and UserDefaults:
import Foundation
#propertyWrapper
struct UserDefault<Value: Codable> {
let key: String
let defaultValue: Value
var value: Value {
get {
let data = UserDefaults.standard.data(forKey: key)
let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(data, forKey: key)
}
}
}
Then, create a data store that holds your settings:
import Combine
import SwiftUI
final class DataStore: BindableObject {
let didChange = PassthroughSubject<DataStore, Never>()
#UserDefault(key: "Settings", defaultValue: [])
var settings: [Settings] {
didSet {
didChange.send(self)
}
}
}
Now, in your view, access your settings:
import SwiftUI
struct SettingsView : View {
#EnvironmentObject var dataStore: DataStore
var body: some View {
Toggle(isOn: $settings.space) {
Text("\(settings.space)")
}
}
}
If you are persisting a one-off struct such that a property wrapper is overkill, you can encode it as JSON. When decoding, use an empty Data instance for the no-data case.
final class UserData: ObservableObject {
#Published var profile: Profile? = try? JSONDecoder().decode(Profile.self, from: UserDefaults.standard.data(forKey: "profile") ?? Data()) {
didSet { UserDefaults.standard.set(try? JSONEncoder().encode(profile), forKey: "profile") }
}
}
I'm supriced no one wrote the new way, anyway, Apple migrated to this method now and you don't need all the old code, you can read and write to it like this:
#AppStorage("example") var example: Bool = true
that's the equivalent to read/write in the old UserDefaults. You can use it as a regular variable.
Another great solution is to use the unofficial static subscript API of #propertyWrapper instead of the wrappedValue which simplifies a lot the code. Here is the definition:
#propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(wrappedValue: Value, _ key: String) {
self.key = key
self.defaultValue = wrappedValue
}
var wrappedValue: Value {
get { fatalError("Called wrappedValue getter") }
set { fatalError("Called wrappedValue setter") }
}
static subscript(
_enclosingInstance instance: Preferences,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
) -> Value {
get {
let wrapper = instance[keyPath: storageKeyPath]
return instance.userDefaults.value(forKey: wrapper.key) as? Value ?? wrapper.defaultValue
}
set {
instance.objectWillChange.send()
let key = instance[keyPath: storageKeyPath].key
instance.userDefaults.set(newValue, forKey: key)
}
}
}
Then you can define your settings object like this:
final class Settings: ObservableObject {
let userDefaults: UserDefaults
init(defaults: UserDefaults = .standard) {
userDefaults = defaults
}
#UserDefaults("yourKey") var yourSetting: SettingType
...
}
However, be careful with this kind of implementation. Users tend to put all their app settings in one of such object and use it in every view that depends on one setting. This can result in slow down caused by too many unnecessary objectWillChange notifications in many view.
You should definitely separate concerns by breaking down your settings in many small classes.
The #AppStorage is a great native solution but the drawback is that is kind of break the unique source of truth paradigm as you must provide a default value for every property.

Is it possible to add an observer on struct variable in Swift?

I need to track the update in a variable of struct type.
Is it possible to add an observer on struct variable in Swift?
Example:
struct MyCustomStruct {
var error:Error?
var someVar:String?
}
class MyClass{
var myCustomStruct:MyCustomStruct?
}
I want to add an observer on myCustomStruct variable.
The standard Swift “property observers” (didSet and willSet) are designed to let a type observe changes to its own properties, but not for letting external objects add their own observers. And KVO, which does support external observers, is only for dynamic and #objc properties NSObject subclasses (as outlined in Using Key-Value Observing in Swift).
So, if you want to have an external object observe changes within a struct, as others have pointed out, you have to create your own observer mechanism using Swift didSet and the like. But rather than implementing that yourself, property by property, you can write a generic type to do this for you. E.g.,
struct Observable<T> {
typealias Observer = String
private var handlers: [Observer: (T) -> Void] = [:]
var value: T {
didSet {
handlers.forEach { $0.value(value) }
}
}
init(_ value: T) {
self.value = value
}
#discardableResult
mutating func observeNext(_ handler: #escaping (T) -> Void) -> Observer {
let key = UUID().uuidString as Observer
handlers[key] = handler
return key
}
mutating func remove(_ key: Observer) {
handlers.removeValue(forKey: key)
}
}
Then you can do things like:
struct Foo {
var i: Observable<Int>
var text: Observable<String>
init(i: Int, text: String) {
self.i = Observable(i)
self.text = Observable(text)
}
}
class MyClass {
var foo: Foo
init() {
foo = Foo(i: 0, text: "foo")
}
}
let object = MyClass()
object.foo.i.observeNext { [weak self] value in // the weak reference is really only needed if you reference self, but if you do, make sure to make it weak to avoid strong reference cycle
print("new value", value)
}
And then, when you update the property, for example like below, your observer handler closure will be called:
object.foo.i.value = 42
It’s worth noting that frameworks like Bond or RxSwift offer this sort of functionality, plus a lot more.
With variables you can use two default observers
willSet - represents moment before variable will be set with new value
didSet - represents moment when variable was set
Also in observer you can work with two values. With current variable in current state, and with constant depending on observer
struct Struct {
var variable: String {
willSet {
variable // before set
newValue // after set, immutable
}
didSet {
oldValue // before set, immutable
variable // after set
}
}
}
And the same you can do for any other stored property, so you can use it for struct variable in your class too
class Class {
var myStruct: Struct? {
didSet {
...
}
}
}
Also you can for example in did set observer of variable post notification with certain name
didSet {
NotificationCenter.default.post(name: Notification.Name("VariableSet"), object: nil)
}
and then you can add certain class as observer for notification with this name
class Class {
init() {
NotificationCenter.default.addObserver(self, selector: #selector(variableSet), name: Notification.Name("VariableSet"), object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: Notification.Name("VariableSet"), object: nil)
}
#objc func variableSet() {
...
}
}
Try this, first create a struct with an action variable and when you create an object of the struct set the action parameter on the action you want. ex.
struct testStruct {
var action: (()->())?
var variable: String? {
didSet {
self.action?()
}
}
}
And inside your main code - main class
var testS = testStruct()
testS.action = {
print("Hello")
}
testS.variable = "Hi"
When you set the testS.variabe = "Hi" it will call the print("Hello")
struct MyCustomStruct {
var error:Error?
var someVar:String?
}
class MyClass{
var myCustomStruct:MyCustomStruct? {
didSet{
print("my coustomeSruct changed")
}
}
}
let aClass = MyClass()
aClass.myCustomStruct?.someVar = " test"
//prints:my coustomeSruct changed