I’ve got a pretty slow mobile connection, and often get frustrated when I have to stare at a spinner while I’m waiting for some app to load. A great way to improve this would be to save the loaded content into a persistent store, and display that content on next app launch.
This is how the iOS Weather app works. Try to put your phone into airplane mode and launch the app. You will see that it displays all the data it had the previous time you’ve launched the app, even though you’re not connected to the Internet. This leads to a much better user experience, since you’re giving users something to look at other than a spinner.
This is especially good with apps that have feeds from the network, since users can see cool content before more content loads.
Let’s say we have an ImageFetcher app that fetches some images from the network. We want our user to see the last fetched image (if there is one) while the new one is loading. I’ll show you how to save and fetch the last requested image using Core Data.
It consists of three main parts: The DataService
, the CoreDataService
and the NetworkService
.
The NetworkService
and CoreDataService
provide image data from the
network or from Core Data, respectively.
The DataService
is responsible for issuing requests to them both, and
passing the ViewController the retrieved data.
In other words, the ViewController
will issue a request for the data and
provide a callback. The DataService
will then at the same time ask both the
CoreDataService
and NetworkService
for the data.
Naturally, the local data will come first, (if there is any) and call the callback the first time.
After the network request is done, the callback will get called a second time
with the new fetched data. DataService
will then also update the app’s
local data with the data from the network.
Note: You could write all of this in one class/struct, but that will make it harder to make changes later on, and this provides a really clean way to think about your code.
Before we start, make sure you’ve created a project with the “Use Core Data” option checked, unless you want to write the necessary Core Data boilerplate.
We’ll start by implementing the NetworkService
. I won’t go too much into
detail here since network requests are not what this article is about.
The main thing to notice here is that our NetworkService
has a
getData(callback: (NSData)->Void)
method, which we’ll use to fetch data from the
network.
struct NetworkService {
func getImage(callback: (NSData)->Void) {
Alamofire.request(.GET, Constants.imageURL)
.responseData { response in
response.result {
case .Success(let data):
callback(data)
case .Failure(let error):
print(error)
}
}
}
}
This is what will save and retrieve our last network request. Before we can implement methods for doing so, we first need to set up Core Data.
We’ll start by making a new Core Data entity in our .xcdatamodeld file. We’ll name the entity “LastImage”, and add an Attribute called “lastImage”. This attribute will hold our image data. We’ll make its type Transformable since that will let us store NSData and use it later on.
Now that we have our entity set up, we need a way to actually store data within it. First we’ll make a property that holds a reference to our application’s managed object context. The context provides information about the data in our app.
We’ll then use that context to get access to our new entity, and hold a reference to it.
import UIKit
import CoreData
struct DataService {
private let context: NSManagedObjectContext
private let entity: NSEntityDescription
struct Keys {
static let lastImage = "lastImage"
static let entityName = "LastImage"
}
init() {
let application = UIApplication.sharedApplication()
let delegate = application.delegate as! AppDelegate
context = delegate.managedObjectContext
entity = NSEntityDescription.entityForName(
Keys.entityName,
inManagedObjectContext: context)!
}
}
We will also need the NSManagedObject of our image data. We’ll make a computed variable that points to that object.
We’ll do that by searching for our entity within the object context, and returning the first result we get.
struct DataService {
//...
var currentImageObject: NSManagedObject? {
let request = NSFetchRequest(entityName: Keys.entityName)
do {
if let results = try context.executeFetchRequest(request) {
return results.isEmpty ? nil : results[0]
} else {
return nil
}
} catch let error {
print("Error retrieving current entity: \(error)")
return nil
}
}
}
We can now easily access our image data *NSManagedObject. *We will save and retrieve data to and from this object. Since we’re only saving our last request, we will always update this one object.
Now we can finally get to the methods for saving and retrieving our last request.
struct DataService {
//...
func retrieveData() throws -> NSData? {
guard let imageObject = currentImageObject else {
return nil
}
let data = imageObject.valueForKey(Keys.lastImage) as? NSData
guard data != nil else {
throw NSError(domain: "Couldn't parse data",
code: 0,
userInfo: nil)
}
return data
}
}
Here we are just retrieving the value for our last image from the NSManagedObject, and casting it to NSData. If there is no object currently in our database, we are returning nil.
We can now retrieve data from our persistent store, but we don’t yet have a way to actually put something in our database. Let’s make a saveData(data:) method.
struct DataService {
//...
func saveData(data: NSData) throws {
if let imageObject = currentImageObject {
imageObject.setValue(data, forKey: Keys.lastImage)
try context.save()
} else {
throw NSError(domain: "Couldn't find image object",
code: 0,
userInfo: nil)
}
}
}
Now our CoreDataService has everything we need. We have a nice, clean (and safe) way of saving and retrieving data from our database.
Now that we made our two ways of retrieving data, it’s time to hook it all up!
Our DataService will use the Network and Core Data services to fetch data.
Since we want to keep our implementation detail to ourselves, the ViewController has no business knowing how or where the data comes from. So, the DataService really only needs one public method: getLocalData(_ callback:).
This methods needs to start fetching data from the local database and the network, and call the callback when any of those is done.
struct WeatherService {
private let dataService = MDataService()
private let networkService = MNetworkService()
func getImageData(callback: (NSData)->()) {
getLocalData(callback)
getDataFromNetwork(callback)
}
}
Let’s implement those two methods.
getLocalData only needs to retrieve data from the *dataService *and handle any errors on the way.
struct WeatherService {
//...
private func getLocalData(callback: (NSData)->()) {
do {
if let data = try dataService.retrieveData() {
callback(data)
}
} catch let error {
print("Error retrieving data: \(error)")
}
}
}
getDataFromNetwork is not really much more complex. The only thing to keep in mind is to not issue the network request on the main thread. Doing this will block our main thread, which means the users will not be able to see the local data until the network data loads, which renders the whole effort useless!
struct WeatherService {
//...
private func getDataFromNetwork(callback: (NSData)->()) {
let qos = QOS_CLASS_USER_INITIATED
let queue = dispatch_get_global_queue(qos, 0)
dispatch_async(queue) {
self.networkService.getImage { data in
dispatch_async(dispatch_get_main_queue()) {
callback(data)
}
}
}
}
}
And that’s our DataService implemented!
Now all our ViewController needs to do is supply one method, and everything will get taken care of for us.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
let dataService = DataService()
override func viewDidLoad() {
super.viewDidLoad()
dataService.getImageData() { data in
imageView.image = UIImage(data: data)
}
}
}
Now when you launch the app, and when the network request goes trough, the data you got will be saved. On the next app launch, that data will be displayed almost immediately, and more data will be loaded in the background replacing the local one.
In the name of all people with terrible mobile data plans, I hope you will implement this into your own applications.