Kapitel 15 Eine umfangreiche App: Das Streichholzspiel auf dem iPhone

Inhalt

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.

15.1 Das Datenmodell

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.

15.1.1. Das Modell für den Haufen

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.


Tipp
Verwenden Sie Variablen oder Konstanten, um feste Werte zu speichern. Versuchen Sie, so weit es geht, magische Zahlen im Programm zu vermeiden. Darunter versteht man Zahlen oder Texte im Programmtext, deren Wert nicht näher erläutert wird und deren Bedeutung daher missverständlich sein kann.
Daher bevorzuge ich eine Eigenschaft 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.

15.1.2. Die bisherige Strategie

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.

15.2 Die grafische Oberfläche

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.

15.2.1. Der Spielbildschirm

IMG

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:

1. Zentrieren Sie den mittleren Button mit der Beschriftung »Nimm 2« horizontal auf dem Bildschirm.
2. Geben Sie dem Button mit der Beschriftung »Nimm 1« den Abstand 30 Pixel nach rechts (dies ist der Button mit der Beschriftung »Nimm 2«).
3. Geben Sie dem Button mit der Beschriftung »Nimm 3« den Abstand 30 Pixel nach links (dies ist wieder der Button mit der Beschriftung »Nimm 2«).
4. Geben Sie allen drei Buttons einen Abstand von 30 Pixel zum Element darüber, das ist in diesem Fall das Label für den Computerzug.

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.

IMG

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.

15.2.2. Der Konfigurationsbildschirm

Für den Konfigurationsbildschirm bauen Sie nun selber einen Übergang in der Datei Main.storyboard:

1. Sie benötigen einen View Controller, also eine Unterklasse von 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.
2. Im Dialog wählen Sie aus der Sektion IOS in der Gruppe SOURCE den Eintrag COCOA TOUCH CLASS aus. Klicken Sie auf NEXT.
3. Als Klassennamen geben Sie im Textfeld für CLASS den Namen »EinstellungenViewController« ein. Es ist nicht zwingend erforderlich, dass Sie die Klasse wirklich »IrgendeinViewController« nennen, aber es ist üblich und man weiß auf einen Blick, worum es sich handelt.

Unter SUBCLASS OF geben Sie nun den Namen »UIViewController« ein.

4. Stellen Sie sicher, dass die Checkbox ALSO CREATE XIB FILE nicht ausgewählt ist und für LANGUAGE der Eintrag SWIFT lautet. Drücken Sie dann den NEXT Button.
5. Speichern Sie die Datei durch Klick auf CREATE.
6. Nun gehen Sie wieder in den grafischen Editor der Datei 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!

7. Nun wählen Sie den neuen Bildschirm aus und klicken dann auf den dritten Reiter in der rechten Spalte. Unter CUSTOM CLASS können Sie nun als CLASS Ihre eigene Klasse »EinstellungenViewController« eingeben (in der Regeln wird Xcode den Namen sofort automatisch vervollständigen, sobald Sie die ersten Zeichen eingegeben haben). Abbildung 15.3 zeigt diesen Schritt.

IMG

Abbildung 15.3 Einstellung der Klasse auf »EinstellungenViewController« auf dem zweiten Bildschirm

8. Der Übergang soll ausgelöst werden, wenn der Benutzer den »i«-Button in der rechten unteren Ecke des ersten Bildschirms antippt. Dies geht in Xcode sehr leicht: Halten Sie die IMG-Taste gedrückt und klicken Sie auf den Button. Halten Sie die Maustaste gedrückt und ziehen Sie die blaue Linie auf den zweiten Bildschirm. Sobald dieser auch blau erscheint, lassen Sie die Maustaste los.
9. Im Popup wählen Sie als ACTION SEGUE den Eintrag PRESENT MODALLY aus.

Dadurch haben Sie einen Übergang vom ersten zum zweiten Bildschirm erstellt, sobald der Benutzer den Button antippt.

10. Konfigurieren Sie den Übergang, indem Sie auf das Symbol des Übergangs klicken und in der rechten Spalte den vierten Reiter auswählen. Nun geben Sie als IDENTIFIER den Namen »zuEinstellungen« ein und als TRANSITION empfehle ich die Auswahl FLIP HORIZONTAL. Dies erzeugt eine nette Animation beim Übergang.

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:

1. Platzieren Sie einen Button in der linken oberen Ecke und beschriften Sie ihn mit »Zurück«.

Geben Sie dem Button aber keinen Übergang zum ersten Bildschirm, das funktioniert leider nicht ganz so einfach!

2. Geben Sie dem Button Abstände von 20 Pixel zum oberen und 10 Pixel zum linken Bildschirmrand.
3. Fügen Sie darunter ein Label ein und beschriften Sie es mit »Anfangszahl der Hölzer:«. Geben Sie dem Label einen Abstand von 20 Pixel zum Button darüber und 10 Pixel zum linken und rechten Bildschirmrand. Ich empfehle Ihnen, die Ausrichtung dex Textes linksbündig zu belassen.
4. Fügen Sie einen Slider unter dem Label ein und setzen Sie den Minimalwert des Sliders auf 12, den Maximalwert auf 24 und den Anfangswert auf 18. Zur Ausrichtung geben Sie dem Slider einen Abstand von jeweils 20 Pixel nach oben, links und rechts.
5. Darunter kommt ein weiteres Label, diesmal mit der Beschriftung »Spielstrategie«. Als Abstände empfehle ich jeweils 10 Pixel nach links und rechts und 30 Pixel nach oben.
6. Unterhalb diese Labels kommt ein Segmented Control, mit dem der Benutzer die Spielstrategie auswählt.

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.

7. Als Ausrichtungsregeln für das Segmented Control empfehle ich einen Abstand nach oben von 20 und nach links von 10 Pixel.

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!

IMG

Abbildung 15.4 Die Konfiguration der »Streichholz«-App auf dem zweiten Bildschirm

15.3 Die Steuerung

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.

15.3.1. Die erste lauffähige Version

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:

1. Beim Laden des Storyboards kümmert sich iOS selbstständig darum, die entsprechenden Klassen der View Controller zu erzeugen. Dies war bisher noch nicht wichtig, ist jetzt aber notwendig, denn dadurch können Sie das Datenmodell an der richtigen Stelle erzeugen.

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.

2. Bevor der Bildschirm angezeigt wird, wird die Methode 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.

15.3.2. Gehe zurück auf Los, ziehe Hölzer ein

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:

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!


Hintergrund
Alertviews und Buttons
Diese Art und Weise, Alertviews mit einem 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.
Das 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 moderne Methode von Swift beruht auf der funktionalen Programmierung und erlaubt es Ihnen, die Reaktion auf das Antippen eines Buttons direkt an der Stelle anzugeben, wo Sie auch das Alertview erzeugen. Dadurch werden komplizierte Programme deutlich einfacher und übersichtlicher.

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.

15.4 Gewinnstrategien für Ihren Computer

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.

15.4.1. Strategien mit Polymorphismus

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.

15.4.2. Eine wirklich gute Strategie

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!


Hintergrund
Rückwärtsinduktion
Diese Art und Weise, die »perfekte« Spielstrategie zu bestimmen, funktioniert für viele Spiele und wirtschaftliche Situationen. Sie wurde zum ersten Mal im Jahre 1944 von John von Neumann und Oskar Morgenstern formuliert und ist unter dem Namen Rückwärtsinduktion, auf Englisch »Backward induction« bekannt.
Wenn Sie mehr darüber wissen wollen, empfehle ich Ihnen, die Themen »Rückwärtsinduktion« und »Spieltheorie« zu recherchieren, zum Beispiel mit dem Buch Erwin Amann, Spieltheorie für Dummies. Wiley-VCH Verlag, 2011, ISBN-13 978-3527706372.

Somit lautet die kluge Spielstrategie des Streichholzspiels:

1. Finde die nächste verbotene Zahl, die kleiner oder gleich der aktuellen Zahl der Streichhölzer ist.
2. Bestimme die Differenz.
3. Ist die Differenz größer null, so nimm diese Zahl an Hölzchen weg.
4. Ansonsten nimm genau ein Hölzchen weg.

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

IMG

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:

IMG

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!

15.4.3. Strategien auswählen

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:

• 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.

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!


Tipp
Die Entscheidung, ob man für bestimmte Aufgaben eine eigene Klasse braucht oder nicht, ist nicht einfach.
Wenn Sie sich nicht sicher sind, empfehle ich Ihnen, diese Entscheidungen mit anderen Entwicklern zu besprechen. Diese haben unter Umständen einen anderen Blickwinkel und können einen neutralen Blick »von außen« auf eine Situation werfen.

/// 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!

15.5 Der Konfigurationsbildschirm

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:

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!

IMG

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:


Hintergrund
Der Delegat Das Prinzip des »Delegaten« findet sich in verschiedenen Stellen von iOS. Darunter versteht man generell eine Klasse, die ein bestimmtes Protokoll implementiert, aber sonst keine Einschränkungen besitzt.
Delegaten sind in den meisten Fällen Steuerungsklassen, die für grafische Elemente oder für andere Steuerungsklassen bestimmte Methoden implementieren, die bei bestimmten Ereignissen (englischsprachig auch als Events bezeichnet) aufgerufen werden.
Die Methoden des Delegaten bezeichnet man auch als Rückruffunktionen, im Englischen Callbacks. Grundsätzlich stellen diese verschiedenen Programmteilen die Möglichkeit zur Verfügung, sich »Bescheid sagen zu lassen, wenn etwas passiert«.
Sie haben eine verwandte Art von Methoden bereits bei der Tabelle kennengelernt  hier war die Steuerung, die als »Datenquelle« fungiert hat, ebenfalls eine bestimmte Form eines Delegaten.

Beim Ändern der Einstellungen passiert nun Folgendes, siehe Abbildung 15.6:

IMG

Abbildung 15.6 Ablauf beim Ändern von Einstellungen auf dem zweiten Bildschirm

Damit das alles funktioniert, müssen Sie folgende Schritte ausführen:

1. Verknüpfen Sie das oberste Label des zweiten Bildschirms mit einer Eigenschaft hoelzerZahlAnzeige im EinstellungenViewController.
2. Verknüpfen Sie den Slider des zweiten Bildschirms mit einer Eigenschaft anfangsZahlAuswahlSlider im EinstellungenViewController.
3. Verknüpfen Sie das Segmented Control des zweiten Bildschirms mit einer Eigenschaft strategieAuswahlControl im EinstellungenViewController.
4. Fügen Sie eine Aktion benutzerBewegtSlider zu EinstellungenViewController hinzu, die aufgerufen wird, sobald der Slider bewegt wird.
5. Ändern Sie die Datei 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.
6. Fügen Sie in der Klasse 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.

15.6 Fortgeschrittenes

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.

15.6.1. Arbeiter und Vermittler

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!

15.6.2. Entwurfsmuster

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.

15.7 Aufgaben

1. Die Buttons, die Sie bisher verwenden, haben einen entscheidenden Nachteil: Sie können jederzeit zwei oder drei Hölzer entfernen. Und zwar selbst dann, wenn nur ein einziges übrig geblieben ist. Diesen Nachteil sollten Sie beseitigen, indem Sie die Buttons, die der Anwender gerade nicht betätigen kann, deaktivieren. Wie das geht, haben Sie bereits in Abschnitt 12.3.4 erfahren: mit der enabled-Eigenschaft.

Ergänzen Sie die Streichholz-App so, dass nur jeweils die Buttons aktiv sind, die auch wirklich gewählt werden können!

2. Gibt es eine Alternative, um nicht drei verschiedene Methoden für die Buttons haben zu müssen? Könnten Sie mit nur einer Methode auskommen?

Hinweis: Diese Aufgabe ist schwierig, weil Sie selbstständig bestimmte Methoden recherchieren müssen.

3. Im Moment spricht das Streichholzspiel kein gutes Deutsch. Es sind »1 Hölzer« übrig und der Computer nimmt »1 Hölzer« bei einem Zug.

Wie können Sie das Streichholzspiel so abändern, dass es grammatisch korrekte Angaben zur Zahl der Hölzer macht?

4. In Abschnitt 15.4.3 hatte ich darüber gesprochen, wie und wann Sie die Klassen des Spiels refaktorieren können. Wäre es sinnvoll, dass Sie zusätzlich in Listing 15.11 die Methoden neuesSpiel und spielerZug in eine eigene Klasse auslagern? Was wäre der Vorteil und was der Nachteil?