Engineering-Praxis

iOS-Schriften im Cloud-Mac-Build zuverlässig prüfen

iOS-Schriften im Cloud-Mac-Build zuverlässig prüfen

Im Entwurf verwendet die Überschrift eine benutzerdefinierte Schrift, und beim lokalen Debuggen sieht alles korrekt aus. Nach der automatisierten Erstellung und Installation der App auf einem Testgerät wird sie jedoch unbemerkt durch eine Systemschrift ersetzt. Solche Probleme führen normalerweise nicht zu einem fehlgeschlagenen Build: Möglicherweise wurde die Schrift nicht in das Target aufgenommen, in Info.plist ist ein falscher relativer Pfad eingetragen oder der Code verwendet den Dateinamen statt des internen PostScript-Namens der Schrift. Am zuverlässigsten ist es daher, nicht weiter das Projektverzeichnis zu untersuchen, sondern nach dem Build auf dem Cloud-Mac direkt das fertige App-Bundle zu prüfen.

Prüfumfang des Schrift-Gates festlegen

Ein Schrift-Gate muss mindestens vier Fragen beantworten: Ist die deklarierte Datei vorhanden, kann Core Text sie analysieren, entspricht ihr interner Name den Konventionen im Code und erzeugen mehrere Dateien doppelte Namen? Geprüft werden sollte das .app-Verzeichnis und nicht der Ordner Resources im Quellcode. Sowohl die Target Membership als auch die Phase Copy Bundle Resources können beeinflussen, was tatsächlich im fertigen Produkt landet.

Prüfpunkt Bedeutung eines Fehlers Empfohlene Maßnahme
Einträge in UIAppFonts Die Build-Konfiguration deklariert die Schrift nicht Info-Konfiguration des Targets korrigieren
Datei im App-Bundle Die Schrift wurde nicht in das Produkt kopiert Target Membership und Ressourcenphase prüfen
PostScript-Name Der im Code verwendete Name ist möglicherweise falsch Tatsächlichen Namen aus dem Schrift-Deskriptor auslesen
Eindeutigkeit der Namen Schriftfamilien oder Versionen stehen in Konflikt Doppelte Datei entfernen oder Ersetzungsbeziehung eindeutig festlegen

Dass eine Schrift in der Oberfläche „fast gleich aussieht“, ist kein belastbares Abnahmekriterium. Bei kurzen englischen Texten fällt ein Fallback möglicherweise kaum auf, doch veränderte Zeichenbreiten beeinflussen Zeilenumbrüche, Schaltflächengrößen und Screenshot-Vergleiche.

Es empfiehlt sich, im Repository eine Datei namens ci/expected-fonts.txt zu verwalten. Jede Zeile enthält einen zulässigen PostScript-Namen. Diese Liste bildet einen stabilen Vertrag zwischen den Schriftaufrufen im Code und den Ressourcendateien. Sie sollte nicht sämtliche Schriften enthalten, die auf den Rechnern der Entwickler installiert sind.

Fakten aus dem fertigen App-Bundle ermitteln

Zunächst wird das App-Bundle mit dem vorhandenen Build-Prozess erzeugt. Anschließend wird sein Pfad an das Prüfskript übergeben. Debug und Release können unterschiedliche Ressourcenkonfigurationen verwenden. Die Release-Pipeline muss deshalb genau die Konfiguration prüfen, die tatsächlich ausgeliefert werden soll. Das Skript darf die Schriften vor der Prüfung nicht beim System registrieren, da eine bereits auf dem Rechner vorhandene gleichnamige Schrift eine fehlende Datei im App-Bundle verdecken könnte.

Das folgende Swift-Skript liest UIAppFonts aus der Info.plist, prüft für jeden Eintrag, ob die Datei vorhanden ist, und extrahiert anschließend mit Core Text die internen Namen. Es wird als ci/check_fonts.swift gespeichert:

import Foundation
import CoreText

let arguments = CommandLine.arguments
guard arguments.count == 2 else {
    FileHandle.standardError.write(Data("usage: check_fonts.swift /path/App.app
".utf8))
    exit(64)
}

let appURL = URL(fileURLWithPath: arguments[1], isDirectory: true)
let plistURL = appURL.appendingPathComponent("Info.plist")
guard
    let data = try? Data(contentsOf: plistURL),
    let plist = try? PropertyListSerialization.propertyList(from: data) as? [String: Any],
    let declared = plist["UIAppFonts"] as? [String],
    !declared.isEmpty
else {
    FileHandle.standardError.write(Data("UIAppFonts is missing or empty
".utf8))
    exit(1)
}

var failed = false
var seen = Set<String>()

for relativePath in declared {
    let url = appURL.appendingPathComponent(relativePath)
    guard FileManager.default.fileExists(atPath: url.path) else {
        FileHandle.standardError.write(Data("missing: \(relativePath)
".utf8))
        failed = true
        continue
    }

    let descriptors = CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as? [CTFontDescriptor] ?? []
    if descriptors.isEmpty {
        FileHandle.standardError.write(Data("unreadable: \(relativePath)
".utf8))
        failed = true
    }

    for descriptor in descriptors {
        let value = CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute)
        guard let name = value as? String, !name.isEmpty else {
            FileHandle.standardError.write(Data("unnamed: \(relativePath)
".utf8))
            failed = true
            continue
        }
        if !seen.insert(name).inserted {
            FileHandle.standardError.write(Data("duplicate: \(name)
".utf8))
            failed = true
        }
        print("\(name)	\(relativePath)")
    }
}

exit(failed ? 1 : 0)

Bei der Ausführung wird der tatsächliche Pfad des Build-Produkts verwendet:

xcrun swift ci/check_fonts.swift "$APP_PATH" | sort > build/actual-fonts.txt
diff -u ci/expected-fonts.txt build/actual-fonts.txt

Wenn die Soll-Liste nur die Namen enthält, kann die Ausgabe zusätzlich durch cut -f1 geleitet werden. Entscheidend ist, dass eine Abweichung einen Exit-Code ungleich null erzeugt und die Pipeline sofort beendet.

PostScript-Namen statt Dateinamen festschreiben

Headline-Bold.otf ist lediglich der Dateiname auf dem Datenträger. Der interne Name der Schrift kann dagegen HeadlinePro-Bold lauten. Font.custom in SwiftUI, die Schriftinitialisierung in UIKit und der Testcode müssen jeweils den internen Namen verwenden. Das Umbenennen der Schriftdatei ändert diesen Namen nicht automatisch. Bei einer Aktualisierung durch den Anbieter kann sich der interne Name jedoch ändern.

Namensänderungen ausdrücklich prüfen lassen

Wenn actual-fonts.txt nicht mit der Liste der zulässigen Namen übereinstimmt, dürfen neue Werte nicht automatisch in der Pipeline akzeptiert werden. Zuerst ist zu klären, ob die Änderung aus einem geplanten Upgrade stammt. Danach müssen die Schriftaufrufe im Projekt durchsucht werden. Die Schriftnamen lassen sich in einer zentralen Quelldatei mit Konstanten zusammenfassen, damit sie nicht über View-Code und Test-Fixtures verstreut sind.

Auch eine einzelne Schriftdatei kann mehrere Deskriptoren enthalten. Das Skript muss alle Namen erfassen und darf sich nicht auf den ersten Eintrag beschränken. Wenn das Team tatsächlich variable Schriften benötigt, sollten außerdem Smoke-Tests auf Oberflächenebene für häufig verwendete Schriftschnitte ergänzt werden. Sie stellen sicher, dass die Achseneinstellungen der Designvorgabe entsprechen.

Laufzeitprüfung und visuelle Abnahme ergänzen

Ein statisches Gate kann belegen, dass die Ressourcenstruktur korrekt ist. Es weist jedoch nicht nach, dass jede Seite den richtigen Schriftschnitt verwendet. Empfehlenswert ist eine ausschließlich im Test-Target verfügbare Schriftübersicht, die Überschriften, Fließtext, Zahlen, chinesische Satzzeichen und lange englische Wörter abdeckt. Diese Seite wird auf einem Simulator mit festgelegter Größe per Screenshot geprüft.

Laufzeitassertionen sollten den fontName der Schriftinstanz prüfen und nicht nur die point size vergleichen. Bei dynamischen Schriften müssen die Tests außerdem mindestens zwei Inhaltsgrößenkategorien abdecken, um mögliche abgeschnittene Inhalte nach der Vergrößerung zu erkennen. Darf die Oberfläche bei einer fehlenden Schrift auf eine Ersatzschrift zurückfallen, muss diese Fallback-Logik ausdrücklich im Code festgehalten werden. Fehlt dagegen eine zentrale Markenschrift, sollte die Testumgebung sofort einen Fehler auslösen.

Bei parallelen Tests benötigt jeder Job ein eigenes Build- und Ergebnisverzeichnis. So kann die von einem Branch erzeugte Soll-Liste nicht die eines anderen Branches überschreiben. Schriftdateien sind Eingaberessourcen und dürfen von Build-Skripten nicht direkt an ihrem ursprünglichen Speicherort verändert werden.

Prüfung in eine reproduzierbare Pipeline integrieren

Die vollständige Reihenfolge lautet: isoliertes Build-Verzeichnis bereinigen, die Zielkonfiguration bauen, das eindeutig bestimmte App-Bundle lokalisieren, die Schriftstruktur prüfen, die Soll-Liste vergleichen und zuletzt die Oberflächentests ausführen. Das Produkt darf nicht mit einem mehrdeutigen find | head -1 ausgewählt werden. Liegen mehrere Apps im selben Verzeichnis, könnte dadurch ein Test-Host oder ein veralteter Build geprüft werden.

Nach einem fehlgeschlagenen Gate sollten actual-fonts.txt, die Liste der Schriftdateien im App-Bundle und relevante Ausschnitte des Build-Protokolls aufbewahrt werden. Nicht benötigte Zugangsdaten dürfen dabei nicht hochgeladen werden. Bei der Fehlersuche werden zuerst fehlende Dateien, danach UIAppFonts und zuletzt die internen Namen geprüft. In dieser Reihenfolge lässt sich schnell zwischen „nicht gebündelt“ und „falscher Aufrufname“ unterscheiden.

Wird ein Schrift-Upgrade genehmigt, sollten Schriftdatei, Soll-Liste, zentrale Namenskonstanten und Screenshot-Baselines gemeinsam in derselben Änderung eingecheckt werden. Dadurch stellt das Zurücksetzen eines beliebigen Commits wieder einen vollständigen, konsistenten Zustand her. Zugleich bleibt das Build-Ergebnis auf dem Cloud-Mac unabhängig von Schriften, die auf einem bestimmten Entwicklerrechner vorinstalliert sind.

Häufig gestellte Fragen

Warum reicht eine Prüfung der Schriftdateien im Repository nicht aus?

Eine Datei kann im Repository liegen, ohne in das Ziel kopiert zu werden. Maßgeblich sind das fertige App-Bundle und dessen UIAppFonts-Einträge.

Verwendet SwiftUI bei custom den Dateinamen der Schrift?

Nicht zuverlässig. Üblicherweise wird der interne PostScript-Name benötigt, der aus dem Schriftdeskriptor gelesen und gegen eine Soll-Liste geprüft werden sollte.

An welcher Stelle der CI sollte die Prüfung laufen?

Nach dem Erzeugen des installierbaren App-Bundles und vor Upload, Signierung der Verteilung oder Veröffentlichung.

Dedizierter physischer Knoten

Deine Mac-mini-Workflows dauerhaft in der Cloud ausführen

Wähle RunAMac M4, die Mietdauer und den Bereitstellungsstandort, und verlagere Entwicklungs-, Build- oder Testaufgaben auf einen dedizierten physischen Rechner.

Mietangebot auswählen