In diesem Kapitel befassen Sie sich nochmals mit dem Streichholzspiel, das Sie bereits in Kapitel 6 kennengelernt haben. Das große Ziel besteht darin, dieses Spiel auf das iPhone zu portieren und dort mit einer grafischen Oberfläche zu versehen. Im Folgenden gehe ich dabei vom Streichholzspiel aus, bei dem man maximal drei Hölzer auf einmal wegnehmen und die anfängliche Zahl der Hölzer zwischen 12 und 24 Stück wählen kann. Außerdem werden Sie eine bessere Strategie kennenlernen, bei der der Computer endlich ein würdiger Gegner wird.
Legen Sie ein neues Projekt in Xcode an und wählen Sie wieder die Vorlage SINGLE VIEW APPLICATION. Nennen Sie das Projekt »Streichholz«. Im Folgenden möchte ich bei der Entwicklung der App wieder nach den Prinzipien von MVC vorgehen. Dabei werde ich mich zunächst um das Datenmodell kümmern und einerseits verschiedene Strategien vorstellen, andererseits aber auch die Implementierung dieser Strategien mit Swift-Objekten vorstellen. Anschließend befasse ich mich mit der grafischen Oberfläche und schließlich auch mit der Steuerung.
Bei der Vorstellung von Objekten in Abschnitt 7.1 habe ich mit dem Beispiel des Objekts HolzHaufen
begonnen. Hier haben Sie eine Klasse geschrieben, die einerseits die aktuelle Zahl der Hölzer kennt und andererseits eine Methode bereitstellt, die Hölzer entfernen kann.
In Abschnitt 6.1.1 haben Sie in Listing 6.2 eine Funktion geschrieben, die einen Computerzug berechnet. Diese könnten Sie nun als Methode zur Klasse HolzHaufen
hinzufügen. Es gibt allerdings ein Problem mit dieser Vorgehensweise: Die Züge des Computers haben eigentlich nicht direkt etwas mit dem Holzhaufen selbst zu tun. Grundsätzlich könnten Sie ja auch mal auf die Idee kommen, zwei Computer oder auch zwei menschliche Gegner gegeneinander spielen zu lassen. Wenn Sie aber plötzlich Zugstrategien in den Holzhaufen packen würden, wäre dies im ersten Fall nicht unbedingt passend, im zweiten Fall ziemlich überflüssig.
Darum empfehle ich Ihnen, die Funktionalität einer Klasse grundsätzlich auf einen einzigen Anwendungszweck zu beschränken und für weitere Anwendungszwecke eine neue Klasse zu definieren. Die Erfahrung zeigt, dass es auf Dauer wesentlich geschickter ist, viele kleine und einfache Klassen zu haben als eine Super-Klasse, die alles kann und alles macht. Für so eine Klasse haben Informatiker sogar einen abfälligen Namen, die sogenannte Gott-Klasse.
/// Repräsentiert einen Holzhaufen. Er speichert die Zahl der
/// Hölzer und bietet die Funktionalität, den Spielstand
/// auszuwerten.
class HolzHaufen {
/// Die Anfangszahl der Hölzer.
var anfangsZahl = 18
/// Die aktuelle Zahl der Hölzer.
var hoelzer = 18
/// Zahl der Hölzer, die pro Zug entfernt werden dürfen.
var nimmMaximum = 3
/// Entferne eine bestimmte Zahl von Hölzern.
func entferneHoelzer(zahl: Int) {
if self.hoelzer > zahl {
// Es gibt genug Hölzer, entferne die gewünschte Zahl.
self.hoelzer --= zahl
} else {
// Entferne alle Hölzer.
self.hoelzer = 0
}
}
/// Maximale Zahl der Hölzer, die ein Spieler nehmen kann.
func grenze() --> Int {
if self.hoelzer < nimmMaximum {
// Ein Spieler darf maximal alle Hölzer nehmen, die es gibt.
return self.hoelzer
} else {
// Ein Spieler darf bis zu nimmMaximum Hölzer nehmen.
return nimmMaximum
}
}
/// Bestimme, ob das Spiel verloren ist.
func istVerloren() --> Bool {
return (self.hoelzer == 0)
}
}
Listing 15.1 Die Klassendefinition der Klasse »HolzHaufen« zur Verwaltung der Streichhölzer
Daher sollte die Klasse HolzHaufen
keine Zugstrategien beinhalten, sondern nur die Informationen, die wirklich notwendig sind, um den Holzhaufen und die möglichen Züge zu verwalten. Dies ist in Listing 15.1 gezeigt. Es gibt eine Eigenschaft für die Zahl der Hölzer beim Spielstart namens anfangsZahl
. Es gibt weiterhin eine Eigenschaft namens hoelzer
, die die Zahl der gerade vorhandenen Hölzer angibt. Außerdem gibt es die Eigenschaft nimmMaximum
mit Voreinstellung drei, die die maximale Zahl, die pro Zug entfernt werden darf, enthält.
nimmMaximum
gegenüber der expliziten Nennung der Zahl »3« im Programmtext.Dann gibt es die drei Methoden entferneHoelzer
, grenze
und istVerloren
. Die erste Methode entferneHoelzer
führt einen Zug aus wobei es keine Rolle spielt, ob dieser Zug von einem Menschen oder dem Computer ausgeführt wurde. Die Methode grenze
liefert die maximale Zahl der Hölzer zurück, die ein Spieler in der aktuellen Situation entfernen kann. Diese kennen Sie bereits von vorher. Neu ist die Methode istVerloren
, die bestimmt, ob das Spiel verloren ist, indem es zurückliefert, ob die aktuelle Zahl von Streichhölzern gleich 0 ist.
Fügen Sie die Klasse HolzHaufen
in einer Swift-Datei namens HolzHaufen.swift
zum Projekt hinzu.
Bisher hat der Computer für seine Züge immer eine zufällige Zahl von Hölzern gewählt und zwar eine Zahl zwischen 1 und der maximal möglichen Zahl von Hölzern. Zunächst empfehle ich, dass Sie diese Strategie in ein Objekt packen, das dann über eine Methode die richtige Zahl der zu entfernenden Hölzer zurückliefern können soll. Erstellen Sie zu diesem Zweck eine neue Klasse namens ComputerZug
. Damit ComputerZug
einen vernünftigen Zug vorschlagen kann, muss diese Klasse den aktuellen Spielzustand des Holzhaufens kennen, das heißt, sie braucht eine Eigenschaft vom Typ HolzHaufen
. Außerdem muss sie in der Lage sein, einen neuen Zug vorzuschlagen, was analog zu Listing 6.2 in Abschnitt 6.1.1 funktioniert.
/// Liefere einen Computerzug.
class ComputerZug {
/// Der Holzhaufen, auf dem gespielt wird.
var haufen: HolzHaufen
/// Konfiguriere die Klasse.
init(meinHaufen: HolzHaufen) {
self.haufen = meinHaufen
}
/// Führe einen Zug aus und gib ihn zurück.
func macheZug() --> Int {
// Die maximale Zahl an Hölzern, die der Computer nehmen darf.
let nimmHoechstens = self.haufen.grenze()
// Bestimme den Zug.
// Der Zug ist eine Zufallszahl zwischen 1 und nimmHoechstens.
let zug = zufallsZahl(oben:nimmHoechstens)
// Führe Zug aus.
self.haufen.entferneHoelzer(zug)
// Gib den Wert zusätzlich zurück.
return zug
}
}
/// Liefere eine Zufallszahl zwischen unten und oben (beide
/// einschließlich).
func zufallsZahl(unten:Int = 1, oben:Int = 3) --> Int {
// Liefere eine Zufallszahl zwischen oben und unten.
let zufall = arc4random_uniform(UInt32(oben -- unten + 1))
let ergebnis = unten + Int(zufall)
// Gib sie zurück.
return ergebnis
}
Listing 15.2 Klasse »ComputerZug« zur Berechnung eines neuen Computerzugs
Daher sieht die Klasse ComputerZug
so aus, wie in Listing 15.2 gezeigt. Ich habe der Vollständigkeit halber ebenfalls die Funktion zufallsZahl
aus Listing 6.1 ans Ende des Listings gesetzt. Fügen Sie dieses Listing in einer Datei ComputerZug.swift
zum Projekt hinzu.
Die Methode macheZug
liefert einen Int
zurück, in dem die Zahl der Hölzer enthalten ist, die der Copmuter entfernt hat. Der Zug wird ebenfalls direkt über die Methode entferneHoelzer
auf dem Haufen entfernt, der über die Eigenschaft haufen
verfügbar ist. Später müssen Sie darauf achten, dass die Eigenschaft haufen
auch wirklich auf den gleichen HolzHaufen
verweist, der auch in den anderen Teilen des Spiels genutzt wird. Das heißt, es darf nur eine einzige Instanz von HolzHaufen
geben und auf diese muss an dieser Stelle verwiesen werden. Dies ist allerdings die Aufgabe der Steuerung, auf die ich später eingehen werde.
Für die grafische Oberfläche möchte ich dem Benutzer ein kleines bisschen mehr an Komfort gönnen. Und zwar soll das Spiel auf einem Bildschirm laufen, allerdings über einen zweiten Bildschirm konfiguriert werden. Daher sind hier zwei Bildschirme zu erstellen: zum einen der Hauptbildschirm, auf dem das Spiel abläuft, und zum anderen der Konfigurationsbildschirm, auf dem Einstellungen vorgenommen werden können.
Abbildung 15.1 Setzen des Typs eines Buttons auf »Detail Disclosure«
Zunächst erstellen Sie die Hauptspielanzeige auf dem ersten Bildschirm der App. Dafür geben Sie zunächst die Zahl der Hölzer mit einem Label oben am Bildschirm aus. Später zeige ich Ihnen dann, wie sich das Ganze richtig hübsch grafisch darstellen lässt. Platzieren Sie das Label 20 Pixel unterhalb der oberen Bildschirmkante und jeweils 10 Pixel vom linken und rechten Rand entfernt. Zentrieren Sie den Text des Labels. Ich empfehle Ihnen ebenfalls, eine Schriftgröße von 20 Punkt auszuwählen. Darunter fügen Sie ein weiteres Label für die Ausgabe des Computerzugs ein. Setzen Sie den linken und rechten Rand wieder auf 10 Pixel, den oberen Rand auf 20 Pixel unterhalb des ersten Labels und wählen Sie als Schriftgröße wieder 20 Punkt.
Für die Eingabe der Züge sind drei Buttons vorgesehen, die darunter platziert werden. Die Buttons können Sie mit »Nimm 1«, »Nimm 2« und »Nimm 3« beschriften. Als Ausrichtungsregeln empfehle ich die folgenden vier Regeln:
Ziehen Sie nun einen weiteren Button in die rechte untere Ecke und wählen Sie in der rechten Spalte im vierten Reiter unter BUTTON als TYPE den Eintrag DETAIL DISCLOSURE, siehe Abbildung 15.1. Mit diesem Button wird der Benutzer später den Konfigurationsbildschirm öffnen. Als Ausrichtungsregeln empfehle ich, den Button jeweils 20 Pixel vom rechten und vom unteren Rand entfernt zu platzieren.
Abbildung 15.2 Das Benutzerinterface des ersten Bildschirms in der »Streichholz«-App auf dem iPhone
Das Ergebnis dieses ersten Benutzerinterface ist in Abbildung 15.2 zu sehen.
Für den Konfigurationsbildschirm bauen Sie nun selber einen Übergang in der Datei Main.storyboard
:
UIViewController
, um den Bildschirm zu verwalten. Fügen Sie dafür eine neue Datei zum Projekt hinzu, indem Sie in der linken Spalte auf die Gruppe Streichholz
rechts klicken und im Menü den Eintrag NEW FILE ... auswählen.
Unter SUBCLASS OF geben Sie nun den Namen »UIViewController« ein.
Main.storyboard
und ziehen einen VIEW CONTROLLER in die mittlere Spalte. Dieser Eintrag sollte der erste in der Liste sein.Wenn Sie den View Controller auf dem grafischen Editor platzieren, ist es ein eigener Bildschirm er ist genauso groß wie die anderen Bildschirme auch!
Abbildung 15.3 Einstellung der Klasse auf »EinstellungenViewController« auf dem zweiten Bildschirm
Dadurch haben Sie einen Übergang vom ersten zum zweiten Bildschirm erstellt, sobald der Benutzer den Button antippt.
Mit diesem Schritten können Sie mehrere Bildschirme verknüpfen und Übergänge einrichten. Starten Sie die App einmal und vergewissern Sie sich, dass die grafische Oberfläche korrekt aussieht und Sie mit dem »i«-Button in der rechten unteren Ecke tatsächlich zum Konfigurationsbildschirm gelangen.
Dummerweise ist dieser noch weiß und Sie haben keinen Weg zurück darum werden Sie sich allerdings erst ein wenig später kümmern. Zunächst einmal empfehle ich Ihnen, diesen Bildschirm mit ein paar ansprechenden grafischen Elementen zu versehen:
Geben Sie dem Button aber keinen Übergang zum ersten Bildschirm, das funktioniert leider nicht ganz so einfach!
Im Moment beherrscht die App tatsächlich nur eine Spielstrategie nämlich eine zufällige Zahl an Hölzern zu nehmen , aber ich verrate Ihnen bereits jetzt, dass es später mehr werden! Deswegen fügen Sie schon mal ein Segmented Control mit folgenden drei Einträgen hinzu:
Die Breite des Segmented Control richtet sich wieder nach der Zahl der Segmente sowie dem Beschriftungstext.
Der komplette Aufbau des zweiten Bildschirms ist in Abbildung 15.4 zu sehen. Beachten Sie, dass durch die Ausrichtungsregeln der Bildschirm hauptsächlich linksbündig und nicht zentriert ausgerichtet ist. Dies finde ich in diesem Fall als angenehmer. Damit ist die grafische Oberfläche auch schon fertig!
Abbildung 15.4 Die Konfiguration der »Streichholz«-App auf dem zweiten Bildschirm
Nun können Sie bereits eine erste Version des Spiels schreiben. Die Funktionen des zweiten Einstellungsbildschirms sowie weitere Verschönerungen kommen dann anschließend hinzu.
In der Steuerung verbinden Sie wieder das Datenmodell mit der grafischen Oberfläche. Zunächst erzeugen Sie die Eigenschaften der beiden Labels des ersten Bildschirms in der Datei ViewController.swift
. Für das erste Label ganz oben erzeugen und verknüpfen Sie eine Eigenschaft namens streichHolzAnzeige
und für das zweite Label darunter erzeugen und verknüpfen Sie eine Eigenschaft namens zugAnzeige
.
Anschließend brauchen Sie geeignete Aktionen für die drei Buttons. Die Aktionen können Sie sinnvollerweise nimmEins
, nimmZwei
sowie nimmDrei
nennen. Erzeugen Sie diese Aktionen ebenfalls in der Implementierungsdatei ViewController.swift
.
Nun müssen Sie sich um zwei Dinge kümmern: zum einen darum, dass das Spiel beim Start der App korrekt aufgesetzt wird. Dazu müssen das Datenmodell also die Klasse HolzHaufen
und die Klasse ComputerZug
korrekt erstellt und in Eigenschaften der Klasse ViewController
abgelegt werden. Zum anderen müssen Sie sich darum kümmern, dass das Spiel korrekt abläuft. Dazu geben Sie zum Spielbeginn eine Anweisung, dass der Spieler den ersten Zug machen soll, im Label zugAnzeige
aus. Züge des Spielers werden dann als Reaktion auf einen der drei Buttons entgegengenommen.
Dafür gibt es zwei verschiedene Stellen:
Dafür wird eine besondere init
-Methode verwendet, diese lautet:
required init?(coder aDecoder: NSCoder)
Diese wird von iOS selbständig aufgerufen und hier erledigen Sie die Erzeugung Ihres eigenen Datenmodells.
viewWillAppear
aufgerufen. Diese war Ihnen bereits einmal in Abschnitt 13.2.2 begegnet.Hier setzen Sie das Spiel mit den korrekten Anfangswerten auf.
Außerdem empfehle ich, eine weitere Methode zu verwenden, um den jeweils aktuellen Spielstand anzuzeigen. Diese nenne ich zeigeSpielstand
und sie sorgt dafür, dass im ersten Label die aktuelle Zahl der Hölzer angezeigt wird.
Wichtig ist nun eine Methode, die den Spielerzug ausführt. Zunächst gibt es drei verschiedene Methoden, nämlich nimmEins
, nimmZwei
und nimmDrei
. Allerdings brauchen Sie den Spielerzug wirklich nur ein einziges Mal zu schreiben, indem Sie eine weitere Methode spielerZug
definieren, die dann die Zahl der Hölzer bekommt, die der Spieler entfernen möchte. Diese Methode funktioniert ähnlich dem Spielfluss aus Listing 6.10, allerdings mit einem wichtigen Unterschied: Sie brauchen keine while
-Schleife, weil sich bei der grafischen Oberfläche Ihr System selbst darum kümmert zu reagieren, sobald der Benutzer einen Button antippt. Es reicht hier völlig aus, wenn Sie die ganze App so schreiben, dass sie nur auf Benutzeraktionen reagiert, ohne dass Sie sich um den ganzen Programmfluss Gedanken machen müssen.
import UIKit
/// Der Hauptbildschirm der Streichholz--App.
class ViewController: UIViewController {
/// Datenmodell für den Spielstand.
var spielHaufen: HolzHaufen
/// Datenmodell für die Computer--Strategie.
var computerStrategie: ComputerZug
/// Initialisiere diese Klasse.
required init?(coder aDecoder: NSCoder) {
// Erzeuge alle Datenmodellklassen.
self.spielHaufen = HolzHaufen()
self.computerStrategie = ComputerZug(meinHaufen: self.spielHaufen)
// Rufe die Basisklasse auf.
super.init(coder: aDecoder)
}
/// Starte ein neues Spiel.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Setze die anfängliche Zahl der Hölzer.
self.spielHaufen.hoelzer = self.spielHaufen.anfangsZahl
// Anfängliche Spielanweisung.
self.zugAnzeige.text = "Machen Sie einen Zug"
// Gib den Spielstand aus.
self.zeigeSpielstand()
}
/// Gib den aktuellen Spielstand aus.
func zeigeSpielstand() {
self.streichHolzAnzeige.text = "Hölzer: \(self.spielHaufen.hoelzer)"
}
/// Das Label für die Zahl der Streichhölzer.
@IBOutlet weak var streichHolzAnzeige: UILabel!
/// Das Label für die Zuganzeige.
@IBOutlet weak var zugAnzeige: UILabel!
/// Benutzer tippt den Button "Nimm 1".
@IBAction func nimmEins(sender: AnyObject) {
self.spielerZug(1)
}
/// Benutzer tippt den Button "Nimm 2".
@IBAction func nimmZwei(sender: AnyObject) {
self.spielerZug(2)
}
/// Benutzer tippt den Button "Nimm 3".
@IBAction func nimmDrei(sender: AnyObject) {
self.spielerZug(3)
}
/// Benutzer macht einen Spielerzug.
func spielerZug(zahl: Int) {
// Führe den Spielerzug aus.
self.spielHaufen.entferneHoelzer(zahl)
// Prüfe, ob der Spieler verloren hat.
if self.spielHaufen.istVerloren() {
// Der Spieler hat verloren.
self.zugAnzeige.text = "Sie haben verloren!"
} else {
// Mache einen Computerzug.
let computerZahl = self.computerStrategie.macheZug()
// Prüfe, ob der Computer verloren hat.
if self.spielHaufen.istVerloren() {
// Der Computer hat verloren.
self.zugAnzeige.text = "Ich habe verloren!"
} else {
// Gib den Computerzug aus.
self.zugAnzeige.text = "Ich nehme \(computerZahl) Hölzer"
}
}
// Gib den Spielstand aus.
self.zeigeSpielstand()
}
}
Listing 15.3 Erste funktionsfähige Version der Streichholz-App in der Datei »ViewController.swift«
Listing 15.3 zeigt, wie diese Methoden in der Datei ViewController.swift
geschrieben werden können. Dies ist bereits eine erste funktionsfähige Version starten Sie das Spiel und spielen Sie es einmal durch. Vergewissern Sie sich, dass es richtig funktioniert und genauso arbeitet wie die Version aus Kapitel 6.
Die App ist jetzt bereits funktionsfähig. Aber ich gebe zu: Sie könnte doch noch viel besser sein. Sie müssen sie jedesmal neu starten, um ein neues Spiel zu beginnen das geht auf dem iPhone gar nicht!
Um zu vermeiden, dass Sie das Spiel jedes Mal neu starten müssen, wenn Sie es einmal durchgespielt haben, muss das Spiel nach dem Ende selbstständig neu starten können. Ein Neustarten passiert im Moment in der Methode viewWillAppear
, die einmal nach dem Start der App aufgerufen wird. Ich empfehle Ihnen nicht, sie selbstständig aufzurufen. Und schon gar nicht, um das Spiel wieder neu zu starten! Sie können aber eine eigene Methode für genau diesen Zweck verwenden. Nennen Sie diese Methode am besten neuesSpiel
. Diese können Sie dann einerseits in viewWillAppear
aufrufen, andererseits aber auch, nachdem das Spiel zu Ende ist. Listing 15.4 zeigt, wie Sie das am einfachsten bewerkstelligen können.
/// Der Bildschirm erscheint.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Starte eine neues Spiel.
self.neuesSpiel()
}
/// Starte ein neues Spiel.
func neuesSpiel() {
// Setze die anfängliche Zahl der Hölzer.
self.spielHaufen.hoelzer = self.spielHaufen.anfangsZahl
// Anfängliche Spielanweisung.
self.zugAnzeige.text = "Machen Sie einen Zug"
// Gib den Spielstand aus.
self.zeigeSpielstand()
}
Listing 15.4 Die Methode »neuesSpiel« und die dadurch vereinfachte Methode »viewWillAppear« in »ViewController.swift«
Starten Sie die App neu und vergewissern Sie sich, dass sie noch genauso funktioniert wie vorher.
Nun soll das Spiel aber auch neu gestartet werden, nachdem es gewonnen oder verloren worden ist. Wenn Sie das Spiel neu starten, nachdem es einmal beendet worden ist, müssen Sie aber auch noch daran denken, dass der Benutzer als Erstes über das Ergebnis informiert werden sollte. Im Moment überschreibt die Methode neuesSpiel
nämlich das Label, in dem bisher das Ergebnis präsentiert worden ist. Dadurch würde der Benutzer nicht mibekommen, ob er denn nun gewonnen oder verloren hat. Wenn aber das neue Spiel gestartet wird, ohne dass er weiß, ob er gewonnen oder verloren hat, so könnte ihn das ziemlich verärgern. Und verärgerte Benutzer wollen Sie nicht haben!
Um das Spielergebnis bekannt zu geben, empfiehlt sich eine Dialogbox, ein sogenanntes Alert View. Glücklicherweise bietet Apple Ihnen so eine Funktion ebenfalls als Teil des Betriebssystems an, sodass Sie diese einfach nur benutzen müssen. Das funktioniert folgendermaßen: Das Objekt, das ein Alertview anzeigt, nennt sich UIAlertController
. Im Gegensatz zu den anderen grafischen Objekten, die Sie bereits früher kennengelernt haben, kann ein Alertview allerdings nicht mit dem grafischen Editor erzeugt und bearbeitet werden. Der Grund ist, dass Alertviews ja nicht ständig angezeigt werden, sondern nur an bestimmten Stellen. Und dass sie anschließend auch wieder verschwinden sollen.
Deswegen müssen Sie Alertviews immer im Programmtext erzeugen. Glücklicherweise ist das nicht allzu schwierig. Im einfachsten Fall brauchen Sie nur drei verschiedene Texte:
Um ein Alertview anzuzeigen, müssen Sie drei Dinge tun:
UIAlertController
erzeugen und dort den Titel und die Nachricht setzen.presentViewController
.Das geht schon mit wenigen Zeilen Programmtext:
// Erzeuge ein Alertview.
let anzeige = UIAlertController(title: "Alarm",
message: "Ihr iPhone hat verloren!",
preferredStyle: .Alert)
// Füge einen Button mit Aktion hinzu.
let buttonAktion = UIAlertAction(title: "Schön",
style: .Default) { (action) --> Void in
// Es soll nichts weiter passieren.
}
anzeige.addAction(buttonAktion)
// Zeige das Alertview an.
self.presentViewController(anzeige,
animated: true,
completion: nil)
Damit erzeugen Sie ein Alertview mit dem Titel »Alarm«, der Nachricht »Ihr iPhone hat verloren!« sowie dem Button »Schön« und zeigen es an. Hinter der Definition der Konstante buttonAktion
folgt ein weiterer Block, in dem ich lediglich den Kommentar »Es soll nichts weiter passieren« geschrieben habe. Tatsächlich handelt es sich hier um einen Funktionsblock, der ausgeführt wird, sobald der Benutzer den Button antippt. Dies ist in der Streichholz-App die richtige Stelle, um das Spiel neu zu starten!
Ein solches Konstrukt hatte ich bereits im fortgeschrittenen Abschnitt 10.6.1 besprochen. Wenn Sie es nicht schon getan haben, so empfehle ich Ihnen, diesen Abschnitt jetzt einmal durchzulesen!
UIAlertController
darzustellen gibt es erst seit iOS 8. Vorher gab es eine Klasse namens UIAlertView
. Seither empfiehlt Apple, nur noch den UIAlertController
und nicht mehr das UIAlertView
zu verwenden.UIAlertView
hatte den Vorteil, dass es ein wenig einfacher war, eine Anzeige zu erzeugen. Aber es war deutlich umständlicher, eine Reaktion auf einen Button zu schreiben. Insbesondere war es nicht möglich, einfach einen Block mit der Reaktion anzugeben, sondern Sie waren gezwungen, eine eigene Methode in Ihrer View Controller-Klasse zu schreiben. Wenn der View Controller dann auch noch mehrere verschiedene Alertviews dargestellt hat, hatten Sie ganz verloren und mussten viel Zusatzaufwand betreiben, um herauszufinden, welcher Button denn gerade von welchem Alertview angetippt wurde.Die Umsetzung dieses neuen Spielablaufs ist in Listing 15.5 gezeigt. Am Spielende wird eine neue Methode spielEnde
aufgerufen, die ein Alertview mit dem Spielergebnis anzeigt und sobald der Button angetippt wird gleich darauf das Spiel neu startet.
/// Beende das Spiel.
func spielEnde(nachricht: String) {
// Erzeuge ein Alertview mit der nachricht.
let anzeige = UIAlertController(title: "Das Spiel ist aus",
message: nachricht,
preferredStyle: .Alert)
// Füge einen Button mit Aktion hinzu.
let buttonAktion = UIAlertAction(title: "Neues Spiel",
style: .Default) { (action) --> Void in
// Beginne ein neues Spiel.
self.neuesSpiel()
}
anzeige.addAction(buttonAktion)
// Zeige das Alertview an.
self.presentViewController(anzeige,
animated: true,
completion: nil)
}
/// Benutzer macht einen Spielerzug.
func spielerZug(zahl: Int) {
// Führe den Spielerzug aus.
self.spielHaufen.entferneHoelzer(zahl)
// Prüfe, ob der Spieler verloren hat.
if self.spielHaufen.istVerloren() {
// Der Spieler hat verloren.
self.spielEnde("Sie haben verloren!")
} else {
// Mache einen Computerzug.
let computerZahl = self.computerStrategie.macheZug()
// Prüfe, ob der Computer verloren hat.
if self.spielHaufen.istVerloren() {
// Der Computer hat verloren.
self.spielEnde("Ich habe verloren!")
} else {
// Gib den Computerzug aus.
self.zugAnzeige.text = "Ich nehme \(computerZahl) Hölzer"
}
}
// Gib den Spielstand aus.
self.zeigeSpielstand()
}
Listing 15.5 Neuer Spielfluss, der am Spielende ein Alertview anzeigt und dann das Spiel neu startet
Starten Sie das Spiel neu und vergewissern Sie sich, dass es beim Spielende tatsächlich das Ergebnis in einem Alertview korrekt anzeigt und anschließend ein neues Spiel startet.
Im Moment macht der Computer einfach nur Zufallszüge. Das ist einerseits sehr gut, denn es ist relativ einfach für Sie, zu gewinnen. Andererseits übersieht der Computer damit auch offensichtliche Gewinnmöglichkeiten und stellt keine Herausforderung dar. Im Folgenden stelle ich zwei weitere Strategien vor. Aber zunächst möchte ich erläutern, wie Sie diese Strategien in das bestehende Programm einbinden können.
Im Augenblick benutzen Sie die Klasse ComputerZug
, um mit der Methode macheZug
einen einzelnen Computerzug durchzuführen. Diese Methode liefert dann eine Zufallszahl zurück, die der Zahl der gerade genommenen Hölzer entspricht. In Abschnitt 11.2 haben Sie erfahren, dass Sie in einer Klasse die Methode der Basisklasse überschreiben können, ohne dass sich dabei das Interface nach außen ändert. Das Verhalten der entsprechenden Methode kann allerdings durchaus anders sein und damit sowohl direkt als auch indirekt die Klasse beeinflussen. Diese Eigenschaft können Sie benutzen, indem Sie eine neue Klasse namens DummerComputerZug
mit Basisklasse ComputerZug
erstellen und dort die Methode macheZug
neu schreiben.
Listing 15.6 zeigt eine wirklich nicht besonders clevere Implementierung, die immer nur genau ein Holz nimmt. Erzeugen Sie einfach mal eine neue Swift-Datei namens DummerComputerZug.swift
mit dieser Klasse.
/// Eine dumme Strategie.
class DummerComputerZug: ComputerZug {
/// Führe einen Zug aus und gib ihn zurück.
override func macheZug() --> Int {
// Führe Zug aus: genau ein Holz entfernen.
let zug = 1
// Führe Zug aus.
self.haufen.entferneHoelzer(zug)
// Gib den Wert zusätzlich zurück.
return zug
}
}
Listing 15.6 Alternative Klasse »DummerComputerZug« mit einer wenig schlauen Strategie
Wenn Sie nun in der init
-Methode der Klasse ViewController
in Listing 15.3 die Zeile
self.computerStrategie = ComputerZug(meinHaufen: self.spielHaufen)
durch die Zeile
self.computerStrategie = DummerComputerZug(meinHaufen: self.spielHaufen)
ersetzen, so verwendet Ihr Computer ganz von selbst nur noch die ganz »dumme« Strategie aus Listing 15.6 . Starten Sie das Programm nochmals neu und vergewissern Sie sich, dass der Computer jetzt bei jedem Zug immer genau ein einzelnes Streichholz entfernt!
Das wirklich Clevere daran ist allerdings, dass Sie dafür nur eine einzige Methode in der Klasse WSDummerComputerZug
schreiben und des Weiteren im Programm nur eine einzige weitere Zeile ändern mussten! Damit können Sie sehr leicht weitere, alternative Strategien implementieren.
Ich wollte aber eigentlich darüber schreiben, den Computer schlauer spielen zu lassen und nicht unbedingt über eine schlaue Möglichkeit, den Computer dümmer spielen zu lassen. Darum geht es jetzt um beides: einerseits den schlauen Polymorphismus im Programm behalten, andererseits aber auch diesen benutzen, um eine wirklich schlaue Strategie umzusetzen.
Für die kluge Strategie müssen Sie sich Folgendes überlegen: Das eigentliche Ziel des Spiels ist es, nicht das letzte Streichholz nehmen zu müssen. Denn wer das letzte Holz nimmt, verliert. Wenn Sie den vorletzten Zug machen, sollte also genau ein einzelnes Hölzchen übrig bleiben. Dies erreichen Sie, indem Sie entweder:
Sollten Sie also jemals in die Lage kommen, zwei, drei oder auch vier Hölzchen vor sich liegen zu haben und am Zug zu sein, so werden Sie das Spiel gewinnen.
Dieselbe Überlegung können Sie nun auch im drittletzten Zug anstellen. Im drittletzten Zug wollen Sie vermeiden, Ihrem Gegner zwei, drei oder vier Hölzchen übrig zu lassen. Denn wenn dieser schlau ist (und dieses Buch gelesen hat), so wird er wissen, dass er damit gewinnen kann. Dies erreichen Sie, indem Sie im drittletzten Zug genau fünf Hölzchen übrig lassen. Dann muss er entweder eines oder zwei oder drei Hölzchen wegnehmen, damit Sie wie oben beschrieben im vorletzten Zug gewinnen können.
Diese Überlegung können Sie nun genauso für den viertletzten Zug, den fünftletzten Zug und so weiter anstellen. Es ergibt sich somit das Muster, dass ein Spieler das Spiel verliert, wenn er in eine Situation kommt, in der bei seinem Zug
1, 5, 9, 13, ...
Hölzchen übrig sind. Das heißt, dass eine wirklich schlaue Strategie darin besteht, immer genau so viele Hölzchen zu entfernen, dass der Gegner mit einer dieser »verbotenen« Zahlen dasteht. Das wiederum heißt, dass ein kluger Spieler (der dieses Geheimnis kennt) das Spiel immer gewinnen wird, wenn er anfängt! Die einzige Ausnahme besteht darin, dass die Startzahl der Hölzer genau einer dieser verbotenen Zahlen entspricht das sollte ein kluger Spieler natürlich tunlichst vermeiden!
Somit lautet die kluge Spielstrategie des Streichholzspiels:
Der Sinn der letzten Regel genau ein Hölzchen wegzunehmen, wenn man in einer Verlustsituation ist ist der, dass sich das Spiel so am längsten hinziehen wird und vielleicht die Chance besteht, dass der Gegner noch einen Fehler macht!
Die nächste verbotene Zahl im ersten Schritt wird dabei folgendermaßen berechnet: Alle verbotenen Zahlen folgen dem Muster, dass sie sich immer um 4 erhöhen. Außerdem fangen sie bei 1 an, das heißt, eine verbotene Zahl folgt immer der Form
Dabei ist n eine Ganzzahl. Für n = 0 finden Sie also die erste verbotene Zahl 1, für n = 1 die zweite verbotene Zahl 5 und so weiter. Die aktuelle Gesamtzahl der Hölzer G erlaubt Ihnen nun zu bestimmen, die wievielte verbotene Zahl Sie genau brauchen:
denn Sie müssen die Gesamtzahl durch 4 teilen, da die verbotenen Zahlen immer Vielfache von 4 sind. Bevor Sie teilen, müssen Sie noch 1 abziehen, denn die verbotenen Zahlen fangen bei 1 an.
/// Eine schlaue Strategie.
class KlugerComputerZug: ComputerZug {
/// Führe einen Zug aus und gib ihn zurück.
override func macheZug() --> Int {
// Finde die nächste "verbotene Zahl".
let verboteneZahl = ((self.haufen.hoelzer--1)/4)*4+1
// Finde die Differenz zur aktuellen Zahl der Hölzer.
let differenz = self.haufen.hoelzer -- verboteneZahl
// Bestimme damit den Zug.
var zug: Int
if differenz > 0 {
// Zwinge den Gegner auf eine "verbotene Zahl".
zug = differenz
} else {
// Falls selbst auf einer "verbotenen Zahl":
// Nimm nur ein Holz weg.
zug = 1
}
// Führe Zug aus.
self.haufen.entferneHoelzer(zug)
// Gib den Wert zusätzlich zurück.
return zug
}
}
Listing 15.7 Klasse »KlugerComputerZug«, die immer den besten Zug spielt
Erstellen Sie eine neue Datei KlugerComputerZug.swift
und schreiben Sie darin eine Klasse KlugerComputerZug
mit Basisklasse ComputerZug
und überschreiben Sie die Methode macheZug
so, wie in der Strategie oben beschrieben. Listing 15.7 zeigt die Methode, mit der der Computer so gut wie möglich spielen kann. Fügen Sie diese Strategie in die Streichholz-App ein und vergewissern Sie sich, dass Ihr Computer nun wirklich gut spielen kann!
Nun können Sie alle drei Strategien kombinieren und sogar eine Methode schreiben, die die Strategie ändern kann, während die App bereits läuft! Dies wird im nächsten Abschnitt sehr nützlich sein.
Ich empfehle Ihnen allerdings, Ihr Programm bei dieser Gelegenheit ein wenig zu refaktorieren. Und zwar wird im Moment die Datenmodellklasse für die Strategie in der Klasse ViewController
erzeugt. Dies ist auch völlig korrekt und entspricht dem MVC-Entwurfsmuster. Wenn Sie aber nun eine neue Funktionalität für das Datenmodell haben nämlich die Fähigkeit, verschiedene Strategien auszuwählen so »passt« diese Funktionalität nicht unbedingt in die Steuerung. Die Strategie selbst ist Teil des Datenmodells und die Auswahl derselben darf in der Steuerung sein. Die Auswirkung der Auswahl einer Strategie jedoch gehört eigentlich ins Datenmodell.
Deswegen schlage ich folgende Refaktorierung vor:
ComputerStrategie
zum Projekt hinzu, die es erlaubt, eine Strategie auszuwählen und dann die gewünschte Strategie auch auswählt.strategieTyp
zur Verfügung, die vom Typ Int
ist und die folgenden drei Strategien zur Auswahl bietet:• Ist diese gleich 0, so soll die Strategie DummerComputerZug
genommen werden. Der Computer spielt dumm und vorhersagbar, er wird immer nur ein Hölzchen nehmen.
• Ist diese gleich 1, so soll die Strategie ComputerZug
lauten. Der Computer spielt unberechenbar und wild und man kann nie vorher sagen, was er tun wird.
• Ist diese gleich 2, so soll die Strategie KlugerComputerZug
benutzt werden.
ComputerStrategie
bietet ebenfalls eine Methode namens macheZug
an, die genauso funktioniert wie die Methode gleichen Namens von ComputerZug
. Sie erlaubt es, Züge nach der korrekten Strategie zurückzuliefern.ViewController
benutzt nicht mehr die Klasse ComputerZug
(oder eine Unterklasse davon). Sie benutzt stattdessen nur noch die Klasse ComputerStrategie
, die sie in ihrer Eigenschaft computerStrategie
speichert.Sie könnten sogar noch einen Schritt weitergehen und sämtliche Datenmodellklassen also den Spielhaufen und die ComputerStrategie – in eine gemeinsame Klasse packen. Für den Spielhaufen der Klasse HolzHaufen
empfehle ich auf jeden Fall, diesen nicht in die Klasse ComputerStrategie
zu packen, denn dieser gehört ja nicht zur Strategie. Sondern nur zum Datenmodell im weitesten Sinne.
Sie könnten hier eine weitere Datenmodellklasse einführen, die die Computerstrategie und den Spielhaufen kombiniert. Allerdings halte ich persönlich diese Lösung für zu kompliziert die Klasse würde lediglich zwei Objekte erzeugen und verbinden und dafür braucht man nicht zwingend eine eigene Klasse. Für ComputerStrategie
sieht die Sache anders aus, denn diese Klasse kümmert sich nicht nur um die Kombination der drei Computerzug-Klassen, sondern auch um die geeignete Auswahl einer dieser Klassen. Hier halte ich eine eigene Klasse für durchaus angebracht!
/// Datenmodellklasse für die Strategie bei Zügen des Computers.
class ComputerStrategie {
/// Datenmodell für Suche des Computer--Zuges.
var computerZugSuche: ComputerZug
/// Datenmodell des Holzhaufens.
var spielHaufen: HolzHaufen
/// Wähle den Typ der Stragie.
var strategieTyp = 0 {
willSet(neueStrategie) {
switch neueStrategie {
case 0:
// Benutze die "dumme" Strategie.
self.computerZugSuche = DummerComputerZug(meinHaufen: self.spielHaufen)
case 1:
// Benutze die "wilde" Strategie.
self.computerZugSuche = ComputerZug(meinHaufen: self.spielHaufen)
default:
// Benutze die "schlaue" Strategie.
self.computerZugSuche = KlugerComputerZug(meinHaufen: self.spielHaufen)
}
}
}
/// Führe einen Zug aus und gib ihn zurück.
func macheZug() --> Int {
return self.computerZugSuche.macheZug()
}
/// Initialisiere diese Klasse.
init(haufen: HolzHaufen) {
// Merke den Holzhaufen (für Änderungen von strategieTyp).
self.spielHaufen = haufen
// Erzeuge die Anfangsstrategie.
self.computerZugSuche = DummerComputerZug(meinHaufen: haufen)
}
}
Listing 15.8 Klasse »ComputerStrategie« zur Verwendung der gewünschten Strategie bei Zügen des Computers
Den konkreten Programmtext der Klasse ComputerStrategie
finden Sie in Listing 15.8 . Erzeugen Sie eine neue Swift-Datei namens ComputerStrategie.swift
und schreiben Sie den Inhalt des Listings dort hinein. Um diese neue Klasse zu verwenden, ändern Sie die Eigenschaften und init
-Methode in ViewController.swift
so wie in Listing 15.9 gezeigt.
/// Datenmodell für den Spielstand.
var spielHaufen: HolzHaufen
/// Datenmodell für die Computerzüge.
var computerStrategie: ComputerStrategie
/// Initialisiere diese Klasse.
required init?(coder aDecoder: NSCoder) {
// Erzeuge alle Datenmodellklassen.
self.spielHaufen = HolzHaufen()
self.computerStrategie = ComputerStrategie(haufen: self.spielHaufen)
// Rufe die Basisklasse auf.
super.init(coder: aDecoder)
}
Listing 15.9 Änderungen der Eigenschaften und »init«-Methode der Klasse »ViewController«
Die Klasse ComputerStrategie
verwendet die willSet
-Methode der Eigenschaft strategieTyp
, so wie Sie es im fortgeschrittenen Abschnitt 7.4.2 beschrieben habe. Beachten Sie, dass Sie in der init
-Methode der Klasse ComputerStrategie
weiterhin die computerStrategie
zusätzlich setzen müssen (und dass Sie jetzt auf DummerComputerZug
stehen sollte), denn die willSet
-Methode der Eigenschaft wird leider nicht benutzt, wenn Sie eine Eigenschaft in der init
-Methode setzen!
Sie haben bereits den zweiten Bildschirm für die Konfiguration vorbereitet. Sie können vom ersten Bildschirm aus mit dem »i«-Button zum zweiten Bildschirm wechseln und dort auch alle grafischen Elemente bedienen. Allerdings funktioniert der Zurück-Button nicht und die Einstellungen haben keine Auswirkungen. Um diese beiden Dingen werden Sie sich jetzt im Folgenden kümmern.
Zunächst soll der »Zurück«-Button richtig funktionieren. Denn wenn Sie keine Möglichkeit haben, vom zweiten Bildschirm wieder zum ersten zurückzukehren, nutzt es überhaupt nichts, wenn die Einstellungen richtig funktionieren! Glücklicherweise ist es relativ einfach, diesen Button zum Funktionieren zu bringen: Öffnen Sie den Assistant-Editor vom grafischen Editor und erzeugen Sie eine Aktion namens benutzerWaehltZurueck
in EinstellungenViewController
, die ausgelöst wird, sobald der Benutzer den »Zurück«-Button antippt. In dieser Aktion sorgen Sie dann dafür, dass der Übergang wieder rückgängig gemacht wird.
Dafür bietet jede Unterklasse von UIViewController
eine Methode namens dismissViewControllerAnimated
an. Diese hat zwei Parameter:
Bool
, der angibt, ob der Übergang eine grafische Animation haben soll oder nicht. Ich empfehle Ihnen, hier true
anzugeben.completion
, der ausgeführt wird, sobald der Übergang beendet ist. Für diese Anwendung reicht es, wenn Sie hier einfach nil
angeben.Die anfänglich gesetzen Methoden des EinstellungenViewController
sind viewDidLoad
und didReceiveMemoryWarning
sowie ein auskommentierter Block für die prepareForSegue
-Methode. Diese können Sie löschen, weil sie hier nicht gebraucht werden. Listing 15.10 zeigt den Programmtext mit funktionsfähigem »Zurück«-Button.
/// Steuerungsklasse für den zweiten Bildschirm.
class EinstellungenViewController {
/// Reaktion auf den Zurück--Button.
@IBAction func benutzerWaehltZurueck(sender: AnyObject) {
// Beende diesen Bildschirm.
self.dismissViewControllerAnimated(true, completion: nil)
}
}
Listing 15.10 Klasse »EinstellungenViewController« für den zweiten Bildschirm mit funktionsfähigem »Zurück«-Button
Nun können Sie zwischen beiden Bildschirm hin- und herwechseln. Und der Wechsel sieht dank der Animation auch sehr hübsch aus! Nur leider werden Ihre Einstellungen nicht übernommen egal, was Sie am Segmented Control einstellen, der Computer stellt sich beim Spielen immer dumm an!
Abbildung 15.5 Das Zusammenspiel der beiden Bildschirme in der Streichholz-App beim Übergang vom 1. zum 2. Bildschirm
Für die Übergabe der Einstellungen des zweiten Bildschirms an den ersten gibt es mehrere Möglichkeiten. Ich wähle im Folgenden eine Möglichkeit, die mit relativ wenig Programmtext auskommt. Sie ist ein bisschen weniger flexibel im Vergleich zu anderen Alternativen, jedoch relativ einfach umzusetzen. Die Grundidee ist in Abbildung 15.5 abgebildet:
EinstellungenViewController
des zweiten Bildschirms und lädt alle grafischen Elemente.prepareForSegue
den zweiten Bildschirm zu konfigurieren.ViewController
) die gerade aktive Strategie in einer Eigenschaft strategie
ein. Außerdem gibt er die aktuelle Startzahl der Hölzer in einer Eigenschaft
anfangsHoelzer
an.EinstellungenViewController
) passt sowohl das Segmented Control als auch das Label und den Slider an.KannEinstellungenAendern
.Beim Ändern der Einstellungen passiert nun Folgendes, siehe Abbildung 15.6:
EinstellungenViewController
die Aktion benutzerBewegtSlider
ausgeführt. Dabei wird das Label
hoelzerZahlAnzeige
geändert, damit es immer die korrekte Startzahl anzeigt.benutzerWaehltZurueck
der aktuelle Wert der Startzahl der Hölzer sowie die gewählte Strategie an den Delegaten übergeben.einstellungenGeaendert
an seinem Delegaten auf dies ist der erste View Controller, der dadurch auf den aktuellen Stand gebracht wird.viewDidAppear
-Methode des ersten View Controllers das Spiel neu gestartet.Abbildung 15.6 Ablauf beim Ändern von Einstellungen auf dem zweiten Bildschirm
Damit das alles funktioniert, müssen Sie folgende Schritte ausführen:
hoelzerZahlAnzeige
im EinstellungenViewController
.
anfangsZahlAuswahlSlider
im EinstellungenViewController
.
strategieAuswahlControl
im EinstellungenViewController
.
benutzerBewegtSlider
zu EinstellungenViewController
hinzu, die aufgerufen wird, sobald der Slider bewegt wird.
ViewController.swift
so, wie in Listing 15.11 gezeigt. Der nicht veränderte Programmtext ist der Vollständigkeit halber nochmals wiederholt. Neu hinzugekommen ist das Protokoll KannEinstellungenAendern
mit der Methode
einstellungenGeaendert
sowie die Methode prepareForSegue
zum Einrichten des zweiten Controllers.
EinstellungeViewController
den Programmtext aus Listing 15.12 ein. Die gesamte Klasse ist hier der Vollständigkeit halber abgedruckt.
Starten Sie das Programm und vergewissern Sie sich, dass nun der zweite Bildschirm korrekt funktioniert und die Einstellungen richtig auf dem ersten Bildschirm übernommen werden! Probieren Sie auch mal, wieder in den zweiten Bildschirm zurückzugehen, und vergewissern Sie sich, dass die App sich die vorgenommenen Einstellungen richtig »gemerkt« hat.
import UIKit
/// Konfigurierbarkeit des Datenmodells.
protocol KannEinstellungenAendern {
/// Übernehme aktuelle Einstellungen.
func einstellungenGeaendert(strategieTyp: Int,
anfangsZahl: Int)
}
/// Die Steuerung des ersten Bildschirms.
class ViewController: UIViewController, KannEinstellungenAendern {
// MARK: Eigenschaften für das Datenmodell.
/// Datenmodell für den Spielstand.
var spielHaufen: HolzHaufen
/// Datenmodell für die Computerzüge.
var computerStrategie: ComputerStrategie
// MARK: Vom Betriebssystem aufrufbar.
/// Initialisiere diese Klasse.
required init?(coder aDecoder: NSCoder) {
// Erzeuge alle Datenmodellklassen.
self.spielHaufen = HolzHaufen()
self.computerStrategie = ComputerStrategie(haufen: self.spielHaufen)
// Rufe die Basisklasse auf.
super.init(coder: aDecoder)
}
/// Der Bildschirm erscheint.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Starte eine neues Spiel.
self.neuesSpiel()
}
/// Der zweite Bildschirm wird erscheinen.
override func prepareForSegue(segue: UIStoryboardSegue,
sender: AnyObject?) {
// Wird nur bei "zuEinstellungen" ausgeführt.
if segue.identifier == "zuEinstellungen" {
// Setze den controller als assoziierten EinstellungenViewController.
let controller = segue.destinationViewController as!
EinstellungenViewController
// Setze die Eigenschaften des EinstellungenViewController.
controller.anfangsHoelzer = self.spielHaufen.anfangsZahl
controller.strategie = self.computerStrategie.strategieTyp
controller.delegate = self
}
}
// MARK: KannEinstellungenAendern
func einstellungenGeaendert(strategieTyp: Int,
anfangsZahl: Int) {
self.computerStrategie.strategieTyp = strategieTyp
self.spielHaufen.anfangsZahl = anfangsZahl
}
// MARK: Eigene Methoden für den Spielablauf.
/// Starte ein neues Spiel.
func neuesSpiel() {
// Setze die anfängliche Zahl der Hölzer.
self.spielHaufen.hoelzer = self.spielHaufen.anfangsZahl
// Anfängliche Spielanweisung.
self.zugAnzeige.text = "Machen Sie einen Zug"
// Gib den Spielstand aus.
self.zeigeSpielstand()
}
/// Gib den aktuellen Spielstand aus.
func zeigeSpielstand() {
self.streichHolzAnzeige.text = "Hölzer: \(self.spielHaufen.hoelzer)"
}
/// Beende das Spiel.
func spielEnde(nachricht: String) {
// Erzeuge ein Alertview mit der nachricht.
let anzeige = UIAlertController(title: "Das Spiel ist aus",
message: nachricht,
preferredStyle: .Alert)
// Füge einen Button mit Aktion hinzu.
let buttonAktion = UIAlertAction(title: "Neues Spiel",
style: .Default) { (action) --> Void in
// Beginne ein neues Spiel.
self.neuesSpiel()
}
anzeige.addAction(buttonAktion)
// Zeige das Alertview an.
self.presentViewController(anzeige,
animated: true,
completion: nil)
}
/// Benutzer macht einen Spielerzug.
func spielerZug(zahl: Int) {
// Führe den Spielerzug aus.
self.spielHaufen.entferneHoelzer(zahl)
// Prüfe, ob der Spieler verloren hat.
if self.spielHaufen.istVerloren() {
// Der Spieler hat verloren.
self.spielEnde("Sie haben verloren!")
} else {
// Mache einen Computerzug.
let computerZahl = self.computerStrategie.macheZug()
// Prüfe, ob der Computer verloren hat.
if self.spielHaufen.istVerloren() {
// Der Computer hat verloren.
self.spielEnde("Ich habe verloren!")
} else {
// Gib den Computerzug aus.
self.zugAnzeige.text = "Ich nehme \(computerZahl) Hölzer"
}
}
// Gib den Spielstand aus.
self.zeigeSpielstand()
}
// MARK: Reaktionen der grafischen Oberfläche.
/// Das Label für die Zahl der Streichhölzer.
@IBOutlet weak var streichHolzAnzeige: UILabel!
/// Das Label für die Zuganzeige.
@IBOutlet weak var zugAnzeige: UILabel!
/// Benutzer tippt den Button "Nimm 1".
@IBAction func nimmEins(sender: AnyObject) {
self.spielerZug(1)
}
/// Benutzer tippt den Button "Nimm 2".
@IBAction func nimmZwei(sender: AnyObject) {
self.spielerZug(2)
}
/// Benutzer tippt den Button "Nimm 3".
@IBAction func nimmDrei(sender: AnyObject) {
self.spielerZug(3)
}
}
Listing 15.11 Vollständige Klasse »ViewController«, die korrekt mit dem zweiten Bildschirm zusammenarbeitet
import UIKit
/// Steuerungsklasse für den zweiten Bildschirm.
class EinstellungenViewController: UIViewController {
// MARK: Eigenschaften, die von außen gesetzt werden.
/// Die anfängliche Zahl der Hölzer.
var anfangsHoelzer = 18
/// Die anfängliche Strategie.
var strategie = 0
/// Der Delegat für die aktuellen Einstellungen.
var delegate: KannEinstellungenAendern?
// MARK: Vom Betriebssystem aufrufbar.
/// Der Bildschirm erscheint.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Setze den Slider auf den aktuellen Startwert.
let startWert = Float(self.anfangsHoelzer)
self.anfangsZahlAuswahlSlider.value = startWert
// Setze das Label auf den aktuellen Wert.
self.aktualisiereLabel()
// Setze das Segmented Control auf den aktuellen Wert.
let jetzigeStrategie = self.strategie
self.strategieAuswahlControl.selectedSegmentIndex = jetzigeStrategie
}
// MARK: Eigene Methode für das Aktualisieren der Anzeige.
/// Passe das Label auf den neuen Sliderwert an.
func aktualisiereLabel() {
let neuerText = "Anfangszahl der Hölzer: \(self.anfangsHoelzer)"
self.hoelzerZahlAnzeige.text = neuerText
}
// MARK: Reaktionen der grafischen Oberfläche.
/// Reaktion auf den Zurück--Button.
@IBAction func benutzerWaehltZurueck(sender: AnyObject) {
// Setze die geänderten Einstellungen am Delegaten.
let jetzigeStrategie = self.strategieAuswahlControl.selectedSegmentIndex
self.delegate?.einstellungenGeaendert(jetzigeStrategie,
anfangsZahl: self.anfangsHoelzer)
// Beende diesen Bildschirm.
self.dismissViewControllerAnimated(true, completion: nil)
}
/// Anzeige der aktuellen Anfangszahl.
@IBOutlet weak var hoelzerZahlAnzeige: UILabel!
/// Slider für die Anfangszahl.
@IBOutlet weak var anfangsZahlAuswahlSlider: UISlider!
/// Segmented Control für die Strategieauswahl.
@IBOutlet weak var strategieAuswahlControl: UISegmentedControl!
/// Reaktion, wenn der Benutzer den Slider bewegt.
@IBAction func benutzerBewegtSlider(sender: AnyObject) {
// Hole den aktuellen Wert des Sliders als Int.
let gewaehlteZahl = Int(self.anfangsZahlAuswahlSlider.value)
// Setze die anfangsHoelzer Eigenschaft.
self.anfangsHoelzer = gewaehlteZahl
// Aktualisiere das Label.
self.aktualisiereLabel()
}
}
Listing 15.12 Vollständige Klasse »EinstellungenViewController« für den zweiten Bildschirm
Das war jetzt eine zugegebenermaßen lange Erklärung für die Handhabung der beiden Bildschirme und deren Zusammenarbeit. Wenn Sie nicht alles gleich auf Anhieb verstanden haben, so versuchen Sie, durch gezielte print
-Anweisungen nachzuvollziehen, wann und wie die einzelnen Methoden aufgerufen werden und was sie dann genau tun.
Diese gezeigten Mechanismen sind allerdings recht elegant und flexibel, sodass Sie wirklich leistungsfähige und mächtige Programme schreiben können, wenn Sie sie einmal verstanden und verinnerlicht haben.
Hier gehe ich zunächst nochmals auf die Rollenverteilung von Klassen ein und fasse dann viele Konzepte, die ich bisher in diesem Buch nach und nach vorgestellt habe, unter dem Oberbegriff Entwurfsmuster zusammen.
Im Datenmodell in Abschnitt 15.4.3 gibt es drei Klassen, die Klasse ComputerZug
und deren Unterklassen DummerComputerZug
und KlugerComputerZug
, die alle drei etwas berechnen und das Ergebnis zurückgeben. Im Falle von DummerComputerZug
ist »berechnen« vielleicht übertrieben, aber es ist doch letzten Endes eine mögliche Strategie, die ausgeführt wird.
Die Klasse ComputerStrategie
hingegen macht selbst keine Berechnungen. Sie sorgt nur dafür, dass die korrekte Computerzug-Klasse benutzt und aufgerufen wird. Die Computerzug-Klassen sind daher Arbeiter, die eine konkrete Aufgabe lösen. Die Klasse ComputerStrategie
hingegen ist eine Vermittler-Klasse, die andere Klassen konfiguriert und dann aufruft und deren Ergebnisse ihrem Aufrufer liefert. Eine solche Unterteilung von Objekten macht sehr viel Sinn und man findet sie in der Praxis häufig.
Sie können reine Vermittler-Klassen verwenden, sobald Sie die Ergebnisse von mehreren Arbeiter-Klassen benutzen wollen. In komplexen Programmen gibt es oftmals wesentlich mehr Vermittler-Klassen als Arbeiterklassen, auch wenn der Aufbau der Vermittler-Klassen selbst sehr einfach erscheint. Die »beste« Aufteilung der Aufgaben in Klassen ist manchmal offensichtlich, oft schwierig und gelegentlich eine Kunstform für sich eine bestimmtes Programm mag perfekt funktionieren, aber plötzlich kann es nicht mehr verändert oder erweitert werden oder es ist unerklärlich langsam und ein Umschreiben für bessere Performance extrem aufwändig. Deswegen empfehle ich Ihnen, gerade diese Arten von Aufteilung mit anderen Entwicklern zu besprechen.
Die besten Programmierer, die ich kenne, hinterfragen und besprechen ihre Entscheidungen!
In diesem Kapitel haben Sie gesehen, wie Sie zwei verschiedene Bildschirme miteinander verknüpfen können und wie im normalen Programmfluss Informationen ausgetauscht werden. Diese Form haben Sie als Delegat kennengelernt. Im früheren Kapitel 13 haben Sie bereits Datenquellen kennengelernt. Dies ist eine Form des Datenaustauschs, der dem Delegaten ähnelt. Sie haben ebenfalls gesehen, wie Sie mit objektorientiertem Polymorphismus verschiedene Strategien implementieren können.
Diese Begriffe und das Zusammenspiel der verschiedenen Klassen sind nicht völlig zufällig entstanden, sondern sie basieren auf jahrelanger Erfahrung mit diesen und ähnlichen Situationen. Einige bestimmte Vorgehensweisen haben sich in vielen Projekten immer wieder bewährt. Genauso wie ein Architekt gewisse Prinzipien für alle seine Häuser wiederverwenden kann, so kann auch ein Programmierer bewährte Vorgehensweisen wiederverwenden. Diese Vorgehensweisen werden daher auch Entwurfsmuster genannt. In vorhergehenden Kapiteln habe ich gelegentlich diese Entwurfsmuster erwähnt, wenn Sie einem davon begegnet sind.
Eine Reihe von bewährten und erfolgreichen Entwurfsmustern ist in einem Buch von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides namens Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software, ISBN 3-8273-2199-9 (Originaltitel im Englischen Design Patterns. Elements of Reusable Object-Oriented Software) zusammengefasst. Dort finden Sie viele Muster, die an verschiedenen Stellen von iOS und MacOS X verwandt werden. Es ist eine empfehlenswerte Literatur für fortgeschrittene Programmierer, die bereits die Entwicklung einfacher Programme und Systeme beherrschen.
Ein weiteres Entwurfsmuster, mit dem Sie das Datenmodell weiter von der Steuerung entkoppeln und separat testen können, stellt das MVVM bzw. MVP-Entwurfsmuster dar. Beispiele und weiterführende Links finden Sie beispielsweise auf der Wikipedia-Seite https://de.wikipedia.org/wiki/Model_View_ViewModel.
enabled
-Eigenschaft.Ergänzen Sie die Streichholz-App so, dass nur jeweils die Buttons aktiv sind, die auch wirklich gewählt werden können!
Hinweis: Diese Aufgabe ist schwierig, weil Sie selbstständig bestimmte Methoden recherchieren müssen.
Wie können Sie das Streichholzspiel so abändern, dass es grammatisch korrekte Angaben zur Zahl der Hölzer macht?
neuesSpiel
und spielerZug
in eine eigene Klasse auslagern? Was wäre der Vorteil und was der Nachteil?