So far, I’ve only covered using data that’s stored locally on your iPhone or Apple Watch. I think it’s about time to start getting data from other sources, so let’s start with getting data from an API.

NASA has this awesome picture of the day API that I’m going to use for this post. You can get an image by signing up for an API key and going to https://api.nasa.gov/planetary/apod. The current image is:

The next thing I need to figure out is how to get data from an API. I’m a vanilla kinda guy, so I’m going to stick with Apple’s URLSession. While there are a ton of libraries to help with networking, it doesn’t sound like they add much value for my use case.

Now it’s time to write some code. My first thought here is that I can wrap this URLSession request inside of an ObservableObject, and have the properties on it change when the image is loaded (We’ll have a property for dataIsLoaded and for the image). This lets SwiftUI take care of reloading the view after the data is loaded.

struct ContentView: View { @ObservedObject var image = UrlImage() var body: some View { VStack { if image.dataIsLoaded { Image(uiImage: image.image!) } } } } class UrlImage: ObservableObject { @Published var dataIsLoaded: Bool = false @Published var image: UIImage? = nil init() { loadImage() } func loadImage() { let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=KEYYYYYYYYY")! var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) {(data, response, error) in guard error == nil else { print ("error: \(error!)") return } guard let content = data else { print("No data") return } DispatchQueue.main.async { self.image = UIImage(data: content) self.dataIsLoaded = true } print("Data loaded") } task.resume() } }

And Crash. That’s what I get for force unwrapping an optional 🙃 Let’s see what this API actually returned.

{"date":"2019-10-16","explanation":"How do binary stars form? To help find out, ESO's Atacama Large Millimeter Array (ALMA) recently captured one of the highest resolution images yet taken of a binary star system in formation. Most stars are not alone -- they typically form as part of a multiple star systems where star each orbits a common center of gravity. The two bright spots in the featured image are small disks that surround the forming proto-stars in [BHB2007] 11, while the surrounding pretzel-shaped filaments are gas and dust that have been gravitationally pulled from a larger disk. The circumstellar filaments span roughly the radius of the orbit of Neptune. The BHB2007 system is a small part of the Pipe Nebula (also known as Barnard 59), a photogenic network of dust and gas that protrudes from Milky Way's spiral disk in the constellation of Ophiuchus. The binary star formation process should be complete within a few million years.","hdurl":"https://apod.nasa.gov/apod/image/1910/BabyBinary_Alma_1881.jpg","media_type":"image","service_version":"v1","title":"BHB2007: A Baby Binary Star in Formation","url":"https://apod.nasa.gov/apod/image/1910/BabyBinary_Alma_960.jpg"}

Turns out, this API doesn’t return just an image, but a json with a link to an image. I can do 2 requests then. When the first one returns, I can pull out url and do a second request to get that image. Using the JSONSerialization library, it looks something like this:

let json = try! JSONSerialization.jsonObject(with: data) let jsonobj = json as! [String: Any] let imageUrl = jsonobj["url"] as! String

Plugging this into my current code and hooking it up to a new request gives me the following:

struct ContentView: View { @ObservedObject var image = UrlImage() var body: some View { VStack { if image.dataIsLoaded { Image(uiImage: image.image!) } } } } class UrlImage: ObservableObject { @Published var dataIsLoaded: Bool = false @Published var image: UIImage? = nil init() { loadJson("https://api.nasa.gov/planetary/apod?api_key=STILLNOTGETTINGMYKEYYYY") } func loadJson(_ urlString: String) { let url = URL(string: urlString)! var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) {(data, response, error) in guard error == nil else { print ("error: \(error!)") return } guard let content = data else { print("No data") return } let json = try! JSONSerialization.jsonObject(with: content, options: []) let jsonobj = json as! [String: Any] self.loadImage(urlString: jsonobj["url"] as! String ) print("Data loaded") } task.resume() } func loadImage(urlString: String) { let url = URL(string: urlString)! var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) {(data, response, error) in guard error == nil else { print ("error: \(error!)") return } guard let content = data else { print("No data") return } // We have to change the object's data on the main thread DispatchQueue.main.async { self.image = UIImage(data: content) self.dataIsLoaded = true } print("Data loaded") } task.resume() } }

Here goes nothing. Lets try running it.

Success! Turns out NASA changes this image around 9PM PST.

That’s about it for this post. Async loading data from an API is extremely easy with SwiftUI. Well, any kinda of data loading is really easy. If you can wrap it in an ObservableObject, you can load it and SwiftUI just works 🙂

Thanks for reading!

Feel free to checkout my code on Github as well as my video on Youtube.