-
Notifications
You must be signed in to change notification settings - Fork 10
/
ios_dev_kurs_app_katalog.tex
1038 lines (679 loc) · 79.8 KB
/
ios_dev_kurs_app_katalog.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[parskip=half, final]{scrreprt}
\input{include/variables}
\input{include/style}
\input{include/code_listing}
\renewcommand{\doctype}{App Katalog}
\renewcommand{\shortdoctype}{App Katalog}
\begin{document}
\maketitle
\tableofcontents
\chapter{Einleitung}
\section{Über dieses Dokument}
Dieser App Katalog enthält Schritt-für-Schritt Anleitungen für die im Rahmen unseres Kurses erstellten Apps sowie die wöchentlich zu bearbeitenden Übungsaufgaben und wird im Verlauf des Semesters kapitelweise auf der Vorlesungswebseite \linkref{http://ios-dev-kurs.github.io/} zur Verfügung gestellt.
Er dient jedoch nur als Ergänzung zum parallel verfügbaren \strong{Skript}, auf das hier häufig verwiesen wird. Dort sind die Erläuterungen zu den verwendeten Technologien, Methoden und Begriffen zu finden.
\section{Workflow mit Git}\label{git_workflow}
Wir arbeiten in diesem Kurs mit der Versionskontroll-Software Git und der Software\-entwicklungs-Plattform GitHub \linkref{https://github.com/}. Mit diesen Werkzeugen kann ich euch Beispielprojekte und Aufgaben bereitstellen, die ihr bearbeiten und mir für Kommentare wieder zur Verfügung stellen könnt. Gleichzeitig lernt ihr dabei direkt den Umgang mit zwei der wichtigsten Werkzeuge in der modernen Softwareentwicklung.
Mit Git können wir Änderungen an einem Projekt, oder \emph{Repository}, in regelmäßigen Abständen in \emph{Commits} speichern. Dann können wir jederzeit zu vorherigen Commits zurückkehren und Änderungen vergleichen. Wer die Speicherpunkte bei Super Mario kennt weiß so etwas zu schätzen.
Außerdem ermöglicht uns Git mit anderen Entwicklern zusammenzuarbeiten. Dazu können wir das Repository auf einem Server wie GitHub bereitstellen. Speichert ein anderer Entwickler Commits in dem Repository, können wir dessen Änderungen mit einem \emph{Merge} mit unseren zusammenführen und dabei gegebenenfalls Konflikte beheben. So wird an Softwareprojekten weltweit zusammengearbeitet.
Da Git ein Kommandozeilenprogramm ist, bedarf es sicherlich einer Eingewöhnung. Wenn ihr noch wenig Erfahrung im Umgang mit der Komandozeile habt könnt ihr zum Einstieg die GitHub Desktop App \linkref{https://desktop.github.com} verwenden, mit der ihr Git über eine graphische Oberfläche bedienen könnt.
\subsection{Ein Repository forken, klonen und bearbeiten}\label{fork_and_clone}
Ich stelle Beispielprojekte und Aufgaben in Repositories wie diesem \linkref{https://github.com/ios-dev-kurs/helloworld} bereit. Verfahrt wie folgt, um es zum Bearbeiten herunterzuladen:
\begin{enumerate}
\item Erstellt einen GitHub Account \linkref{https://github.com/join}, wenn ihr noch keinen habt. Ladet euch die GitHub Desktop App herunter, wenn ihr eine graphische Oberfläche der Kommandozeile vorzieht.
\item Ihr habt nur Lese-Zugriff auf mein Repository. Ihr müsst daher erst einen \emph{Fork} \linkref{https://guides.github.com/activities/forking/} des Repositories erstellen und es damit auf euren Account kopieren. Klickt dazu einfach auf den \emph{Fork} Button auf der Repository-Seite.
\item Euren Fork könnt ihr nun nach Belieben bearbeiten. In diesem Beispiel ist das Fork-Repository unter der URL \url{https://github.com/dein-username/helloworld} verfügbar. Ihr könnt die Kommandozeile oder die GitHub Desktop App verwenden um das Repository herunterzuladen, oder zu \emph{klonen} \linkref{http://gitref.org/creating/\#clone}. Im Terminal lautet der Befehl dazu:
\begin{shcode}
git clone https://github.com/dein-username/helloworld
\end{shcode}
\item Nun könnt ihr an dem Projekt arbeiten. Mit folgendem Befehlt könnt ihr jederzeit überprüfen, welche Dateien sich geändert haben:
\begin{shcode}
git status
\end{shcode}
\item Speichert in regelmäßigen Abständen \emph{Commits} \linkref{http://gitref.org/basic/\#commit}. Jeder Arbeitsschritt sollte durch einen Commit repräsentiert werden. Achtet darauf, dass das Projekt bei jedem Commit funktionsfähig ist. In der Kommandozeile erstellt ihr einen Commit wie folgt:
\begin{shcode}
# Status überprüfen
git status
# Alle Änderungen dem nächsten Commit hinzufügen
git add --all
# Commit durchführen
git commit -m "Kurze Beschreibung der Änderungen"
\end{shcode}
\item Ihr könnt euer lokales Repository jederzeit mit eurem Repository auf GitHub abgleichen. Daher könnt ihr auch problemlos auf verschiedenen Rechnern an einem Projekt arbeiten. Führt einen \emph{Push} \linkref{http://gitref.org/remotes/\#push} oder \emph{Pull} \linkref{http://gitref.org/remotes/\#pull} in der Kommandozeile aus, oder klickt den \emph{Sync} Button in der GitHub Desktop App:
\begin{shcode}
# Fortschritt auf GitHub veröffentlichen
git push
# Änderungen von GitHub herunterladen
git pull
\end{shcode}
\end{enumerate}
\subsection{Eine Aufgabe per Pull-Request einreichen}\label{pull_request}
Habt ihr eine Aufgabe fertig und möchtet Sie einreichen, oder wenn ihr Hilfe benötigt, erstellt eine \emph{Pull-Request} \linkref{https://help.github.com/articles/creating-a-pull-request/}. Damit erhalte ich eine Benachrichtigung mit den Änderungen eures Forks im Vergleich zu meinem Original-Repository. Geht wie folgt vor:
\begin{enumerate}
\item Speichert eure Änderungen in einem Commit und veröffentlicht sie auf GitHub, wenn ihr es noch nicht getan habt.
\item Klickt auf der Repository-Seit den Button \emph{New Pull Request}, überprüft die Änderungen und klickt dann \emph{Create Pull Request}.
\item Gebt der Pull-Request einen Titel und beschreibt kurz die Änderungen. Erwähnt, wenn etwas nicht funktioniert, sodass ich euch helfen kann. Klickt schließlich auf \emph{Create Pull Request}.
\end{enumerate}
\begin{lecture} % Lecture 1
\chapter{Hello World}
Was ist schon ein Programmierkurs, der nicht mit einem klassischen \emph{Hello World} Programm beginnt? Wir werden jedoch noch einen Schritt weitergehen und diesen Gruß graphisch vom iOS Simulator und, soweit vorhanden, direkt von unseren eigenen iOS Geräten ausgeben lassen. Dabei stoßen wir auf unseren ersten \emph{Swift} Code und lernen die IDE \emph{Xcode} kennen. Wir arbeiten außerdem direkt mit der Versionskontroll-Software \emph{Git}, einem der Grundbausteine nahezu jedes Softwareprojekts.
\skriptref{Xcode, Programmieren in Swift, Versionskontrolle mit Git} sowie das Buch \emph{The Swift Programming Language} \linkref{https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/}
\section{"{}Hello World!"{} auf Simulator und Gerät}
\begin{enumerate}
\item Ich habe ein Beispielprojekt bereitgestellt, anhand dessen wir einen ersten Blick auf die Programmierung einer iOS App werfen. Das Ziel ist, das Projekt herunterzuladen, eine erste, einfache App zu schreiben und mir das Ergebnis für etwas \emph{konstruktive Kritik} zur Verfügung zu stellen. Dazu verwenden wir die Versionskontroll-Software \emph{Git} und die Softwareentwicklungs-Plattform \emph{GitHub}, die zu den Werkzeugen gehören, auf denen Softwareprojekte weltweit aufbauen und ohne die moderne Programmierung kaum noch denkbar ist.
Die erste Anweisung lautet:
\strong{\emph{Klont} einen \emph{Fork} des \emph{Repositories} \url{https://github.com/ios-dev-kurs/helloworld}.}
Wenn ihr noch mit keinem dieser Begriffe etwas anfangen könnt, seid beruhight: Wir werden noch so viel mit Git und GitHub arbeiten, dass ihr am Ende dieses Kurses Experten im Umgang damit seid. Befolgt zunächst einfach die Anweisungen in \autoref{fork_and_clone} \emph{Workflow mit Git} bis ihr das Beispielprojekt heruntergeladen habt.
\item Öffnet das Xcode-Projekt \filename{HelloWorld.xcodeproj} und macht euch mithilfe des Kapitels \emph{Xcode} im Skript mit der Benutzeroberfläche vertraut. In der Toolbar oben findet ihr auf der linken Seite die Steuerelemente des Compilers. Wählt das \emph{Target} \emph{HelloWorld} und ein Zielsystem, bspw. den \emph{iPhone 6s} Simulator, und klickt die \strong{\emph{Build \& Run}} Schaltfläche. Das Target wird nun kompiliert und generiert ein \emph{Product}, also unsere App, die im Simulator ausgeführt wird. Das Tastenkürzel für \emph{Build \& Run} in Xcode ist \keys{\cmd + R}.
\item Besonders spannend ist diese App natürlich noch nicht. Das ändern wir jetzt spektakulär, indem wir unseren \strong{ersten Swift Code} schreiben um eine Ausgabe hinzuzufügen. Wählt die Datei \filename{AppDelegate.swift} links im \emph{Project Navigator} aus.
\item Die Methode \swiftinline{application(_:didFinishLaunchingWithOptions:)} wird zu Beginn der Ausführung der App aufgerufen. Ersetzt den Kommentar dort mit einem Gruß zur Ausgabe in der Konsole:
\begin{swiftcode}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
print("Hello World!")
return true
}
\end{swiftcode}
\item Wenn wir unsere App nun erneut mit \emph{Build \& Run} \keys{\cmd + R} kompilieren und ausführen, sehen wir den Text \str{Hello World!} in der Konsole. Dazu wird der zweigeteilte Debug-Bereich unten automatisch eingeblendet \abbref{img:helloworld_helloworld}. Ist der Konsolenbereich zunächst versteckt, kann er mit der Schaltfläche in der rechten unteren Ecke angezeigt werden.
\includegraphicsc[\screenshotwidth]{img/helloworld_helloworld.png}{img:helloworld_helloworld}{In der Konsole des Debug-Bereichs werden Ausgaben der laufenden App angezeigt}
\item Wenn ihr ein iOS Gerät dabei habt, verbindet es per USB-Kabel mit eurem Mac und wählt das Gerät in der Toolbar als Zielsystem aus. Mit einem \emph{Build \& Run} wird die App nun kompiliert, auf dem Gerät installiert und ausgeführt. In der Konsole erscheint wieder die Ausgabe \str{Hello World!}, diesmal direkt vom Gerät ausgegeben.
\end{enumerate}
\section{Graphisches "{}Hello World!"{}}
Natürlich wird ein Benutzer unserer App von den Ausgaben in der Konsole nichts mitbekommen. Diese dienen bei der Programmierung hauptsächlich dazu, Abläufe im Code nachzuvollziehen und Fehler zu finden. Unsere App ist also nur sinnvoll, wenn wir die Ausgaben auch auf dem Bildschirm darstellen können.
\skriptref{Xcode / Interface Builder}
\begin{enumerate}
\item Zur Gestaltung der Benutzeroberfläche oder \emph{User Interface (UI)} verwenden wir ein \emph{Storyboard}. Wählt im Project Navigator die Datei \str{main.storyboard} aus.
\item Der Editor-Bereich zeigt nun den Interface Builder. In diesem Modus möchten wir häufig eine angepasste Konfiguration des Xcode-Fensters verwenden, es bietet sich also an, mit \keys{\cmd + T} einen neuen Tab zu öffnen. Blendet dann mit den Schaltflächen auf der rechten Seite der Toolbar den Navigator- und Debug-Bereich links und unten aus und den Inspektor rechts ein. Wählt dort außerdem zunächst den Standard-Editor, also die linke der drei Schaltflächen \abbref{img:helloworld_ib}.
\includegraphicsc[\screenshotwidth]{img/helloworld_ib.png}{img:helloworld_ib}{Für den Interface Builder verwenden wir eine angepasste Fensterkonfiguration mit dem Inspektor anstatt des Navigators}
\item Unser UI besteht bisher nur aus einer einzigen Ansicht, oder \emph{Scene}. Ein Pfeil kennzeichnet die Scene, die zum Start der App angezeigt wird. Im Inspektor rechts ist unten die Object Library zu finden. Wählt den entsprechenden Tab aus, wenn er noch nicht angezeigt wird \abbref{img:helloworld_ib}.
\item Durchsucht die Liste von Interfaceelementen nach einem Objekt der Klasse \swiftinline{UILabel}, indem ihr das Suchfeld unten verwendet, und zieht ein Label irgendwo auf die erste Scene. Doppelklickt auf das erstellte Label und tippt \str{Hello World!}.
\item Ein \emph{Build \& Run} mit einem iPhone-Zielsystem zeigt diesen Gruß nun statisch auf dem Bildschirm an.
\item Habt ihr das Label im Interface Builder ausgewählt, zeigt der Inspektor Informationen darüber an. Im \emph{Identity Inspector} könnt ihr euch vergewissern, dass das Objekt, was zur Laufzeit erzeugt wird und das Label darstellt, ein Objekt der Klasse \swiftinline{UILabel} ist. Im \emph{Attributes Inspector} stehen viele Optionen zur Auswahl, mit denen Eigenschaften wie Inhalt, Schrift und Farbe des Labels angepasst werden können.
\item Natürlich möchten wir unser UI zur Laufzeit mit Inhalt füllen und den Benutzer mit den Interfaceelementen interagieren lassen können. Zieht ein \swiftinline{UIButton}- und \swiftinline{UITextField}-Objekt auf die Scene und positioniert sie passend \abbref{img:helloworld_ui}. Mit dem Attributes Inspector könnt ihr dem Button nun den Titel \str{Say Hello!} geben und für das Text Field einen Placeholder \str{Name} einstellen.
\includegraphicsc[.6\textwidth]{img/helloworld_ui.png}{img:helloworld_ui}{Mit einem Text Field, einem Button und einem Label erstellen wir ein simples UI}
\item Damit sich das Layout an jede Bildschirmgröße automatisch anpasst, verwenden wir nun \emph{Auto Layout}. Die Schaltflächen dazu findet ihr in der unteren rechten Ecke des Interface Builder Editors. Markiert mit gedrückter \keys{\cmd}-Taste die drei Interfaceelemente und klickt auf das Linke der Symbole mit dem Titel \emph{Stack}, sodass die Elemente in eine \emph{Stack View} eingebettet werden. Dieses praktische Objekt positioniert die enthaltenen Elemente automatisch relativ zueinander. Wählt die Stack View aus und konfiguriert im Attributes Inspector \emph{Axis Vertical}, \emph{Alignment Fill}, \emph{Distribution Fill} und \emph{Spacing 8}. Zusätzlich müssen wir Regeln aufstellen, wie die Stack View auf dem Bildschirm positioniert werden soll. Dazu erstellen wir \emph{Constraints} mit den beiden mittleren der Auto Layout Schaltflächen. Befestigt die Stack View links und rechts am Rand und zentriert sie vertikal.
\item Zur Laufzeit der App wird für jedes im Storyboard konfigurierte Interfaceelement ein Objekt der entsprechenden Klasse erstellt und dessen Attribute gesetzt. Um nun im Code auf die erstellten Objekte zugreifen und auf Benutzereingaben reagieren zu können, verwenden wir \emph{IBOutlets} und \emph{IBActions}.
Blendet den Inspektor aus und wählt stattdessen den Assistant-Editor (mittlere Schaltfläche) in der Toolbar. Stellt den Modus in der Jump bar auf \emph{Automatic}. Im Assistant wird automatisch die Implementierung des übergeordneten View Controllers eingeblendet \abbref{img:helloworld_assistant}.
\includegraphicsc[\screenshotwidth]{img/helloworld_assistant.png}{img:helloworld_assistant}{Mithilfe des Assistants können Interface-Builder und Code nebeneinander angezeigt werden.}
\item \emph{View Controller} sind Objekte einer Subklasse von \swiftinline{UIViewController}, die jeweils einen Teil der App steuern. Diese sind zentrale Bestandteile einer App, mit denen wir uns noch detailliert beschäftigen werden. Ein erster View Controller zur Steuerung dieser ersten Ansicht ist im Projekt bereits enthalten.
Fügt dieser Klasse \swiftinline{ViewController: UIViewController} Attribute für das \swiftinline{UILabel} und das \swiftinline{UITextField} hinzu und kennzeichnet diese mit \swiftinline{@IBOutlet}. Implementiert außerdem eine mit \swiftinline{IBAction} gekennzeichnete Methode, die aufgerufen werden soll, wenn der Benutzer den \swiftinline{UIButton} betätigt:
\begin{swiftcode}
import UIKit
class ViewController: UIViewController {
@IBOutlet var nameTextfield: UITextField!
@IBOutlet var greetingLabel: UILabel!
@IBAction func greetingButtonPressed(_ sender: UIButton) {
print("Hello World!")
}
}
@end
\end{swiftcode}
\item Nun zieht mit gedrückter \keys{\ctrl}-Taste eine Linie von dem Textfeld und dem Label im Interface Builder auf das jeweilige Attribut im Code. Die Codezeile wird dabei blau hinterlegt. Zieht außerdem genauso eine Line von dem Button auf die zuvor definierte Methode. Im Connection Inspector könnt ihr die IBOutlets und IBActions eines ausgewählten Objekts betrachten und wieder entfernen. Dieser Prozess ist im Skript noch detaillierter beschrieben.
\item Versucht nun einen \emph{Build \& Run}. Betätigt ihr den Button, wird die Methode ausgeführt und der Gruß \str{Hello World!} in der Konsole ausgegeben!
\item Um die App nun alltagstauglich zu gestalten, muss dieser Gruß natürlich personalisiert und auf dem Bildschirm angezeigt werden. Dazu verwenden wir das Attribut \swiftinline{text} der Klassen \swiftinline{UITextField} und \swiftinline{UILabel} und zeigen einen personalisierten Gruß an, wenn im Text Field ein Name geschrieben steht:
\begin{swiftcode}
@IBAction func greetingButtonPressed(_ sender: UIButton) {
if let name = nameTextfield.text, !name.isEmpty {
greetingLabel.text = "Hello \(name)!"
} else {
greetingLabel.text = "Hello World!"
}
}
\end{swiftcode}
Nach einem \emph{Build \& Run} erhalten wir unser erstes interaktives Interface, in dem ihr im Textfeld einen Namen eintippen könnt und persönlich begrüßt werdet \abbref{img:helloworld_final}!
\includegraphicsc[\iphonewidth]{img/helloworld_final.png}{img:helloworld_final}{Drücken wir auf den Button, werden wir persönlich begrüßt. Sehr praktisch!}
\item Die App ist fertig! Eure Eltern werden stolz auf euch sein. Gebt mir nun die Gelegenheit, eure Arbeit zu kommentieren. Wir verwendet dazu wieder Git, um die Änderungen, an denen ihr gerade gearbeitet habt, zu speichern, hochzuladen und mir zur Verfügung zu stellen. Befolgt dazu die weiteren Anweisungen in \autoref{pull_request}, bis ihr mir eine \emph{Pull-Request} geschickt habt.
\end{enumerate}
\begin{exc}
\begin{excitem}{simpleui}{Simple UI}{2}
Erstellt einen Fork des Repositories \url{https://github.com/ios-dev-kurs/simpleui} und schreibt eine App mit einigen Interfaceelementen, die etwas sinnvolles tut. Stellt mir das Ergebnis anschließend als Pull-Request zur Verfügung.
Implementiert eines der folgenden Beispiele oder eine eigene Idee. Ich freue mich auf kreative Apps!
\begin{description}
\item[Counter] Auf dem Bildschirm ist ein Label zu sehen, das den Wert eines Attributs \swiftinline{var count: Int} anzeigt, wenn eine Methode \swiftinline{updateLabel} aufgerufen wird. Buttons mit den Titeln \str{+1}, \str{-1} und \str{Reset} ändern den Wert dieses Attributs entsprechend und rufen die \swiftinline{updateLabel}-Methode auf.
\item[BMI] Nach Eingabe von Gewicht $m$ und Größe $l$ wird der Body-Mass-Index\linkref{http://de.wikipedia.org/wiki/Body-Mass-Index} $BMI=m/l^2$ berechnet und angezeigt.
\item[RGB] In drei Textfelder kann jeweils ein Wert zwischen 0 und 255 für die Rot-, Grün- oder Blau-Komponenten eingegeben werden. Ein Button setzt die Hintergrundfarbe \swiftinline{self.view.backgroundColor} entsprechend und ein weiterer Button generiert eine zufällige Hintergrundfarbe. Ihr könnt noch einen \swiftinline{UISwitch} hinzufügen, der einen Timer ein- und ausschaltet und damit die Hintergrundfarbe bei jedem Timerintervall zufällig wechselt (s. Hinweis).
\end{description}
\begin{exchinweise}
\item In der nächsten Vorlesung lernen wir die Objektorientierte Programmierung in Swift systematisch. Orientiert euch für diese Aufgabe an der \emph{HelloWorld} App und versucht die Funktionalität mit den folgenden Hinweisen zu implementieren. Wenn ihr nicht weiter kommt, schickt mir eine Pull-Request mit einem Kommentar und ich helfe euch.
\item Das Attribut \swiftinline{text} von \swiftinline{UILabel} und \swiftinline{UITextField} gibt eine \emph{optionale} Zeichenkette \swiftinline{String?} zurück. Ihr werdet euch mit solchen \emph{Optionals} solange herumärgern, bis ihr sie zu schätzen lernt. Verwendet die \emph{Optional Binding} Syntax um das Optional zu entpacken:
\begin{swiftcode}
if let name = nameTextfield.text {
// name existiert und kann verwendet werden
} else {
// nameTextfield.text hat keinen Wert
}
\end{swiftcode}
\item Einen \swiftinline{String} könnt ihr schnell in eine ganze Zahl \swiftinline{Int} oder eine Dezimalzahl \swiftinline{Double} umwandeln. Da dies fehlschlagen kann, gibt auch diese Operation einen Optional \swiftinline{Int?} bzw. \swiftinline{Double?} zurück, den wir entpacken müssen:
\begin{swiftcode}
// Sei text ein String
if let number = Double(text) {
// text konnte in eine Zahl number umgewandelt werden
}
\end{swiftcode}
\item Definiert ein Attribut wie \swiftinline{var count: Int} mit einem Startwert:
\begin{swiftcode}
class ViewController: UIViewController {
var count: Int = 0
// ...
}
\end{swiftcode}
\item Natürlich gibt es die grundlegenden Rechenoperationen \swiftinline{+-*/} in Swift. Diese Operationen können mit der Zuweisung zu einer Variablen verbunden werden, um bspw. eine Variable \swiftinline{count} um \swiftinline{1} zu erhöhen:
\begin{swiftcode}
count += 1
\end{swiftcode}
\item Eine Farbe wird durch die Klasse \swiftinline{UIColor} repräsentiert. Der \emph{Initializer} \swiftinline{UIColor(red:green:blue:alpha:)} akzeptiert jeweils Werte zwischen 0 und 1:
\begin{swiftcode}
let color = UIColor(red: 1, green: 0, blue: 0, alpha: 1) // rot
\end{swiftcode}
\item Die Funktion \swiftinline{arc4random_uniform(n)} gibt eine Pseudozufallszahl $x$ mit $0<=x<n$ aus.
\item Wenn ein \swiftinline{UISwitch} betätigt wird, kann das Event genauso mit einer IBAction verbunden werden wie das eines \swiftinline{UIButton}. Mit einem Attribut \swiftinline{var randomTimer: NSTimer?} können wir dann die Methode für das zufällige Wechseln der Hintergrundfarbe implementieren:
\begin{swiftcode}
var randomTimer: Timer?
@IBAction func switchValueChanged(_ sender: UISwitch) {
if sender.on {
randomTimer = Timer.scheduledTimer(timeInterval: 0.15, target: self, selector: #selector(randomButtonPressed(_:)), userInfo: nil, repeats: true)
} else {
randomTimer?.invalidate()
randomTimer = nil
}
}
\end{swiftcode}
Somit wird periodisch die Methode \swiftinline{randomButtonPressed(_:)} aufgerufen, die natürlich implementiert sein muss.
\end{exchinweise}
\end{excitem}
\end{exc}
\end{lecture}
\begin{lecture} % Lecture 2
\chapter{A Swift Tour}
Für unsere ersten Apps hat eine gute Portion Intuition für ein wenig Swift Code ausgereicht. Bevor wir tiefer in die App-Programmierung einsteigen beschäftigen wir uns einmal genauer mit der Programmierung in Swift.
Apple bietet mit dem Buch \emph{The Swift Programming Language} eine hervorragende Dokumentation und ein einführendes Kapitel mit den Namen \emph{A Swift Tour}. Das Buch findet ihr sowohl online \linkref{https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/} als auch im iBooks Store \linkref{https://itunes.apple.com/de/book/swift-programming-language/id881256329?mt=11} immer in aktueller Version. Auch das letzte Kapitel \emph{Language Reference} ist sehr spannend wenn ihr euch für den detaillierten Aufbau der Sprache und deren Grammatik interessiert.
Löst die Übungsaufgaben mit eurem Wissen aus der Vorlesung und den zugehörigen Materialien auf der Vorlesungwebseite. Zieht zuerst das Buch \emph{The Swift Programming Language} zu Rate wenn ihr nicht weiterkommt. Schickt mir bei weiteren Fragen eine Pull-Request oder einen Xcode Playground per Email.
\strong{Xcode Playgrounds} eignen sich hervorragend um Swift Code auszuprobieren und um Code zu schreiben der die Infrastruktur einer App nicht erfordert, wie bspw. die Übungsaufgaben \emph{Fibonacci}, \emph{Primzahlen} und \emph{Poker}. Erstellt einen Playground mit \menu{File > New > Playground...} oder \keys{\cmd + \shift + \Alt + N}.
Die Übungsaufgaben \emph{Fibonacci} und \emph{Primzahlen} sind optional und an Kursteilnehmer gerichtet, die noch wenig oder keine Programmierkenntnisse mitbringen. Auch erfahrene Programmierer können aber anhand dieser Aufgaben die Swift Syntax kennenlernen und versuchen die Aufgaben so \emph{swifty} wie möglich zu lösen.
\begin{exc}
\begin{excitem*}{fibonacci}{Fibonacci}{1}
Schreibt in einem Xcode Playground einen Algorithmus der alle Folgenglieder $F_n < 1000$ der Fibonaccifolge
\begin{equation}
F_n = F_{n-1} + F_{n-2}
\end{equation}
\begin{equation}
F_1=1, F_2=2
\end{equation}
in der Konsole ausgibt.
\exchinweis{Versucht's mit einer \swiftinline{while}-Schleife und zwei Variablen für die letzten beiden Folgenglieder, die außerhalb der Schleife definiert wurden. Wer die Aufgabe richtig \emph{swifty} lösen will kann stattdessen ein \swiftinline{struct: FibonacciSequence} schreiben welches das \swiftinline{Sequence} Protokoll erfüllt.}
\end{excitem*}
\begin{excitem*}{primenumbers}{Primzahlen}{1}
\begin{enumerate}
\item Schreibt eine Funktion \swiftinline{func isPrimeNumber(_ n: Int) -> Bool} die eine Zahl annimmt und \swiftinline{true} zurückgibt, wenn diese eine Primzahl ist, andernfalls \swiftinline{false}.
\exchinweis{Iteriert in einer \swiftinline{for}-Schleife durch alle Zahlen von 2 bis n: \swiftinline{for i in 2..<n}. Überspringt den Schleifenschritt mit \swiftinline{continue} wenn \swiftinline{i} gleich \swiftinline{n} ist. Prüft sonst mit dem Modulo-Operator \swiftinline{|\%|} den Rest der Division \swiftinline{n |\%| i} der beiden Zahlen. Ist dieser \swiftinline{0} so ist \swiftinline{n} durch \swiftinline{i} teilbar und ihr könnt \swiftinline{false} zurückgeben: \swiftinline{return false}. Gebt sonst nach dem Durchlauf der Schleife \swiftinline{true} zurück.}
\item Schreibt dann eine Funktion \swiftinline{func primeNumbers(upTo maxNumber: Int) -> [Int]} die alle Primzahlen bis \swiftinline{maxNumber} als Liste \swiftinline{[Int]} (kurz für \swiftinline{Array<Int>}) zurückgibt.
\exchinweis{Erstellt zuerst eine leere Liste von \swiftinline{Int}s: \swiftinline{var primeNumbers: [Int] = []}. Iteriert dann in einer \swiftinline{for}-Schleife durch die Zahlen \swiftinline{1...maxNumber} und fügt die Zahl der Liste mit \swiftinline{primeNumbers.append(n)} hinzu wenn sie \swiftinline{isPrimeNumber(n)} erfüllt. Gebt die Liste mit \swiftinline{return primeNumbers} nach Schleifendurchlauf zurück. Richtig \emph{swifty} könnt ihr die Aufgabe auch in einer Zeile lösen indem ihr die Methode \swiftinline{filter} von \swiftinline{SequenceType} verwendet.}
\end{enumerate}
\end{excitem*}
\begin{excitem}{chatter}{Chatter}{2}
In dieser Aufgabe schreiben wir zusammen an einer App!
Forkt das Repository \url{https://github.com/ios-dev-kurs/chatter} und erstellt eine Pull Request um eure Lösung einzureichen oder Fragen zu stellen.
\begin{enumerate}[label=\roman*.]
\item Die \emph{Chatter} App ist in der \filename{README.md} Datei beschrieben. Ihr könnt euch die Projektdateien anschauen und die App im Simulator oder auf euren Geräten ausführen und ausprobieren. Wenn euch interessiert, wie die App aufgebaut ist, lest die \filename{README.md} und die Kommentare im Code.
\item Ihr habt nun sicherlich erkannt worum es in der App geht: Instanzen verschiedener Subklassen von \swiftinline{Chatter} chatten miteinander. Dabei überschreiben die Subklassen jeweils nur die Implementierung weniger Methoden, die in der \swiftinline{Chatter} Klasse dokumentiert sind.
Eure Aufgabe ist es nun, eine eigene Subklasse zu schreiben und damit euren Beitrag zu dieser App zu leisten! Ihr könnt einen bekannten Charakter darstellen oder einen Neuen erschaffen. Ich übernehme eure Pull-Request dann in das Original-Repository, sodass euer Charakter mit denen aller anderen Kursteilnehmer chatten kann.
Erstellt dazu mit \keys{\cmd + N} eine neue \filename{.swift}-Datei mit dem Namen eures Charakters und platziert sie im Xcode Project Navigator unter \menu{chatter > Model > Chatters}. Orientiert euch an \swiftinline{Yoda.swift} um eure neue Subklasse von \swiftinline{Chatter} zu implementieren.
\item Überschreibt die relevanten Methoden in eurer Subklasse wie in der \filename{README.md} Datei beschrieben. Hier könnt ihr einfach zufällige Chatnachrichten generieren, oder auch komplexere Mechaniken einbauen, sodass eine etwas natürlichere Konversation zustande kommt.
In eurer eigenen Subklasse könnt ihr dabei beliebig Code schreiben und bspw. Attribute einführen, um den Zustand eures Charakters darzustellen, wenn ihr möchtet. Er oder sie (oder es?) könnte bspw. mit jeder Nachricht wütender werden oder dergleichen.
\item Sichert eure Änderungen regelmäßig in Commits, wenn der Code fehlerfrei kompiliert. Achtet darauf nur Änderungen eurer Subklasse und nur wenn nötig Änderungen in anderen Dateien zu committen. Die \filename{project.pbxproj} Datei enthält Informationen zu den Projektdateien - da ihr neue Dateien hinzugefügt habt, gehört diese zum Commit dazu.
\item Mit eurem Fork des Repositories auf GitHub könnt ihr eure Änderungen jederzeit abgleichen. Bei der Gelegenheit bietet es sich an auch die neuesten Änderungen aus dem Original-Repository herunterzuladen, sodass ihr die neuen Charaktere der anderen Kursteilnehmer erhaltet:
\begin{shcode}
git pull https://github.com/ios-dev-kurs/chatter.git master
\end{shcode}
Die \shinline{pull} Operation versucht, die heruntergeladenen Änderungen mit den lokalen Änderungen zusammenzuführen. Das klappt nicht immer ohne Konflikte. Da jeder von euch dem Projekt eine Datei hinzufügt ändert sich jeweils die \filename{project.pbxproj} Datei. Treten Konflikte auf, müsst die die Datei in einem Texteditor öffnen und nach den Konfliktmarkierungen suchen:
\begin{shcode}
<<<<<<< HEAD:
# lokaler Code vor dem Merge
=======
# durch den Merge veränderter Code
>>>>>>>
\end{shcode}
Behebt den Konflikt indem ihr die Konfliktmarkierungen löscht und den Code dazwischen gegebenenfalls anpasst. Dann könnt ihr den Merge committen:
\begin{shcode}
git add --all
git commit
\end{shcode}
Euer Repository enthält dann sowohl den aktuellen Stand des Original-Repositories, als auch eure Änderungen.
\item Wenn ihr mit eurer neuen \swiftinline{Chatter} Subklasse zufrieden seid schickt mir eine Pull-Request. So werden eure Änderungen in das Original-Repository integriert und tauchen auch bei den anderen Teilnehmern auf, wenn diese das nächste mal einen \shinline{git pull} durchführen.
Ich bin gespannt auf eure Implementierungen!
\end{enumerate}
\end{excitem}
\begin{excitem}{emails}{Poker}{3}
In dieser Aufgabe berechnen wir die Wahrscheinlichkeit für einen \emph{Flush} beim Poker.
Forkt das Repository \url{https://github.com/ios-dev-kurs/poker} und erstellt eine Pull Request um eure Lösung einzureichen oder Fragen zu stellen.
\begin{enumerate}[label=\roman*.]
\item Zuerst modellieren wir die Spielkarten. Eine Karte \swiftinline{Card} hat immer eine \emph{Farbe} \swiftinline{Suit} (Karo, Herz, Pik oder Kreuz) und einen \emph{Rang} \swiftinline{Rank} (2 bis 10, Bube, Dame, König oder Ass).
Wir modellieren \swiftinline{Card} als Struct, und \swiftinline{Suit} und \swiftinline{Rank} als Enums. Warum verwenden wir keine Klasse für \swiftinline{Card}? Warum eignet sich ein Enum so hervorragend für \swiftinline{Suit} und \swiftinline{Rank}? Beantwortet diese Fragen kurz in einem Kommentar im Playground.
\item Schreibt zwei Enums \swiftinline{enum Suit: Int} und \swiftinline{enum Rank: Int} mit ihren jeweiligen Fällen (\swiftinline{case diamonds} usw.). Bei den Rängen \emph{2} bis \emph{10} schreibt ihr am besten die Zahl aus (\swiftinline{case two} usw.).
Implementiert jeweils eine \emph{Computed Property} \swiftinline{var description: String} in der ihr mithilfe einer \swiftinline{switch}-Abfrage für jeden Fall ein Symbol zurückgebt. \strong{Tipp:} Für Karo, Herz, Pik und Kreuz gibt es Unicode-Symbole\linkref{http://en.wikipedia.org/wiki/Playing_cards_in_Unicode}! Außerdem verlangt das Protokoll \swiftinline{CustomStringConvertible} nur das Attribut \swiftinline{description}, schreibt also z.B. \swiftinline{enum Suit: Int, CustomStringConvertible} damit das Symbol in \swiftinline{print}s verwendet wird.
Schreibt dann einen \swiftinline{struct Card} mit zwei Attributen \swiftinline{let suit: Suit} und \swiftinline{let rank: Rank}, sowie einer \emph{Computed Property} \swiftinline{var description: String}, die einen aus Farbe und Rang zusammengesetzten String zurückgibt. Lasst auch \swiftinline{Card} das \swiftinline{CustomStringConvertible} Protokoll erfüllen.
\item Nun können wir eine Poker Hand modellieren. Schreibt den \swiftinline{struct PokerHand} mit einem Attribut \swiftinline{let cards: [Card]} und einer \emph{Computed Property} \swiftinline{var description: String}, die die \swiftinline{description} der Karten kombiniert.
Um einfach zufällige Poker Hände generieren zu können, implementiert einen Initializer \swiftinline{init()}, der eine Hand aus fünf zufälligen Karten erstellt. \strong{Wichtig:} Da aus einem Deck von paarweise verschiedenen Karten gezogen wird, darf keine Karte doppelt vorkommen.
\begin{exchinweise}
\item Da wir \swiftinline{Suit} und \swiftinline{Rank} von \swiftinline{Int} abgeleitet haben, können wir Zufallszahlen generieren und die Enums daraus erstellen:
\begin{swiftcode}
let rndSuit = Suit(rawValue: Int(arc4random_uniform(4)))!
let rndRank = Rank(rawValue: Int(arc4random_uniform(13)))!
let rndCard = Card(suit: rndSuit, rank: rndRank) // Eine zufällige Spielkarte
\end{swiftcode}
\item Die Funktion \swiftinline{contains} könnte hilfreich sein, um das Vorhandensein von Karten zu überprüfen. Um diese mit \swiftinline{Card} verwenden zu können muss \swiftinline{Card} das \swiftinline{Equatable} Protokoll erfüllen. Schreibt \swiftinline{extenstion Card: Equatable {}} und dann außerhalb:
\begin{swiftcode}
func ==(lhs: Card, rhs: Card) -> Bool {
return lhs.suit == rhs.suit && lhs.rank == rhs.rank
}
\end{swiftcode}
\end{exchinweise}
Erstellt ein paar Poker Hände und lasst euch die \swiftinline{description} ausgeben. Habt ihr etwas gutes gezogen?
\item Implementiert nun ein weiteres Enum \swiftinline{enum Ranking: Int} mit den Fällen \swiftinline{case highCard, flush, straightFlush} usw., die ihr bspw. auf Wikipedia\linkref{http://en.wikipedia.org/wiki/List_of_poker_hands} findet.
Fügt dann dem \swiftinline{struct PokerHand} eine Computed Property \swiftinline{var ranking: Ranking} hinzu. Implementiert hier einen Algorithmus, der prüft, ob ein \emph{Flush} vorliegt. Dann soll \swiftinline{.flush} zurückgegeben werden, ansonsten einfach \swiftinline{.highCard}.
\item Wir können nun einige tausend Hände generieren und die Wahrscheinlichkeit für einen Flush abschätzen. Fügt einfach folgenden Code am Ende des Playgrounds ein:
\begin{swiftcode}
var rankingCounts = [Ranking : Int]()
let samples = 1000
for i in 0...samples {
let ranking = PokerHand().ranking
if rankingCounts[ranking] == nil {
rankingCounts[ranking] = 1
} else {
rankingCounts[ranking]! += 1
}
}
for (ranking, count) in rankingCounts {
print("The probability of being dealt a \(ranking.description) is \(Double(count) / Double(samples) * 100)%")
}
\end{swiftcode}
Die Ausführung kann etwas dauern, justiert ggfs. \swiftinline{samples}. Stimmt die Wahrscheinlichkeit ungefähr mit der Angabe auf Wikipedia überein?
\item \strong{Extra:} Ihr könnt das Programm nun noch erweitern und versuchen, die anderen Ränge zu überprüfen. Dabei könnten Hilfsattribute wie \swiftinline{var hasFlush: Bool} oder \swiftinline{var pairCount: Int} nützlich sein. Bekommt es jemand es jemand hin, eine Funktion zu schreiben, die zwei Hände vergleicht und den Sieger bestimmt? \strong{Tipp:} Dazu könnte es hilfreich sein, die Fälle des \swiftinline{enum: Ranking} um \emph{Associated Attributes} zu erweitern.
\end{enumerate}
\end{excitem}
\end{exc}
\end{lecture}
\begin{lecture} % Lecture 3
\chapter{iOS App Architektur}
Wir haben nun die Grundlagen der Programmierung in Swift gelernt. Jetzt können wir uns der Programmierung komplexerer Apps zuwenden. Dabei lernen wir die \emph{Architektur} von iOS Apps kennen und verwenden Konzepte zur Strukturierung unseres Programmcodes, die für größerere Softwareprojekte notwendig sind.
Die grundlegende Architektur einer iOS App illustriert das Repository \url{https://github.com/ios-dev-kurs/bare} und der Abschnitt \emph{iOS App Lifecycle} im Skript. Schaut euch insbesondere auch die Commitfolge \shinline{git log} des Repositories an. Jeder Commit stellt einen Schritt vom minimal ausführbaren Code bis zu einer funktionsfähigen App mit Storyboard dar. Ein Commit wird durch seinen \emph{Hash} identifiziert, der auch von \shinline{git log} ausgegeben wird. Diesen könnt ihr verwenden, um das Verzeichnis in den Zustand zu einem bestimmten Commit zu versetzen:
\begin{shcode}
git checkout e35cec8e71ffb87d19b837cf48c12837329a6d82 # z.B. Hash des ersten Commits
\end{shcode}
Sobald unsere App startet und ein Storyboard lädt sind liegt die Verantwortung bei uns. Unsere Aufgabe ist nun das \emph{Software Engineering}. Viele Konzepte sind von der Plattform und Programmiersprache unabhängig gültig (siehe bspw. das \emph{DRY - Don't repeat yourself} Prinzip \linkref{https://de.wikipedia.org/wiki/Don’t_repeat_yourself}). Ziel ist, unseren Programmcode übersichtlich, flexibel und erweiterbar zu strukturieren. Damit vermeiden wir automatisch Fehler und verlieren uns nicht in der Komplexität des Codes.
Die iOS App Entwicklung orientiert sich konsequent am \emph{Model-View-Controller Konzept} der Programmierung oder Varianten dieses Konzepts. Es ist nicht nur in Apples Frameworks wie \swiftinline{UIKit} rigoros umgesetzt sondern stellt auch die Grundlage für die weitere Konzeption unserer Apps dar und wird auch in vielen anderen Bereichen der Softwareentwicklung verwendet. Das Konzept ist im Skript beschrieben und sollte bei Entscheidungen zur Architektur einer App stets zu Rate gezogen werden.
\skriptref{Das Model-View-Controller Konzept}
\begin{exc}
\begin{excitem}{lifetime}{Lifetime}{2}
In dieser Aufgabe zeigen wir für die Kontakte auf unseren iOS Geräten die Zeit seit ihrem Geburtstag an.
Forkt das Repository \url{https://github.com/ios-dev-kurs/lifetime} und erstellt eine Pull-Request um eure Lösung einzureichen oder Fragen zu stellen.
\begin{enumerate}
\item Die \emph{Model}-Komponente \mvcindicatormodel der App ist bereits implementiert. Wir verwenden das \swiftinline{Contacts} Framework von Apple, das Zugriff auf die Kontakte ermöglicht. In \filename{Lifetime.swift} erweitern wir die Klasse \swiftinline{CNContact} um ein Computed Attribute \swiftinline{lifetime: TimeInverval?}.
\mvcindicatorcontroller Um auf Kontakte zuzugreifen stellen wir Anfragen an einen \swiftinline{CNContactStore}. Das \swiftinline{AppDelegate} erstellt einen solchen und reicht ihn an den \swiftinline{ContactListViewController} weiter. Dieser lädt die Kontakte und soll sie nun anzeigen. Dazu verwaltet er eine \swiftinline{UITableView}, die nach dem \emph{Delegate}-Prinzip nach Bedarf Anfragen nach Zahl und Inhalt der anzuzeigenden Zeilen stellt. Diese Anfragen muss der \swiftinline{ContactListViewController} beantworten. Eure erste Aufgabe ist, zu diesem Zweck das \swiftinline{UITableViewDatasource} Protokoll zu implementieren.
Implementiert das Protokoll in einer Erweiterung in \filename{ContactListViewController.swift}. Die folgenden drei Methoden muss jede Implementierung des Protokolls mindestens bereitstellen:
\begin{swiftcode}
extension ContactListViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1 // Wir zeigen die Kontakte zunächst in einer einzelnen Section an
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contacts.count // Für jeden Kontakt soll eine Zeile angezeigt werden
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// VIEW-Komponente: Frage die Table View nach einer wiederverwendbaren Zelle
let cell = tableView.dequeueReusableCell(withIdentifier: "LifetimeCell", for: indexPath) as! LifetimeCell
// MODEL-Komponente: Bestimme den Kontakt für diese Zeile
let contact = contacts[indexPath.row]
// CONTROLLER-Komponente: Konfiguriere die Zelle für den Kontakt
cell.configure(for: contact)
if let _ = contact.lifetime {
cell.selectionStyle = .default
cell.accessoryType = .disclosureIndicator
} else {
cell.selectionStyle = .none
cell.accessoryType = .none
}
return cell
}
}
\end{swiftcode}
\item Es fehlt noch die \mvcindicatorview\emph{View}-Komponente, also die Zelle die wir oben bereits verwenden. Erstellt eine neue Datei \filename{LifetimeCell.swift} und platziert sie im Project Navigator unter \emph{View}. Implementiert darin die Klasse \swiftinline{class LifetimeCell: UITableViewCell}:
\begin{swiftcode}
import UIKit
import Contacts
class LifetimeCell: UITableViewCell {
func configure(for contact: CNContact) {
textLabel?.text = CNContactFormatter.string(from: contact, style: .fullName)
if let lifetime = contact.lifetime {
let lifetimeFormatter = DateComponentsFormatter()
lifetimeFormatter.allowedUnits = .day
lifetimeFormatter.unitsStyle = .full
detailTextLabel?.text = lifetimeFormatter.string(from: lifetime)
} else {
detailTextLabel?.text = nil
}
}
}
\end{swiftcode}
Wir gestalten die Zelle im \filename{Main.storyboard}. Zieht aus der Object Library eine \swiftinline{UITableViewCell} auf die \swiftinline{UITableView}. Ändert im Attributes Inpector ihren \emph{Style} zu \emph{Right Detail} und gebt ihr den \emph{Identifier} \str{LifetimeCell}. Wählt dann den Identity Inspector und ändert die Klasse der Zelle zur gerade implementierten \swiftinline{LifetimeCell}. Beide Schritte sind notwendig, sodass die zuvor implementierte \swiftinline{tableView(_:cellForRowAt:)} Methode fehlerfrei ausgeführt wird.
\item Nun könnt ihr die App ausführen und seht bereits eine Liste der Kontakte!
\mvcindicatorcontroller Wenn wir einen Kontakt antippen soll dessen Detailansicht angezeigt werden, die von einem anderen View Controller verwaltet wird. Im Skript könnt ihr euch über die \emph{View Controller Hierarchie} informieren. Der \swiftinline{ContactListViewController} wird bereits von einem \swiftinline{UINavigationController} verwaltet, den wir nun verwenden um einen \swiftinline{ContactDetailViewController} anzuzeigen.
Im \filename{Main.storyboard} können wir den Übergang zwischen den View Controllern durch eine \emph{Storyboard Segue} implementieren. Zieht mit gedrückter \keys{\ctrl}-Taste eine Line von der \str{LifetimeCell} zum \swiftinline{ContactDetailViewController} und wählt im Popup \emph{Show}. Wählt die so erstellte Segue aus und gebt ihr im Attributes Inspector den \emph{Identifier} \str{showContactDetail}.
\item Schließlich müssen wir den ausgewählten Kontakt im \swiftinline{ContactListViewController} noch an den \swiftinline{ContactDetailViewController} weitergeben. Dazu dient die \swiftinline{prepare(for:sender:)} Methode:
\begin{swiftcode}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.identifier! {
case "showContactDetail":
guard let indexPath = self.tableView.indexPathForSelectedRow else { break }
let contact = contacts[indexPath.row]
let contactDetailViewController = segue.destination as! ContactDetailViewController
contactDetailViewController.contact = contact
default:
break
}
}
\end{swiftcode}
Sehr ähnlich könnt ihr noch die \swiftinline{shouldPerformSegue(withIdentifier:sender:)} Methode implementieren und für \str{showContactDetail} \swiftinline{contact.lifetime != nil} zurückgeben, sodass die Detailansicht nur für Kontakte mit gültigem Geburtstag angezeigt wird.
\item Unsere App zeigt uns jetzt, wie lang unsere Kontakte schon leben!
Statt den \emph{Right Detail} Stil für die \swiftinline{LifetimeCell} zu verwenden könnt ihr die Zelle natürlich auch selbst gestalten. Wenn ihr noch Erweiterungen einbauen möchtet für die ihr weitere Attribute von \swiftinline{CNContact} benötigt, wie bspw. \swiftinline{CNContactImageDataKey}, müsst ihr diese der Liste \swiftinline{requiredContactKeysToFetch} in \swiftinline{ContactListViewController} hinzufügen, sodass sie ebenfalls aus dem \swiftinline{CNContactStore} geladen werden.
Schickt eine Pull-Request wenn ihr mit eurer App zufrieden seid oder um Fragen zu stellen.
\end{enumerate}
\end{excitem}
%\end{lecture}
%\begin{lecture} % Lecture 4
\begin{excitem}{seasonizer}{Seasonizer}{3}
Mit den Apps der vergangenen Vorlesungen haben wir die Grundlagen der Programmierung für iOS Geräte und einige wichtige Architekturkonzepte kennengelernt. Mit diesem Wissen lässt sich bereits ein Großteil der zur Verfügung stehenden Frameworks anwenden und viele Funktionen, die iOS Geräte bieten, in unsere eigenen Apps integrieren.
%Aus gegebenem Anlass implementieren wir in dieser App einige häufig verwendete Funktionen wie Multi-Touch Gesten und Kamera Integration, um beim nächsten Familienfest nicht nur mit unseren neu erworbenen Programmierfähigkeiten anzugeben, sondern auch noch Fotos von Familienmitgliedern festlich dekorieren zu können \abbref{img:seasonizer}.
Zusammenfassend implementieren wir in dieser App noch einmal die grundlegende iOS App Architektur. Dabei lernen wir zusätzlich die Integration von Multi-Touch Gesten und der Kamera kennen, um beim nächsten Familienurlaub nicht nur mit unseren neu erworbenen Programmierfähigkeiten anzugeben, sondern auch noch Fotos von Familienmitgliedern sommerlich dekorieren zu können \abbref{img:seasonizer}.
\includegraphicsc[\iphonewidth]{img/seasonizer_summer.png}{img:seasonizer}{Mit der Seasonizer App lassen sich Freunde und Familienmitglieder sommerlich dekorieren.}
\strong{Features der Seasonizer App:}
\begin{itemize}
\item Die App besitzt eine Hauptansicht mit Navigation Bar und Toolbar. Den Inhalt füllt eine Image View für das Foto und eine darüberliegende View mit transparentem Hintergrund für die Accessories.
\item Es gibt einen Button in der Toolbar mit dem ein Foto von der Kamera oder Foto Bibliothek ausgewählt werden kann, das anschließend von der Image View angezeigt wird.
\item Ein weiterer Button zeigt modal eine Liste von Accessories an. Wird ein Accessory ausgewählt, wird es der Hauptansicht hinzugefügt.
\item Die Accessories lassen sich mit Gesten verschieben, skalieren, drehen und löschen.
\item Mit einem Action Button kann das Bild über verschiedene Kanäle wie Nachrichten, Email oder Facebook verteilt werden.
\item Die Elemente wie Bild und Accessories werden gespeichert und beim nächsten Start der App wiederhergestellt.
\item Ein Toolbar Button dient dem Zurücksetzen der Benutzeroberfläche.
\end{itemize}
Forkt das Repository \url{https://github.com/ios-dev-kurs/seasonizer} und vervollständigt die App. Erstellt eine Pull-Request um eure Lösung einzureichen oder Fragen zu stellen.
\subsubsection{Hinweise}
\begin{itemize}
\item \strong{Navigation Bar und Toolbar} lassen sich mit einem Navigation Controller sehr einfach anzeigen. Dieser besitzt ein Attribut \emph{Shows Toolbar}, das im Interface Builder aktiviert werden kann. Wenn wir dann Buttons als Objekte der \swiftinline{UIBarButtonItem} Klasse einem View Controller hinzufügen zeigt der Navigation Controller diese in der Toolbar an. \swiftinline{UIBarButtonItem} bietet bereits viele Stile wie \swiftinline{UIBarButtonSystemItemCamera} zur Auswahl im Attributes Inspector an. Für's Layout gibt es außerdem den Stil \emph{Flexible Space}.
\item Um den \swiftinline{AccessoryListViewController} modal mit Titelleiste anzuzeigen verpacken wir ihn wiederum in einem Navigation Controller \abbref{img:seasonizer_ui}. Gebt die Liste der Accessories in der \swiftinline{prepare(for:sender:)} Methode an den \swiftinline{AccessoryListViewController} weiter. Implementiert darin dann das \swiftinline{UITableViewDatasource} Protokoll.
\includegraphicsc{img/seasonizer_ui.png}{img:seasonizer_ui}{Navigation Controller bieten eine einfache Möglichkeit, Navigationsleisten und Toolbars anzuzeigen}
\item Verwendet \emph{Unwind Segues} um den \swiftinline{AccessoryListViewController} wieder zu verlassen. Definiert dazu eine \swiftinline{@IBAction func unwindToCanvas(_ segue: UIStoryboardSegue)} Methode im \swiftinline{CanvasViewController}. Erstellt die Unwind Segues dann von einem \emph{Cancel} Button und von der \swiftinline{AccessoryCell} indem ihr eine Verbindung zur \emph{Exit} Schaltfläche zieht. Gebt den Segues jeweils einen Identifier. In der \swiftinline{prepare(for:sender:)} Methode des \swiftinline{AccessoryListViewController} könnt ihr dann das \swiftinline{selectedAccessory} setzen und es in der \swiftinline{unwindToCanvas(_:)} Methode wieder auslesen.
\end{itemize}
\end{excitem}
\end{exc}
\end{lecture}
\begin{lecture} % Lecture 5
\chapter{Methoden}
Wir können nun durch sorgfältige Konstruktion der View und View Controller Hierarchien bereits recht komplexe iOS Apps aufbauen. Das Model View Controller Konzept hilft uns dabei den Programmcode zu strukturieren. Jetzt können wir Lösungen zu verbreiteten Aufgaben der Softwareentwicklung kennen lernen und uns mit Methoden des Software Engineering befassen.
\section{Kommunikation mit einer REST API}
Für eine Software, die eine Aufgabe erfüllt, sind deren \emph{Schnittstellen} häufig noch wichtiger als die Funktionsweise des Programms. Durch Schnittstellen können andere Entwickler Teilprobleme an spezialisierte und getestete Software auslagern, ohne deren Implementierungsdetails kennen zu müssen. Nur so können wir Apps schreiben, die eine bestimme Aufgabe hervorragend erfüllen und das Ergebnis dann weitergeben.
Eine iOS App kann Schnittstellen zu anderen Apps oder Services des Betriebssystems anbieten. Bspw. kann sich ein Social Network darauf spezialisieren, dem Benutzer einen Ort vorzuschlagen, und diesen dann an eine Navigationsystem-App weitergeben, die auf das Routing spezialisiert ist. Solche Schnittstellen werden unter iOS als \emph{Deep Links} mit \emph{URL Schemata} realisiert.
Häufig \emph{nutzen} iOS Apps Schnittstellen. Sie bieten eine Benutzeroberfläche an, um eine Schnittstelle zu konsumieren. Dabei handelt es sich meist um einen Server, der plattformunabhängig Daten in Form einer REST API bereitstellt. Der Server akzeptiert HTTP Anfragen in Form einer URL, und verarbeitet diese oder gibt Daten in einem Format wie JSON zurück. URL und JSON folgen dabei einem definierten Schema.
\begin{exc}
\begin{excitem}{apiclient}{API Client}{2+3}
Mit der \emph{API Client} App implementieren wir eine Benutzeroberfläche für eine REST API. Wir konzentrieren uns dabei auf die \mvcindicatormodel\emph{Model} und \emph{Controller} Komponenten unserer App. Neben asynchronen Netzwerkanfragen lernen wir auch den \emph{Error Handling} Mechanismus und \emph{Generics} in Swift kennen. Außerdem integrieren wir Frameworks anderer Entwickler in unsere App. Indem wir die Schnittstellen dieser Frameworks nutzen können wir auf sorgfältig konzipierten, funktionsfähigen und ausgiebig getesteten Code zurückgreifen. So müssen wir uns nicht erneut mit einem Problem befassen, für das es bereits hervorragende Lösungen gibt, und können uns stattdessen auf die Funktionalität unseres Programms konzentrieren.
Im \emph{APIClient} Repository \linkref{https://github.com/knly/apiclient/} findet ihr ein Projekt, in das bereits vier hervorragende Open-Source Frameworks mithilfe des Dependency Managers \emph{CocoaPods} \linkref{https://cocoapods.org} integriert sind:
\begin{description}
\item[Moya\linkref{https://github.com/Moya/Moya}] Network abstraction layer written in Swift.
\item[Freddy\linkref{https://github.com/bignerdranch/Freddy}] A reusable framework for parsing JSON in Swift.
\item[Alamofire\linkref{https://github.com/Alamofire/Alamofire}] Elegant HTTP Networking in Swift
\item[AwesomeCache\linkref{https://github.com/aschuch/AwesomeCache}] Delightful on-disk cache (written in Swift)
\end{description}
CocoaPods lädt die in der \filename{Podfile} Datei angegebenen Frameworks in das \filename{Pods/} Verzeichnis und integriert sie zusammen mit dem Projekt in einen Xcode Workspace. Verwendet also immer den \filename{APIClient.xcworkspace} statt des \filename{.xcodeproj}.
Euch steht es frei, eine Benutzeroberfläche für eine API eurer Wahl zu schreiben. Eine Sammlung von vielen interessanten APIs findet ihr bspw. hier \linkref{https://www.reddit.com/r/webdev/comments/3wrswc/what_are_some_fun_apis_to_play_with/}:
\begin{itemize}
\item Natürlich viele \strong{Wetter} APIs \linkref{https://developer.forecast.io}
\item Die APIs der \strong{NASA} \linkref{https://data.nasa.gov/developer}
\item Spieleentwickler wie \strong{Blizzard} \linkref{https://dev.battle.net/io-docs}
\item Comic-Helden von \strong{Marvel} \linkref{http://developer.marvel.com/docs}
\item Computational Services wie \strong{Wolfram Alpha} \linkref{http://products.wolframalpha.com/api/}
\item Nährstoffdaten für Lebensmittel der \strong{National Nutrient Database} \linkref{https://ndb.nal.usda.gov/ndb/api/doc}
\item Vom gleichen Entwickler wie die \strong{PokeAPI} \linkref{http://pokeapi.co} aus der Vorlesung \abbref{img:pokeapi} ist die Star Wars API \strong{SWAPI} \linkref{https://swapi.co} und eignet sich ebenfalls sehr gut.
\end{itemize}
\includegraphicsc{img/pokeapi.png}{img:pokeapi}{Die PokeAPI: \emph{Finally; all the Pokémon data you'll ever need, in one place, and easily accessible through a modern RESTful API.}}
Im Repository findet ihr unter dem Branch \shinline{pokedex} die Beispiel-Implementierung für die PokeAPI aus der Vorlesung \abbref{img:pokedex}. Ihr könnt das Repository klonen und mit
\begin{shcode}
git checkout pokedex
\end{shcode}
in den Branch wechseln und die App ausführen, oder den Branch auf GitHub \linkref{https://github.com/iOS-Dev-Kurs/apiclient/tree/pokedex} anschauen.
\includegraphicsc[\iphonewidth]{img/pokedex.png}{img:pokedex}{Die Pokedex App zeigt Daten der PokeAPI an.}
Wie in der Vorlesung am Beispiel der PokeAPI vorgeführt könnt ihr grob wie folgt vorgehen:
\begin{enumerate}[label=\arabic*.]
\item Sucht euch eine API aus und überlegt euch eine passende Benutzeroberfläche. Fangt klein an und beschränkt euch zunächst auf nur einen oder wenige Endpoints der API! Pokemon-Attacken \linkref{http://pokeapi.co/docsv2/\#moves}, Planeten im Star Wars Universum \linkref{https://swapi.co/documentation\#planets}, Marvel Comic Charaktere \linkref{http://developer.marvel.com/docs}... oder lieber das Wetter \linkref{https://developer.forecast.io/docs/v2}?
Endpoints mit wenigen Verweisen zu anderen Endpoints sind einfacher zu bearbeiten als bspw. Listen, die ihr für eine Table View dynamisch nach-laden müsst. Beginnt mit einem Textfeld und einem \emph{Laden} Button bevor ihr eure Benutzeroberfläche ausbaut.
\item Modelliert dann die API Endpoints als Fälle eines Enums und implementiert das \swiftinline{Moya.TargetType} Protokoll:
\begin{swiftcode}
import Moya
enum YourAPI: Moya.TargetType {
case firstEndpoint(someParameter: String, anotherParameter: Int)
// ...
var baseURL: NSURL { return NSURL(string: "http://yourapi.com")! }
var path: String {
switch self {
case .firstEndpoint(someParameter: let a, _): return "/firstEndpoint/\(a)"
}
}
var method: Moya.Method { return .GET }
var parameters: [String : AnyObject]? {
switch self {
case .firstEndpoint(_, anotherParameter: let b): return [
"anotherParameter": b,
]
}
}
var sampleData: NSData { return "".dataUsingEncoding(NSUTF8StringEncoding)! }
}
\end{swiftcode}
Die Modellierung der PokeAPI findet ihr hier \linkref{https://github.com/iOS-Dev-Kurs/apiclient/blob/pokedex/APIClient/PokeAPI.swift}.
\item Erstellt im App Delegate einen \swiftinline{MoyaProvider} für eure API und gebt ihn an euren ersten View Controller weiter.
\item Führt bei Betätigung eines Buttons oder in \swiftinline{viewDidLoad} testweise Requests aus:
\begin{swiftcode}
// Where `api` is the `MoyaProvider`:
api.request(target) { result in
switch result {
case .Success(let response):
do {
try response.filterSuccessfulStatusCodes()
print(response)
} catch {
print(error)
}
case .Failure(let error):
print(error)
}
}
\end{swiftcode}
Denkt daran, dass für ungesicherte HTTP Requests eine \swiftinline{NSAppTransportSecurity} Ausnahme in der \filename{Info.plist} Datei eingetragen sein muss. Den Eintrag könnt ihr aus der \filename{Info.plist} Datei im \shinline{pokedex} Branch \linkref{https://github.com/iOS-Dev-Kurs/apiclient/blob/pokedex/APIClient/Info.plist} kopieren.
\item Verwendet das \swiftinline{Freddy} Framework um den Rückgabewert der Request von \swiftinline{NSData} to \swiftinline{JSON} zu parsen:
\begin{swiftcode}
import Freddy
let json = try JSON(data: response.data)
\end{swiftcode}
\item Modelliert nun eure Datenstrukturen. Schaut euch dazu die Dokumentation zu den API Endpoints an und implementiert Structs, die die Rückgabewerte der Endpoints genau repräsentieren. Implementiert das \swiftinline{Freddy.JSONDecodable} Protokoll für jedes Struct. Beschränkt euch zunächst nur auf die wichtigsten Attribute. Die Beispiel-Implementierung für die PokeAPI findet ihr hier \linkref{https://github.com/iOS-Dev-Kurs/apiclient/blob/pokedex/APIClient/Pokedex.swift} und hier \linkref{https://github.com/iOS-Dev-Kurs/apiclient/blob/pokedex/APIClient/Pokemon.swift}.
\item Jetzt könnt ihr das Ergebnis einer Request in euer Model parsen und die View-Komponente eurer App damit konfigurieren:
\begin{swiftcode}
let model = try Model(json: json) // Where `Model` is `Freddy.JSONDecodable`
// Configure view according to model
\end{swiftcode}
\end{enumerate}
Modelliert mindestens einen Endpoint einer API eurer Wahl und zeigt das Ergebnis auf dem Bildschirm an, um die volle Punktzahl für diese Aufgabe zu erhalten. Mit komplexeren Implementierungen könnt ihr euch bis zu drei Extrapunkte verdienen. Schickt eine Pull-Request wenn ihr mit eurer App zufrieden seid, oder um Fragen zu stellen. Ich bin gespannt auf eure Umsetzungen!
\end{excitem}
\end{exc}
\end{lecture}
\begin{lecture}
\section{Unit Tests}
In der Softwareentwicklung müssen wir natürlich kontinuierlich überprüfen, ob unsere Software funktioniert. Zunächst besteht so ein Test meist einfach darin, die App im Simulator oder auf einem Gerät auszuführen und auszuprobieren, ob sie sich wie erwartet verhält. Wird die App jedoch immer umfangreicher, können wir manuell nicht immer wieder alle möglichen Situationen testen, die in der App auftreten können. Teile der Software, die mal zuverlässig funktioniert haben, könnten dann durch neuen Code fehlerhaft werden, ohne, dass wir es bemerken.
Mit \emph{Unit Tests} automatisieren wir daher solche Tests unserer Software. Für jedes Stück Code (jede \emph{Unit} Code) das wir schreiben, fügen wir auch \emph{Tests} hinzu, die diesen Code ausführen und das Ergebnis überprüfen. Diese Tests können wir dann jederzeit ausführen und damit sicherstellen, dass der getestete Code noch immer wie erwartet funktioniert. Wenn ein Test durchfällt erfahren wir außerdem genau, welcher Code fehlerhaft ist. Wir können das Problem dann beheben und die Tests erneut ausführen, bis alle Tests bestehen.
Je detaillierter und vielseitiger unsere Tests sind, desto genauer können wir feststellen, wie stabil unsere Software ist. Damit gehören Unit Tests zu den wichtigsten Werkzeugen moderner Softwareentwicklung, und Softwareprojekte wie die Open-Source Frameworks, die wir im vorherigen Abschnitt kennengelernt haben, werden nach dem Umfang ihrer Unit Tests (\emph{Coverage}) bewertet \linkref{https://codecov.io/github/Moya/Moya?branch=master}.
Da Unit Tests häufig und regelmäßig ausgeführt werden sollten, völlig automatisiert sind und durch ihren Umfang recht viel Zeit in Anspruch nehmen können werden sie häufig von einem \emph{Continuous Integration} System wie \emph{Travis\linkref{https://travis-ci.org}} anstatt auf dem System des einzelnen Entwicklers ausgeführt. So können die Tests bspw. bei jedem \emph{Push} auf GitHub und für jede Pull-Request automatisch ausgeführt werden um die Qualität und Stabilität des Codes zu bewahren.
\begin{exc}
\begin{excitem}{unittests}{Unit Tests}{2}
In dieser Aufgabe erweitern wir die \emph{APIClient} App um Unit Tests. Wir schreiben dabei sowohl Tests, die möglichst detailliert unseren Programmcode ausführen und überprüfen, also auch UI Tests, die die App im Simulator ausführen und Benutzereingaben simulieren.
\begin{enumerate}[label=\arabic*.]
\item Im APIClient Repository \linkref{https://github.com/iOS-Dev-Kurs/apiclient} habe ich das Open-Source Framework \emph{Nimble\linkref{https://github.com/Quick/Nimble}} mit CocoaPods hinzugefügt und in zwei neue Test-Targets integriert. Fügt zuerst diese Änderungen am Original-Repository in euren Fork ein, indem ihr einen \emph{Rebase} durchführt:
\begin{shcode}
git pull --rebase https://github.com/ios-dev-kurs/apiclient.git master
\end{shcode}
Mit einem Rebase wird die Commitfolge verändert, daher müsst ihr euer Repository auf GitHub anschließend mit eine \emph{Force Push} überschreiben:
\begin{shcode}
git push --force origin master
\end{shcode}
\item Erstellt eine neue Datei \filename{YourAPITests.swift} im \filename{APIClientTests} Verzeichnis und achtet darauf, dass ihr sie dem Target \emph{APIClientTests} zuweist \abbref{img:testtarget}. Erstellt darin eine neue Subklasse von \swiftinline{XCTestCase}:
\begin{swiftcode}
import XCTest
// Import the App module with access to all internal types and functions
@testable import APIClient
// Nimble provides excellent testing functionality
import Nimble
// Every subclass of `XCTestCase` can provide tests
class YourAPITests: XCTestCase {
override func setUp() {
super.setUp()
// Called before the invocation of each test
}
override func tearDown() {
super.tearDown()
// Called after the invocation of each test
}
}
\end{swiftcode}
\includegraphicsc[\iphonewidth]{img/testtarget.png}{img:testtarget}{Weist Swift-Dateien mit Tests dem richtigen Target zu, wenn ihr die Datei erstellt.}
\item Jetzt könnt ihr euren ersten Test schreiben. Jede Methode einer Subklasse von \swiftinline{XCTestCase} mit Signatur \swiftinline{() -> Void}, deren Name mit \str{test} beginnt, wird als Unit Test erkannt:
\begin{swiftcode}
func testSomething() {
// Call some unit of code and check its result with `XCTAssert` or Nimble's `expect` assertions:
expect(1 + 1) == 2
expect(try JSON(jsonString: "{}")).toNot(throwError())
}
\end{swiftcode}
Diese Methoden erscheinen dann im Xcode \emph{Test Navigator} (\keys{\cmd+5}) und werden bei einem \emph{Test} (\keys{\cmd+U}) automatisch ausgeführt. Ihr könnt sie mit der Schaltfläche am linken Rand des Editors auch einzeln ausführen.
Fügt eurem \swiftinline{XCTestCase} nun also Tests hinzu, mit denen ihr euren Code möglichst detailliert überprüft. Ihr könnt auch weitere Subklassen von \swiftinline{XCTestCase} erstellen, um eure Tests zu gliedern. Beginnt dabei bei der Model-Komponente eurer App und versucht Tests zu schreiben, die möglichst kleine Abschnitte eures Codes ausführen, wie bspw. nur eine bestimmte Funktion oder einen Initializer. Überprüft dann das Ergebnis mithilfe der \swiftinline{XCTAssert} oder Nimble's \swiftinline{expect} Assertions \linkref{https://github.com/Quick/Nimble}.
Anders als bei regulärem Programmcode könnt ihr in Tests beliebig langen und ausführlichen Code schreiben und solltet insbesondere den Test-Methoden möglichst deskriptive Namen geben. Achtet außerdem darauf, in euren Assertions keine Logik zu schreiben, sondern vergleicht ein Ergebnis direkt mit bestimmten Werten: Der Test \swiftinline{expect(1 + 1) == 2} im obigen Beispiel wäre sinnlos, würden wir den \swiftinline{+} Operator auch auf der rechten Seite verwenden!
Eine Beispiel-Implementierung einiger Tests findet ihr im \shinline{pokedex} Branch \linkref{https://github.com/iOS-Dev-Kurs/apiclient/tree/pokedex}. Es ist hilfreich, eine Datei mit zu testendem Code rechts im Assistant anzuzeigen (\keys{\Alt}-Klick), dessen Funktionalität durchzugehen und dabei Tests dafür zu schreiben. Beginnt bspw. mit einem \swiftinline{JSONDecodable} Struct und dessen Initializer.
Um die \swiftinline{JSONDecodable} Initializer zu testen, ohne Netzwerkanfragen an die API stellen zu müssen, stellt das \swiftinline{Moya.TargetType} Protokoll das Attribut \swiftinline{sampleData: NSData} bereit, das ihr in eurem Enum vermutlich noch nicht vollständig implementiert habt. Gebt dort für jeden Endpoint einen JSON-String mit den relevanten Attributen zurück, der wie die echten API-Antworten strukturiert ist. Hinweis: Es gibt keine mehrzeiligen Strings in Swift und Anführungszeichen im String müsst ihr als \str{\textbackslash"{}} \emph{escapen}, wie hier \linkref{https://github.com/iOS-Dev-Kurs/apiclient/blob/pokedex/APIClient/PokeAPI.swift}.
Testet natürlich auch die asynchrone API Abfrage. Dazu können wir im Test eine \emph{Expectation} erstellen und mit einem Timeout darauf warten, dass diese asynchron erfüllt wird:
\begin{swiftcode}
func testSomethingAsynchronous() {
let expectation = expectationWithDescription("Something asynchronous")
// Do something asynchronously and call `expectation.fulfill()` when it's done.
waitForExpectationsWithTimeout(10, handler: nil)
}
\end{swiftcode}
\item Mit Unit Tests lässt sich die Model-Komponente ideal testen. Auch die View- und Controller-Komponenten können mit Unit Tests überprüft werden, wenn wir den Code dafür richtig strukturieren. Dabei hilft insbesondere, das Model-View-Controller Konzept zum \emph{Model-View-ViewModel} Konzept zu erweitern \linkref{https://www.objc.io/issues/13-architecture/mvvm/}. Doch erst mit \emph{UI Tests} können wir unsere App auf einer Ebene testen, auf der wir tatsächlich Benutzereingaben simulieren und überprüfen, wie unsere App darauf reagiert.
UI Tests verwenden den \emph{Accessibility} Mechanismus von iOS, der auch engeschränkten Benutzern bei der Bedienung von Apps hilft. Dazu vergeben wir \emph{Accessibility Identifier} an Views im Storyboard und im Code und können die zugehörigen Interface-Elemente damit in Tests überprüfen.
Erstellt eine neue Subklasse von \swiftinline{XCTestCase} und weist sie diesmal dem Target \emph{APIClientUITests} zu. Startet die App in deren \swiftinline{setUp} Methode:
\begin{swiftcode}
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
\end{swiftcode}
\item In Test-Methoden dieser Subklasse könnt ihr die angezeigten Interface-Elemente nun über ihren \swiftinline{accessibilityIdentifier} abfragen, Benutzereingaben simulieren und Asserts ausführen:
\begin{swiftcode}
func testSomeUserInteraction() {
let app = XCUIApplication()
// Retrieve interface elements by their `accessibilityIdentifier`
let searchTextfield = app.textFields["searchTextfield"]
// Assert on their properties such as `label` or `exists`
expect(searchTextfield.label).to(beEmpty())
// Simulate user interaction
searchTextfield.tap()
// Type something and hit enter
searchTextfield.typeText("Search Term\n")
// Continuously assert on a property until an expectation is met
let resultLabel = app.staticTexts["result"]
expect(resultLabel.label).toEventually(equal("Expected Result"), timeout: 10)
// ...
}
\end{swiftcode}
Die Accessibility Identifier der Views müsst ihr dafür im Storyboard im \emph{Identity Inspector} (\keys{\cmd+\Alt+3}) oder im Code setzen. Für Views in Table View Cells scheint dies bisher nur im Code zu funktionieren:
\begin{swiftcode}
// In UITableViewCell subclasses:
override func awakeFromNib() {
super.awakeFromNib()
self.nameLabel.accessibilityIdentifier = "name"
}
\end{swiftcode}
Um Text in Textfelder zu tippen muss die Option \emph{Connect Hardware Keyboard} des Simulators ausgeschaltet sein \abbref{img:connecthardwarekeyboard}. Außerdem scheint der UI Test beim Start der App bisher nicht korrekt zu warten, bis die App Benutzereingaben annimmt. Ein Workaround ist, die folgende Methode in \swiftinline{setUp} nach dem Start der App aufzurufen:
\begin{swiftcode}
private func waitForResponsiveness() {
let wait = expectationWithDescription("wait")
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
wait.fulfill()
}
waitForExpectationsWithTimeout(10, handler: nil)
}
\end{swiftcode}
\includegraphicsc[\screenshotwidth]{img/connecthardwarekeyboard.png}{img:connecthardwarekeyboard}{Nur mit ausgeschaltetem Hardware Keyboard funktioniert die simulierte Texteingabe bisher halbwegs zuverlässig.}
Wenn ihr einen UI Test ausführt startet nun eure App und die Benutzereingaben werden simuliert. Wenn ihr innerhalb eines Tests mit Klick auf den linken Editorrand einen Breakpoint setzt und die Ausführung der App anschließend an der Stelle stoppt, könnt ihr sogar die \emph{Recording}-Funktion verwenden, um eure Benutzereingaben aufzunehmen und automatisch in Code zu übersetzen! Klickt dazu auf die rote \emph{Aufnehmen}-Schaltfläche in der Leiste über dem Debug-Bereich. Der generierte Code ist natürlich häufig nicht optimal und ihr solltet ihn noch editieren, bevor ihr den Test committed, doch diese Funktion kann sehr hilfreich sein.
Schreibt UI Tests für jeden Pfad der Benutzerführung in eurer App.
\end{enumerate}
Ihr könnt eure Tests nun jederzeit mit \keys{\cmd+U} ausführen und damit überprüfen, dass euer Code noch funktioniert.
Am rechten Rand des Editors zeigt euch Xcode dann außerdem Informationen zur \emph{Coverage} an und markiert insbesondere solche Bereiche eures Codes rot, die in den Tests nicht aufgerufen wurden. Wählt ihr einen Test im \emph{Report Navigator} (\keys{\cmd+8}) aus, seht ihr neben einer Übersicht der ausgeführten Tests auch die Coverage eures Projekts, einzelner Dateien und Funktionen. Eine hohe Coverage heißt nur, dass der Code während des Tests ausgeführt wurde, und nicht, wie detailliert die Tests sind!
Versucht, eine Coverage von mindestens 80\% zu erreichen. Schickt dann erneut eine Pull-Request an das APIClient Repository.
\end{excitem}
\end{exc}
\end{lecture}
\begin{lecture}
\section{Data Persistence}
Selten kommt eine App ohne Daten aus, die über die Auführung der App hinaus gespeichert werden. Insbesondere ist eines der wichtigsten Merkmale, die Apps von mobilen Webseiten unterscheidet, dass der Benutzer sie auf seinem Gerät personalisieren kann. Außerdem erwartet der Benutzer, dass er die App in dem Zustand wiederfindet, in der er sie verlassen hat. Da das Betriebssystem die App jedoch jederzeit beenden kann, muss die App Daten persistent speichern, um sie bei der nächsten Ausführung wiederherstellen zu können.
Um Daten zwischen Geräten desselben Benutzers zu teilen und damit ein konsistentes Benutzererlebnis zu ermöglichen, kann eine App Daten außerdem recht einfach nicht nur lokal sondern in der Cloud speichern.
Je nach Anforderung können wir auf verschiedene Mechanismen zur persistenten Datenspeicherung zurückgreifen:
\subsection{User Defaults}\label{sec:userdefaults}
Häufig erfordert die Personalisierung einer App lediglich die Speicherung einiger Einstellungen. Einfache Benutzerdaten können wir mithilfe der \swiftinline{NSUserDefaults} Klasse persistent speichern und auslesen:
\begin{swiftcode}
// speichern
NSUserDefaults.standardUserDefaults().setObject("Alice", forKey:"user_name")
// auslesen
let userName = NSUserDefaults.standardUserDefaults().objectForKey("user_name") as? String
\end{swiftcode}
Es können nur Objekte der Objective-C Typen \swiftinline{NSString} (implizit konvertierbar in Swift's \swiftinline{String}), \swiftinline{NSNumber} (implizit konvertierbar in Swift's \swiftinline{Int} und \swiftinline{Double}), \swiftinline{NSDate} und \swiftinline{NSData} oder ausschließlich mit solchen Objekten gefüllte Instanzen von \swiftinline{NSArray} und \swiftinline{NSDictionary} (bzw. ihre Swift-Äquivalente) in dieser Form gespeichert werden. Andere Datentypen müssen wir in \swiftinline{NSData} konvertieren, um sie in \swiftinline{NSUserDefaults} zu speichern. Für Klassen wie \swiftinline{UIImage} existieren dazu einfache Lösungen:
\begin{swiftcode}
// `UIImage` in `NSData` serialisieren
let imageData = UIImagePNGRepresentation(image) // oder `UIImageJPEGRepresentation` je nach Bedarf
// speichern
NSUserDefaults.standardUserDefaults().setObject(imageData, forKey:"image_data")
// auslesen
if let imageData = NSUserDefaults.standardUserDefaults().objectForKey("image_data") as? NSData {
let image = UIImage(data: imageData)
}
\end{swiftcode}
\subsection{Caches}
Für etwas komplexere Datentypen wie bspw. eigene Structs oder Klassen, oder Anforderungen wie ablaufende Caches, können wir auf spezialisierte Swift Frameworks zurückgreifen. Einige hervorragende Frameworks sind:
\begin{description}
\item[Pantry\linkref{https://github.com/nickoneill/Pantry}] \emph{The missing light persistence layer for Swift}
\item[Haneke\linkref{https://github.com/Haneke/HanekeSwift}] \emph{A lightweight generic cache for iOS written in Swift with extra love for images.}
\item[AwesomeCache\linkref{https://github.com/aschuch/AwesomeCache}] \emph{Delightful on-disk cache (written in Swift)}
\end{description}
\subsection{State Preservation}
Das Betriebssystem kann eine App jederzeit beenden. Der Benutzer der App erwartet dabei, dass die Benutzeroberfläche beim nächsten Start der App wiederhergestellt wird und Eingaben erhalten bleiben.
UIKit bietet mit \emph{State Preservation} ein sehr einfach zu integrierendes System zur Wiederherstellung der Benutzeroberfläche. Konzeptionell sollten mit diesem Mechanismus keine Daten der Model-Komponente gespeichert werden. Stattdessen bezieht sich das State Preservation System auf die View- und Controller-Komponente zugeordnet.
Elemente der Benutzeroberfläche identifizieren wir anhand des \swiftinline{restorationIdentifier: String} Attributs im Storyboard oder im Code. Bei der Wiederherstellung wird für jeden dieser Identifier ein entsprechendes Objekt angefordert, oder bei Verwendung eines Storyboards automatisch erstellt. View Controller und Views können dazu die \swiftinline{encodeRestorableStateWithCoder(_:)} und \swiftinline{decodeRestorableStateWithCoder(_:)} Methoden implementieren, um ihre Darstellung zu archivieren und wiederherzustellen. Im iOS App Programming Guide \linkref{https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/} ist das State Preservation System ausführlich dokumentiert.
\subsection{Datenbanken: Core Data und Realm}\label{sec:coredata}
Gehen die Anforderungen an die persistente Datenspeicherung unserer Model-Komponente über einfaches Caching einzelner Werte hinaus, benötigen wir eine relationale Datenbank. Momentan bietet sich uns die Wahl zwischen Apple's etabliertem \emph{Core Data} Framework und dem modernen Framework \emph{Realm\linkref{https://realm.io}}. Core Data ist ein Relikt aus jahrelanger Softwareentwicklung auf Apple's Plattformen in Objective-C und damit sehr mächtig und vielseitig und liegt einer Vielzahl an Apps im Store zugrunde. Realm hingegen ist eine plattformübergreifende Datenbankimplementierung für mobile Geräte mit expliziter Unterstützung für Swift, jedoch noch nicht so vollständig und in Apple's Infrastruktur integriert wie Core Data.
Beide Frameworks erlauben uns mit Swift Objekten zu arbeiten, denen eine Datenbank zugrunde liegt. Wir erstellen Objekte einer Subklasse von \swiftinline{NSManagedObject} bzw. \swiftinline{Realm.Object} und arbeiten mit diesen Objekte, wie wir es gewohnt sind. Als \swiftinline{@NSManaged} bzw. \swiftinline{dynamic} markierte Attribute dieser Klassen werden in der Datenbank gespeichert. Dazu gehören auch \emph{To-One} und \emph{To-Many} Beziehungen zu anderen verwalteten Objekten. So können wir ganze Objekt-Graphen von Core Data bzw. Realm persistent verwalten lassen.
Mit \strong{Core Data} modellieren wir mit dem \emph{Xcode Model Editor} zuerst das \emph{Data Model} der Datenbank \abbref{img:coredata_modeleditor} mit \emph{Entities} und ihren \emph{Attributen} und \emph{Beziehungen}. Anschließend implementieren wir eine Subklassen von \swiftinline{NSManagedObject} für jede Entity.
\includegraphicsc{img/coredata_modeleditor.png}{img:coredata_modeleditor}{Core Data Models können wir im \emph{Xcode Model Editor} graphisch bearbeiten.}
Um von Core Data verwaltete Objekte abzufragen führen wir eine \emph{Fetch Request} aus. Wir konfigurieren dazu ein Objekt der Klasse \swiftinline{NSFetchRequest} und übergeben es an einen \swiftinline{NSManagedObjectContext}.
Ein \swiftinline{NSManagedObjectContext} ist unsere Verbindung zu Core Data. Meist erstellen wir einen Context im App Delegate und geben ihn an unsere View Controller weiter, sodass diese die von Core Data verwalteten Objekte abfragen können. Diese Objekte sind dann immer mit dem Context assoziiert. Wenn wir ihre Attribute verändern entspricht dies ungesicherten Änderungen in ihrem Context. Erst, wenn wir den Context speichern, werden die Änderungen persistent gesichert.