Vorschau auf Swift 4

Seit die Entwicklung von Swift öffentlich erfolgt und Entwicklungs-Snapshots und der Quellcode jederzeit verfügbar sind, sind geplante und bereits implementierte neue Features von Swift kein Geheimnis mehr. Aber für die meisten Entwickler interessieren sich erst dann für die neuen Features, wenn diese auch in Xcode zur Verfügung stehen — also ab der WWDC. Parallel zu meiner Überarbeitung meines Swift-Buchs werden ich in diesem Beitrag über die wichtigsten Neuerungen berichten. Dieser Artikel ist also work in progress. Ich habe vor, den Text bis Anfang September immer wieder zu ergänzen. (Erste Version 5.7.2017, letztes Update 14.7.2017. Der Artikel berücksichtigt auch in Swift 3.1 hinzugefügte Neuerungen.)

Offene Bereiche (Ranges)

Bereiche (Ranges) können jetzt ohne explizite Ober- bzw. Untergrenze formuliert werden (SE 0172). Dabei gibt es drei Syntaxformen:

let ar = Array(1...10)
ar[3...]   // [4, 5, ..., 9, 10]
ar[...3]   // [1, 2, 3, 4]
ar[..<3]   // [1, 2, 3]

Beachten Sie, dass 0 wie in den meisten anderen Programmiersprachen das erste Element bezeichnet, 1 das zweite usw.

Die neue Range-Syntax ist auch in switch-Konstruktionen erlaubt:

let n = 5
switch n {
case ...5:
  print("Bis 5 (inklusive)")
case 6...:
  print("Ab 6 (inklusive)")
default:
  print("Sonst etwas (nicht möglich, aber syntaktisch erforderlich")
}

Zeichenketten (Strings)

Zeichenketten sind wieder Collections. Genau genommen implementiert String das Protokoll StringType, und dieses wiederum BidirectionalCollection und RangeReplaceableCollection.

Deswegen kann eine Schleife über alle Zeichen einer Zeichenkette jetzt ohne die bisher erforderliche characters-Eigenschaft gebildet werden:

let s = "Hello World!"
for c in s {  // Schleife über alle Zeichen
  print(c)
}

Auch andere Operationen lassen sich mit deutlich kompakteren Code durchführen:

print(s.count)  // Ausgabe 12
let r = String(s.reversed())
print(r)       // Ausgabe "!dlroW olleH"

Leider hat man sich nicht dazu durchringen können, den Zugriff auf Teilzeichenketten wie bei Arrays durch Integer-Subscripts zu erlauben. Die Schreibweisen s[3] (viertes Zeichen) oder s[0..<3] bzw. s[..<3] (jeweils die ersten drei Zeichen) sind also NICHT erlaubt.

Mehrzeilige Zeichenketten

Endlich kommt Swift mit mehrzeiligen Zeichenketten zurecht (SE 0168). Diese beginnen mit """ und enden mit """. Der Whitespace vor der zweiten Dreierkombination muss exakt so in allen vorangegangenen Zeilen angegeben werden und wird aus der endgültigen Zeichenkette entfernt.

Innerhalb des Texts muss als einziges Zeichen der Backslash verdoppelt werden, um ihn in die Zeichenkette einzufügen. Die Swift-typische Syntax \(ausdruck) ist auch in mehrzeiligen Zeichenketten erlaubt.

let n = 3
let s = """
  abc\\
  n = \(n)
  def "xy"
  """
print(s)
// Ausgabe: abc\
//          n = 3
//          efg "xy"
Substring-Klasse

Für Teilzeichenketten gibt es die neue Substring-Klasse (SE 0163). Einen Substring erhalten Sie beispielsweise, wenn Sie eine Teilzeichenkette in der Form s[startindex...endindex] ermitteln.

let s = "Lorem ipsum dolor. Sit amet."
let start = s.startIndex
if let end = s.index(of: ".") {
  let sentence1 = s[start...end]  // Datentyp Substring
  print(sentence1)                // Ausgabe "Lorem ipsum dolor."

  // alternativ ohne 'start'-Variable
  let sentence1 = s[...end]       // Datentyp Substring
}

Ein Substring teilt viele gemeinsame Eigenschaften mit der String-Klasse. Der entscheidende Unterschied besteht darin, dass ein Substring die Zeichenkette nicht selbst speichert, sondern auf einen Ausschnitt der zugrundeliegenden String-Instanz verweist. Das ist effizient und spart Speicherplatz. Allerdings kann ein winziger Substring dazu führen, dass ein riesiges String-Objekt nicht aus dem Speicher entfernt werden kann. Wenn ein Substring dauerhaft benötigt wird, sollte er vorher in ein neues String-Objekt umgewandelt werden. In manchen Fällen erkennt Xcode das Problem und zeigt eine entsprechende Warnung an.

Collections, Arrays, Dictionaries

swapAt

Zwei Elemente einer MutableCollection können nun mit swapAt vertauscht werden (SE 0173). Das trifft z.B. auf Arrays zu:

var ar = [7, 12, 25, 49]
ar.swapAt(1, 3)
ar  // [7, 49, 25, 12]
Sets und Dictionaries

Eine Menge sinnvoller Erweiterungen hat es bei Sets und Dictionaries gegeben (SE 0165). Beispielsweise lassen sich Dictionaries nun einfacher aus zwei Arrays mit Keys und Values initialisieren:

let deutsch = ["eins", "zwei", "drei"]
let english = ["one", "two", "three"]
let mydict = Dictionary(uniqueKeysWithValues: zip(deutsch, english))
mydict["zwei"]   // two

Auf Dictionaries kann nun die filter-Methode angewendet werden:

let otherDict = [1: "eins", 2: "zwei", 3: "drei", 4: "vier"]
let filteredDict = otherDict.filter {$0.key % 2 == 0}
filteredDict   // [2: "zwei", 4: "vier"]

Beim Zugriff auf Dictionary-Elemente kann nun ein Default-Value angegeben werden. Dieser Wert wird verwendet, wenn es den Key (noch) nicht gibt:

otherDict[1, default: "unbekannt"]  // "eins"
otherDict[5, default: "unbekannt"]  // "unbekannt"

Das vereinfacht viele Algorithmen, z.B. den folgenden, der die Anzahl gleicher Buchstaben zählt (Idee aus SE 0165):

let s = "Lorem ipsum"
var charcount: [Character: Int] = [:]
for c in s {
  charcount[c, default: 0] += 1
}
print(charcount)
// Ausgabe  ["i": 1, "r": 1, "m": 2, "p": 1, "L": 1, 
//           "o": 1, "e": 1, " ": 1, "s": 1, "u": 1]
prefix und drop für Sequences

Sequences können nun mit prefix(while:) bzw. drop(while:) verarbeitet werden. prefix testet die ersten Elemente der Sequenz und liefert alle zurück, bis die while-Bedingung zum ersten Mal nicht zutrifft. Der Rest der Sequenz wird nicht mehr bearbeitet.

drop agiert umgekehrt und verwirft solange Elemente vom Beginn der Sequenz, bis die while-Bedingung zum ersten Mal nicht mehr erfüllt ist. Der Rest der Sequenz wird zurückgegeben.

let lst = [1, 2, 3, 4, 1, 2, 3, 4]
let result1 = lst.prefix(while: {$0 < 3})
result1  // [1, 2]

let result2 = lst.drop(while: {$0 < 3})
result2  // [3, 4, 1, 2, 3, 4]

prefix und drop stehen bereits seit Swift 3.1 zur Verfügung (SE 0045).

Element im Sequences-Protokoll

Das Protokoll Sequence kennt den neuen generischen Typ Element, das wie folgt definiert ist:

associatedtype Element where Self.Element == Self.Iterator.Element

Es kann also als Kurzschreibweise anstelle von Iterator.Element verwendet werden. Das macht den Code übersichtlicher, führt aber zu Inkompatibilitäten, wenn Sie in Swift-3-Code in einem von Sequence abgeleiteten Typ eine eigene Element-Eigenschaft definiert haben.

Equatable und Hashable

In Swift 4 erfüllen alle elementaren Datentypen die Protokolle Equatable und Hashable. Das gilt nicht nur für Int, String etc., sondern auch für aus der Foundation für Swift adaptierte Strukturen wie Date oder URL (SR 2388).

private versus fileprivate

Das Hin und Her bei der Kennzeichnung von Zugriffsebenen hat in Swift 3 für viel Konfusion gesorgt und wurde in Hunderten, wenn nicht in Tausenden Beiträgen in der Swift-Evolution-Mailing-List diskutiert. Viele Entwickler wollten zurück zum Swift-2-Modell, der entsprechende Vorschlag SE 0159 wurde aber abgelehnt.

Für Swift 4 hat man sich letztlich zu einer winzigen Verbesserung durchringen können (SE 0169): Kurz zusammengefasst ist private nun weniger restriktiv und erlaubt den Zugriff auf Elemente eines Typs auch in Erweiterungen, sofern diese in der gleichen Datei formuliert sind. Damit ist das ungeliebte Schlüsselwort fileprivate viel seltener als bisher erforderlich.

// Code-Beispiel aus SE 0169; der gesamte Code muss sich
// in einer Datei befinden
struct S {
    private var p: Int

    func f() { 
        use(g())    // ok, g() is accessible within S
    }
}

extension S {
    private func g() {
        use(p)      // ok, g() has access to p, since it is in an extension on S.
    }
}

extension S {
    func h() {
        use(g())    // ok, h() has access to g() since it defined in the access control scope for S.
    }
}

Eine Kurzzusammenfassung der fünf Schlüsselwörter für Zugriffsebenen sieht damit in Swift 4 so aus:

open         // Zugriff und Vererbung in anderen Modulen
public       // Zugriff auch in anderen Modulen, 
             // aber Vererbung nur im eigenen Modul
internal     // Zugriff und Vererbung nur im eigenen Modul
fileprivate  // Zugriff und Vererbung nur in der aktuellen Datei
private      // Zugriff nur in der aktuellen Klasse sowie in
             // Erweiterungen in der gleichen Datei

KeyPath-Ausdrücke

In aktuellen Swift-Versionen können Sie unkompliziert Referenzen auf Funktionen übergeben:

// schon bisher: Referenzen auf Funktionen
func f(n: Int) -> Int {
  return 2*n
}

let funcref = f
print(funcref(3))  // Ausgabe 6

Neu in Swift 4 ist die Möglichkeit, auch Referenzen auf Eigenschaften zu formulieren (SE 0161). Sie können also gewissermaßen den Pfad zu einer Eigenschaft ausdrücken, die Auswertung aber erst später durchführen. Wie das folgende Beispiel zeigt, müssen Keypath-Ausdrücke mit dem Zeichen \ beginnen. Um den Key Path auf eine konkrete Instanz anzuwenden und eine Eigenschaft auszulesen oder auch zu verändern verwenden Sie die Schreibweise obj[keyPath: kp].

class Rect {
  var x: Int
  var y: Int
  init(x: Int, y: Int) {
    self.x = x; self.y = y
  }
}

let r1 = Rect(x:2, y:3)
let r2 = Rect(x:7, y:2)
let pathToX = \Rect.x        // Datentyp ReferenceWritableKeyPath<Rect, Int>
print(r1[keyPath: pathToX])  // Ausgabe 2
print(r2[keyPath: pathToX])  // Ausgabe 7

(SE 0161) gibt einige weitere Beispiele, die etwas komplexer sind, gegenwärtig aber nicht funktionieren, weil Subscripts in KeyPath-Ausdrücken noch nicht implementiert sind (zuletzt getestet in Xcode 9, Beta 3).

Übrigens gab es schon in Swift 3 mit #keyValue einen Weg, durch Referenzen auf Eigenschaften zuzugreifen. Diese Möglichkeit war aber mit vielen Einschränkungen verbunden und eine Notlösung.

Protokolle / Generics / Extensions

  • In generischen Protokollen können Sie nun für den generischen Datentyp Bedingungen formulieren (Associated Type Constraints, SE 0142), z.B. so: associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element

  • Swift 3.1 erlaubt verschachtelte Typen mit generischen Parameter innerhalb von generischen Typen (Nested Generics, schon verfügbar seit Swift 3.1, SR 1446).

    struct OuterNonGeneric {
           struct InnerGeneric<T> {}
    }
    struct OuterGeneric<T> {
           struct InnerNonGeneric {}
           struct InnerGeneric<T> {}
    }
    extension OuterNonGeneric.InnerGeneric {}
    extension OuterGeneric.InnerNonGeneric {}
    extension OuterGeneric.InnerGeneric {}
    
  • Es ist nun möglich, einen Datentyp zu formulieren, der sowohl einer Klasse als auch diversen Protokollen entsprechen muss (SE 0156). Die offizielle Bezeichnung lautet Class and Subtype existentials. Die Syntax ist wie bei der Kombination mehrerer Protokolle, wobei am Anfang aber eine Klasse steht: Class & Protocol1 & Protocol2 ....
    protocol P {}       // Code-Beispiel aus SE 0156
    struct S {}
    class C {}
    class D : P {}
    class E : C, P {}
    let u: S & P        // Compiler error: S is not of class type
    let v: C & P = D()  // Compiler error: D is not a subtype of C
    let w: C & P = E()  // Compiles successfully
    
  • Der Datentyp von Subscripts (also obj[whatever]) kann jetzt generisch definiert werden (SE 0148).

  • Extensions können nun so formuliert werden, dass sie nur für bestimmte Datentypen gelten (schon verfügbar seit Swift 3.1, SR 1009).

    // Array erweitern, aber nur für Int-Arrays
    extension Array where Element == Int {
      func sum() -> Int {
        return self.reduce(0) { $0 + $1 }
      }
    }
    let data = [1, 2, 3, 4]
    data.sum()  // Ergebnis 10
    

Codable- und Decodable-Protokolle inkl. JSON-Unterstützung

Das (De-)Serialisieren eigener Daten war bisher recht umständlich. In Swift 4 stehen nun die neuen Protokolle Decodable und Encodable zur Verfügung (oder beide gemeinsam als typealias Codable). Für grundlegende Datentypen gibt es sogar eine Default-Implementierung, d.h., Sie müssen zur Implementierung keinen eigenen Code hinzufügen. Weitere Details: SE 0166 und SE 0167

Die folgenden Zeilen zeigen ein Anwendungsbeispiel für den JSON-Encoder und -Decoder:

struct Author : Codable {
  let name: String
}
struct Book : Codable {
  let title: String
  let isbn: String
  let authors: [Author]
}
let swiftbook = Book(title: "Raspberry Pi",
                     isbn: "978-3-8362-5859-3",
                     authors: [Author(name: "Kofler"),
                               Author(name: "Kühnast"),
                               Author(name: "Scherbeck")])
let encoder = JSONEncoder()
let jsondata = try encoder.encode(swiftbook)
if let jsonstr = String(data: jsondata, encoding: .utf8) {
  print(jsonstr)
  // Ausgabe:
  // {"title":"Raspberry Pi",
  //  "authors":[{"name":"Kofler"}, {"name":"Kühnast"},
  //             {"name":"Scherbeck"}],
  //  "isbn":"978-3-8362-5859-3"}
}
let decoder = JSONDecoder()
let bookcopy = try decoder.decode(Book.self, from: jsondata)
print(bookcopy.title)
print(bookcopy.isbn)

@objc

In früheren Versionen hat Swift häufig Klassen implizit mit @objc gekennzeichnet, um sie mit Objective-C-Code kompatibel zu machen. In neuen Swift-4-Projekten gibt es derartige Automatismen zwar noch immer (z.B. bei @IBOutlets und @IBActionss), aber die implizite @objc-Kennzeichnung ist viel seltener und muss nun bei Bedarf explizit durch das Voranstellen von @objc erfolgen (SE 0160). Das Ziel dieser Maßnahme ist es, unnötige @objc-Auszeichnungen zu verhindern und damit die Größe des Kompilats zu reduzieren. SE 0160 spricht je nach App von einem Einsparungspotential zwischen 6 und 8 Prozent.

Relativ kompliziert ist allerdings der Umgang mit Swift-3-Projekten: Wenn Sie ein Swift-3-Projekt auf Swift 4 portieren, empfiehlt der Migrator die Option Minimize @objc inference. Das gibt Ihnen die Möglichkeit, Ihren Code entsprechend der Debugging-Ausgaben anzupassen und Ihrem Code gegebenenfalls fehlende @objc-Attribute explizit hinzuzufügen. Dabei helfen Ihnen Warnmeldungen im Debugging-Fenster. Gleichzeitig bleibt aber der Modus Swift 3 @objc inference aktiv, und deswegen zeigt Xcode die folgende Warnung an:

The use of Swift 3 @objc inference in Swift 4 mode is deprecated. Please address deprecated @objc inference warnings, test your code with “Use of deprecated Swift 3 @objc inference” logging enabled, and disable Swift 3 @objc inference.

Diese Warnung werden Sie erst los, wenn Sie den Swift-3-Kompatibilitätsmodus in Xcode in den Build Settings abstellen (siehe auch stackoverflow). Sollten Sie irgendwo ein erforderliches @objc-Attribut vergessen haben, wird Ihr Programm jetzt allerdings einen Fehler auslösen.

In Projekten, die von Swift 3 auf Swift 4 portiert wurden, gilt angeblich ein @objc-Kompatibilitätsmodus. Es kann in den Build Settings des Targets deaktiviert werden.

Der Swift-4-Migrator fügt in manchen Fällen automatisch das @objc-Attribut hinzu, wenn er dies für notwendig erachtet. Das ist z.B. der Fall, wenn Sie #selector verwenden, um eine als action aufzurufende Methode anzugeben (GestureRecognizer etc.).

Keine parallelen Schreibzugriffe auf Variablen

In Swift 3 war es in seltenen Fällen möglich, dass eine Variable bzw. Eigenschaft (genau genommen deren Speicher) gleichzeitig von mehreren Code-Teilen verändert wurde. In Multi-Threading-Anwendungen kann das zu Konflikten führen. Die Fehlersuche ist dann extrem schwierig, weil sich das Problem schwer reproduzieren lässt. Swift 4 macht es besser und verbietet den parallelen Schreibzugriff auf Variablen (siehe SE 0176 sowie das Ownership Manifesto).

Wenn der Swift-4-Compiler Code entdeckt, durch den es zu einem parallelen Schreibzugriff kommen kann, löst er nun einen Fehler aus und Sie sind gezwungen, den Code zu ändern. Der einfachste Fall, bei dem das vorkommen kann, ist eine Swap-Operation. Aus diesem Grund gibt es für Collections die neue swapAt-Methode.

Aktuell kann ich hier leider kein Real World-Beispiel anbieten. Die Beispiele aus SE 0176 wirken eher konstruiert.

NSNumber-Bridging

Die Umwandlung von NSNumber-Zahlen in native Swift-Datentypen hat in der Vergangenheit viele Probleme verursacht. In Zukunft sollen es etwas weniger werden (SE 0170). Insbesondere wird ein Casting mit as? nur noch dann durchgeführt, wenn es dabei zu keinem Überlauf kommt:

let n = NSNumber(value: 1234)
let i = n as? Int8   // nil (lieferte in Swift 3 das Ergebnis -46)

Failable numeric initializers

Die Datentypen Int, Int8, etc., Float, Float80 und Double haben neue Init-Funktionen in der Form init?(exactly:) erhalten. Wenn eine Konversion möglich ist, liefern sie die gewünschte Zahl, sonst nil. Diese init-Funktion ist schon seit Swift 3.1 verfügbar (SE 0080).

let i = 1234
if let ui = UInt8(exactly: i) {
  print(i)
} else {
  print("UInt8 hat nil zurückgegeben.")
  // das ist bei diesem Beispiel der Fall
}

Swift Package Manager

Große Änderungen hat es im Swift Package Manager gegeben. Die wichtigsten Neuerungen sind in diesem Posting zusammengefasst: Swift 4 Package Manager Update

Swift 3.2

Als Hilfestellung bei der Portierung von Code gibt es Swift 3.2. In Swift 3.2 sind bereits ein Großteil der Neuerungen von Swift 4 verfügbar, lediglich einige Features, die zu Inkompatibilitäten im Vergleich zu Swift 3.1 führen, wurden ausgespart.

Vorhandener Swift-3-Code sollte also vollständig kompatibel zu Swift 3.2 sein. Dennoch können Sie bereits viele Neuerungen von Swift 4 ausprobieren. Swift 3.2 kann insofern ein Zwischenschritt bei der Portierung sein.

Die Portierung kann per Target erfolgen. Bei Projekten, die aus mehreren Targets bestehen, können Sie also 3.2- mit 4.0-Targets kombinieren.

Xcode 9

Xcode sieht grundsätzlich weitgehend unverändert aus. Hinter den Kulissen gibt es aber durchaus Änderungen. Die größte ist der Code-Editor, der intern komplett neu implementiert wurde (laut WWDC-Vorträgen übrigens in Swift). Der Editor soll dadurch viel schneller geworden sein, was mir aber nicht aufgefallen ist. Außerdem erhält er nun endlich Refactoring-Funktionen auch für Swift (aber nicht im Playground, nur in »richtigen« Projekten).

Beim Erstellen eines Playgrounds haben Sie nun die Wahl zwischen verschiedenen Typen (Templates): Blank, Game, Map und SingleView.

Der Simulator für iOS, tvOS etc. sieht hübscher aus als bisher. Es dürfen nun mehrere Instanzen gleichzeitig laufen, also z.B. ein Simulator für ein iPhone und ein zweiter für ein iPad.

Folder im Projekt-Navigator entsprechen nun standardmäßig echten Verzeichnissen innerhalb des Projekts.

In der aktuellen Xcode-Beta 3 ist der Beta-Zustand noch klar bemerkbar: Beispielsweise funktioniert das Einklappen von Klassen/Strukturen/Funktionen (Code Folding) noch nicht, die alt-Click-Links in die Dokumentation funktionieren oft nicht etc.

Migrationserfahrungen

Ich habe aktuell erst einige Beispielprogramme aus meinem Buch auf Swift 4 umgestellt:

  • Etliche Programme laufen ohne jede Änderung bzw. nur mit den wenigen, vom Migrator durchgeführten Änderungen.

  • Fallweise ist Handarbeit erforderlich, vor allem bei Änderungen in den APIs, die der Migrator nicht oder falsch berücksichtigt.

  • Bei der Drag&Drop-Programmierung gibt es offensichtlich Änderungen: NSFilenamesPboardType is unavailable in Swift: use NSPasteboard.writeObjects(_:) with file URLs. Der Migrator gibt ansonsten keine Hilfe. Damit muss ich mich erst beschäftigen.

  • Die größten Probleme bereiten Programme, die auf externe Bibliotheken zurückgreifen (z.B. gloss für JSON, SWXMLHash für XML). gloss ist jetzt eigentlich überflüssig, aber es ist natürlich mit Arbeit verbunden, den Code auf den neuen JSONDecoder umzubauen. SWXMLHash liegt erfreulicherweise bereits in einer Swift-4-kompatiblen Version vor, verhält sich aber in etlichen Details anders als bisher. Die Schreibweise xml["root"]["catalog"]["book"][1]["author"].element?.text funktioniert so nicht mehr.

Quellen

Apple

Sonstige

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.