UserDefaults keypath publisher doesn't fire beyond first value - swift

Given a obj-c keypath
#objc dynamic var someProp: String { string(forKey: "someProp") }
A regular publisher:
private let sub = UserDefaults.standard.publisher(for: \.someProp).sink { print($0) }
This publishes only works for the first value (e.g. the current value).
However observing the sub publisher from SwiftUI works fine:
.onReceive(pub) { value in
print("received", value)
}
This publishes any subsequent updates.
Any ideas why the former doesn't work?
Edit: Here is a minimal reproducible example:
public extension UserDefaults {
#objc dynamic var value1: Int {
integer(forKey: "string1")
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
private let pub = UserDefaults.standard.publisher(for: \.value1)
var body: some View {
Button("Add") {
var value = UserDefaults.standard.value(forKey: "value1") as? Int ?? 0
value += 1
debugPrint("SET", value)
UserDefaults.standard.set(value, forKey: "value1")
}
.onReceive(pub) { value in
debugPrint("UI", value)
}
}
class ViewModel: ObservableObject {
private let sub = UserDefaults.standard.publisher(for: \.value1).sink {
debugPrint("SUB", $0)
}
}
}

The error here is how you access and assign your values in the Button action. You are setting the values for the key value1. But the publisher observes the key string1 with the dynamic var named value1.
TLDR: You confused the dynamic var with your key
I would recommend you ommit the access via .value(forKey: "") and use only your dynamic var.
public extension UserDefaults {
#objc dynamic var value1: Int {
// add getter and setter
get{
integer(forKey: "string1")
}
set{
set(newValue, forKey: "string1")
}
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
private let pub = UserDefaults.standard.publisher(for: \.value1)
var body: some View {
Button("Add") {
//here
UserDefaults.standard.value1 += 1
debugPrint("SET", UserDefaults.standard.value1)
}
.onReceive(pub) { value in
debugPrint("UI", value)
}
}
class ViewModel: ObservableObject {
private let sub = UserDefaults.standard.publisher(for: \.value1).sink {
debugPrint("SUB", $0)
}
}
}
Prints:
"SUB" 0
"UI" 0
"SET" 1
"SUB" 1
"UI" 1
"SET" 2
"SUB" 2
"UI" 2
"SET" 3
"SUB" 3
"UI" 3

Related

#ObservedObject not updating after successful network call

I've hit a brick wall in my widget extension. I'm using AlamoFire and ObjectMapper to match the networking we have in the main app. I can tell that my AlamoFire network call is getting triggered and that I'm getting results back, and in the correct, expected format. However, saving the response of that network call to a #Published var doesn't seem to be working. My view and models/structs are below:
struct WidgetEntryView: View {
var entry: ResourceCategoryEntry
#ObservedObject var viewModel = WidgetResourcesView(widgetSize: .medium)
var body: some View {
if UserDefaults.forAppGroup.object(forKey: "sessionToken") as? String == nil {
PleaseLogIn()
} else if viewModel.mediumResources.count < 1 {
ErrorScreen()
} else {
MediumResourcesView(resources: viewModel.mediumResources)
}
}
}
class WidgetResourcesView: ObservableObject {
#Published var resourceGroups: [WidgetResouceGroup] = [WidgetResouceGroup]()
var widgetSize: WidgetSize = .small
var selectedCategory: String?
init(widgetSize: WidgetSize) {
self.widgetSize = widgetSize
self.selectedCategory = UserDefaults.forAppGroup.string(forKey: ResourceCategoryEntry.userDefaultKey)
getResources()
}
func getResources() {
WidgetNetworkService.getResources(widgetSize: self.widgetSize.rawValue, selectedCategory: self.selectedCategory) { resourceGroups in
DispatchQueue.main.async {
self.resourceGroups = resourceGroups
}
} failure: { _ in
print("Error Received")
}
}
var mediumResources: [WidgetResource] {
var resources = [WidgetResource]()
if let featuredResourceGroup = resourceGroups.featuredResourceGroup {
for resource in featuredResourceGroup.resources { resources.append(resource) }
}
if let nonFeaturedResourceGroup = resourceGroups.nonFeaturedResourceGroup {
for resource in nonFeaturedResourceGroup.resources { resources.append(resource) }
}
return resources
}
}
class WidgetResouceGroup: NSObject, Mappable, Identifiable {
var id = UUID()
var widgetCategory: WidgetCategory = .featured
var resources = [WidgetResource]()
required init?(map: Map) {}
func mapping(map: Map) {
id <- map["section"]
widgetCategory <- map["section"]
resources <- map["resources"]
}
}
typealias WidgetResourceGroupCollection = [WidgetResouceGroup]
extension WidgetResourceGroupCollection {
var featuredResourceGroup: WidgetResouceGroup? {
return first(where: {$0.widgetCategory == .featured})
}
var nonFeaturedResourceGroup: WidgetResouceGroup? {
return first(where: {$0.widgetCategory != .featured})
}
}
class WidgetResource: NSObject, Mappable, Identifiable {
enum ResourceType: String {
case text = "text"
case audio = "audio"
case video = "video"
}
var id = 0
var title = ""
var imageInfo: WidgetImageInfo?
var resourceType: ResourceType = .text
required init?(map: Map) {}
func mapping(map: Map) {
id <- map["object_id"]
title <- map["title"]
imageInfo <- map["image_info"]
resourceType <- map["content_type"]
}
}
You can use the objectWillChange - Property in your observable object to specifiy when the observable object should be refreshed.
Apple Dev Doku
Example by Paul Hudson
WidgetEntryView instantiates WidgetResourcesView using the ObservedObject wrapper. This causes a new instance of WidgetResourcesView to be instantiated again on every refresh. Try switching that to StateObject, and the original object will be kept in memory between view updates. I believe this is the only change needed, but I’m away so can’t test it!

SwfitUI creating a function that acts like a #published variable

I have a class which has two published variables:
class Controller: ObservableObject {
#Published var myArray: Array<Int>: [1,2,3,4,5]
#Published var currIndex: Int = 0
func currItem() -> Int {
return myArray[curIndex]
}
}
I want my view to subscribe to the function "currItem" instead of the currIndex variable
Is there an elegant way to achieve it?
without subscribing to the function I need to write some boilerplate code:
struct myView: View {
var controller: Controller = Controller()
var body: some View {
Text(controller.myArray[controller.currIndex]) // <-- Replace this with controller.currItem()
}
}
You can do it even much better than that, like this:
import SwiftUI
struct ContentView: View {
#StateObject var controller: Controller = Controller()
var body: some View {
Text(controller.currItem?.description ?? "Error!")
}
}
class Controller: ObservableObject {
#Published var myArray: Array<Int> = [1,2,3,4,5]
#Published var currIndex: Int? = 0
var currItem: Int? {
get {
if let unwrappedIndex: Int = currIndex {
if myArray.indices.contains(unwrappedIndex) {
return myArray[unwrappedIndex]
}
else {
print("Error! there is no such index found!")
return nil
}
}
else {
print("Error! you did not provide a value for currIndex!")
return nil
}
}
}
}

Getting items from [Any?] in a ForEach loop to display text

Below is some code I have been trying
import SwiftUI
struct AnyOptional: View {
private var optionalArray: [Any?] = [1, 2, 3]
var body: some View {
VStack {
ForEach(optionalArray) { i in
Text("\(i)")
}
}
}
}
extension Optional: Identifiable {
public var id: String { self as! String }
}
struct AnyOptional_Previews: PreviewProvider {
static var previews: some View {
AnyOptional()
}
}
I had a similar problem with [String] which I solved by using this extension
extension String: Identifiable {
public var id: String { self }
}
but now I get an error saying Any? must inherit from NSObject.
Is there an easier way to do this?
A possible solution is to use your already created id extension:
var body: some View {
VStack {
ForEach(optionalArray) { i in
Text(i.id)
}
}
}
Note that not all objects can be casted down to String (self as! String will fail if the object can't be cast to String).
A better way is to use String(describing:).
For this you can create another extension (updated to remove the word Optional if there's some value):
extension Optional {
public var asString: String {
if let value = self {
return .init(describing: value)
}
return .init(describing: self)
}
}
and use it in the ForEach loop:
var body: some View {
VStack {
ForEach(optionalArray, id: \.asString) { i in
Text(i.asString)
}
}
}

Convert a #State into a Publisher

I want to use a #State variable both for the UI and for computing a value.
For example, let's say I have a TextField bound to #State var userInputURL: String = "https://". How would I take that userInputURL and connect it to a publisher so I can map it into a URL.
Pseudo code:
$userInputURL.publisher()
.compactMap({ URL(string: $0) })
.flatMap({ URLSession(configuration: .ephemeral).dataTaskPublisher(for: $0).assertNoFailure() })
.eraseToAnyPublisher()
You can't convert #state to publisher, but you can use ObservableObject instead.
import SwiftUI
final class SearchStore: ObservableObject {
#Published var query: String = ""
func fetch() {
$query
.map { URL(string: $0) }
.flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
.sink { print($0) }
}
}
struct ContentView: View {
#StateObject var store = SearchStore()
var body: some View {
VStack {
TextField("type something...", text: $store.query)
Button("search") {
self.store.fetch()
}
}
}
}
You can also use onChange(of:) to respond to #State changes.
struct MyView: View {
#State var userInputURL: String = "https://"
var body: some View {
VStack {
TextField("search here", text: $userInputURL)
}
.onChange(of: userInputURL) { _ in
self.fetch()
}
}
func fetch() {
print("changed", userInputURL)
// ...
}
}
Output:
changed https://t
changed https://ts
changed https://tsr
changed https://tsrs
changed https://tsrst
The latest beta has changed how variables are published so I don't think that you even want to try. Making ObservableObject classes is pretty easy but you then want to add a publisher for your own use:
class ObservableString: Combine.ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<String, Never>()
var string: String {
willSet { objectWillChange.send() }
didSet { publisher.send(string) }
}
init(_ string: String = "") { self.string = string }
}
Instead of #State variables you use #ObservableObject and remember to access the property string directly rather than use the magic that #State uses.
After iOS 14.0, you can access to Publisher.
struct MyView: View {
#State var text: String?
var body: some View {
Text(text ?? "")
.onReceive($text.wrappedValue.publisher) { _ in
let publisher1: Optional<String>.Publisher = $text.wrappedValue.publisher
// ... or
let publisher2: Optional<String>.Publisher = _text.wrappedValue.publisher
}
}
}

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.