Een tweede app in XCode

TableViewController

Maak een nieuw project aan en kies net als vorige keer de Single View Application. Noem het project “NEXUS”. Klik nu in de Project Navigator met de rechter muisknop (of Ctrl + linkermuisknop / trackpad) op ViewController.swift en kies Delete uit het pop-up menu.

Kies in het volgende scherm Move to Trash:

Kies nu uit het menu File, New, File of de toetsencombinatie Cmd + N om een nieuw bestand aan je project toe te voegen, en selecteer uit de categorie iOS, Source de Cocoa Touch Class.

In het volgende scherm, veranderd (via het uitklapmenu of door typen) de subclass van UIViewController naar UITableViewController. Hernoem de class als NEXUSTableViewController.

Doorloop de rest van de stappen om het bestand toe te voegen, je ziet het dan verschijnen in de Project Navigator:

Ik heb het bestand voor mijn eigen overzicht gesleept naar de regel onder AppDelegate.swift, maar dit is niet noodzakelijke voor de werking van het programma. Open nu Main.storyboard en verwijder de bestaande ViewController zodat het scherm leeg is:

Als volgende stap ga je een TableViewController toevoegen uit de Object Library:

Je moet XCode nog vertellen dat dit ook de Initial View Controller is, dat doe je in de Attributes Inspector (rechter kolom) met de ViewController in je storyboard geselecteerd:

In de (rechter) Utilities kolom vind je links naast het icoontje van de Attributes inspector het icoontje van de Identity inspector (tekst verschijnt door er met de muis overheen te gaan en even te wachten). Selecteer deze en je ziet bovenin de rechter kolom het volgende:

Hier kun je XCode vertellen welk codebestand gebruikt dient te worden door de ViewController. In dit geval is dat dus onze nieuwe NEXUSTableViewController:

Run nu de app. Als alles goed is gegaan, zie je dit:

Het lijkt misschien alsof je niets gedaan hebt behalve wat lijntjes toegevoegd aan je scherm, maar onder de motorkap heb je veel gedaan. Je hebt het fundament gelegd voor de TableView zoals je die kent uit veel iOS toepassingen (bv Mail) en door het bestand een subclass te maken van UITableViewController is er veel boilerplate code (lees: zooi die je anders zelf moet intypen) alvast toegevoegd aan het programma. Open NEXUSTableViewController.swift maar eens;

We hebben lang niet al deze code nodig nu, maar je ziet wel dat er al veel voor je is klaar gezet dat je kunt gebruiken indien gewenst. Leuker kunnen we het niet maken, wel makkelijker! :-)

Je ziet twee dingen die we nog niet eerder zijn tegengekomen. Ten eerste een andere manier om commentaar in je code toe te voegen:

// Met een dubbele forward slash kun je een regel commentaar maken

/* Als je 
meerdere regels wilt gebruiken, 
kan dat ook zo
*/

Op die manier zijn sommige functies wel al voor je klaar gezet, maar in de vorm van commentaar, zodat je er geen last van hebt als je ze niet wilt gebruiken.

Verder zie je // MARK: - ... staan, daarmee kun je de code binnen een bestand wat beter organiseren zoals je in onderstaande afbeelding ziet. Daarin heb ik alle code die als commentaar stond weggehaald, behalve de prepareForSegue functie waaromheen ik /* en */ verwijderd heb.

Nu wordt het tijd om data te gaan toevoegen aan de tabel, maar voor we dat doen moeten we nog een ding regelen. Als je in iOS door een lange tabel heen scrollt, lijkt het alsof er allemaal nieuwe velden (cellen) komen. In werkelijkheid mag je het vergelijken met een roltrap: de cellen die bovenaan verdwijnen uit beeld worden hergebruikt en verschijnen onderaan weer in beeld, maar met nieuwe inhoud. Daartoe wil XCode een Reuse Identifier hebben, dat is gewoon een naam die je zelf aan de Table View Cell geeft. Je kunt dat doen in de Attributes Inspector als je de Prototype Cell in je storyboard geselecteerd hebt:

Een werkende tabel

Nu we de TableView geconfigureerd hebben voor gebruik, wordt het tijd voor wat code. Open NEXUSTableViewController.swift en typ in de regel direct onder het class statement de volgende tekst:

let nexusCriteria = ["Geen nekpijn", "Geen intoxicatie", "Normaal bewustzijn", "Geen neurologische uitval", "Geen afleidende pijnlijke letsels"]

Lokaliseer nu de volgende functie:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
    }

en vervang de commentaarregel door return 1. Dit geeft aan dat je 1 sectie (categorie) in je tabel hebt, in dit geval de vijf NEXUS criteria.

Daaronder vind je deze functie:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
    }

Dit geeft het aantal rijen aan dat de tabel moet bevatten. We kunnen zelf gaan tellen hoeveel items onze array bevat, maar dat kunnen we beter aan de computer overlaten voor het geval er later nog eens wat verandert. Vervang de commentaarregel daarom door return nexusCriteria.count.

Tot slot hebben we de derde noodzakelijke functie welke aangeeft hoe de cell eruit komt te zien (geconfigureerd wordt):

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)

        // Configure the cell...

        return cell
    }

Hier moeten we de juiste cell identifier ingeven, dus de tekst reuseIdentifier mag vervangen door ChecklistCell. De dubbele aanhalingstekens blijven staan, deze waarde is een String. Tot slot geven we aan wat we in de cell willen zien: onder de commentaarregel typ je cell.textLabel?.text = nexusCriteria[indexPath.row]. Dit vertelt XCode dat de eigenschap (property) text van de textLabel van onze cell gelijk wordt aan het corresponderende item van de array nexusCriteria. Via indexPath kun je uitlezen welke cell dat is: indexPath.row verwijst naar de rij, en indexPath.section naar de sectie (categorie) waar we er hier maar 1 van hebben (dus kan er geen verwarring ontstaan, daarom hoeven we deze hier niet apart te benoemen). Dat vraagteken heet een optional in Swift, en gaan we in deze introductie workshop niet verder op in. Je hoeft er ook niet aan te denken, als je ’m vergeet zal XCode je corrigeren.

Als het goed is, ziet het eerste deel van je class er ongeveer zo uit:

En als je de app nu runt in de simulator, zie je dit:

Het goede nieuws van tabellen is, dat ze automatisch al goed uitzien als je het apparaat kantelt naar de landscape modus:

Het is alleen lelijk dat die tabel doorloopt tot helemaal boven in beeld, dus je mag de Table View Controller embedden in een Navigation Controller zoals je dat vanmorgen ook hebt gedaan:

Run opnieuw:

Klik nu eens op een rij van de tabel in de simulator:

Leuk, maar er gebeurt niets. Dat klopt, want daarvoor hebben we ook nog geen code opgegeven! Sluit de simulator, open Main.storyboard en voeg een View Controller toe en creëer een segue van de prototype cell naar de nieuwe View Controller. Kies de optie Show onder Selection segue:

Voeg nu een nieuwe Cocoa Touch Class toe aan het project met de volgende instellingen:

Koppel deze aan je nieuwe View Controller. Je project is nu gereed voor de volgende stap: het toevoegen van een Web View aan de (Result) View Controller.

Wijzig de positie en grootte zodat deze de gehele Result View Controller vult (en vergeet de constraints niet!).

De onderliggende code ziet er nu nog als volgt uit:

De functie didReceiveMemoryWarning en de als commentaar gemarkeerde code daaronder kun je verwijderen. Daarna open je de Assistant Editor die je rechts bovenin beeld herkent aan dit icoontje:

Je krijgt nu onderstaand beeld te zien:

Sleep nu met de Ctrl-toets ingedrukt vanaf de Web View naar ResultViewController.swift naar de regel onder de class declaratie en laat los. Je krijgt dan de volgende dialoog te zien:

Een outlet is een manier om user interface componenten te kunnen benaderen vanuit code. In dit geval geven we de Web View een naam waarmee we hem middels code kunnen bedienen, namelijk resultsWebView. De volgende regel wordt nu door XCode toegevoegd aan je Swift file: @IBOutlet weak var resultsWebView: UIWebView!. Je kunt nu weer terug naar de Standard Editor:

Resultaat tonen

Ga naar ResultViewController.swift (dat kan ook in de Assistant Editor, maar ik vind het overzichtelijker om te werken vanuit de Standard Editor als dat kan) en identificeer de overgebleven methode viewDidLoad waarin je opdrachten kunt meegeven die direct na het laden van het scherm uitgevoerd moeten worden. Daarbinnen gaan we nu een stukje tekst meegeven aan de Web View om te kijken of deze naar behoren werkt. Voeg binnen de accolades van de functie de volgende twee regels toe onder de regel super.viewDidLoad():

let htmlString = "test"
resultsWebView.loadHTMLString(htmlString, baseURL: nil)

Run nu de app in de simulator en klik op een rij (cell) in de tabel. Je ziet dan dit:

Mooi, dat werkt! Dit lijkt misschien onzin, maar je hebt nu de bevestiging dat alle koppelingen werken: de nieuwe View Controller wordt geopend, de Web View staat correct, de tekst wordt geladen. Het is eenvoudig om de tekst nadien aan te passen, zolang de basis maar functioneert. Het is wel prettig om dat alvast te weten!

Nu is het doel van deze app een checklist te ontwikkelen, en dan is het niet handig om de gebruiker bij het aanklikken van een rij direct naar het volgende scherm te sturen. Wat we uiteindelijk willen, is dat de gebruiker de vijf items kan aanvinken en daarna om feedback kan vragen. Daar gaan we nu enkele aanpassingen voor doorvoeren. Mocht je het niet al hebben gedaan, sla je project dan even op. Als er bij de volgende stap iets misgaat, kun je eenvoudig terug.

Wis de segue die van de prototype cell uitgaat en voeg een button toe onder diezelfde prototype cell:

Van deze button mag je nu een nieuwe segue maken naar de Result View Controller op dezelde manier als daarstraks. Run de app in de simulator en test of dit werkt:

Nu gaan we daadwerkelijk de checklist implementeren. Voeg de volgende functie toe aan de code voor je Table View Controller onder de cellForRowAtIndexPath functie:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

}

Als je de auto-completion gebruikt, let er dan op dat je didSelectRowAtIndexPath kiest en niet didDeselectRowAtIndexPath! Dat heeft me ooit twee uur tijd gekost om die vergissing te ontdekken in mijn eigen code.

Uit de omschrijving van bovenstaande functie heb je al begrepen dat deze functie aangeeft wat er moet gebeuren als je de rij selecteert. Typ de volgende code in binnen de functie (tussen de accolades dus):

let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark   // let op punt aan begin!

Run de app en test wat er gebeurt. Het vinkje verschijnt als je op de rij klikt, maar de rij blijft geselecteerd. Laten we dat als eerst veranderen: voeg de volgende toe aan de nieuwe functie:

tableView.deselectRowAtIndexPath(indexPath, animated: true)

Probeer het nog maar eens!

Nu werkt het aanvinken van items prima, maar het uitvinken nog niet. Ook dat is logisch, want we hebben tot nu toe alleen gezegd dat als de rij wordt geselecteerd dan de cell dan een checkmark krijgt. Om te kunnen wisselen tussen gechecked en niet gechecked, kunnen we de regel cell?.accessoryType = .Checkmark vervangen door deze code:

if cell?.accessoryType == UITableViewCellAccessoryType.None {
            cell?.accessoryType = .Checkmark
        } else {
            cell?.accessoryType = .None
        }

De code is simpel: indien de rij niet aangevinkt is, dan aanvinken; anders het vinkje verwijderen. Op deze manier hebben we een werkende checklist, resteert nog er iets mee te doen! :-) En daarvoor willen we weten of alle items zijn aangevinkt! Daarvoor gaan we de code wat aanpassen. Ten eerste mag je onder je class declaratie de volgende regel toevoegen:

var numberOfCheckedItems = 0

Verder mag de didSelectRowAtIndexPath functie als volgt worden aangepast:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
        let cell = tableView.cellForRowAtIndexPath(indexPath)
        
        if cell?.accessoryType == UITableViewCellAccessoryType.None {
            cell?.accessoryType = .Checkmark
            numberOfCheckedItems += 1
        } else {
            cell?.accessoryType = .None
            numberOfCheckedItems -= 1
        }
    
    }

De prepareForSegue functie vullen we als volgt in:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        // Get the new view controller using segue.destinationViewController.
        let resultViewController = segue.destinationViewController as! ResultViewController
        
        // Pass the selected object to the new view controller.
        if numberOfCheckedItems == nexusCriteria.count {
            resultViewController.resultText = "Er hoeft geen rontgenfoto van de nek gemaakt te worden."
            resultViewController.commentText = "Alle NEXUS criteria zijn geselecteerd."
        }
        
    }

Je maakt eerst een koppeling (in feite een soort outlet) aan voor je Result View Controller. Daarna check je of alle items zijn aangevinkt, en geef je een waarde mee aan twee variabelen uit die Result View Controller. Die twee variabelen moeten we nog toevoegen aan de ResultViewController.swift, onder de class declaratie:

var resultText = ""
var commentText = ""

De code voor htmlString veranderen we in:

let htmlString =    "<html><body style=\"font-family: Arial\">" +
                            "<h3>\(resultText)</h3>" +
                            "<p>\(commentText)</p>" +
                            "</body></html>"

Deze code bevat HTML (Hypertext Markup Language), de Internet opmaaktaal, welke door de Web View wordt weergegeven. Run de app maar eens in de simulator:

Nu nog iets doen met de situatie als niet alle items zijn aangevinkt. Het is mijn eer te na om de gebruiker te melden dat “een of meerdere items” niet zijn aangevinkt, aangezien we weten of het 1 item is of meerdere. Dit is mijn oplossing:

if numberOfCheckedItems == nexusCriteria.count {
            resultViewController.resultText = "Er hoeft geen rontgenfoto van de nek gemaakt te worden."
            resultViewController.commentText = "Alle NEXUS criteria zijn geselecteerd."
        } else {
            resultViewController.resultText = "Het lijkt verstandig een rontgenfoto van de nek te maken."
            if numberOfCheckedItems == nexusCriteria.count - 1 {
                resultViewController.commentText = "Een NEXUS criterium ontbreekt."
            } else {
                resultViewController.commentText = "Meerdere NEXUS criteria ontbreken."
            }
        }

Ofwel zo:

Je kunt eens proberen om de app op je eigen device te draaien nu!

Download

Je kunt de complete code van dit project hier downloaden.

Medical device regelgeving

Een intro-workshop over medische apps zou niet compleet zijn zonder een kort punt van aandacht over regelgeving. In Europa hebben we de Medical Device Directives (MEDDEV) die beschrijven wanneer software beschouwd dient te worden als medical device. Dat geldt ook voor mobile medical applications, en NICTIZ heeft een whitepaper gemaakt die een handig stroomschema hiervoor bevat.

Kort samengevat: als de app meer doet dan “opslag, archivering, datacompressie of simpele zoekopdrachten” en bedoeld is voor diagnostische of therapeutische doeleinden, is de app een medisch hulpmiddel en behoeft deze CE-markering. De risicoklasse van de app bepaalt het verder traject: bij een klasse 1 apparaat mag je die certificering nog zelf doen, bij een hogere risicoklasse moet dat via een zogeheten Notified Body en dan kunnen de kosten snel oplopen.

Maak je een Engelstalige app dan heb je ook iets te maken met de FDA en hun (in 2015 nog niet-bindende) guidance voor mobile medical applications. Dit artikel geeft ook een goed overzicht.

Bovenstaande app valt al in die categorie, en als je deze app officieel zou willen uitbrengen, ben je er dus niet met de ontwikkeling van een stukje software alleen. Enerzijds maakt het dat lastig, anderzijds heb je als ontwikkelaar ook voordeel van een kwaliteitswaarborging voor het product dat je uitbrengt. Het scheidt het kaf van het koren, en dat zal naar de toekomst toe alleen maar belangrijker worden.

Afsluiting

Je hebt in deze twee dagen een introductie gekregen in het programmeren middels de taal Swift, en het bouwen van apps in de XCode omgeving. Je hebt wellicht de app kunnen testen op je eigen iOS device, en je hebt kort iets geleerd over regelgeving over medische hulpmiddelen. Dat lijkt me een mooi resultaat!

Als je je verder wilt bekwamen in het ontwikkelen van apps voor iOS kan ik je de volgende bronnen aanraden:

RayWenderlich.com: met name de iOS Apprentice serie is uitermate behulpzaam!

Techotopia.com: ik vond met name iOS App Development erg bruikbaar!

StackOverflow.com: dé website waar je met concrete vragen terechtkunt!

Tot slot: zou je deze vragenlijst willen invullen? Het duurt 1–2 minuten en je zou mij er mee helpen!

Veel succes en plezier met programmeren!

Pieter