Migration einer Legacy iOS-App

Avatar von Christopher Stock

Im letzten Sprint haben wir die mobile iOS-Applikation eines unserer Kunden von Swift 3 nach Swift 5 migriert und dabei die App auch für die aktuellste iOS-Plattform iOS 13 lauffähig gemacht. In diesem Artikel möchte ich Euch einen Einblick in den Ablauf dieser Migration geben und Euch alle in diesem Zuge durchgeführten Migrationsschritte kurz vorstellen.

Warum musste migriert werden?

Es war zwangsläufig erforderlich, unsere iOS-App auf eine moderne Swift-Version anzuheben, da Xcode ab der Version 10.2 (Januar 2019) das Kompilieren von in Swift 3 geschriebenen Quellcodes und Bibliotheken gänzlich nicht mehr unterstützt. Beim Bauen des Projekts mit dieser Version von Xcode wurden somit zahlreiche Stellen im Quellcode mit Compilerfehlern bemängelt und der Build dadurch als fehlgeschlagen quittiert.

Im Sinne unseres Kunden wollten wir die Zukunftssicherheit der mobilen iOS-App sicherstellen und sie auch für kommende Versionen von iOS erhalten. Daher haben wir uns mit allen Details der Migration von Swift 3 auf Swift 5 auseinandergesetzt, intern detailliert dokumentiert und erfolgreich abgeschlossen.

Progressive Migration

Von der Migration waren zwei Codebstandteile betroffen: Eine umfangreiche Codebasis eigens erstellter Swift-Dateien und die Einbindung diverser externer Bibliotheken.

Die Migration erfolgte progressiv – so wurden in den folgenden drei Etappen jeweils erst alle externen Pods und anschließend der eigene Quellcode auf die jeweils nächsthöhere Compilerversion umgestellt:

  • Im ersten Schritt von Swift 3 nach Swift 4.
  • Im zweiten Schritt von Swift 4 nach Swift 4.2.
  • Im dritten und letzten Schritt von Swift 4.2 nach Swift 5.

Update aller Pods

Das iOS-Projekt inkludiert insgesamt 20 Pods, die im Rahmen der Migration mit Hilfe des Paketmanagers CocoaPods und der gleichnamigen zentralen Anlaufstelle cocoapods.org aktualisiert wurden. Für jeden einzelnen Pod können hier alle bisher veröffentlichten Versionen eingesehen und deren designierte Swift-Compilerversion in Erfahrung gebracht werden.

Da zu Beginn der Migration jeder Pod auf die Compilerversion Swift 3 abgestimmt war, musste für jeden Pod eine neuere Version für die jeweils nächsthöhere Compilerversion ermittelt und eingesetzt werden. In Xcode wurde anschließend für jeden eingebundenen Pod explizit die neue Compilerversion eingestellt. Das erfolgt über die Entität Pods bei den Build Settings des jeweiligen Pods:

Update des eigenen Quellcodes

Analog zu den Pods muss auch für die eigene Codebasis immer eine bestimmte Compilerversion angegeben werden. Diese Einstellung erfolgt über das Register Build Settings des eigenen Projekttargets in Xcode:

Beseitigen aller Compilerfehler

Da der eigene und der fremde Quellcode der iOS-Anwendung zu Beginn der Migration komplett in Swift 3 verfasst war, meldete der Compiler beim Umstellen auf eine höhere Compilerversion zahlreiche Fehler. Somit konnte die Anwendung zunächst nicht mehr erfolgreich bauen. Diese Compilerfehler konnten in die folgenden Kategorien eingeteilt und strukturiert gelöst werden:

Beim Zugriff auf die Pods

Durch die Aktualisierungen der Pods kam es zu geringfügigen Breaking Changes beim Zugriff auf die von den Pods bereitgestellte API aus unserem Quellcode heraus. Diese Compilerfehler traten lediglich vereinzelt auf und konnten mittels kurzer Recherchen nach dem spezifischen Anpassungsproblem des entsprechenden Pods schnell behoben werden.

Beim Zugriff auf veraltete Swift API

Zahlreicher waren die Compilerfehler durch die Anhebungen der Compilerversionen für unseren eigenen Projektcode. Dies betraf viele Zugriffe auf das iOS-Framework, die in den Compilerversionen Swift 4, Swift 4.2 und Swift 5 diverse Umbenamungen, Gruppierungen und typisiertere Restrukturierungen erfahren haben. Einige Beispiele hierfür sind:

Swift 3 Swift 5
NSFontAttributeName NSAttributedString.Key.font
UIKeyboardFrameEndUserInfoKey UIResponder.keyboardFrameEndUserInfoKey
UITableViewAutomaticDimension UITableView.automaticDimension

Bei Verwendung von Objective-C-Selektoren

Ein großes Aufkommen von Compilerfehlern stellte die Verwendung von Objective-C-Selektoren dar. Alle mittels dieser Selektoren aufgerufenen Funktionen müssen ab Swift 4 explizit mit einem @objc-Präfix annotiert werden, damit der Compiler sie als Ziel des Selektors akzeptiert.

 
    let tapRecognizer = UITapGestureRecognizer(target: self, action: 
    #selector(self.onHomeButtonTapped(_:)))

    @objc func onHomeButtonTapped(_ sender: UITapGestureRecognizer) {
        ...
    }
 

Beim Zugriff auf statische Variablen

Letztlich wurden Compilerfehler beim Zugriff auf statische Variablen gemeldet, deren Behebung sich als simpel gestaltete. Der aktuelle Swift-Compiler 5 erfordert die explizite Angabe des Klassennamens in der die statische Variable ansiedelt, bevor man sie ansprechen kann:

 
    import RealmSwift

    class DatabaseHelper {

        static let realmBaseVersion: UInt64 = 1

        /// Retrives and returns the current version of the Realm database schema.
        /// 
        /// - Returns: The current Realm db version. Defaults to 1.
        lazy var currentVersion: UInt64 = {
            guard let value = Bundle.main.object(forInfoDictionaryKey: "RealmCurrentVersion") as? UInt64 else {
                return DatabaseHelper.realmBaseVersion
            }
            return value
        }()
 

Migration auf iOS 13

Zeitgleich mit unserem Migrations-Sprint erschien die neueste iOS-Version iOS 13. Hier mussten wir feststellen , dass unsere Anwendung zur Laufzeit an einer bestimmten Stelle konsequent abstürzte. Die Ursache war der Aufruf einer Systemfunktion, die unter iOS 13 nicht mehr unterstützt wurde.

Konkret betraf das die Einfärbung der Statusleiste oberhalb der als Notch bezeichneten Bildschirmaussparung. Hierfür bietet Apple ab der Version iOS 13 standardmäßig keinen Zugriff mehr. Das Problem konnte durch den Einsatz einer alternativer Systemmethode, die ab iOS 13 zur Verfügung steht, schnell behoben werden. Die konstante Umstellung auf diese neue Systemmethode führte aber im Gegenzug dazu, dass die App nicht mehr auf älteren iOS-Plattformen betrieben werden konnte.

Zur Realisierung dieser Anforderung bietet Swift Konstrukte an, um auf die iOS-Version des Zielgeräts einzugehen. Mit dieser Technik kann ein bestimmter Codeblock definiert werden, der zur Laufzeit bedingt ausgeführt wird und somit nur für iOS-Versionen vor iOS 13 durchlaufen oder analog dazu erst ab der iOS-Version iOS 13 ausgeführt werden:

 
    if #available(iOS 13.0, *) {

        // being performed for devices from iOS 13.0 on

        let app = UIApplication.shared
        let statusBarHeight: CGFloat = app.statusBarFrame.size.height

        let statusbarView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: statusBarHeight))
        statusbarView.backgroundColor = UIColor.red
        view.addSubview(statusbarView)
    } else {

        // being performed for devices before iOS 13.0

        let statusBar = UIApplication.shared.value(forKeyPath: "statusBarWindow.statusBar") as? UIView
        statusBar?.backgroundColor = UIColor.red
    }
 

Hilfswerkzeuge beim Migrieren

Apples integrierte Entwicklungsumgebung Xcode hat sich im Zuge der Migration als ein hervorragendes Hilfswerkzeug herausgestellt. Dabei haben sich die folgenden integrierten Tools als besonders hilfreich erwiesen:

Xcode Autofix

Viele Compilerfehler in unserer eigenen Codebasis konnten mit sehr wenig Aufwand gefixt werden, da Xcode hierfür die Anwendung einer Autofix-Maßnahme anbot. Damit konnte die erforderliche Codeänderung mit einem Klick umgesetzt werden.

Im Zuge automatischer Konvertierungen von Swift 4 auf Swift 4.2 wurden vom Xcode-Konvertierungstool sogar an vereinzelten Stellen automatisch Helper-Funktionen implementiert, die mittels einer Hashmap die Überführung von String-Werten in die entsprechenden typisierten Werte übernahmen.

Xcode Debugger

Eine großartige Hilfe bei der Umstellung auf Swift 5 war auch der Debugger, mit dem sich der Ablauf und die Funktionsweise spezieller Codeteile genau untersuchen lassen. Hiermit konnte der korrekte Ablauf eines Legacy-Workarounds sichergestellt werden, der durch das Ersetzen einer Objective-C Initializer-Funktionen geschaffen wurde und darüberhinaus die Ursache eines notorischen Fehlverhaltens zur Laufzeit war.

XCTest Framework

Da vereinzelte Codestellen manuelle Anpassungen erforderten, war es erforderlich, sie vor unabsehbaren ungewünschten Verhalten abzusichern. Hierfür bot es sich an, diese betroffenen Codestellen im Vorfeld der Änderung mit Swift Unit Test Cases aus dem XCTest Framework abzudecken. Somit konnte sichergestellt werden, dass alle Codestellen nach deren Anpassung noch die gleichen gewünschten Resultate lieferten und die Umstellung fehlerfrei durchgeführt wurde.

Fazit

Da es sich bei iOS um eine sehr gut konzipiertes und durchdachtes Ökosystem handelt, konnte die Migration unserer Anwendung von Swift 3 auf Swift 5 sehr strukturiert und stringent durchgeführt werden. Auch die Einbindung externer Bibiotheken und deren schrittweise Aktualisierungen mit Hilfe des Paketmanagers CocoaPods funktioniert stabil und zuverlässig.

Xcode und die dort integrierten Werkzeuge waren dabei eine sehr große Hilfe. So konnte mit einer geringen Anzahl an manuellen Änderungen und mit einem hohen Maß an Pragmatismus die Migration abgeschlossen werden.

Daher braucht man keine Scheu davor haben, ältere iOS-Apps auf den aktuellsten Stand der iOS-Technik zu migrieren.

Über Feedback oder Rückfragen zum Thema Swift und die Migration von iOS-Applikationen können Sie mich sehr gerne via christopher.stock@mayflower.de erreichen.

Avatar von Christopher Stock

Kommentare

Schreibe einen Kommentar

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


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.