How to use NSCache - ios

Can someone give an example on how to use NSCache to cache a string?
Or anyone has a link to a good explanation? I can't seem to find any..

You use it the same way you would use NSMutableDictionary. The difference is that when NSCache detects excessive memory pressure (i.e. it's caching too many values) it will release some of those values to make room.
If you can recreate those values at runtime (by downloading from the Internet, by doing calculations, whatever) then NSCache may suit your needs. If the data cannot be recreated (e.g. it's user input, it is time-sensitive, etc.) then you should not store it in an NSCache because it will be destroyed there.
Example, not taking thread safety into account:
// Your cache should have a lifetime beyond the method or handful of methods
// that use it. For example, you could make it a field of your application
// delegate, or of your view controller, or something like that. Up to you.
NSCache *myCache = ...;
NSAssert(myCache != nil, #"cache object is missing");
// Try to get the existing object out of the cache, if it's there.
Widget *myWidget = [myCache objectForKey: #"Important Widget"];
if (!myWidget) {
// It's not in the cache yet, or has been removed. We have to
// create it. Presumably, creation is an expensive operation,
// which is why we cache the results. If creation is cheap, we
// probably don't need to bother caching it. That's a design
// decision you'll have to make yourself.
myWidget = [[[Widget alloc] initExpensively] autorelease];
// Put it in the cache. It will stay there as long as the OS
// has room for it. It may be removed at any time, however,
// at which point we'll have to create it again on next use.
[myCache setObject: myWidget forKey: #"Important Widget"];
}
// myWidget should exist now either way. Use it here.
if (myWidget) {
[myWidget runOrWhatever];
}

#implementation ViewController
{
NSCache *imagesCache;
}
- (void)viewDidLoad
{
imagesCache = [[NSCache alloc] init];
}
// How to save and retrieve NSData into NSCache
NSData *imageData = [imagesCache objectForKey:#"KEY"];
[imagesCache setObject:imageData forKey:#"KEY"];

Sample code for caching a string using NSCache in Swift:
var cache = NSCache()
cache.setObject("String for key 1", forKey: "Key1")
var result = cache.objectForKey("Key1") as String
println(result) // Prints "String for key 1"
To create a single app-wide instance of NSCache (a singleton), you can easily extend NSCache to add a sharedInstance property. Just put the following code in a file called something like NSCache+Singleton.swift:
import Foundation
extension NSCache {
class var sharedInstance : NSCache {
struct Static {
static let instance : NSCache = NSCache()
}
return Static.instance
}
}
You can then use the cache anywhere in the app:
NSCache.sharedInstance.setObject("String for key 2", forKey: "Key2")
var result2 = NSCache.sharedInstance.objectForKey("Key2") as String
println(result2) // Prints "String for key 2"

sample Project
Add CacheController.h and .m file from the sample project to your project. In class where you want to cache data , put the below code.
[[CacheController storeInstance] setCache:#"object" forKey:#"objectforkey" ];
you can set any object using this
[[CacheController storeInstance] getCacheForKey:#"objectforkey" ];
to retrive
Important: The NSCache class incorporates various auto-removal policies. if you want cache the data for permanent or you want to remove cached data in a specific time see this answer.

Shouldn't the cached objects implement the NSDiscardableContent protocol?
From the NSCache class reference:
A common data type stored in NSCache objects is an object that implements the NSDiscardableContent protocol. Storing this type of object in a cache has benefits, because its content can be discarded when it is not needed anymore, thus saving memory. By default, NSDiscardableContent objects in the cache are automatically removed from the cache if their content is discarded, although this automatic removal policy can be changed. If an NSDiscardableContent object is put into the cache, the cache calls discardContentIfPossible on it upon its removal.

Related

Updating a property in a struct inside an array

In my app I download a load of JSON.
I then store that as an array of structs and use that to populate a UITableView.
One of the properties of the struct is an NSURL for an image. Another property is an optional UIImage.
The struct has a mutating function downloadImage which uses the URL to download the image and store it in its property.
Like this...
struct SearchItem {
// other properties...
let iconURL: NSURL
var icon: UIImage?
mutating func downloadImage() -> Task<UIImage> {
let tsc = TaskCompletionSource<UIImage>()
NSURLSession.sharedSession().downloadTaskWithURL(iconURL) {
(location, response, error) in
if let location = location,
data = NSData(contentsOfURL: location),
image = UIImage(data: data) {
self.icon = image
tsc.setResult(image)
return
}
tsc.setError(NSError(domain: "", code: 1, userInfo: nil))
}.resume()
return tsc.task
}
}
The problem I'm having is this. (and I have been stumped by this in the past).
I have an array [SearchItem] that I use to populate the tableview.
In cellForRow I have the code... if let searchItem = items[indexPath.row]...
It then checks if the image is nil and downloads...
if let image = searchItem.icon {
cell.imageView.image = image
} else {
searchItem.downloadImage().continueOnSuccessWith(Executor.MainThread) {
_ in
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
}
But this never goes through to put the image into the cell. This is because the SearchItem is struct and so pass-by-value. So the search item that I am downloading the image for is not the same SearchItem as the one stored in the array.
How can I ensure that the image that is downloaded is then stored into the SearchItem inside the actual array?
Use classes.
You're getting a copy of searchItem in your cellForRow method. Whatever you do to this, will be done only to that copy. What you actually want is for the changes you make to that copy to be applied to the version in the array.
Therefore you want reference semantics, therefore use classes.
You could dance around re-inserting the updated copy into the original array if you liked, but what does that gain you besides a line of extra code and probably some other problems.
Structs are lightweight data objects that are not passed by reference, but instead copies itself as needed when you a) pass it to a new function, b) try and access it in a block. Arrays in Swift also work slightly differently than their Obj-C counterparts. When you have an Array of class objects the array will be a reference type, and you'll be able to achieve what you're trying to achieve here. But on the other hand if the Array is of Structs the array looses its reference semantics and uses copy-by-value instead.
This difference is really powerful when used appropriately, you can greatly optimise your code, make it run faster, have less errors produced by mutable object references having changes happen in multiple parts of your code, etc. But it's up to you as a developer to see where the gains of these optimisations are useful or where it makes sense to use objects instead.

Core Data deleteObject sets all properties to nil

I'm a little new CoreData.
When I call deleteObject() with object on my NSManagedContext object, its setting all the properties in the object to nil. Is there anyway for me to avoid this? I don't want to be nullified.
My project is in Swift.
You're misunderstanding the purpose of CoreData. It's a way of managing a persistent store, which means that whatever you tell your context, is absolute. So if you deleteObject(), that object gets prepared for deletion and you're not supposed to touch it anymore.
Instead, you want some kind of mirror object, that allows you to create a new copy of the NSManagedObject for in-memory use. You could do it like this;
struct MirrorManaged {
var text: NSString
}
class Managed: NSManagedObject {
#NSManaged var text: NSString
func copyToMemory() -> MirrorManaged {
return MirrorManaged(text: self.text)
}
}

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!

How do I access a json array created in a function across my application?

This has to be easy and I'm just missing something. I have a function in my application that is posting to a server, getting a response, and turning that response into an NSDictionary, and then turning one of the values sets in that NSDictionary into an NSArray. I need to access the values in this array outside of the scope of the function that created them. How can I make a json response available to my whole application if it is created within a specific function? Thanks!
There's a few ways you can do this, one of them is, as #GoodSp33d mentioned, NSUserDefaults another way is by using a completion handler and if you are open to using singletons, that's an option as well.
NSUserDefaults
// settings
var jo : [NSObject : AnyObject] = [
"a" : "1.0",
"b" : "2.0"
]
// store your value
userDefaults.setObject(jo, forKey: akey)
// optionally synchronize
var isOk = userDefaults.synchronize()
// safely retrieve your value
if let data0 = userDefaults.dictionaryForKey(akey) {
print(data0)
} else {
print("not set")
}
(source: Martin R)
note: this method actually persists the data to local storage making
it available across multiple sessions
#diatrevolo added:
It should be stated for consistency purposes that NSUserDefaults are
not always persistent across platforms. tvOS user defaults, for
example, get purged by the system
Singleton Method
There are a lot of people, myself included, that prefer not to use this approach but other consider this perfectly valid. This is the singleton approach. Setting your value to this property makes it globally available within your app.
struct sharedResult {
static var sharedDict: NSDictionary = nil
}
(more singleton approaches)
note: this method holds your data for the session only.
Completion Handler
However, I personally like to wrap my HTTP Requests in a closure / completion handler and when I have my object back from the server, I pass it into the completion handler.
// definition
func HTTPConnect(completion: (result: Bool)->()) {
// make http request {
// once you have your result pass it back
completion(result: myResult)
// }
}
// call from where you need the data
HTTPConnect() { (result) -> () in
// do stuff with the result
print(result)
}
Note: this method doesn't hold your data at all. It does, however, make it
easy for you to pass your value from one controller to another.

NSManagedObject timeStamp update

I want to track changes of NSManagedObject properties, in order to keep NSData *lastUpdate property "up to date"
There are several approaches to get Notified when NSManagedObject changes its properties
I. First is to override the setter Methods of all properties you want to track. Which is quite complicated in NSManaged object - check it here
II. Second could be a good one. You can just override "didChangeValueForKey" method That is called on every property change.
-(void)didChangeValueForKey:(NSString *)key{
[super didChangeValueForKey:key];
NSLog(#"Value for key:%# has changed", key);
}
Unfortunately we should not override this method due to the documentation that says...:
"You must not override this method."
III. Key-value-observing leads us back to IInd approach, with overriding "didChangeValueForKey".
upd.
IV. I tried to override -willSave method
-(void)willSave{
NSArray *observedKeys = #[#"name", #"imageType"];
NSDictionary * changesALL = self.changedValues;
for (id key in changesALL){
if ([observedKeys containsObject:key]){
self.lastUpdate = [NSDate date];
NSLog(#"updated For key: %#", key);
}
}
}
This led infinitive loop, which is described in documentation.
(altho the right approach is described here, so I've answered this question already)
If you want to update a persistent property value, you should typically test for equality >of any new value with the existing value before making a change. If you change property >values using standard accessor methods, Core Data will observe the resultant change >notification and so invoke willSave again before saving the object’s managed object >context. If you continue to modify a value in willSave, willSave will continue to be called >until your program crashes.
For example, if you set a last-modified timestamp, you should check whether either you >previously set it in the same save operation, or that the existing timestamp is not less >than a small delta from the current time. Typically it’s better to calculate the timestamp >once for all the objects being saved (for example, in response to an >NSManagedObjectContextWillSaveNotification).
A suitable solution for your use case to override the willSave method and use it to set the new lastUpdated value. This method is called automatically on dirty objects before they are saved into the context.
If you need to verify what is dirty you can use the contents of the changedValues property.
So after all I figured out that the best solution to track changes of Managed Object is to register for NSManagedObjectContextWillSaveNotification instead, and set the timestamp on all updated and inserted objects in the managed object context. The registered method could look like this:
-(void)contextWillSave:(NSNotification *)notify
{
NSManagedObjectContext *context = [notify object];
NSDate *dateOfTheLastModification = [NSDate date];
for (NSManagedObject *obj in [context insertedObjects]) {
[obj setValue:dateOfTheLastModification forKey:#"lastUpdate"];
}
for (NSManagedObject *obj in [context updatedObjects]) {
[obj setValue:dateOfTheLastModification forKey:#"lastUpdate"];
}
}
This assumes that all your entities have a lastModifiedDate attribute, otherwise you have to check the class of the objects.
To avoid the infinite loop, try this magic:
- (void)willSave{
if(![self.changedValues objectForKey:#"localModificationDate"]){
self.localModificationDate = [NSDate date];
}
else{
[super willSave];
}
}
Once the modification date has been set it won't set it again for the current save. There is a side affect that if the save fails and you save successfully again, I reckon the date will be the from the previous save attempt.
This is fine if you are saving the context after every edit, but the usual design of core data is to only save either at app suspend or after a long time. So it's likely the lastUpdate will be needed for something before then and it won't have the new value yet.

Resources