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.
Related
I am working on an iOS app which has a watchOS app as well. I have a settings bundle so when some settings is changed in the iPhone's settings application, the userdefaults of the app will be changed. And whenever a userdefault is updated, I need to send that to the watch app through watch connectivity.
For sending it to watch, I would need to know if a userdefault value is updated. I went through a lot of articles but nothing seems to be working out in my case.
The property wrapper #AppStorage is good if I need to listen for a userdefault and update a view. But thats not my case. I don't need to update a view. I need to know the change in userdefault and pass it on to watch.
I created a swift class with the following code,
import Foundation
import Combine
class UserDefaultsHelper: ObservableObject {
#Published var settings1: String = UserDefaults.standard.settings1 {
didSet {
UserDefaults.standard.settings1 = settings1
}
}
#Published var settings2: String = UserDefaults.standard.settings2 {
didSet {
UserDefaults.standard.settings2 = settings2
}
}
private var cancellable: AnyCancellable?
init() {
cancellable = UserDefaults.standard.publisher(for: \.settings1)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.settings1 { // avoid cycling !!
self.settings1 = newValue
}
})
cancellable = UserDefaults.standard.publisher(for: \.settings2)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.settings2 { // avoid cycling !!
self.settings2 = newValue
}
})
}
}
// define key for observing
extension UserDefaults {
#objc dynamic var settings1: String {
get { string(forKey: "settings1") ?? "DEFAULT_VALUE" }
set { setValue(newValue, forKey: "settings1") }
}
#objc dynamic var settings2: String {
get { string(forKey: "settings2") ?? "DEFAULT_VALUE" }
set { setValue(newValue, forKey: "settings2") }
}
}
But the problem is, I am not sure where should i call this class from. If I call from the content view, I get the error
Static method 'buildBlock' requires that 'UserDefaultsHelper' conform to 'View'.
So what is the right way to do this. The requirement is to observe the userdefaults and then a call a method in the WatchConnectivity class to pass it on to the watch.
I set my WidgetView using value of UserDefaults.
To share data between widget and core app, I set appGroup already.
let appGroupId = "group.com.myAppGroupId"
Text(UserDefaults(suiteName: appGroupId)!.object(forKey: "githubId") as? String ?? "??")
It shows well in preview, but when I add it to home screen, it cannot read property. And in same condition, when I rebuild app (cmd+r), widget in the home screen read value of UserDefaults well.
I cannot guess the reason.
+++++ Add more code
Actually, I defined my githubId as UserDefaults like this using property wrapper.
extension UserDefaults {
enum Key: String {
case githubId
}
static let shared: UserDefaults = {
let appGroupId = "group.com.myAppGroupId"
return UserDefaults(suiteName: appGroupId)!
}()
#UserDefault(key: .githubId)
static var githubId: String?
}
And my property wrapper is seems like this.
#propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
var container: UserDefaults = .shared
private let publisher = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get {
return container.object(forKey: key) as? Value ?? defaultValue
}
set {
if let optional = newValue as? AnyOptional, optional.isNil {
container.removeObject(forKey: key)
} else {
container.set(newValue, forKey: key) //this is where I set my value
}
publisher.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
return publisher.eraseToAnyPublisher()
}
}
extension UserDefault where Value: ExpressibleByNilLiteral {
init(key: UserDefaults.Key, _ container: UserDefaults = .shared) {
self.init(key: key.rawValue, defaultValue: nil, container: container)
}
}
In LoginViewModel , I set githubId value.
class LoginViewModel: ObservableObject {
#Published var githubId = UserDefaults.githubId ?? ""
private var subscriptions = Set<AnyCancellable>()
init() {
$githubId
.sink { githubId in
UserDefaults.githubId = githubId
}
.store(in: &subscriptions)
}
}
Finally I use this value in my app, and widget extension using UserDefaults.githubId
The code I wrote above ( UserDefaults(suiteName: appGroupId)!.object(forKey: "githubId") ) long version of UserDefaults.githubId .
Is it an option to use #AppStorage ?
Your code could then look like this:
#AppStorage('githubId) var id:String
Text(id)
.sink won't work in a widget.
Whatever you want to display in a widget has to be done when you setup the timeline. Widgets aren't listening for changes.
You have to reload the widget timelines from the app when there are changes. consider each timeline entry like a screenshot.
WidgetCenter.shared.reloadAllTimelines()
Reloading is metered so make sure you have a check and don't reload excessively.
When developing UIKit apps it's quite easy to mock the UserDefaults by defining a protocol and injecting the needed implementation in the UIViewController.
protocol Defaults {
var numberOfHandsPlayed: Int { get set }
}
struct AppDefaults: Defaults {
static let shared = AppDefaults()
private let userDefaults = UserDefaults.standard
struct Keys {
private init() {}
static let numberOfHandsPlayed = "numberOfHandsPlayed"
}
var numberOfHandsPlayed: Int {
get {
userDefaults.integer(forKey: Keys.numberOfHandsPlayed)
}
set(numberOfHandsPlayed) {
userDefaults.setValue(numberOfHandsPlayed, forKey: Keys.numberOfHandsPlayed)
}
}
}
struct MockDefaults: Defaults {
var numberOfHandsPlayed: Int {
get {
// mocking behaviour
}
set(numberOfHandsPlayed) {
// mocking behaviour
}
}
class PracticeViewController: UIViewController {
private var defaults: Defaults?
// defaults can be set to MockDefaults or AppDefaults
}
But now with SwiftUI I can do the following using the #AppStorage property wrapper:
#AppStorage("numberOfHandsPlayed") var numberOfHandsPlayed: Int = 3
Is there a clean solution in SwiftUI to mock this property wrapper and have the same flexibility as in my UIKit example?
The AppStorage allows to specify storage to be used (by default standard UserDefaults), so this feature can be utilised for your purpose.
One of possible approaches is to subclass standard user defaults, and mock it later if/where needed.
class MyUserDefaults: UserDefaults {
// override/add anything needed here
}
struct DemoView: View {
#AppStorage("numberOfHandsPlayed", store: MyUserDefaults()) var numberOfHandsPlayed: Int = 3
// ... other code
I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}
I have an EnvironmentObject which stores user data. After making a call to Firestore I'm filling it with data and using it everywhere. The problem occurs in my view model. I can easily retrieve data from this model but for more complex things I need to use a view model.
Can EnvironmentObject be used as a model for a view model or it should be used only for local preferences in the app, like storing some default values or preferences?
Is it better to use a separate model for my UserViewModel?
While it is very easy to access its data, It is very hard to fill it with dynamic data after making an external call for example. Especially in the SceneDelegate, where it's almost impossible because I can not make a network call there.
SceneDelegate
let userData = UserData()
Auth.auth().addStateDidChangeListener { (auth, user) in
if user != nil {
if let userDefaults = UserDefaults.standard.dictionary(forKey: "userDefaults") {
userData.profile = Profile(userDefaults: userDefaults)
userData.uid = userDefaults["uid"] as? String ?? ""
userData.documentReference = Firestore.firestore().document(userDefaults["documentReference"] as! String)
userData.loggedIn = true
}
}
}
window.rootViewController = UIHostingController(rootView: tabViewContainerView.environmentObject(userData))
UserData
final class UserData: ObservableObject {
#Published var profile = Profile.default
#Published var loggedIn: Bool = Auth.auth().currentUser != nil ? true : false
#Published var uid: String = ""
#Published var documentReference: DocumentReference = Firestore.firestore().document("")
#Published var savedItems = [SavedItem]()
init(document: DocumentSnapshot? = nil) {
if let document = document {
print("document")
let messagesDataArray = document["saved"] as? [[String: Any]]
let parsedMessaged = messagesDataArray?.compactMap {
return SavedItem(dictionary: $0)
}
self.savedItems = parsedMessaged ?? [SavedItem]()
}
}
}
UserViewModel
class UserViewModel: ObservableObject {
#Published var userData: UserData?
init(userData: UserData? = nil) {
self.userData = userData
}