Saving object to be accessible anywhere - ios

I have a condition hereby let's say a user has logged in by calling an API. And the response contain's user's details. Is there anyway we can save the details of the user as an object and be accessible globally? Below is how I used ObjectMapper to call the api
For the model class:
import ObjectMapper
class User: Mappable {
var id: Int?
var userId: Int?
var orgId: Int?
var type: Int?
var email: String?
var lang: String?
var nickname: String?
var status: Int?
var token: String?
required init(map: Map) {
mapping(map: map)
}
func mapping(map: Map) {
id <- map["id"]
userId <- map["userId"]
orgId <- map["orgId"]
type <- map["type"]
failedAttempt <- map["failedAttempt"]
email <- map["email"]
lang <- map["lang"]
nickname <- map["nickname"]
status <- map["status"]
token <- map["token"]
}
}
And from my Networking file,
func postLogin(params: [String: Any], controller: UIViewController, completion: #escaping (User) -> Void) {
alamofire("/login", method: .post, token: false, params: params, controller: controller) { result in
if let userDetails = Mapper<User>().map(JSON: result as! [String: Any]) {
DispatchQueue.main.async {
completion(userDetails)
}
}
}
}
Some solutions may be using UserDefaults but it's just not practical to be using 9 UserDefaults to save the 9 keys that we got from this response. What are the suggested ways of approach we can go about this where user logged in, we save these details as an object globally and even when after closing the app, the details are not reseted? Thank you all

I agree that saving 9 individual keys in UserDefaults is not practical. But why not encode the whole thing as JSON and save the resulting Data?
extension User: Codable { }
// Assuming user is an instance of User
guard let userJSON = try? JSONEncoder().encode(user) else {
// handle encoding failure
}
let userDefaultsKeyForUser = "com.yourdomain.snazzyapp.userInfo"
UserDefaults.standard.set(userJSON, forKey: userDefaultsKeyForUser)
I think Swift will automatically synthesize Codable conformance for your User class, but you might need to make it final. Or implement Codable explicitly.
To retrieve it
guard let userJSON = UserDefaults.standard.data(forKey: userDefaultsKeyForUser) else {
// Handle missing data (first run maybe?)
}
guard let user = try? JSONDecoder().decode(User.self, from: userJSON) else {
// Handle decoding failure
}
Alternative encoding/decoding methods
Although conforming to Codable would be the preferred way to do this, that can sometimes be a problem for classes, particularly when inheritance is involved. In particular Codable is a combination of Encodable and Decodable. Decodable is the one that is sometimes a problem for classes. The issue has to do with the required init(from decoder: Decoder) throws having to be declared directly in the class, not in an extension, and then you might have trouble with encoding the super. If your class is the base class that shouldn't be a problem. This is mainly a problem when you inherit from AppKit/UIKit classes.
If for some reason User can't conform to Codable, you can use NSCoding instead. It works a little differently.
In the worst case you could implement methods where you manually encode/decode each property explicitly to data. You could even store them as binary, but you'll probably need to do something like store byte counts for the strings so you know where each one starts and ends, and you'll need to come up with a scheme to indicate when they are nil in the encoding. It's not as flexible as JSON, which is why it's a last resort... although I will note that it is much faster. That wouldn't matter much for your User class, but I wrote a neural network library where I wanted to save the model at checkpoints during training, which meant encoding many layers of very large matrices. Millions of parameters. Reading and writing the model was about 20x faster with my own binary encoding than letting Codable handle it, even when I had Codable saving it as a binary plist.
A third option would be to re-work your User class. Either make it a struct, or use a struct internally to store its properties and use computed properties to get and set them in your class. All of your class's properties already conform to Codable, so Swift can definitely synthesize Codable conformance for a struct with those properties. If you still wrap the struct inside of a class, then you just have an initializer that takes a Data, and do the same decoding I showed above, to set the wrapped struct, and an encode method (or even a computed var) that encodes the struct as above and returns the Data.
I don't think you'll need these alternate solutions, but I mention them just in case I'm wrong, and because they're useful to know about for future situations.

You have to ask yourself how you would like to access this object with compilator autocompletion from different places in your app. Secondly, what is the life span of the object? The object should be accessible during login / logout, app session or app life time?
Let's start with the object life span.
login / logout, the object can be stored in memory
app session, the object can be stored in memory
app install / delete, the object should be stored in UserDefaults, Database or a file.
Autocompletion Driven Development aka ADD:
Deciding where to store userDetails object and how to access it
Let's say that you have a logic in a view controller which hide or unhide a view in a initial configuration defined in viewDidLoad. Write some prototype code to decide how you would like to access the 'userDetails' object from different places of the app.
func viewDidLoad() {
super.viewDidLoad()
if userDetails.status {
//userDetails is a global instance of a user object
}
if session.userDetails.status {
//session is a global instance of a Session object which has a property which stores a User instance in userDetails
}
if Session.current.userDetails.status {
// Session is a type and 'current' is a singleton of that type which has a userDetails property which stores an instance of User type
}
if User.userDetails.status {
// User is your type which can have a static property 'userDetails' which stores a User instance
}
}

Related

Dynamically retrieving data of different types based on user input using Swift

New to Swift, I am trying to build an iOS app that allows a user to construct dynamic reports. I have created several model classes meant to represent various types of data to report on, e.g. Widgets and Gadgets, and associated service classes for these models containing the logic needed to retrieve their data. Examples for a model and service here:
class Widget: BaseModel, Equatable, Identifiable {
let id: UUID
let value: Int
let timeStamp: Date
init(rawData: RawData) {
self.value = rawData.quantity
self.timeStamp = rawData.date
self.id = UUID()
}
func toString() -> String {
return "\(self.value) widgets"
}
static func == (lhs: Widget, rhs: Widget) -> Bool {
return lhs.id == rhs.id
}
}
class WidgetService: BaseService {
let queryService: QueryService = QueryService()
func fetchSamplesInDateRange(dates: (startDate: Date, endDate: Date)?) async -> [Widget] {
// get samples and return them
}
}
These are based off of two protocols with associated types, BaseModel and BaseService. What I'm trying to do in this very early proof of concept stage is implement a simple view that will allow a user to select a type of data (corresponding to one of my Models) and run a query for all data for that Model within a date range. But I am completely stuck on how to implement the model selection.
I have a basic view file I've been working with that just has one button that executes a fetch function, populating a state variable results.
func fetchData() async -> Void {
let modelService: some BaseService = // not sure what to do here
let result: [some BaseModel] = await modelService.fetchSamplesInDateRange(dates: nil)
results = result
}
Because BaseModel and BaseService have associated types they can't be used in the way above as type constraints, I know, I put them there to give an idea of what I'm trying to do.
I had thought to put the class names as strings, choosable via options in a Picker, and then use something like NSClassFromString to get the right class for the service, but ultimately this just doesn't seem workable as I have to declare the concrete service class type at some point, this solution just moves around where I run into the Protocol 'BaseModel' can only be used as a generic constraint because it has Self or associated type requirements error.
Any idea what I'm missing here? This sort of thing using base classes, extensions, generics, just doesn't seem like it should be this difficult as other languages handle it fine (though admittedly with less type safety).

Swift 5 Default Decododable implementation with only one exception

Is there a way to keep Swift's default implementation for a Decodable class with only Decodable objects but one exception?
So for example if I have a struct/class like that:
struct MyDecodable: Decodable {
var int: Int
var string: String
var location: CLLocation
}
I would like to use default decoding for int and string but decode location myself.
So in init(from decoder:) i would like to have something like this:
required init(from decoder: Decoder) throws {
<# insert something that decodes all standard decodable properties #>
// only handle location separately
let container = try decoder.container(keyedBy: CodingKeys.self)
location = <# insert custom location decoding #>
}
Is there a way to keep Swift's default implementation for a Decodable class with only Decodable objects but one exception
Unfortunately no. To be Decodable all properties must be Decodable. And if you are going to write a custom init you must initialize (and therefore decode) all properties yourself.
Apple knows this is painful and has given some thought to the matter, but right now a custom init for a Decodable is all or nothing.
As has been suggested in a comment you might work around this by splitting your struct into two separate types. That way you could have a type with just one property, you initialize it manually, and you’re all done.

Advantages/Disadvantages of parsing data using initializer vs computed properties

Let's say I have a json data object I would like to parse into an object of my own. I've came across two ways of doing this. The first is to use an initializer like so:
class DataModelOne {
let firstProperty: String?
let secondProperty: String?
init(json: [Sting: AnyObject]) {
self.firstProperty = json["firstProperty"] as? String
self.secondProperty = json["secondProperty"] as? String
}
}
This would be called like so:
let object = DataModelOne(json: data)
where data is your JSON that you are trying to parse.
The second method that I have come across is by using computed properties:
class DataModelTwo {
let json: [String: AnyObject]
init(json: [String: AnyObject]) {
self.json = json
}
var firstProperty: String? {
return json["firstProperty"] as? String
}
var secondProperty: String? {
return json["secongProperty"] as? String
}
}
This would be initialized in the same way as above:
let object = DataModelTwo(json: data)
Aside from the fact that you couldn't set the properties once the data has been cast using DataObjectTwo, as these are computed properties and so get-only, what are the advantages/disadvantages of parsing using these two methods?
I don't see any real advantage to the computed properties. I prefer the first option, because:
This decouples your data model from the JSON. With the first option you can instantiate a DataModelOne without any JSON, and assign properties manually.
It's clearer because the parsing happens in one place.
You don't have to keep the dictionary around like you do with the computed properties. So if the dictionary contains a lot of other data that can be discarded, it can free up some memory.
The only advantage of the computed properties I can think of is that it delays accessing the Dictionary to the last moment. If you never access a property, it will never have to reach into the Dictionary in the first place. But the increase in performance will be negligible.
Lastly, I would rename the initialiser to something like init(values:). You're not initialising it with JSON, but with a plain Swift Dictionary.

Swift 4 What is the best way to store an array of custom types persistently?

I have an array which contains objects that each have custom and non-primitive type properties like URL and StorageReference (Firebase). What is the best way to store the contents of this array persistently? I thought of using Realm, but Realm only stores objects that have primitive type properties. The amount of objects retrieved from persistent storage will continue to increase as the app is used more because there will be more and more items to retrieve from Firebase.
Realm supports the following property types: Bool , Int , Int8 , Int16
, Int32 , Int64 , Double , Float , String , Date , and Data.
So, ideally you should be able to cover any custom types by converting them to a suitable primitive type.
For instance, you can save your URL as a String while saving to Realm and convert back when retrieved.
If you don't want to include third party frameworks you can use the Codable protocol and save things with NSKeyedArchiver. https://developer.apple.com/documentation/foundation/nskeyedarchiver
Simple example:
struct A: Codable {
var myString: String? // primitive types are already codable
}
struct B: Codable {
var myInt: Int?
var a: A? // custom types need to be Codable
}
// save
let myObject = B()
NSKeyedArchiver.archiveRootObject(myObject, toFile: "/path/to/archive")
// load
let myObject = NSKeyedUnarchiver.unarchiveObjectWithFile("/path/to/archive") as? B
If your custom type is more complex you might want to implement the custom encode/decode methods to have more control over how you data is saved and loaded. https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

How to record date, time, and score in swift

I am creating a simple quiz app. I am planning to show some kind of "history" where the user can see the following:
Date and time of playing
Score for that particular session
How do I do that?
As of the Date and Time of playing, I saw this thread on SO: How to get the current time as datetime
However, how do I "RECORD" the date(s) and time(s) the user played the game?
Regarding the Score data, I am using:
NSUserDefaults.standardUserDefaults().setInteger(currentScore, forKey: "score")
However, I am only able to get the CURRENT SCORE. How do I record the score(s) the user got for EACH session on different date(s) and time(s)?
Please note that I have no problem in getting the user's CURRENT SCORE. I need help in storing or recording the user's score(s) in multiple sessions.
For instance, I wanted to display something like this:
Date: 2/7/16
Time: 7:00 AM
Score: 70/100
NSUserDefaults probably isn't right for what you are trying to do. I recommend using NSCoding for simple data storing. Core Data may be too complicated for something this simple. However, if you plan on saving a large data model with relationships, Core Data is the way to go.
NSCoding
NSCoding has two parts:
Encoding and decoding
Archiving and unarchiving
NSHipster explains this perfectly:
NSCoding is a simple protocol, with two methods: -initWithCoder: and encodeWithCoder:. Classes that conform to NSCoding can be serialized and deserialized into data that can be either be archived to disk or distributed across a network.
That archiving is performed by NSKeyedArchiver and NSKeyedUnarchiver.
Session
Even without NSCoding, it is suggested to represent data with objects. In this case, we can use the very creative name Session to represent a session in the history.
class Session: NSObject, NSCoding {
let date: NSDate // stores both date and time
let score: Int
init(date: NSDate, score: Int) { // initialize a NEW session
self.date = date
self.score = score
super.init()
}
required init?(coder aDecoder: NSCoder) { // decodes an EXISTING session
if let decodedDate = aDecoder.decodeObjectForKey("date") as? NSDate {
self.date = decodedDate
} else {
self.date = NSDate() // placeholder // this case shouldn't happen, but clearing compiler errors
}
self.score = aDecoder.decodeIntegerForKey("score")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(date, forKey: "date")
aCoder.encodeInteger(score, forKey: "score")
}
}
The above code in English, in order from top to bottom:
Defining the class, conforming to NSCoding
The properties of a session: the date (+ time) and the score
The initializer for a new session - simply takes a date and score and creates an session for it
The required initializer for an existing session - decodes the date and score that is saved
decodeObjectForKey: simply does what it says (decodes an object using a key), and it returns AnyObject?
decodeIntegerForKey:, however, returns Int. If none exists on file, it returns 0, which is why it isn't optional. This is the case for most of the decoding methods except for decodeObjectForKey:
The required method for encoding an existing session - encodes the date and score
The encoding methods are just as straightforward as the decoding methods.
That takes care of the Session class, with the properties ready for NSCoding. Of course, you could always add more properties and methods.
SessionHistory
While the sessions itself are nice, an object to manage the array of sessions is needed, and it also needs to conform to NSCoding. You could also add this code to an existing class.
class SessionHistory: NSObject, NSCoding {
var sessions = [Session]()
required init?(coder aDecoder: NSCoder) {
if let decodedSessions = aDecoder.decodeObjectForKey("sessions") as? [Session] {
self.sessions = decodedSessions
} else {
self.sessions = [] // another compiler error clearer
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(sessions, forKey: "sessions")
}
override init() { // Used for convenience
super.init()
}
}
English translation:
Defining the manager, conforming to NSCoding
Add property for the array of sessions
Next two NSCoding methods do nearly the same thing as Session. Except this time, it is with an array.
Initializer for a new manager, which will be used below.
NSCoding looks at this manager class and sees that it needs to encode an array of sessions, so then NSCoding looks at the Session class to see what to encode for those sessions.
NSKeyedArchiver/NSKeyedUnarchiver and Singletons
While all the NSCoding is set up now, the final step is to incorporate NSKeyedArchiver and NSKeyedUnarchiver to actually save and load the data.
The two important methods are NSKeyedArchiver.archiveRootObject(_, toFile:) and NSKeyedUnarchiver.unarchiveRootObjectWithFile:
Note that both methods need a file. It automagically creates the file for you, but you need to set a location. Add this to SessionHistory:
static var dataPath: String {
let URLs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let URL = URLs[0]
return URL.URLByAppendingPathComponent("savehistory").path! // Put anything you want for that string
}
That simply finds a location for the file. You could, of course, find somewhere else to put the file.
With the data path ready, you can use the two methods I mentioned earlier. I like to use a modified version of a singleton for the manager class to make sure I'm using the same array of objects. In the SessionHistory class:
private static var history: SessionHistory!
static func appHistory() -> SessionHistory {
if history == nil {
if let data = NSKeyedUnarchiver.unarchiveObjectWithFile(dataPath) as? SessionHistory {
history = data
} else {
history = SessionHistory()
}
}
return history
}
This creates a private static property to store the one session history of the app. The static method checks if the session history is nil. If so, it returns the current history on file and loads the file into the history property. Otherwise, it creates a new empty session history. After that, or if the history property already stores something, it returns the history property.
Usage
All the setup for NSCoding and NSKeyedArchiver is done. But how do you use this code?
Each time you want to access the session history, call
SessionHistory.appHistory()
Wherever you want to save the session history, call
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
Sample usage would work like this:
let session = Session(date: someRandomDate, score: someRandomScore)
SessionHistory.appHistory().sessions.append(session)
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
The session history will automatically be loaded from the file when accessed via SessionHistory.appHistory().
You don't really need to "link" the classes per se, you just need to append the sessions to the sessions array of the session history.
Further Reading
NSHipster is a good, simple introduction to NSCoding.
Apple's NSCoding guide, although older and in Objective-C, goes deeper into NSCoding.
To store scores for each session, you'll need some sort of data structure associated with each user. You could use a dictionary of key value pairs that associates a date with a score. That way you would have a score for each date stored for the user.
You need to use a database to store such data with an undefined count of records.
Check out the Core Data Programming Guide by Apple here
You could then create an entity named history where you store records of the user's game play history, by inserting a new object into the entity every time the user finishes a game.
When you need to show the results, you'd create an NSFetchRequest over an NSManagedObjectContext to get all the results and display/filter them as you'd like.
Hope that helps!

Resources