Een inleiding tot Swift deel 2

In het eerste artikel van deze inleidende serie over Swift hebben we gesproken over de filosofie van Swift, hebben we eerst de syntaxis bekeken en enkele belangrijke verschillen met Objective-C benadrukt. In dit artikel gaan we verder met het onderzoeken van de syntaxis van Swift. Je zult ook meer leren over optionals en zien hoe geheugenbeheer werkt in Swift.

1. Conditionals en loops

Als

Als statements identiek zijn in Swift en Objective-C met uitzondering van twee subtiele verschillen:

  • ronde haakjes rond de voorwaarde variabele zijn optioneel
  • accolades zijn vereist

Dit zijn ongeveer het enige verschil met if-statements in Objective-C.

ranges

Zoals we in het eerste artikel zagen, omvat Swift twee bereikoperators ... < en ... om een ​​bereik van waarden op te geven. Deze twee operatoren zijn de operator met half gesloten bereik en de operator met gesloten bereik.

Een half-gesloten bereik, zoals 1 ... <5, vertegenwoordigt de waarden 1, 2, 3 en 4, met uitzondering van 5. Een gesloten bereik, zoals 1 ... 5, vertegenwoordigt de waarden 1, 2, 3, 4 en bevat 5.

Bereiken kunnen worden gebruikt in voor loops, rangschikking subscript en zelfs in schakelaar statements. Bekijk de volgende voorbeelden.

// voor lusvoorbeeld voor i in 1 ... <10   // iterates from 1 to 9
// array subscript voorbeeld laat someArray = ["apple", "pair", "peach", "watermelon", "strawberry"] voor fruit in someArray [2 ... <4]  println(fruit)  // outputs: peach and watermelon
// schakel voorbeeld switch someInt case 0: // doe iets met 0 case 1 ... <5: // do something with 1,2,3,4 case 5… 10: // do something with 5,6,7,8,9,10 default: // everything else 

Schakelaar

Switch-statements zijn krachtiger in Swift dan in Objective-C. In Objective-C moet het resultaat van de expressie van een switch-statement van het type integer zijn en de waarden van elke case-statement een constante of een constante expressie zijn. Dit is niet waar in Swift. In Swift kunnen de case-statements van elk type zijn, inclusief reeksen.

In Swift heeft de schakelaar nee breken uitspraken en deze vallen niet automatisch van de ene zaak naar de andere. Bij het schrijven van een switch-instructie moet ervoor worden gezorgd dat alle voorwaarden worden afgehandeld door de case-instructies. Als u dit niet doet, resulteert dit in een compileerfout. Een zekere manier om aan alle voorwaarden te voldoen, is door een standaard case statement.

Hier is een voorbeeld van een schakelaar verklaring met Draad gevallen:

laat groente = "rode paprika" schakel plantaardig case "selderij": laat vegetableComment = "voeg wat rozijnen toe en maak mieren op een houtblok." case "komkommer", "waterkers": laat vegetableComment = "Dat zou een goede theesandwich maken." standaard: laat vegetableComment = "Alles smaakt goed in soep."  

In Swift vallen case-statements niet standaard door. Dit was een bewuste ontwerpbeslissing om veelvoorkomende fouten te voorkomen. Als een specifiek geval moet worden opgelost, kunt u de fallthrough sleutelwoord om dit aan te geven aan de compiler.

switch someInt case 0: // doe iets met 0 case 1: // doe iets met 1 case 2: // doe iets met 2 fallthrough default: // doe iets voor al het andere // case 2 zal doorvallen naar default geval

Het stopt hier niet. Swift voegt twee andere functies toe om te schakelen, waarde bindingen en de waar clausule. Waardebinding wordt gebruikt met de case laat sleutelwoorden om een ​​constante te binden met de overeenkomende case. De where-component voegt een extra voorwaarde toe aan de case-instructie met behulp van de waar trefwoord.

Deze twee concepten worden beter uitgelegd met voorbeelden. Het volgende codeblok laat zien hoe waarde binding werken.

let somePoint = (xaxis: 2, yaxis: 0) switch somePoint case (let x, 0): println ("op de x-as met een x-waarde van \ (x)") case (0, let y): println ("op de y-as met de waarde ay van \ (y)") case let (x, y): println ("elders op (\ (x), \ (y))")

De eerste case-statement, zaak (laat x, 0), zal overeenkomen met de waarden waar Y-as is gelijk aan 0 en elke waarde voor Xaxis, en we binden Xaxis naar de constante X te gebruiken in de case-statement.

Hier is een voorbeeld van de Where-component in actie.

laat groente = "rode peper" schakel plantaardig case "selderij": println ("voeg wat rozijnen toe en maak mieren op een log.") hoes laat x waar x.hasSuffix ("peper"): println ("Ik ben allergisch to \ (x) ") standaard: println (" Alles smaakt goed in soep. ") // uitgangen: ik ben allergisch voor rode paprika

2. Functies en sluitingen

functies

Het definiëren van functies en sluitingen is eenvoudig in Snel. Objective-C-ontwikkelaars kennen ze beter als functies en blokken.

In Swift kunnen functieparameters standaardwaarden hebben, wat doet denken aan scripttalen, zoals PHP en Ruby.

De syntaxis voor functies is als volgt:

func functionName (parameterName: Type = DefaultValue) -> returnType [...] return returnType; 

Schrijf een zeg hallo functie die een parameter nodig heeft naam van type Draad en retourneert een Bool wanneer succesvol, schrijven we het volgende:

func sayHello (name: String) -> Bool println ("hello \ (name)"); geef waar terug;  sayHello ("World") // output // hallo Wereld

Een standaardwaarde doorgeven voor de naam parameter, zou de implementatie van de functie er als volgt uitzien:

func sayHello (name: String = "World") -> Bool println ("hello \ (name)"); geef waar terug;  sayHello () // output // hallo World sayHello ("mike") // output // hallo mike

Een functie die volledig afwezig is in Objective-C zijn tupels. In Swift kunnen functies meerdere waarden retourneren in de vorm van een tupel. Tupels worden behandeld als een enkele variabele, wat betekent dat u het kunt doorgeven, net als een variabele.

Tuples zijn heel gemakkelijk te gebruiken. In feite hebben we al met tuples gewerkt in het vorige artikel toen we een woordenboek opsomden. In het volgende codefragment is het sleutel / waarde-paar een tupel.

for (key, value) in someDictionary println ("Key \ (key) heeft value \ (value)"

Hoe worden tuples gebruikt en hoe profiteer je ervan? Laten we een ander voorbeeld bekijken. Laten we het bovenstaande aanpassen zeg hallo functie om een ​​Booleaanse waarde terug te geven als deze succesvol is, evenals het resulterende bericht. We doen dit door een tuple terug te sturen, (Bool, String). De bijgewerkte zeg hallo functie ziet er als volgt uit:

func sayHello (name: String = "World") -> (Bool, String) let greeting = "hello \ (name)" return (true, greeting);  let (success, greeting) = sayHello () println ("sayHello resulteerde in succes: \ (succes) met groet: \ (groet)"); // output // sayHello resulteerde in succes: 1 met groet: hallo World 

Tuples staan ​​al heel lang op het verlanglijstje van veel Objective-C-programmeurs.

Een andere leuke eigenschap van tuples is dat we de geretourneerde variabelen een naam kunnen geven. Als we het vorige voorbeeld opnieuw bekijken en namen geven aan de variabelen van het tuple, krijgen we het volgende:

func sayHello (name: String = "World") -> (succes: Bool, greeting: String) let greeting = "hello \ (name)" return (true, greeting);  laat status = sayHello () println ("sayHello resulteerde in succes: \ (status.success) met groet: \ (status.greeting)"); // output // sayHello resulteerde in succes: 1 met groet: hallo World 

Dit betekent dat in plaats van het definiëren van een afzonderlijke constante voor elk retourelement van een tuple, we toegang kunnen krijgen tot de geretourneerde tuple-elementen met behulp van puntnotatie zoals getoond in het bovenstaande voorbeeld, status.success en status.greeting.

sluitingen

Sluitingen in Swift zijn hetzelfde als blokken in Objective-C. Ze kunnen inline worden gedefinieerd, als parameter worden doorgegeven of door functies worden geretourneerd. We gebruiken ze precies zoals we blokken in Objective-C zouden gebruiken.

Het definiëren van sluitingen is ook eenvoudig. Eigenlijk is een functie een speciaal geval van sluitingen. Het is dus geen wonder dat het definiëren van een sluiting veel lijkt op het definiëren van een functie.

Sluitingen zijn van het eersteklas type, wat betekent dat ze kunnen worden doorgegeven en geretourneerd door functies, net als elk ander type, zoals IntDraadBool, etc. Afsluitingen zijn in essentie codeblokken die later kunnen worden opgeroepen en hebben toegang tot het bereik waarin ze zijn gedefinieerd.

Het maken van een naamloze sluiting is net zo eenvoudig als het omwikkelen van een codeblok in accolades. De parameters en het retoursoort van de sluiting worden gescheiden van het lichaam van de sluiting met de in trefwoord.

Laten we zeggen dat we een afsluiting willen definiëren die terugkeert waar als een getal gelijk is, kan die sluiting er ongeveer zo uitzien:

let isEven = (nummer: Int) -> Bool in laat mod = nummer% 2 terug (mod == 0)

De ISEVEN sluiting duurt een Int als zijn enkele parameter en retourneert een Bool. Het type van deze sluiting is (nummer: Int) -> Bool, of (Int -> Bool) in het kort. We kunnen bellen ISEVEN overal in onze code, net zoals we een codeblok zouden oproepen in Objective-C.

Om een ​​sluiting van dit type als een parameter van een functie door te geven, gebruiken we het type van de sluiting in de definitie van de functie:

let isEven = (nummer: Int) -> Bool in laat mod = nummer% 2; return (mod == 0);  func verifyIfEven (nummer: Int, verifier: (Int-> Bool)) -> Bool return verifier (number);  verifyIfEven (12, isEven); // geeft true verifyIfEven terug (19, isEven); // geeft false als resultaat

In het bovenstaande voorbeeld, de verificateur parameter van de verifyIfEven functie is een afsluiting die we doorgeven aan de functie.

3. Klassen en structuren

Klassen

Het is tijd om te praten over de hoeksteen van objectgeoriënteerd programmeren, klassen. Klassen, zoals eerder vermeld, worden gedefinieerd in een enkel implementatiebestand met een .snel uitbreiding. Eigendomsverklaringen en methoden zijn allemaal gedefinieerd in dat bestand.

We maken een klas met de klasse sleutelwoord gevolgd door de naam van de klas. De implementatie van de klasse is verpakt in een paar accolades. Net als in Objective-C, is de naamgevingsconventie voor klassen het gebruik van de bovenste kamelenbehuizing voor klassenamen.

class Hotel // properties // functions

Een instantie van de maken Hotel klasse die we schrijven:

laat h = Hotel ()

In Swift hoeft u niet te bellen in het op objecten als in het wordt automatisch voor ons gebeld.

Class inheritance volgt hetzelfde patroon als in Objective-C, een dubbele punt scheidt de klassenaam en die van de superklasse. In het volgende voorbeeld, Hotel erft van de BigHotel klasse.

klas BigHotel: Hotel 

Net als in Objective-C gebruiken we de puntnotatie om toegang te krijgen tot de eigenschappen van een object. Swift gebruikt de puntnotatie echter ook om klasse- en instantiemethoden aan te roepen, zoals u hieronder kunt zien.

// Objective-C UIView * view = [[UIView alloc] init]; [self.view addSubview: view]; // Swift let view = UIView () self.view.addSubview (bekijk)

eigenschappen

Een ander verschil met Objective-C is dat Swift geen onderscheid maakt tussen instantievariabelen (ivars) en eigenschappen. Een instantievariabele is een eigenschap.

Het declareren van een eigenschap is net als het definiëren van een variabele of een constante, met behulp van de var en laat zoekwoorden. Het enige verschil is de context waarin ze zijn gedefinieerd, dat wil zeggen, de context van een klasse.

class Hotel let rooms = 10 var fullRooms = 0

In het bovenstaande voorbeeld, kamers is een onveranderlijke waarde, een constante, ingesteld op 10 en fullRooms is een variabele met een beginwaarde van 0, die we later kunnen veranderen. De regel is dat eigenschappen moeten worden geïnitialiseerd wanneer ze worden gedeclareerd. De enige uitzondering op deze regel zijn optionals, die we in een moment bespreken.

Berekende eigenschappen

De Swift-taal definieert ook berekende eigenschappen. Berekende eigenschappen zijn niets meer dan mooie getters en setters die geen waarde opslaan. Zoals hun naam aangeeft, worden ze on the fly berekend of geëvalueerd.

Hieronder ziet u een voorbeeld van een berekende eigenschap. Ik heb de kamers eigendom van een var voor de rest van deze voorbeelden. Je zult later ontdekken waarom.

class Hotel var rooms = 10 var fullRooms = 0 var description: String get return "Grootte van hotel: \ (kamers) kamers capaciteit: \ (fullRooms) / \ (rooms)"

Omdat het Omschrijving property is read-only en heeft alleen een terugkeer verklaring, we kunnen de krijgen sleutelwoorden en accolades en alleen de terugkeer uitspraak. Dit is steno en dat is wat ik in de rest van deze tutorial zal gebruiken.

class Hotel var rooms = 10 var fullRooms = 0 var description: String return "Grootte van hotel: \ (kamers) kamers capaciteit: \ (fullRooms) / \ (kamers)"

We kunnen ook lees-schrijf-berekende eigenschappen definiëren. In onze klas Hotel, we willen een lege kamers eigendom dat het aantal lege kamers in het hotel krijgt, maar we willen ook updaten fullRooms wanneer we de lege kamers berekende eigenschap. We kunnen dit doen door de reeks sleutelwoord zoals hieronder getoond.

class Hotel var rooms = 10 var fullRooms = 0 var description: String return "Grootte van hotel: \ (kamers) kamers capaciteit: \ (fullRooms) / \ (kamers)" var emptyRooms: Int get return rooms - fullRooms set // newValue constant is hier beschikbaar // met de ingevoerde waarde if (newValue < rooms)  fullRooms = rooms - newValue  else  fullRooms = rooms     let h = Hotel() h.emptyRooms = 3 h.description // Size of Hotel: 10 rooms capacity:7/10

In de lege kamers setter, de nieuwe waarde constant wordt aan ons overhandigd en vertegenwoordigt de waarde die aan de setter is doorgegeven. Het is ook belangrijk op te merken dat berekende eigenschappen altijd worden gedeclareerd als variabelen, met behulp van de var zoekwoord, omdat hun berekende waarde kan veranderen.

methoden

We hebben eerder al functies besproken in dit artikel. Methoden zijn niets meer dan functies die aan een type zijn gebonden, zoals een klasse. In het volgende voorbeeld implementeren we een instantiemethode, bookNumberOfRooms, in de Hotel klas die we eerder hebben gemaakt.

class Hotel var rooms = 10 var fullRooms = 0 var description: String return "Grootte van hotel: \ (kamers) kamers capaciteit: \ (fullRooms) / \ (kamers)" var emptyRooms: Int get return rooms - fullRooms set // newValue constant is hier beschikbaar // met de ingevoerde waarde if (newValue < rooms)  fullRooms = rooms - newValue  else  fullRooms = rooms    func bookNumberOfRooms(room:Int = 1) -> Bool if (self.emptyRooms> room) self.fullRooms ++; return true else return false let h = Hotel () h.emptyRooms = 7 h.description // Grootte van hotel: 10 kamers capaciteit: 3/10 h.bookNumberOfRooms (kamer: 2) // geeft waar terug h .beschrijving // Grootte van het hotel: 10 kamers capaciteit: 5/10 h.bookNumberOfRoom () // geeft waarheidsgetrouw h.description // Hotelgrootte: 10 kamers capaciteit: 6/10 

initialiseerders

De standaard initialisator voor klassen is in het. In de in het functie, stellen we de beginwaarden in van de instantie die is gemaakt.

Als we bijvoorbeeld een Hotel subklasse met 100 kamers, dan hebben we een initializer nodig om de kamers eigendom aan 100. Vergeet niet dat ik eerder ben veranderd kamers van een constante naar een variabele in de Hotel klasse. De reden is dat we inherited constants in een subklasse niet kunnen wijzigen, alleen overgenomen variabelen kunnen worden gewijzigd.

class BigHotel: Hotel init () super.init () rooms = 100 let bh = BigHotel () println (bh.description); // Grootte van het hotel: 100 kamers capaciteit: 0/100

Initializers kunnen ook parameters gebruiken. In het volgende voorbeeld ziet u hoe dit werkt.

class CustomHotel: Hotel init (size: Int) super.init () rooms = size let c = CustomHotel (grootte: 20) c.description // Grootte van het hotel: 20 kamers capaciteit: 0/20

Methoden en gecompileerde eigenschappen overschrijven

Dit is een van de coolste dingen in Swift. In Swift kan een subklasse zowel methoden als berekende eigenschappen overschrijven. Om dit te doen, gebruiken we de override trefwoord. Laten we de Omschrijving berekend eigendom in de CustomHotel klasse:

class CustomHotel: Hotel init (size: Int) super.init () rooms = size negeren var description: String return super.description + "Howdy!"  let c = CustomHotel (grootte: 20) c.description // Hotelgrootte: 20 kamers capaciteit: 0/20 Howdy! 

Het resultaat is dat Omschrijving retourneert het resultaat van de superklassen Omschrijving methode met de string "Howdy!" eraan toegevoegd.

Wat cool is aan override methoden en berekende eigenschappen is de override trefwoord. Wanneer de compiler de override sleutelwoord, het controleert of de superklasse van de klasse de methode implementeert die wordt genegeerd. De compiler controleert ook of de eigenschappen en methoden van een klasse in strijd zijn met eigenschappen of methoden hoger in de overervingsboom.

Ik weet niet hoe vaak een typfout in een overschreven methode in Objective-C me heeft doen vloeken, omdat de code niet werkte. In Swift vertelt de compiler u precies wat er mis is in deze situaties.

structuren

Structuren, gedefinieerd met de struct zoekwoord, zijn krachtiger in Swift dan in C en Objective-C. In C definiëren structs alleen waarden en verwijzingen. Swiftstructs zijn net als Cstructs, maar ze ondersteunen ook berekende eigenschappen en methoden.

Alles wat u met een klasse kunt doen, kunt u doen met een structuur, met twee belangrijke verschillen:

  • structuren ondersteunen geen overerving zoals klassen doen
  • structuren worden per waarde doorgegeven terwijl klassen door verwijzing worden doorgegeven

Hier zijn een paar voorbeelden van structuren in Swift:

struct Rect var origin: Point var size: Size var area: Double return size.width * size.height func isBiggerThanRect (r: Rect) -> Bool return (self.area> r.area) struct Point var x = 0 var y = 0 struct Grootte var width = 0 var height = 0

4. Optionals

Oplossing voor een probleem

Optionals zijn een nieuw concept als u afkomstig bent van Objective-C. Ze lossen een probleem op dat we allemaal tegenkomen als programmeurs. Wanneer we een variabele benaderen waarvan we de waarde niet zeker weten, retourneren we meestal een indicator, een sentinel, om aan te geven dat de geretourneerde waarde geen waarde heeft. Laat me dit illustreren met een voorbeeld van Objective-C:

NSString * someString = @ "ABCDEF"; NSInteger pos = [someString rangeOfString: @ "B"]. Location; // pos = 1

In het bovenstaande voorbeeld proberen we de positie van te vinden @ "B" in someString. Als @ "B" wordt gevonden, de locatie of positie wordt opgeslagen pos. Maar wat gebeurt er als @ "B" is niet gevonden in someString?

In de documentatie staat dat rangeOfString: retourneert een NSRange met plaats ingesteld op de NSNotFound constante. In het geval van rangeOfString:, de schildwacht is NSNotFound. Sentinels worden gebruikt om aan te geven dat de geretourneerde waarde niet geldig is.

In Cocoa zijn er veel toepassingen van dit concept, maar de waarde van de peilstok verschilt van context tot context, 0, -1, NUL, NSIntegerMax, INT_MAX, nul, etc. Het probleem voor de programmeur is dat ze zich moet herinneren welke sentinel in welke context wordt gebruikt. Als de programmeur niet voorzichtig is, kan ze een geldige waarde voor een schildwacht verwarren en vice versa. Swift lost dit probleem met optionals op. Om Brian Lanier te citeren: "Optionals zijn de enige sentinel om ze allemaal te regeren."

Optionals hebben twee staten, een nul staat, wat betekent dat de optionele geen waarde bevat, en een tweede status, wat betekent dat deze een geldige waarde heeft. Zie Optionals als een pakket met een indicator om u te laten weten of de inhoud van het pakket geldig is of niet.

Gebruik

Alle typen in Swift kunnen optioneel worden. We definiëren een optionele door het toevoegen van een ? na de typeaangifte als volgt:

laat someInt: Int? // someInt == nul

We kennen een waarde toe aan een optioneel pakket, net zoals we dat doen met constanten en variabelen.

someInt = 10 // someInt! == 10

Onthoud dat optionals als pakketten zijn. Toen we verklaarden let someInt: Int?, we definieerden een lege box met een waarde van nul. Door de waarde toe te wijzen 10 Optioneel bevat het vak een geheel getal dat gelijk is aan 10 en zijn indicator of status wordt niet nul.

Om de inhoud van een optionele te gebruiken, gebruiken we de ! operator. We moeten er zeker van zijn dat de optionele waarde een geldige waarde heeft voordat deze wordt uitgevouwen. Als u dit niet doet, veroorzaakt dit een runtime-fout. Dit is hoe we toegang krijgen tot de waarde die is opgeslagen in een optionele:

if (someInt! = nil) println ("someInt: \ (someInt!)") else println ("someInt heeft geen waarde") // someInt: 10

Het bovenstaande patroon is zo gewoon in Swift dat we het bovenstaande codeblok kunnen vereenvoudigen door te gebruiken optionele binding met de indien toegestaan zoekwoorden. Bekijk het bijgewerkte codeblok hieronder.

if let value = someInt println ("someInt: \ (value)") else println ("someInt heeft geen waarde")

Optionals zijn het enige type dat een nul waarde. Constanten en variabelen kunnen niet worden geïnitialiseerd of ingesteld op nul. Dit maakt deel uit van het veiligheidsbeleid van Swift, alle niet-optionele variabelen en constanten moet hebben een waarde.

5. Geheugenbeheer

Als je je herinnert, toen we ARC introduceerden, gebruikten we het sterk en zwak sleutelwoorden om objecteigendom te definiëren. Swift heeft ook een sterk en zwak eigendomsmodel, maar introduceert ook een nieuw model, zonder eigenaar. Laten we eens kijken naar elk objecteigendommodel in Swift.

sterk

Sterke referenties zijn de standaard in Swift. Meestal bezitten we het object waarnaar we verwijzen en wij zijn degenen die verantwoordelijk zijn voor het in leven houden van het object waarnaar wordt verwezen.

Aangezien sterke referenties de standaard zijn, is het niet nodig om een ​​sterke referentie naar een object expliciet te behouden, elke referentie is een sterke referentie.

zwak

Een zwakke referentie in Swift geeft aan dat de referentie verwijst naar een object waarvoor we niet verantwoordelijk zijn om in leven te blijven. Het wordt voornamelijk gebruikt tussen twee objecten die de ander niet nodig hebben om in de buurt te zijn, zodat het object zijn levenscyclus kan voortzetten.

Er is er een maar, echter. In Swift moeten zwakke verwijzingen altijd variabelen zijn met een optioneel type, omdat ze zijn ingesteld op nul wanneer het verwezen object is toegewezen. De zwak keyword wordt gebruikt om een ​​variabele als zwak te declareren:

zwakke var-weergave: UIView?

zonder eigenaar

Unowned-referenties zijn nieuw voor Objective-C-programmeurs. Een niet-gebruikte referentie betekent dat we niet verantwoordelijk zijn voor het in leven houden van het object waarnaar wordt verwezen, net als zwakke verwijzingen.

Het verschil met een zwakke referentie is dat een niet-gebruikte referentie niet is ingesteld op nul wanneer het object waarnaar het verwijst wordt deallocated. Een ander belangrijk verschil met zwakke referenties is dat niet-gebruikte referenties worden gedefinieerd als een niet-optioneel type.

Uitgesloten referenties kunnen constanten zijn. Een object dat niet in bezit is bestaat niet zonder de eigenaar ervan en daarom is de referentie die niet wordt gebruikt, nooit nul. Uitgesloten referenties hebben de zonder eigenaar sleutelwoord vóór de definitie van de variabele of constante.

niet-eigendom var-weergave: UIView

Conclusie

Snel is een geweldige taal die veel diepgang en potentieel heeft. Het is leuk om programma's mee te schrijven en het verwijdert veel van de boilerplate code die we in Objective-C schrijven om te zorgen dat onze code veilig is.

Ik raad de Swift programmeertaal ten zeerste aan, die gratis beschikbaar is in de iBooks Store van Apple.