Kom igång med IOS-utveckling, del 4

En app behöver en datakälla, och ofta finns den källan på internet. I del 4 av artikelserien visar 99mac hur man använder de befintliga verktygen för att hämta data från ett publikt API.

I min app ska jag använda SL:s officiella API:er för realtidsinformation och platsuppslagningar. På trafiklab.se finns all nödvändig dokumentation, och där kan du även skapa dina personliga API-nycklar.

Separat Framework

Jag behöver använda min API-klient både i huvudappen och i mitt tillägg för Idag-vyn i Notiscenter. Ett tillägg (eng. extension) behandlas helt separat från sin huvudapp, så för att dela kod mellan dem måste jag organisera klienten i ett fristående Framework som länkas in i de två delarna.

Ett Framework i Xcode är ett separat projekt där man organiserar kod på ett modulärt sätt som ska gå att återanvända i flera av varandra oberoende appar. Ett väldesignat Framework exponerar ett antal publika klasser och metoder, medan implementationsdetaljer förblir dolda för de anropande metoderna.

  1. Välj File > New > Project > Framework & Library > Cocoa Touch Framework

  2. Välj att lägga den i ditt workspace om du har ett

  3. Med det nya projektet markerat, välj File > New > File… -> Swift File

  4. Välj ett lämpligt namn, min API-klient får exempelvis heta SLAPI.swift

Designbeslut

Nu är det dags att fundera på upplägg. Ju mer tankejobb man lägger ner i förväg, desto bättre kommer det att flyta på när man börjar koda. Vilka publika klasser och metoder ska finnas? Hur ska datan som levereras se ut? Ska det vara synkront eller asynkront?

I min klient behöver jag bara två publika metoder i själva klientklassen. En för platsuppslag, och en för att hämta avgångar för en given hållplats. Allt annat hålls icke-publikt och internt i klienten för att bibehålla abstraktion och moduläritet.

Platsuppslag behöver bara leverera en lista över sökträffar, så den levererar lämpligen en NSArray. Avgångar grupperas efter trafikslag, så där är det bättre med en NSDictionary. Jag väljer även att definiera två hjälpklasser, SLSite och SLDeparture för att paketera datan snyggt och prydligt.

Att hämta data från internet kan innebära långa svarstider, så jag behöver en asynkron klient för att inte låsa användargränssnittet under hämtningen. UI som laggar och hänger sig korta stunder är ett säkert sätt att se oproffsig ut och skrämma bort användare.

Asynkrona metoder kan implementeras på olika sätt, exempelvis med delegation-protokollet eller med en så kallad completion handler. Jag väljer delegation, så i mitt framework får jag även inkludera ett delegate-protokoll som implementeras i View Controllern som använder klienten.

Tredjepartsbibliotek

SL:s API levererar data i JSON-format. Att parsa det kan vara ganska omständigt i Swift, så jag tar hjälp av SwiftyJSON som finns som öppen källkod. SwiftyJSON gör det betydligt enklare att jobba med JSON.

Tänk på att inte alla open source-licenser tillåter användning i IOS-appar hur som helst. Kolla vad som gäller innan du använder ett tredjepartsbibliotek. SwiftyJSON ligger under MIT-licensen som tillåter användning så länge man lägger in en notis som innehåller dess licensvillkor någonstans i sin app.

Delegate-protokollet

Här behövs fyra metoder. Dels för att leverera hämtade data, och dels för att leverera ett NSError-objekt om något går fel. Två delegate-metoder för varje publik metod i klientklassen alltså.

Jag definierar delegate-protokollet så här:

public protocol SLAPIDelegate {
    func siteLookupComplete(sites: NSArray)
    func siteLookupFailed(error: NSError)
    func getDeparturesComplete(departures: NSDictionary)
    func getDeparturesFailed(error: NSError)
}

Om man vill kan man lägga till "optional" i början för att de inte ska vara obligatoriska, om man inte använder båda klientmetoderna i sin delegate.

Klientklassen

Till klientklassen väljer jag samma namn som till filen, det vill säga SLAPI. Init-funktionen bör ta ett argument för att ange delegate-objektet.

Jag börjar med att lägga in kodskelett för de metoder jag har beslutat om, för att få lite översikt. Sedan implementerar jag dem i tur och ordning.

public class SLAPI {
    
    let APIKEY_SITE_LOOKUP = ”din-api-nyckel”
    let APIKEY_GET_DEPARTURES = ”din-api-nyckel”
    let APIURL = "http://api.sl.se/api2/"
    
    var delegate : SLAPIDelegate? = nil
    
    public init(delegate: SLAPIDelegate) {
        self.delegate = delegate
    }
    
    public func searchSite(query: String) {

    }
    
    public func getDepartures(siteID: Int) {

    }

    func buildGetDepartureURL(site: Int) -> NSURL {
        // TODO!
        return NSURL()
    }
    
    func buildSearchURL(query : String) -> NSURL {
        // TODO!
        return NSURL()
    }
}

Här har jag även lagt in några konstanter för API-nycklar och adressen till SL:s API, samt variabel för delegate-objektet. De två ickepublika metoderna på slutet ska bara vara interna hjälpmetoder för att skapa ett NSURL-objekt utifrån API-konstanterna.

NSURLSession

Arbetshästen för att hämta data via internet är NSURLSession-klassen. Den ersätter den äldre NSURLConnection som till stor del kommer att fasas ut i #IOS 9. NSURLSession har stöd för både delegate och completion handler vid asynkron hämtning.

Med delegate blir det mer kod att skriva, men den är lämplig om du behöver hålla koll på anslutningen medan den körs, exempelvis för att visa en förloppsindikator vid större datamängder. Jag har inget behov av det, så jag väljer completion handler för att hålla nere mängden kod.

En completion handler är ett block med kod (en så kallad Closure i Swift och flera andra språk) som skickas med som argument till NSURLSession, och exekveras när hämtningen är klar. En closure kapslar även in sitt scope, så även om den exekveras av en annan metod i en annan tråd kommer alla variabelreferenser peka på samma sak som i det scope där blocket definierades.

Jag implementerar searchSite-metoden så den ser ut såhär:

public func searchSite(query: String) {
        let request = NSURLRequest(URL: buildSearchURL(query))

        let handler = {(data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
            if let err = error {
                // Anropa delegate-metoden om något gick fel
                self.delegate?.siteLookupFailed(err)
                return
            }
            var sites = NSMutableArray()
            let json = JSON(data: data)
            // ...
            // Parsa JSON och spara data i sites-arrayen
            // ...

            // Anropa delegate-funktionen för att leverera data
            self.delegate?.siteLookupComplete(sites)
        }
        let conn = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
        conn.dataTaskWithRequest(request, completionHandler: handler).resume()
    }

Här skapar jag först ett NSURLRequest-objekt som representerar API-anropet. Då använder jag buildSearchURL-metoden som förstås måste implementeras korrekt först. Därefter definierar jag min completion handler. Den koden exekveras alltså inte nu, utan lagras i en closure som körs när all data är hämtad.

Nu är det dags att skapa ett NSURLSession-objekt, som jag sedan lägger till en task i. Som du ser anger jag min handler som argument till metoden dataTaskWithRequest. Metoden resume() startar en task.

I det här läget returnerar metoden searchSite, och hämtningen av data sker asynkront i bakgrunden. När hämtningen är klar körs min completion handler som i sin tur anropar delegate-metoden hos den som anropade searchSite.

Metoden getDepartures implementeras på motsvarande sätt, och sen är grovjobbet klart. Nu återstår bara hjälpmetoderna och dataklasserna, men det går jag inte in på detalj i den här artikeln eftersom det är så pass specifikt för mitt projekt.

Länkar