knuspermagier.de

Er bloggt noch? Krass!

Serie: SwiftUI-Kennenlernen
Post bearbeiten

Was muss ich diesmal lernen?

  1. Wie macht man einen weiteren Screen?
  2. Wie funktioniert [navigationController pushViewController:controller] in SwiftUI?
  3. Wie baue ich ein Formular und befülle es mit Daten, die ich aus der API lade?
  4. Wie mache ich einen PATCH-Request mit JSON-Payload?

Na dann mal keine Zeit verlieren!


Weitere Screens anlegen

Ein weiterer Screen ist einfach nur ein View. Theoretisch kann man auch einfach Text("Hello World") auf den Navigation Stack pushen. Okay.

Des Weiteren finde ich es sehr erfreulich, dass man bei Swift auch erstmal alles in eine Datei hauen kann. Für’s schnelle Prototypen, wie ich es gerne mag, aufräumen kann ich, wenn ich tot bin (oder wenn ich mich dafür entschieden habe, dass ich diese App wirklich bauen will), ist sowas immer sehr hilfreich.

struct EditPost: View {
    var postId: String

    var body: some View {
        VStack() {
            Text("Detailseite: \(postId)")
        }
        .navigationBarTitle("Detail")
        .navigationBarItems(trailing:
                Button(action: {
                    print("Save")
                }) {
                    Text("Speichern")
                }

        )
    }
}

Ungefähr so. Ich hab auch schon einen Speichern-Button oben rechts hingemacht. Das war auch relativ einfach, wie man sieht. Ich bin mir noch nicht so richtig sicher, wie ich das finde, dass man alles mögliche einfach in solchen gechainten Methodenaufrufen macht. Aber letztendlich ist das halt wohl einfach die SwiftUI DSL.

Das Navigieren gestaltet sich auch erstmal einfach. Man wrappt seinen VStack, der die UITableViewRow darstellt einfach in einen NavigationLink:

NavigationLink(destination: EditPost(postId: item.id)) {
    // VStack, …
}

Schon geht das Formular auf! Jetzt muss es nur noch was anzeigen.

Formular

Erstmal Titel und Text, was braucht ein Blogpost sonst noch so? (/me schielt auf seine overengineerten Blueprints). Also ein TextField für den Titel rein, schön. Der Text bekommt auch ein TextField, da muss ich sicher nur irgendwie numberOfLines auf 0 setzen, oder auf -1, wer weiß.

Moment mal, die Property gibt es gar nicht? Gibt es vielleicht einen ganz anderen View dafür? TextArea? Pustekuchen. Naaaja. Google erzählte mir schnell, dass der aktuelle Way to Go wohl das Wrappen eines UITextView ist, aber auch, dass es in iOS14 einen TextEditor gibt. Juchu! Bis dahin kopierte schonmal so einen UITextView-Wrapper von StackOverflow und fügte ihn ein. Vielleicht doch mal Catalina und Xcode Beta installieren?

VStack() {
    TextField("Titel", text: $title)
    TextView(text: $text)
}

Sehr übersichtlich bisher auf jeden Fall!

Formular befüllen

Hier kommen wir zu dem Teil, mit dem ich wohl am meisten Zeit verbrauchte. Wie bekomme ich die Daten aus dem API-Request für die Page-Details in das Formular.

Ich erstellte nun also noch ein ObservableObject um die Detail-Daten zu laden und band es als @ObservedObject var fetch = FetchSinglePost() in den View ein.

Das Problem hierbei: Ich kann bei TextField und TextView nicht fetch.post.title übergeben, weil er irgendwie ein <Binding> erwartet. Ich muss also den Kram aus fetch.post.title irgendwie in self.title, die @State-Variable bekommen.

Kein Problem, dachte ich, der FetchSinglePost-Kram ist ja extra observierbar, dann observiere ich also mal.

.onAppear {
    fetch.$post.sink { (post) in
        print("changed \(post?.title)")

        if(post?.title != nil) {
            self.title = post?.title ?? ""
            self.text = post?.content?.text ?? ""
        }
    }
}

Der erste Versuch klappte schon einmal nicht. Wenn ich das Fetch oben als Property von der View-Struct definiere, wird der Request sofort ausgeführt, und die Daten sind da, bevor onAppear passiert und ich los-sinke.

Mein erster Versuch war also das var fetch = FetchSinglePost(postId: self.postId) mit ins onAppear zu ziehen, damit ich mich direkt dranhängen kann. Funktionierte schonmal besser, ich bekam immerhin ein changed nil auf die Konsole! Toll!

Der Request ging aber durch, Daten kamen an und wurden geparsed, warum kommt kein weiteres Log-Statement?

Des Rätsels Lösung, ist wieder sowas, was man sicher gelernt hätte, würde man nicht durch Google-Trial-And-Error lernen, sondern ein Buch lesen. sink liefert ein Cancellable-Dingsbums zurück. Wenn man das ignoriert und nicht irgendwo speichert, wird es direkt wieder aufgesammelt und entsorgt, und niemand sagt einem mehr Bescheid, dass sich etwas geändert hat. Also schnell

@State var cancellable: Any = nil

Oben rein, den sink-Rückgabewert rein und alles ist gut. Ich bekomme mitgeteilt, wenn sich was ändert und kann es in den State schreiben, juchu!

Insgesamt war mir das jetzt aber doch etwas kompliziert, ich möchte also nicht ausschließen, dass ich nicht etwas komplett falsch gemacht habe — mein googlen lieferte jedenfalls kein besseres Ergebnis.

Request

Nachdem das bauen des Formulars überstanden war, war der Request eigentlich wieder ein Kinderspiel.

Durch ein StackOverflow-Snippet wurde ich darauf aufmerksam, dass man auch einfach “dumme” Dictionaries bauen kann und die zu JSON serialisieren kann, ohne vorher alles in structs zu gießen und mit dem JSONDecoder zu arbeiten.

var json = [String:Any]()
json["text"] = self.text

let data = try JSONSerialization.data(withJSONObject: json, options: [])

var request = URLRequest(url: url)

request.httpMethod = "PATCH"
request.httpBody = data
request.setValue("Basic xxx", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")

let task = URLSession.shared.dataTask(with: request).resume()

Es speichert, was für ein schönes Gefühl!


Aktueller Nerv-Status:


Ich glaube am meisten nervt mich am mobilen Kirby-Panel, wie fummelig klein der Bild-Upload-Button ist. Also versuchen wir doch noch einen Bild-Upload einzubauen!

Post-Serie SwiftUI-Kennenlernen:

Kommentare, Feedback und andere Anmerkungen? Schreib mir eine E-Mail 🤓