Apple Playground oder Xcode - was ist besser?

Nach längerer Zeit hat Apple seine App Playground(s) wieder aktualisiert. Und ich wollte einmal testen, ob Playground für eine echte Entwicklungsumgebung taugt oder nicht.

Es gibt Leute die coden ganze Apps auf dem iPad mit Playground, und inzwischen bietet diese Software sich dafür auch an. Sowohl auf dem Mac als auch auf dem iPad ist die Software verfügbar, und ich habe mal ein Projekt damit gemacht. 

Als ich noch in China war, habe ich mir einen kleinen Vokabeltrainer gebaut, damals noch mit Objective-C und auf Leopard. Den aktualisierten Code in Swift habe ich mir mit ChatGPT erstellen lassen, was sehr gut funktioniert hat. 

import SwiftUI

// MARK: - Datenmodell

/// Unser eigener Datentyp für Vokabeln.
/// Enthält das Fremdwort, fünf Antwortmöglichkeiten und den Index der richtigen Antwort.
struct Vokabel: Codable, Identifiable {
    var id: UUID = UUID()
    var foreignWord: String
    var options: [String]
    var correctIndex: Int
}

// MARK: - VokabelStore (Datenhaltung)

/// Diese Klasse verwaltet die Vokabeln und speichert bzw. lädt sie aus einer Datei.
class VokabelStore: ObservableObject {
    @Published var vokabelen: [Vokabel] = []
    
    // Speicherort in den Dokumenten des Users
    private let savePath: URL = {
        let documentDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        return documentDir.appendingPathComponent("vokabelen.json")
    }()
    
    init() {
        load()
    }
    
    /// Fügt eine neue Vokabel hinzu und speichert die Daten.
    func add(vokabel: Vokabel) {
        vokabelen.append(vokabel)
        save()
    }
    
    /// Speichert die Vokabeln als JSON in die Datei.
    func save() {
        do {
            let data = try JSONEncoder().encode(vokabelen)
            try data.write(to: savePath)
        } catch {
            print("Fehler beim Speichern: \(error)")
        }
    }
    
    /// Lädt die Vokabeln aus der Datei.
    func load() {
        do {
            let data = try Data(contentsOf: savePath)
            vokabelen = try JSONDecoder().decode([Vokabel].self, from: data)
        } catch {
            print("Fehler beim Laden oder Datei nicht vorhanden: \(error)")
        }
    }
}

// MARK: - Eingabemodus

/// In diesem View können neue Vokabeln eingegeben werden.
struct EingabemodusView: View {
    @ObservedObject var vokabelStore: VokabelStore
    
    @State private var foreignWord: String = ""
    @State private var option1: String = ""
    @State private var option2: String = ""
    @State private var option3: String = ""
    @State private var option4: String = ""
    @State private var option5: String = ""
    @State private var correctIndex: Int = 0  // 0...4
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Vokabel eingeben")
                .font(.largeTitle)
                .padding(.bottom, 10)
            
            Form {
                Section(header: Text("Fremdwort")) {
                    TextField("Fremdwort", text: $foreignWord)
                }
                
                Section(header: Text("Antwortmöglichkeiten")) {
                    TextField("Option 1", text: $option1)
                    TextField("Option 2", text: $option2)
                    TextField("Option 3", text: $option3)
                    TextField("Option 4", text: $option4)
                    TextField("Option 5", text: $option5)
                }
                
                Section(header: Text("Richtige Antwort auswählen")) {
                    Picker("Richtige Option", selection: $correctIndex) {
                        Text("Option 1").tag(0)
                        Text("Option 2").tag(1)
                        Text("Option 3").tag(2)
                        Text("Option 4").tag(3)
                        Text("Option 5").tag(4)
                    }
                    .pickerStyle(RadioGroupPickerStyle())
                }
            }
            .padding()
            
            HStack {
                Spacer()
                Button("Vokabel speichern") {
                    // Eingaben validieren
                    let options = [option1, option2, option3, option4, option5]
                    guard !foreignWord.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
                          options.allSatisfy({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) else {
                        return  // Hier könnte man eine Fehlermeldung anzeigen.
                    }
                    
                    let neueVokabel = Vokabel(foreignWord: foreignWord,
                                              options: options,
                                              correctIndex: correctIndex)
                    vokabelStore.add(vokabel: neueVokabel)
                    
                    // Felder zurücksetzen
                    foreignWord = ""
                    option1 = ""
                    option2 = ""
                    option3 = ""
                    option4 = ""
                    option5 = ""
                    correctIndex = 0
                }
                .padding()
            }
        }
        .padding()
    }
}

// MARK: - Lernmodus

/// In diesem View werden die Vokabeln in 10 Runden abgefragt.
struct LernmodusView: View {
    @ObservedObject var vokabelStore: VokabelStore
    
    @State private var currentQuestion: Vokabel?
    @State private var currentRound: Int = 1
    @State private var score: Int = 0
    @State private var showResult: Bool = false
    @State private var answerSelected: Bool = false
    @State private var feedback: String = ""
    
    var body: some View {
        VStack {
            // Falls noch keine Vokabeln vorhanden sind:
            if vokabelStore.vokabelen.isEmpty {
                Text("Keine Vokabeln vorhanden. Bitte fügen Sie im Eingabemodus Vokabeln hinzu.")
                    .padding()
            }
            // Falls eine Frage geladen ist:
            else if let question = currentQuestion {
                Text("Runde \(currentRound) von 10")
                    .font(.headline)
                Text("Übersetze: \(question.foreignWord)")
                    .font(.title)
                    .padding(.vertical, 10)
                
                // Alle 5 Antwortmöglichkeiten anzeigen:
                ForEach(0..<question.options.count, id: \.self) { index in
                    Button(action: {
                        if !answerSelected {
                            checkAnswer(selectedIndex: index)
                        }
                    }) {
                        Text(question.options[index])
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.blue.opacity(0.2))
                            .cornerRadius(8)
                    }
                    .padding(4)
                    .disabled(answerSelected)
                }
                
                if answerSelected {
                    Text(feedback)
                        .font(.subheadline)
                        .padding(.vertical, 5)
                    
                    Button("Weiter") {
                        nextQuestion()
                    }
                    .padding(.top, 5)
                }
            }
        }
        .padding()
        .onAppear(perform: startQuiz)
        .alert(isPresented: $showResult) {
            Alert(title: Text("Quiz beendet"),
                  message: Text("Dein Endergebnis: \(score) Punkte"),
                  dismissButton: .default(Text("OK"), action: {
                      startQuiz()  // Quiz zurücksetzen
                  }))
        }
    }
    
    /// Startet das Quiz neu
    func startQuiz() {
        currentRound = 1
        score = 0
        nextQuestion()
    }
    
    /// Lädt die nächste Frage oder zeigt das Ergebnis, wenn 10 Runden erreicht sind.
    func nextQuestion() {
        answerSelected = false
        feedback = ""
        if currentRound > 10 {
            showResult = true
        } else {
            // Wähle zufällig eine Vokabel aus dem Store
            currentQuestion = vokabelStore.vokabelen.randomElement()
        }
    }
    
    /// Prüft die Antwort des Nutzers.
    func checkAnswer(selectedIndex: Int) {
        answerSelected = true
        guard let question = currentQuestion else { return }
        if selectedIndex == question.correctIndex {
            score += 1
            feedback = "Richtig!"
        } else {
            score -= 1
            feedback = "Falsch! Richtige Antwort: \(question.options[question.correctIndex])"
        }
        currentRound += 1
        
        // Falls dies die 10. Runde war, wird nach kurzer Verzögerung das Ergebnis angezeigt.
        if currentRound > 10 {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                showResult = true
            }
        }
    }
}

// MARK: - Hauptansicht mit TabView

/// Hier wird zwischen Eingabe- und Lernmodus gewechselt.
struct ContentView: View {
    @StateObject var vokabelStore = VokabelStore()
    
    var body: some View {
        TabView {
            EingabemodusView(vokabelStore: vokabelStore)
                .tabItem {
                    Text("Eingabemodus")
                }
            LernmodusView(vokabelStore: vokabelStore)
                .tabItem {
                    Text("Lernmodus")
                }
        }
        .frame(minWidth: 600, minHeight: 400)
    }
}

// MARK: - App-Einstiegspunkt

@main
struct VokabelTrainerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Mit Swift Playground ging es ganz einfach: 

Einfach eine Neue App erstellen und schon hat man zwei Dateien, den ContentView, wo wir bis auf den ///Mark: - App-Einstiegspunkt den gesamten Text reinwerfen. Und die MyApp, wo dann alles so bleiben kann. 

Klick auf den ersten Punkt mit der Unterschrift App-Einstellungen öffnet ein Fenster, wo man Name, Icon (Entweder wählt man eins aus den Vorhandenen oder lädt ein neues hoch) und grundsätzliche Einstellung wie minimale macOS Version und Fähigkeiten, wie Dateizugriff, Netzwerkzugriff, Kamerazugriff o.ä. eingeben kann. 

Dort kann man sein fertiges Programm (auf dem Mac) direkt im Programmordner installieren oder sich in seinen Developeraccount anmelden und die App gleich im Apple Mac Store anmelden. 

Ich habe für die App gerade mal fünf Minuten gebraucht und sie lief. 

Einziger Nachteil: man ist auf die Spezifika von iOS Spezialisiert. Den Pickerstyle RadioGroupPickerStyle() gibt es unter iOS nicht, daher wirft das Programm einen Fehler aus. Ansonsten ein sehr entspanntes Arbeiten.

Die oberen Icons starten das Programm, man kann fertige Bausteine wie UI-Elemente (Texteingaben, Bilder, Picker, Menüs etc.) einfügen, dazu Styles, Farben, Icons, ohne sie sich merken zu müssen. Natürlich hat man Zugriff auf die Entwicklerdokumentation im Hilfemenü.

Nun zu Xcode.

Hier habe ich mehr wie ein typischer Mac Programmierer gearbeitet. Ich habe das Datenmodel, die beiden Unterviews LernmodusView und EingabeView in eigene Dateien gepackt. 

Auch das geht vergleichsweise schnell, nur muss man hier aufpassen. Bei den Unterviews habe ich die Vorschau entfernt, da diese nicht funktioniert hat. Der Contentview funktioniert aber. 

Hier brauchte ich (aber nur, weil ich weiß wie es geht) knapp sieben Minuten. 

Bei Xcode ist der Vorteil, dass man nicht nur Apps für Mac und iOS erstellen kann, sondern auch watchOS und VisionOS, sowie noch Apps mit Objective-C, C und C++. Auch ist ein echter Debugger eingebaut. 

Fazit: Swift Playground ist wesentlich schlanker, dafür mehr an iOS angepasst. Da es ausschließlich mit Cataclyst arbeitet, muss man hinsichtlich des nativen Aussehens ein paar wenige Abstriche machen. Dafür ist der Workflow wesentlich einfacher als mit Xcode. Die Möglichkeit, direkt aus der App mit einem Klick im Appstore veröffentlichen zu können, ist schon genial. Playgrounds braucht kein Xcode, um zu funktionieren. Dazu kommen umfangreiche Lernangebote, die sich vor allem an Anfänger richten. Aber auch professionelle Entwickler können hier noch eine ganze Menge lernen. 

Xcode ist das Schlachtschiff unter den Entwicklerumgebungen. Selbst wenn man mit anderen Programmiersprachen arbeitet kommt man nicht um Xcode herum, denn die gesamte Compiler, Linker und Toolchain ist in Xcode enthalten und nicht einzeln erhältlich.Wer zusätzlich für watchOS, visionOS und tvOS entwickeln will, muss die große Umgebung installieren.