SwiftUI Use EnvironmentObject as a model for my ViewModel - ios

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
}

Related

UserDefaults implementation always has null in extension

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.

iOS Widget does not read UserDeafults value for the first time in the home screen

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.

View not being updated when ViewModel gets new data

I have an app that will make an API call once a day to retrieve new data.
When the app launches it will check if new data is available, save new data to core data, call core data to show data in a view.
Issue
After saving new data to core data my view will not display the newly save data, but instead display the default dummy data from my model. The new data will be displayed after relaunching the app.
Question
How can I display the newly save data in the view without having to relaunch the app? I'm using MVVM pattern. Below is my code with links to my code.
UVIndexNowModel
class UVIndexNowModel: ObservableObject
{
#Published var uvIndex: Int
#Published var dateTime: Date
init(uvIndex: Int = 0, dateTime: Date = Date())
{
self.uvIndex = uvIndex
self.dateTime = dateTime
}
}
UVIndexNowViewModel
class UVIndexNowViewModel: NSObject, ObservableObject
{
private var isFirstAppearance = true
private let moc = PersistentStore.shared.context
private let nowController: NSFetchedResultsController<UVHour>
#Published var data: UVIndexNowModel = UVIndexNowModel()
#Published var coreDateError: Bool = false
override init()
{
nowController = NSFetchedResultsController(fetchRequest: UVHour.uvIndexNowRequest,
managedObjectContext: moc,
sectionNameKeyPath: nil, cacheName: nil)
super.init()
nowController.delegate = self
do {
try nowController.performFetch()
let results = nowController.fetchedObjects ?? []
setUVIndex(from: results)
} catch {
print("failed to fetch items!")
}
}
func setUVIndex(from hours: [UVHour])
{
let date = Date()
let formatter = DateFormatter()
formatter.timeZone = .current
formatter.dateFormat = "MMM/d/yyyy hh a"
let todaystr = formatter.string(from: date)
print("UVIndexNowVM.setUVIndex() size of UVHour array: \(hours.count)")
for i in hours
{
let tempDateStr = formatter.string(from: i.wrappedDateTime)
if todaystr == tempDateStr
{
print("UV Now VM Date matches! \(tempDateStr)")
self.data.uvIndex = i.wrappedUVIndex
self.data.dateTime = i.wrappedDateTime
break
}
}
}
}
extension UVIndexNowViewModel: NSFetchedResultsControllerDelegate
{
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
print("UVIndexNowViewModel controllerDidChangeContent was called. New Stuff in DB")
guard let results = controller.fetchedObjects as? [UVHour] else { return }
setUVIndex(from: results)
}
}
UVIndexNowView
struct UVIndexNowView: View {
#ObservedObject var vm = UVIndexNowViewModel()
var body: some View {
VStack {
Text("\(vm.data.dateTime)")
Text("UV Index: \(vm.data.uvIndex)")
}
}
}
As your UVIndexNowModel is-a ObservableObject you need to observe it in separated view, like
struct UVIndexNowView: View {
#ObservedObject var vm = UVIndexNowViewModel()
var body: some View {
UVIndexNowDataView(data: vm.data) // << here !!
}
}
and
struct UVIndexNowDataView: View {
#ObservedObject var data: UVIndexNowModel
var body: some View {
VStack {
Text("\(data.dateTime)")
Text("UV Index: \(data.uvIndex)")
}
}
}

Swift MVVM Binding (Using Boxing)

I am trying to implement MVVM Architecture pattern using Boxing. I have done it simply by Adding the Boxing Class:
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
func bind(listener: Listener?) {
self.listener = listener
}
func bindAndFire(listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init(_ v: T) {
value = v
}}
And then In the ViewController I have referenced a ViewModel, this is my View Controller:
class SignUpViewController: UIViewController {
// UI Outlets
#IBOutlet weak var emailLoginTextField: FloatLabelTextField!
#IBOutlet weak var passwordLoginTextField: FloatLabelTextField!
var viewModel = AuthenticationViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.user.email.bind{
self.emailLoginTextField.text = $0
}
}}
And This is my View Model:
class AuthenticationViewModel{
let defaults = UserDefaults.standard
let serviceManager = ServiceManager()
var user = User()
func signupUser(email : String?, password: String?){
let parameters : [String:String] = ["email":emailField, "password": password!, "system": "ios"]
serviceManager.initWithPOSTConnection(server: Utitlites.getServerName(), parameters: parameters, methodName: "/api/user/register", completion: { (responseData , errorMessage) -> Void in
let json = (responseData as AnyObject) as! JSON
print(json)
if ErrorHandling.handleErrorMessage(responseData: responseData).0 == true {
self.defaults.set("userId", forKey: json["user"]["id"].stringValue)
//self.userId.value = json["user"]["id"].stringValue
self.user = User(json: json)
}
})
}}
And this is my Model:
class User{
var id = Dynamic("")
var name = Dynamic("")
var email = Dynamic("")
init(){
}
init(json: JSON){
id.value = json["user"]["id"].stringValue
email.value = json["user"]["email"].stringValue
}}
My Question is:
MVVM Architecture wise, is it right to access the model using this line in the ViewController:
viewModel.user.email.bind{
self.emailLoginTextField.text = $0
}
Because I can see now that the View is accessing the Model which I think is not what MVVM Stands for. I need someone to clarify
The best practice to go about this (imo) and according to this raywanderlich video at 31:18 is to actually set the Model to be private, your VC doesn't need to know about it at all, only the ViewModel.
After that, set getters for the Model in the ViewModel like this:
var id: Dynamic<String> = Dynamic("")
var name: Dynamic<String> = Dynamic("")
var email: Dynamic<String> = Dynamic("")
And then, in your ViewModel also, set the User object to have a didSet notifier that will update the ViewModel's data accordingly:
private var user = User() {
didSet {
id = user.id
name = user.name
email = user.email
}
}
Now, you can access these properties only from the ViewModel instead of the Model directly:
viewModel.email.bind{
self.emailLoginTextField.text = $0
}
Oh, and don't forget to set the properties on the Model to be just regular strings ;)

Casting struct to NSUserDefaults in Swift?

Is there a way I can cast this Swift data set in someway to a form that is acceptable to NSUserDefauts? i.e. NSObject NSSet? (p.s. I realize NSUserDefaults isn't for this type of data, but I'm just testing)
struct Users {
var name: String = ""
var stores: [Store]
}
struct Store {
var name: String = ""
var clothingSizes = [String : String]()
}
init() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let usersPeople = userDefaults.valueForKey("Users") as?
I think you can use Dictionary. You'll need to make method to wrap data to struct and vice versa.
For example:
var users : [String: AnyObject]()
users["name"] = "SomeName"
users["stores"] = yourStoreArray
NSUserDefaults.standardUserDefaults().setObject(users, forKey: "Users")
something like that.
And when you need to get struct
if let myDictionaryFromUD = userDefaults.objectForKey("Users") as? [String:AnyObject]{
self.users = Users(myDictionaryFromUD["name"], stores: myDictionaryFromUD["stores"] as! [Store])
}
I aslo assume that you will save to userDefaults array of Users. In this case, you will save [[String: AnyObject]] but mechanics the same.
I don't know that this is the best way to do this but you can try this.
struct Users {
var name: String = ""
var stores: [Store]
}
struct Store {
var name: String = ""
var clothingSizes = [String : String]()
}
var Advert = [Users]()
init() {
NSUserDefaults.standardUserDefaults().setObject(Advert[0].name, forKey: "NameYouWant")
}

Resources