Calling a web API, processing JSON and update your view in Swift 3

To get familiar with iOS development and Swift 3, I started a little side project. This involved calling a web API to retrieve some data in JSON. This data then had to be parsed into a Swift object and I wanted to display the results on screen in a UITextView once available.

I had no idea how to do any of these steps and so I spent a lot of time reading documentation before I was able to implement the solution. In this article I want to explain how I did it.

In my example, I’m calling the API of the Internet Game Database (IGDB) to search for video games. Here’s the function I wrote:

func searchGames(searchTerm: String, completion: @escaping ([BasicGameInfo]) -> Void) {
    let urlString = "https://igdbcom-internet-game-database-v1.p.mashape.com/games/?fields=*&limit=10&offset=0&order=release_dates.date%3Adesc&search=\(searchTerm)" // [1]

    if let url = URL(string: urlString) {
        let sessionConfig = URLSessionConfiguration.default
        let xHTTPAdditionalHeaders = [
            "X-Mashape-Key":"YOUR_API_KEY",
            "Accept":"application/json"
        ]
        sessionConfig.httpAdditionalHeaders = xHTTPAdditionalHeaders // [2]

        let urlSession = URLSession(configuration: sessionConfig) // [3]
        urlSession.dataTask(with: url, completionHandler: { data, response, error in // [5]
            var result: [BasicGameInfo] = []

            if error == nil && data != nil {
                let json = try? JSONSerialization.jsonObject(with: data!, options: []) // [6]
                if let jsonArray = json as? [Any] { // [7]
                    for case let jsonDict as [String: Any] in jsonArray { // [8]
                        if let game = BasicGameInfo(json: jsonDict) { // [9]
                            result.append(game)
                        }
                    }
                }
            }
            completion(result) // [10]
        }).resume() // [4]
    }
}

The function gets the search term as well as a closure passed in as parameters. Because the API call is asynchronous, the searchGames function does not return anything. I’ll explain the passed in closure later, first I’ll describe how I implemented the IGDB API call.

The first step is to build the URL, which consists of a static part and the search term that is included as the last URL parameter 1. The API expects certain information in the HTTP headers of incoming requests. To provide this information, in this case a private API key and the resulting format (JSON), it has to be added to the httpAdditionalHeaders of a URLSessionConfiguration as a dictionary [2].

This modified URLSessionConfiguration is used to create a URLSession, which is, according to the class reference, generally used for downloading content [3]. The actual work takes place in tasks. The URLSession can create tasks to retrieve content (dataTask), to retrieve content and save it to a file (downloadTask), to upload data (uploadTask) or to establish a bidirectional TCP/IP connection (streamTask).

For my use case, I used a dataTask. The dataTask function gets the URL to be called and a completionHandler closure, which is called after the task is completed, passed in. The closure is implemented anonymously directly in the parameter definition. The function resume() is called on the created dataTask to actually begin execution [4].

So what happens here? The resume() function starts the task asynchronously, which means that our searchGames function doesn’t wait for the results. Instead, we reach the end of the function and return nothing while the dataTask is still running in a background thread.

How do we get the results of the task? That’s where the completionHandler comes in. When the task is finished, it calls the completionHandler and passes three values in: the retrieved data (that’s what we’re interested in), the HTTP response and an error (if one occurred) [5]. In my example, I wanted to have the results of the API call as an array of BasicGameInfo objects, a simple class that I’ve created.

To begin with we have to convert the JSON data into an object. This simply consists of calling the JSONSerialization class function jsonObject [6]. Depending on the API, the resulting object is either an array or a dictionary. In my example case, it’s an array with one entry for each game found. Each entry is a dictionary object with all kinds of attributes for that game. That’s what the two casts are for. First I cast the root object into an array [7] and then I cast each entry into a dictionary [8]. Each dictionary stands for one game and is used to create a BasicGameInfo object [9]. I implemented a failable initializer in the class BasicGameInfo that looks like this:

init?(json: [String: Any]) {
    guard let title = json["name"] as? String else {
        return nil
    }

    self.title = title
}

The only thing this failable initializer does is get the value for the key “name” from the given dictionary and set it as the title of the game. If the key “name” doesn’t exist in the dictionary, the initializer fails (returns nil).

The desired array of BasicGameInfo objects is now present in the completionHandler. The next step is to hand this array back to the caller of the searchGames function. That’s what the call completion(result) in the completionHandler does [10]. I guess now is the right time to explain the closure that gets passed into the searchGames function. This closure takes an array of BasicGameInfo objects as a parameter, doesn’t have a return value and is called completion. So the call completion(result) in the completionHandler calls exactly that closure. The @escaping annotation is necessary because the closure is not used directly in the searchGames function but in the completionHandler after the function is already finished.

In my example, the searchGames function is called from a ViewController:

    IgdbManager().searchGames(searchTerm: "zelda", completion: { games in
        self.searchResultsLabel.isHidden = false
        var newText = ""
        for game in games {
            print(game.title)
            newText = newText + game.title + "\n"
        }
        DispatchQueue.main.async {
            self.searchResultsLabel.text = newText
            self.searchResultsLabel.setNeedsDisplay()
        }
    })

The list of resulting games is passed into the completion closure that is implemented here. In the closure, the game titles are concatenated and then set as the text of a UITextView. The calls that actually update the UI have to take place in the main thread, so we need to use Grand Central Dispatch (GCD) for that. Luckily, using GCD became a lot more pleasant with Swift 3.

That’s all the steps necessary to finish my example use case. We’ve seen how to call a web API, process the JSON results and update our view with the retrieved data. I hope this article is helpful and I would love to get your feedback.

Tweet about this on TwitterShare on FacebookShare on Google+Share on TumblrEmail this to someoneFlattr the author

Workflow: iPhone-Fotos auf einem Synology-NAS bereitstellen und sichern

Der Umgang mit digitalen Fotos ist eine Wissenschaft für sich. Im Jahr 2015 möchte man möglichst von allen Geräten jederzeit auf die eigenen Fotos zugreifen können. Das funktioniert nur, wenn die Fotos zentral in der Cloud abgelegt werden. Derzeit werben einige große Anbieter wie Dropbox oder Apple darum, die Fotos der Benutzer beheimaten zu dürfen. Mittlerweile kann man auf Knopfdruck aktivieren, dass neue Aufnahmen sofort und automatisch zum jeweiligen Dienst hochgeladen werden. Unbedarfte Nutzer kommen so in den Genuss einer sehr bequemen Lösung.

Ich möchte meine Fotos aber nicht irgendwo in der Cloud ablegen, zumal keiner der populären Anbieter eine Ende-zu-Ende-Verschlüsselung anbietet und somit zum einen die Mitarbeiter des Cloud-Anbieters und zum anderen potenziell auch andere Interessenten die Möglichkeit haben, meine Fotos auszuwerten.

Bei mir zuhause habe ich ein NAS von Synology stehen, welches Software bietet, um Fotos zentral bereitzustellen und welches so eingerichtet werden kann, dass es von außerhalb des Heimnetzwerks, also von überall, erreichbar ist. Um dieses NAS herum habe ich mir einen Workflow eingerichtet, der es mir ebenfalls erlaubt, immer und überall von jedem Gerät auf meine Fotos zuzugreifen, allerdings ohne die Nutzung eines populären Cloud-Anbieters. Stattdessen liegen die Fotos gemütlich im heimischen Wohnzimmer.

Neben der Zugriffsmöglichkeit auf die Fotos und der Nutzung einer privaten Cloud ist mir wichtig, dass die Fotos strukturiert abgelegt sind, dass ich für den Zugriff keine proprietäre Software benötige, dass die Fotos regelmäßig gesichert werden und dass der Workflow weitestgehend automatisch abläuft, so dass ich möglichst wenig Zeit für die Organisation meiner Fotos aufwenden muss.

Mein aktueller Workflow erfüllt diese Bedingungen unter Nutzung eines iPhones mit der App CameraSync, einer OwnCloud-Installation, eines Macs mit der Software Hazel und eines Synology-NAS mit der Software Photo Station. Es folgt der Workflow im Detail.

Hochladen der Bilder zu OwnCloud

Ich fotografiere ausschließlich mit dem iPhone. Im ersten Schritt müssen die Bilder dieses verlassen. Dafür benutze ich die App CameraSync. Diese kann Fotos zu verschiedenen Zielen hochladen, unter anderem auch in WebDAV-Ordner. Mit dieser App könnte ich die Bilder direkt auf mein NAS hochladen, allerdings sollen sie vorher noch umbenannt und in eine Ordnerstruktur einsortiert werden. Diese Schritte laufen auf meinem Mac ab.

Daher lade ich die Fotos nicht direkt auf mein NAS hoch, sondern in einen WebDAV-Ordner in meiner OwnCloud. OwnCloud ist vergleichbar mit bekannten Cloud-Diensten wie Dropbox, bietet aber den Vorteil, dass man die Daten selbst hostet. Meine OwnCloud habe ich auf meinem Webspace bei All-Inkl installiert, mit ein wenig Bastelei könnte man sie allerdings auch auf einem Synology-NAS zum Laufen bekommen.

Auf dem Mac ist die OwnCloud-Clientsoftware installiert, die die Inhalte der OwnCloud mit einem Ordner auf dem Mac synchronisiert. Auf diese Weise landen die Fotos, die ich in die OwnCloud hochlade, letztlich automatisch auf dem Mac.

Verarbeitung der Fotos mit Hazel

Auf dem Mac habe ich das Tool Hazel installiert. Dieses kann bestimmte Ordner überwachen und auf den Dateien, die es findet dann nach bestimmten Regeln Aktionen ausführen. Ich habe dort eingerichtet, dass der OwnCloud-Ordner überwacht wird, in den ich die Fotos hochgeladen habe.

Zunächst benennt Hazel die Dateinamen der Fotos um. Dafür liest es aus den Metadaten der Bilder Aufnahmedatum, -uhrzeit und -ort aus und generiert daraus einen Dateinamen. Meine Fotos haben dann einen Dateinamen im Format “2015-04-05 at 12-18-18 – LA(53,587) – LO(10,044).jpg”. Anschließend werden die Fotos in eine Ordnerstruktur bestehend aus Aufnahmejahr und Aufnahmemonat verschoben. Für das obere Beispiel wäre das “Fotos/2015/2015-04-April/”.

Die Umbenennung der Dateien und das Sortieren in die Ordner funktioniert völlig automatisch ohne mein Zutun im Hintergrund. Wenn die Fotos auf dem Mac angekommen sind, lösche ich sie aus der Camera Roll meines iPhones. Das muss ich leider manuell tun, es gibt aber im Gegenzug die Sicherheit, dass kein Foto verloren gegangen ist.

Bereitstellung auf dem NAS

Auf dem Synology-NAS habe ich das Paket Photo Station installiert. Dieses stellt die Fotos eines definierten Ordners auf dem NAS in einer schicken Weboberfläche dar. Außerdem gibt es korresponierende Apps für iOS und Android, von denen aus man auf die Fotos zugreifen kann. Standardmäßig wird jeder Ordner, den Photo Station findet, in der Software als Album dargestellt. Somit habe ich ohne weiteres Zutun automatisch für jeden Monat ein Album mit den dort entstandenen Aufnahmen. Ich muss lediglich noch die Ordner von meinem Mac in den Photo Station-Ordner auf dem NAS verschieben.

Selbst wenn ich auf einem Gerät die Photo Station-Software gerade nicht im Zugriff habe, kann ich noch auf die Bilder zugreifen, wenn ich Zugriff auf den Ordner des NAS habe. Die Fotos sind nämlich nach wie vor dort im Dateisystem sortiert und nicht in einer proprietären Bibliothek, die ich nur mit der Photo Station-Software lesen und verwalten kann.

Automatisches Online-Backup der Daten

Für die regelmäßige Sicherung habe ich einen Account bei iDrive. Die Server von iDrive stehen zwar in der USA, die Daten kann man aber mit einem privaten Schlüssel Ende-zu-Ende verschlüsseln. Außerdem ist der Dienst vergleichsweise günstig und bietet eine App für Synology an, mit der man direkt die Daten eines NAS sichern kann.

Dort habe ich eingerichtet, dass mein NAS seine Daten inklusive der Fotos jede Nacht nach iDrive sichert und mir anschließend eine Erfolgsmeldung per E-Mail sendet. So habe ich jeden Morgen das gute Gefühl, dass meine Daten gesichert sind.

Optimierungspotenzial

So stellt sich mein aktueller Foto-Workflow dar. Er erfordert nur noch sehr wenig Zeitaufwand meinerseits, beinhaltet aber noch einige manuelle Schritte: Ich starte den Upload der Bilder in die OwnCloud mit CameraSync manuell an und später muss ich die hochgeladenen Fotos manuell vom iPhone löschen sowie die sortierten Fotos manuell vom Mac auf das NAS kopieren.

Das automatische Kopieren vom Mac auf das NAS könnte ich sicherlich mit Hazel einfach umsetzen, für die manuellen Schritte am iPhone suche ich allerdings noch nach Verbesserungen. Toll wäre beispielsweise eine App, die neue Fotos automatisch in einen WebDAV-Ordner hochladen kann und diese nach erfolgreichem Upload dann vom iPhone löscht. Eine solche ist mir derzeit allerdings nicht bekannt.

Wenn ihr Optimierungsvorschläge für meinen Workflow oder eigene effiziente Workflows habt, würde ich mich über Kommentare sehr freuen!

Tweet about this on TwitterShare on FacebookShare on Google+Share on TumblrEmail this to someoneFlattr the author