Swift cache tableview json data - ios

I wonder if its possible to cache tableView JSON data?
In my VC I have this variable:
//Categories json data
var categories: JSON! = []
Then later inside a alamofire api call I get the json data and assign it like:
self.categories = JSON["catData"]
self.tableView.reloadData()
But is there any way to cache this data so I dont have to make a API call everytime?

You can create a singleton DataCache class where you can store all the data you want to cache. Prefer to store data inside a dictionary for specific key
class DataCache: NSObject {
static let sharedInstance = DataCache()
var cache = [String, AnyObject]()
}
Now in your api call, call this method
DataCache.sharedInstance.cache["TableViewNameKey"] = JSON["catData"]
self.categories = JSON["catData"] // Set this property for first time when you hit API
and in viewWillAppear(animated: Bool) method
if let lCategories = DataCache.sharedInstance.cache["TableViewNameKey"] {
self.categories = lCategories
}

Swiftlycache can be used for data caching. You can directly cache the data that complies with the codable protocol. It's very convenient. If you need it, you can try it
https://github.com/hlc0000/SwiftlyCache

Related

Save User model in UserDefaults and append stored Users to array

I have a User model declared like this:
struct User: Codable {
let nickname : String
let fullName: String
let profilePicUrl: URL
}
Then I save the followed user like this
let followedUser = User(nickname: username, fullName: fullName, profilePicUrl: url)
UserDefaults.standard.set(try! PropertyListEncoder().encode(followedUser), forKey: "user")
Next, in the ViewController which should display the followed users count I retrieve the UserDefaults data like this
var followedUsers = [User]()
if let storedObject: Data = UserDefaults.standard.object(forKey: "user") as? Data {
do {
let storedUser: User = try PropertyListDecoder().decode(User.self, from: storedObject)
followedUsers.append(storedUser)
} catch {
print(error)
}
}
Now, my followedUsers array is being updated with the last stored user each time. How can I append my followedUsers array in a proper way?
If you use Codable , then it make sense to use JSONEncoder & JSONDecoder and store the users as an array
do {
// How to save multiple records
var followers = [User]() // Your array of users
let data = try JSONEncoder().encode(followers) //Convert to Data
UserDefaults.standard.set(data, forKey: "data") // Save to User Default
// How to fetch multiple records
// Check If data available in user default
if let da = UserDefaults.standard.data(forKey: "data") {
// Convert Data back to your cod-able struct
let stored = try JSONDecoder().decode([User].self, from: da)
}
}
catch {
print(error)
}
struct User: Codable {
let nickname , fullName: String
let profilePicUrl: URL
}
Don't encourage using userDefault , you can go with CoreData / Realm
As #Sateesh and #Sanjukta said, storing the users in an array and saving that to UserDefaults is one solution.
The reason why your followedUsers array is updated with only one user, is that when you create a new user, you update the same key in UserDefaults each time.
It should be noted that UserDefaults is mainly used to store small amounts of data. Depending on how many User objects you plan to store, UserDefaults might not be the best solution.
The two solutions for data storage that come to mind are Core Data and Realm. Core Data is an Apple framework with which you can storage data on device. You can read more about it here: Core Data
The second option is to use Realm. Realm is is an open source database management system and can be used for on-device data storage too.
If you are interested in the differences between Realm and Core Data, for starters I can recommend this article.
Personally I use Realm in my own projects because I find it easier to implement and faster than Core Data. Please note that this is just my personal preference and others might disagree. Make sure you listen to other opinions and experiment with both before picking sides.
So let's suppose you want to store the User in Realm, you would need to do the following:
Install Realm for your project.
In your podfile add the following: pod 'RealmSwift'.
Run pod install in terminal and use the newly created .xcworkspace from now on.
In Xcode, in AppDelegate import RealmSwift and insert the following code to the didFinishLaunchingWithOptions method:
do {
_ = try Realm()
} catch {
print("Error initialising new Realm \(error)")
}
This initializes a new default Realm, where your User data will be saved.
Create a new User.swift file. In that, import RealmSwift and insert the following:
class User: Object {
#objc dynamic var nickName: String = ""
#objc dynamic var fullName: String = ""
#objc dynamic var profilePicURL: URL?
}
This creates a Realm object, which will contain the data for your users.
Save the followed user like this:
In the view controller where you want to save the user data import RealmSwift, and under the class declaration create a new instance of realm by let realm = try! Realm()
When you have the user data, save to Realm with the following code:
:
let followedUser = User()
do{
try realm.write {
followedUser.nickName = username
followedUser.fullName = fullName
followedUser.profilePicURL = url
realm.add(followedUser)
}
} catch {
print("Error saving to persistent container! \(error)")
}
In the view controller where you need the user data, create an instance of Realm just like before with let realm = try! Realm()
, retrieve users from Realm with the following code: let followedUsers = realm.objects(User.self)
This retrieves all Users from the default realm.
If you need to count the followedUsers you can do so by: followedUsers.count
Hopes this approach helps you to achieve what you wanted in the first place.
Please make the array of object then store the array in userDefaults.
After that you can easily retrive it.
It may help you. Thank you.

Passing generic Class as argument to function in swift

Below is my method in which there is fetch I make on a Managed object Class Appointment. I need to use same function for other similar managed object Classes. How do I pass different "Class" as parameter every time as I need. And also use it to fetch as I have currently "Appointment" Class. I might need to use Generics may be. Dont know how though.
func getAppointmentArray(aPredicate : String , aModel : Any) -> [Any]
{
var apptArr = [Any]()
let fetchRequest = NSFetchRequest<Appointment>(entityName: "Appointment")
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.predicate = NSPredicate(format: aPredicate)
do{
let records = try managedObjectContext.fetch(fetchRequest)
if let records = records as? [NSManagedObject]{
if !records.isEmpty{
print("coreData apptmnt result : \(records)")
var appointment : Appointment?
for obj in records
{
}
}else{
print("No records found")
apptArr = []
}
}
}catch{
print("Error")
apptArr = []
}
return apptArr
}
The good folks at Objc.io provide a really good approach for this. First declare a protocol which inherits 'NSFetchRequestResult' protocol as below.
protocol Managed: class, NSFetchRequestResult {
static var entityName: String { get }
}
Now we can provide a very convenient protocol extension for our protocol 'Managed'.
We do the check 'Self: NSManagedObject' as we want the static method entity() of the NSManagedObject class to get the 'NSEntityDescription' describing the entity associated with our class. Particularly this way we get the entity name dynamically(and conveniently too) for all our ManagedObjects that conform to our protocol.
extension Managed where Self: NSManagedObject {
static var entityName: String { return entity().name! }
}
We now improve the protocol extension by providing a method which conveniently creates a fetch request and then calls a configuration block which might be used to configure the created fetch request by whoever calls it. At the end of this method we do a fetch using the created request.
extension Managed where Self: NSManagedObject {
static var entityName: String { return entity().name! }
//Continued
static func fetch(in context: NSManagedObjectContext, configurationBlock: (NSFetchRequest<Self>) -> ()) -> [Self] {
let request = NSFetchRequest<Self>(entityName: Self.entityName)
configurationBlock(request)
return try! context.fetch(request)
}
}
As you can see we do the following things here:
We make good use of protocols and protocol extensions for making our life easy.
We get the entity name without needing to write a method for each concrete managed object class that we might create. This is reusable for every managed object class that will conform to 'Managed'
The fetch method that we wrote makes use of the dynamic and convenient entityName.
The fetch method again makes use of Self which is implementation independent here. This way we make FetchRequests which are generic in itself.
We provide a convenient way to configure the request to whoever calls this method.
And at atlast we return result after fetching which is also dynamic [Self]
To see our protocol in action we can do this for your case:
class Appointment: NSManagedObject, Managed{
//properties for attributes
//etc...
//Will I get free implementation for entity name and a fetch method
//without writing extra code ?
//Yes why not
}
Testing our hard earned knowledge:
let aPredicate = "......
let context: NSManagedObjectContext.....
let appointments = Appointment.fetch(in: context) { fetchRequest in
//Configuration code like adding predicates and sort descriptors
fetchRequest.predicate = NSPredicate(format: aPredicate)
}
You can use the same pattern for any other ManagedObjects if they conform to the protocol. Eg a Doctor ManagedObject subclass conforming to our Managed protocol:
let aPredicate = "......
let context: NSManagedObjectContext.....
let doctors = Doctor.fetch(in: context) { fetchRequest in
//Configuration code like adding predicates and sort descriptors
fetchRequest.predicate = NSPredicate(format: aPredicate)
}
for the generic you can do something like this:
class FetchingDataHandler<T>{
func getAppointmentArray<T>(forClass : T, aPredicate : String , aModel : Any) -> [Any]
{
}
}

Trying to mutate array and object passed as parameters to an asynchronous function

So if you look at the code below you will see the following line fails with:
cannot use mutating member on immutable value: 'forecasts' is a 'let' constant
caused by forecasts.append(forecast). I have a service file which you see below called ForecastService and I am using the function to asynchronously grab JSON in my view controller from an API. I am trying to pass an object and an array of objects to the function which I then want to mutate on so I can use the updated values in my view controller to update my UI. I want to construct forecast objects and push them into the the array which I initialized in the view controller.
Any clarification on how I can get around this problem would be great.
import Foundation
import Alamofire
class ForecastService {
static let sharedInstance = ForecastService()
private init() {}
func downloadForecastDetails(forecast: Forecast, forecasts: [Forecast], completed: #escaping DownloadComplete) {
let url = URL(string: FORECAST_URL)!
Alamofire.request(url).responseJSON { response in
let result = response.result.value
if let dict = result as? Dictionary<String, AnyObject> {
if let list = dict["list"] as? [Dictionary<String, AnyObject>] {
for obj in list {
let forecast = Forecast(weather: obj)
forecasts.append(forecast)
}
}
}
completed()
}
}
}
The problem is not the asynchronous block, but the mechanism of parameter forecasts. It's copied when you call the function, even if you declare it as var. Because Array is Swift is value type, it's the Swift's policy. (https://developer.apple.com/swift/blog/?id=10)
You can try to call forecasts.append outside the asynchronous block, it will still fail. So you need another way to pass a reference type (not value type) into the function.

Why `NSUserDefaults` not allowed to store objects of Custom Classes?

I've seen that NSUserDefaults do not allow objects other than Array, Dictionary, String, NSData, NSNumber, Bool, NSDate. But
Why it is not allowed to store other objects?
What if I need to store some properties as a single object? If I do it using dictionary, I've to write the keys somewhere which I've used to store the value. So, what could be the other alternatives.
What were the problems will arise if Apple allows the other objects also to store.
What if we use CoreData instead NSUserDefaults. I Know NSUserDefaults is globally available.
What is the best way to make a value available globally even after we relaunched the app if get terminated?
As suggested by #Hoa, previously I forgot mention NSCoding option also
What if we have many properties in a custom class, Do we need to encode and decode all of them using NSCoding methods?
You can save any object to a plist file as long as it is compliant with the NSCoding protocol.
You can use code similar to this:
+(id) readObjectFromFile:(NSString*) sFileName
{
return [NSKeyedUnarchiver unarchiveObjectWithFile:sFileName];
}
+(bool) saveObject:(id <NSCoding>) anObject ToFile:(NSString*) sFileName
{
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:anObject];
NSError * error;
[data writeToFile:sFileName options:NSDataWritingAtomic error:&error];
if (error)
{
NSLog(#"Save Cats Data error: %#", error.description);
return NO;
}
return YES;
}
Swift Version:
func readObjectFromFile(sFileName: String) -> AnyObject {
return NSKeyedUnarchiver.unarchiveObjectWithFile(sFileName)
}
func saveObject(anObject: AnyObject, ToFile sFileName: String) -> Bool {
var data: NSData = NSKeyedArchiver.archivedDataWithRootObject(anObject)
var error: NSError
data.writeToFile(sFileName, options: NSDataWritingAtomic, error: error)
if error != nil {
print("Save Cats Data error: \(error.description)")
return false
}
return true
}
To learn more about the NSCoding protocol you can read:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSCoding_Protocol/
The intention behind NSUserDefaults is to save contextual data relevant to the application state, for example saving the user's preferences, the state of the application when the user stopped using it (so you can return to that state when it fires up), login session data, etc..
Core Data is a more appropriate way to store persistent data, you can map your data model as you like and has a broader variety of options to save datatypes.
Whilst NSUserDefaults is available "everywhere", this should not be a turning point to decide if this is a better option for saving your data.
You can write a singleton class that serves as a data provider so that you can access your data in the same way you access the NSUserDefaults shared instance. You just need to keep in mind that this class or module should do only one thing, serve as an interface between your model and your implementation, so you do not store any objects in this class, just use it to pass over the requests to get and save data to CoreData.
This class could look something like this:
class CoreDataProvider {
static let sharedInstance = SUProvider()
let managedObjectContext : NSManagedObjectContext
let sortDescriptor: NSSortDescriptor
let fetchRequest: NSFetchRequest
private init(){
managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
fetchRequest = NSFetchRequest(entityName: "MyObject")
sortDescriptor = NSSortDescriptor (key:"objectAttribute", ascending:true)
self.fetchRequest.sortDescriptors = [self.sortDescriptor]
}
func getSavedObjects() -> [MyObject]? {
fetchRequest.sortDescriptors = [sortDescriptor]
do {
return try self.managedObjectContext.executeFetchRequest(fetchRequest) as? [MyObject]
} catch {
print("no records found")
}
}
}
Which you would use like this:
func getAllRecords() {
let records = CoreDataProvider.sharedInstance.getSavedObjects()
//- Do whatever you need to do
}
A temporary way to store object is to create json strings of your dictionaries or arrays. I have used it in some low scale apps. You can then store those string in NSUserDefault.., and when you need to use it, you can extract it, and you can use Object Mapper library that will automatically map the json data to object type.
example, you can create a function in your class extension that parses json data to your objects any time you need it.
I would suggest using the above method only for small scale apps. If you are going for high traffic/large scale app, you might wanna look into Core Data or even SQlite3..
Feel free to ask any question
Reference to Object Mapper library is here

Realm and Parse integration: how to save the realm

I'm trying to create a bundle realm for my application. I thought it should be quite simple. Since I already have all needed records in Parse, I just have to:
create realm models and objects
load parse records to realm objects
save the realm
So, I created two realm models:
class RealmContainer : Object {
dynamic var rContainerId: String! //should be equal objectId from Parse
dynamic var rContainerName: String! //should be equal "name" field from Parse
...
var messages = List<RealmMessage>()
override static func primaryKey() -> String? {
return "rContainerId"
}
}
and
class RealmMessage : Object {
dynamic var rMessageId: String!
...
dynamic var rParentContainer: RealmContainer!
}
Getting results from Parse seems to be working. Also my realm objects are also good
var allUserContainers: [RealmContainer] = []
I was able to populate this array with values from Parse. But when I'm trying to save this realm, I'm getting a) nothing or b) error message
My code (this one'll get nothing):
let realm = try! Realm()
try! realm.write {
realm.add(self.allUserContainers[0])
print(Realm().path)
print(realm.path)
}
My code (this one'll get nothing too):
let realm = try! Realm()
try! realm.write {
realm.create(RealmContainer.self, value: self.allUserContainers[0], update: true)
print(Realm().path)
print(realm.path)
}
My code 3 (this will get me an error message "Terminating app due to uncaught exception 'RLMException', reason: 'Illegal recursive call of +[RLMSchema sharedSchema]. Note: Properties of Swift Object classes must not be prepopulated with queried results from a Realm"):
//from firstViewController, realm is a global variable
let realm = try! Realm()
//another swift module
try! realm.write {
realm.create(RealmContainer.self, value: self.allUserContainers[0], update: true)
print(Realm().path)
print(realm.path)
}
Obviously I don't properly understand how it should work, but I tried several swift/realm tutorials and they were actually straightforward. So, what did I do wrong?
Update
So, I updated my code to make it as much simple/readable as possible. I have a Dog class, and I am trying to get Dogs from Parse and put them to Realm.
AppDelegate.swift
let realm = try! Realm() //global
Dog.swift
class Dog : Object {
dynamic var name = ""
dynamic var age = 0
}
User.swift (getDogs and putDogs functions)
class User {
var newDogs:[Dog] = []
...
func getDogs() {
self.newDogs = []
let dogsQuery = PFQuery(className: "Dogs")
dogsQuery.limit = 100
dogsQuery.findObjectsInBackgroundWithBlock { (currentModes, error) -> Void in
if error == nil {
let tempModes:[PFObject] = currentModes as [PFObject]!
for var i = 0; i < tempModes.count; i++ {
let temp = Dog()
temp.name = currentModes![i]["dogName"] as! String
self.newDogs.append(temp)
}
} else {
print("something happened")
}
}
}
...
func putDogs() {
print(self.newDogs.count)
try! realm.write({ () -> Void in
for var i = 0; i < newDogs.count; i++ {
realm.add(newDogs[i])
}
})
try! realm.commitWrite() //doesn't change anything
}
error message still the same:
Terminating app due to uncaught exception 'RLMException', reason:
'Illegal recursive call of +[RLMSchema sharedSchema]. Note: Properties
of Swift Object classes must not be prepopulated with queried
results from a Realm
I believe I just have some global misunderstanding about how Realm is working because it is extremely simple configuration.
About your RealmSwift code : You have implemented it right.
When you declare a class of realm in swift, it's a subclass of Object class. Similarly for the parse it's subclass of PFObject.
You custom class have to have only one base class. So can't use functionalities of both the libraries Parse as well as Realm.
If you have to have use both Parse and Realm, you need to declare two different classes like RealmMessage and ParseMessage.
Retrieve data for ParseMessage from parse and copy properties to RealmMessage.
Why you want to use both Parse and Realm ?
parse also provides local data store by just writing Parse.enableLocalDatastore() in appDelegate before
Parse.setApplicationId("key",
clientKey: "key")
Your Realm Swift code seems fine. Realm is very flexible when creating objects using (_:value:update:), in being able to take in any type of object that supports subscripts. So you could even directly insert PFObject if the schemas matched.
The problem seems to be how you're populating the allUserContainers array. As the error says, you cannot fetch a Realm Object from Realm and then try and re-add it that way. If you're trying to update an object already in Realm, as long as the primary key properties match, you don't need to supply the whole object back again.
Can you please revisit the logic of how your allUserContainers variable is being populated, and if you can't fix it, post the code into your question?
Sidenote: You probably don't need to define your Realm properties as implicitly unwrapped as well. This is the recommended pattern:
class RealmContainer : Object {
dynamic var rContainerId = ""
dynamic var rContainerName = ""
}
Actually I found what was wrong: as i suspected it was a stupid mistake, some property in my class was UIImage type, and Realm just can't work with this type (even if I'm not trying to put objects of this class into realm). So, my bad. I am slightly embarassed by it, but will not delete my question because error messages from Realm were not very understandable in this case

Resources