MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Wie ich mit Realm und SwiftUI in einer Woche eine Chart-Spitzen-App geschrieben habe

Einen Elden Ring Quest Tracker bauen

Ich habe Skyrim geliebt. Ich verbrachte glücklich mehrere hundert Stunden damit, es zu spielen und abzuspielen. Als ich kürzlich von einem neuen Spiel hörte, Skyrim der 2020er , ich musste es kaufen. So beginnt meine Saga mit Elden Ring, dem gewaltigen Open-World-Rollenspiel mit Story-Anleitung von George R.R. Martin.

Innerhalb der ersten Stunde des Spiels lernte ich, wie brutal Souls-Spiele sein können. Ich habe mich in interessante Felshöhlen verkrochen, nur um so weit drinnen zu sterben, dass ich meinen Leichnam nicht mehr bergen konnte.

Ich habe alle meine Runen verloren.

Ich staunte ehrfürchtig, als ich mit dem Aufzug hinunter zum Siofra-Fluss fuhr, nur um festzustellen, dass mich ein grausamer Tod erwartete, weit weg vom nächsten Ort der Gnade. Ich rannte tapfer weg, bevor ich wieder sterben konnte.

Ich traf gespenstische Gestalten und faszinierende NPCs, die mich mit ein paar Dialogzeilen verführten… die ich sofort wieder vergaß, sobald es nötig war.

10/10, sehr zu empfehlen.

Eine Sache an Elden Ring hat mich besonders geärgert – es gab keinen Quest-Tracker. Um der Sache willen habe ich ein Notes-Dokument auf meinem iPhone geöffnet. Das war natürlich noch lange nicht genug.

Ich brauchte eine App, die mir hilft, RPG-Spieldetails zu verfolgen. Nichts im App Store entsprach wirklich dem, wonach ich suchte, also müsste ich es anscheinend schreiben. Es heißt Shattered Ring und ist jetzt im App Store erhältlich.

Tech-Auswahlmöglichkeiten

Tagsüber schreibe ich Dokumentationen für das Realm Swift SDK. Ich hatte kürzlich eine SwiftUI-Vorlagen-App für Realm geschrieben, um Entwicklern eine SwiftUI-Starter-Vorlage zur Verfügung zu stellen, auf der sie aufbauen können, komplett mit Anmeldeabläufen. Das Realm Swift SDK-Team hat ständig SwiftUI-Funktionen ausgeliefert, was es – meiner wahrscheinlich voreingenommenen Meinung nach – zu einem absolut einfachen Ausgangspunkt für die App-Entwicklung gemacht hat.

Ich wollte etwas, das ich superschnell bauen kann – teilweise, damit ich wieder Elden Ring spielen kann, anstatt eine App zu schreiben, und teilweise, um andere Apps auf den Markt zu bringen, während alle noch über Elden Ring reden. Ich konnte nicht Monate brauchen, um diese App zu erstellen. Ich wollte es gestern. Realm + SwiftUI sollte das möglich machen.

Datenmodellierung

Ich wusste, dass ich Quests im Spiel verfolgen wollte. Das Questmodell war einfach:

class Quest: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var isComplete = false
    @Persisted var notes = ""
}

Alles, was ich wirklich brauchte, war ein Name, ein bool zum Umschalten, wenn die Quest abgeschlossen war, ein Notizfeld und eine eindeutige Kennung.

Als ich jedoch über mein Gameplay nachdachte, wurde mir klar, dass ich nicht nur Quests brauchte – ich wollte auch Orte im Auge behalten. Ich bin in so viele coole Orte hineingestolpert – und schnell wieder herausgekommen, als ich anfing zu sterben –, die wahrscheinlich interessante Nicht-Spieler-Charaktere (NPCs) und tolle Beute hatten. Ich wollte in der Lage sein, den Überblick zu behalten, ob ich einen Ort geräumt hatte oder einfach davongelaufen war, damit ich mich daran erinnern konnte, später zurückzukehren und ihn mir anzusehen, sobald ich bessere Ausrüstung und mehr Fähigkeiten hatte. Also habe ich ein Location-Objekt hinzugefügt:

class Location: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var isCleared = false
    @Persisted var notes = ""
}

Hmm. Das sah dem Quest-Modell sehr ähnlich. Brauchte ich wirklich ein separates Objekt? Dann dachte ich an einen der frühen Orte, die ich besuchte – die Kirche von Elleh – die einen Schmiedeamboss hatte. Ich hatte noch nichts getan, um meine Ausrüstung zu verbessern, aber es wäre vielleicht schön zu wissen, an welchen Orten in Zukunft der Schmiedeamboss stand, wenn ich irgendwohin gehen wollte, um ein Upgrade durchzuführen. Also habe ich einen weiteren bool hinzugefügt:

@Persisted var hasSmithAnvil = false

Dann dachte ich darüber nach, dass derselbe Ort auch einen Händler hatte. Vielleicht möchte ich in Zukunft wissen, ob ein Standort einen Händler hatte. Also habe ich einen weiteren bool hinzugefügt:

@Persisted var hasMerchant = false

Toll! Standortobjekt sortiert.

Aber… da war noch etwas anderes. Ich bekam immer wieder all diese interessanten Geschichten von NPCs. Und was ist passiert, wenn ich eine Quest abgeschlossen habe – müsste ich zu einem NPC zurückkehren, um eine Belohnung zu erhalten? Dazu müsste ich wissen, wer mir die Quest gegeben hat und wo sie sich befinden. Es ist an der Zeit, ein drittes Modell hinzuzufügen, den NPC, der alles zusammenhält:

class NPC: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var isMerchant = false
    @Persisted var locations = List<Location>()
    @Persisted var quests = List<Quest>()
    @Persisted var notes = ""
}

Toll! Jetzt konnte ich NPCs verfolgen. Ich könnte Notizen hinzufügen, die mir helfen, den Überblick über diese interessanten Leckerbissen zu behalten, während ich darauf wartete, was sich entfalten würde. Ich konnte Quests und Orte NPCs zuordnen. Nach dem Hinzufügen dieses Objekts wurde deutlich, dass dies das Objekt war, das die anderen verband. NPCs sind an Orten. Aber ich wusste aus einiger Lektüre im Internet, dass sich manchmal NPCs im Spiel bewegen, sodass Orte mehrere Einträge unterstützen müssten – daher die Liste. NPCs geben Quests. Aber das sollte auch eine Liste sein, denn der erste NPC, den ich traf, gab mir mehr als eine Quest. Varre, direkt vor dem Shattered Graveyard, als Sie das Spiel zum ersten Mal betreten, sagte mir, ich solle „den Fäden der Gnade folgen“ und „zum Schloss gehen“. Richtig, sortiert!

Jetzt konnte ich meine Objekte mit SwiftUI-Eigenschaften-Wrappern verwenden, um mit der Erstellung der Benutzeroberfläche zu beginnen.

SwiftUI Views + Magical Property Wrapper von Realm

Da alles vom NPC abhängt, würde ich mit den NPC-Ansichten beginnen. Die @ObservedResults Property Wrapper bietet Ihnen eine einfache Möglichkeit, dies zu tun.

struct NPCListView: View {
    @ObservedResults(NPC.self) var npcs

    var body: some View {
        VStack {
            List {
                ForEach(npcs) { npc in
                    NavigationLink {
                        NPCDetailView(npc: npc)
                    } label: {
                        NPCRow(npc: npc)
                    }
                }
                .onDelete(perform: $npcs.remove)
                .navigationTitle("NPCs")
            }
            .listStyle(.inset)
        }
    }
}

Jetzt konnte ich durch eine Liste aller NPCs iterieren, hatte ein automatisches onDelete Aktion zum Entfernen von NPCs und könnte Realms Implementierung von .searchable hinzufügen als ich bereit war, Suche und Filter hinzuzufügen. Und es war im Grunde eine Linie, um es mit meinem Datenmodell zu verbinden. Habe ich erwähnt, dass Realm + SwiftUI fantastisch ist? Es war einfach genug, dasselbe mit Locations und Quests zu tun und es App-Benutzern zu ermöglichen, auf jedem Weg in ihre Daten einzutauchen.

Dann könnte meine NPC-Detailansicht mit dem @ObservedRealmObject funktionieren Eigenschafts-Wrapper, um die NPC-Details anzuzeigen und die Bearbeitung des NPCs zu vereinfachen:

struct NPCDetailView: View {
    @ObservedRealmObject var npc: NPC

    var body: some View {
        VStack {
            HStack {
            Text("Notes")
                 .font(.title2)
                 Spacer()
            if npc.isMerchant {
                Image(systemName: "dollarsign.square.fill")
            }
        Spacer()
        Text($npc.notes)
        Spacer()
        }
    }
}

Ein weiterer Vorteil des @ObservedRealmObject war, dass ich den $ verwenden konnte Notation, um ein schnelles Schreiben zu initiieren, sodass das Notizfeld nur editierbar wäre. Benutzer könnten eintippen und einfach weitere Notizen hinzufügen, und Realm würde einfach die Änderungen speichern. Keine Notwendigkeit für eine separate Bearbeitungsansicht oder das Öffnen einer expliziten Schreibtransaktion zum Aktualisieren der Notizen.

Zu diesem Zeitpunkt hatte ich eine funktionierende App und ich hätte sie problemlos versenden können.

Aber… mir kam ein Gedanke.

Eines der Dinge, die ich an Open-World-RPG-Spielen geliebt habe, war, sie als verschiedene Charaktere und mit verschiedenen Auswahlmöglichkeiten zu spielen. Vielleicht würde ich Elden Ring als eine andere Klasse wiederholen wollen. Oder – vielleicht war dies kein spezieller Elden Ring-Tracker, aber vielleicht könnte ich ihn verwenden, um jedes RPG-Spiel zu verfolgen. Was ist mit meinen D&D-Spielen?

Wenn ich mehrere Spiele verfolgen wollte, musste ich meinem Modell etwas hinzufügen. Ich brauchte ein Konzept für so etwas wie ein Spiel oder einen Durchspielvorgang.

Iteration des Datenmodells

Ich brauchte ein Objekt, um die NPCs, Orte und Quests einzuschließen, die ein Teil von diesem waren Playthrough, damit ich sie von anderen Playthroughs trennen konnte. Was wäre, wenn das ein Spiel wäre?

class Game: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var npcs = List<NPC>()
    @Persisted var locations = List<Location>()
    @Persisted var quests = List<Quest>()
}

In Ordnung! Toll. Jetzt kann ich die NPCs, Orte und Quests in diesem Spiel verfolgen und sie von anderen Spielen unterscheiden.

Das Game-Objekt war leicht zu konzipieren, aber als ich anfing, über @ObservedResults nachzudenken In meinen Ansichten wurde mir klar, dass das nicht mehr funktionieren würde. @ObservedResults gibt alle Ergebnisse für einen bestimmten Objekttyp zurück. Wenn ich also nur die NPCs für dieses Spiel anzeigen wollte, müsste ich meine Ansichten ändern.*

  • Swift SDK Version 10.24.0 fügte die Möglichkeit hinzu, die Swift-Abfragesyntax in @ObservedResults zu verwenden , mit dem Sie Ergebnisse mithilfe von where filtern können Parameter. Ich refaktorisiere definitiv, um dies in einer zukünftigen Version zu verwenden! Das Swift SDK-Team hat ständig neue SwiftUI-Extras veröffentlicht.

Oh. Außerdem bräuchte ich eine Möglichkeit, die NPCs in diesem Spiel von denen in anderen Spielen zu unterscheiden. Hr. Jetzt könnte es an der Zeit sein, sich mit Backlinking zu befassen. Nachdem ich mich in den Realm Swift SDK-Dokumenten umgesehen hatte, fügte ich dies dem NPC-Modell hinzu:

@Persisted(originProperty: "npcs") var npcInGame: LinkingObjects<Game>

Jetzt konnte ich die NPCs mit dem Spielobjekt verknüpfen. Aber leider werden meine Ansichten jetzt komplizierter.

Aktualisieren der SwiftUI-Ansichten für die Modelländerungen

Da ich jetzt nur eine Teilmenge meiner Objekte möchte (und das war vor der @ObservedResults update), habe ich meine Listenansichten von @ObservedResults umgestellt zu @ObservedRealmObject , das Spiel beobachten:

@ObservedRealmObject var game: Game

Jetzt habe ich immer noch die Vorteile des schnellen Schreibens, um NPCs, Orte und Quests im Spiel hinzuzufügen und zu bearbeiten, aber mein Listencode musste ein wenig aktualisiert werden:

ForEach(game.npcs) { npc in
    NavigationLink {
        NPCDetailView(npc: npc)
    } label: {
        NPCRow(npc: npc)
    }
}
.onDelete(perform: $game.npcs.remove

Immer noch nicht schlecht, aber eine andere Beziehungsebene, die es zu berücksichtigen gilt. Und da dies nicht @ObservedResults verwendet , konnte ich die Realm-Implementierung von .searchable nicht verwenden , müsste es aber selber umsetzen. Keine große Sache, aber mehr Arbeit.

Eingefrorene Objekte und Anhängen an Listen

Jetzt, bis zu diesem Punkt, habe ich eine funktionierende App. Ich könnte das so versenden, wie es ist. Alles ist immer noch einfach, da die Property Wrapper von Realm Swift SDK die ganze Arbeit erledigen.

Aber ich wollte, dass meine App mehr kann.

Ich wollte Orte und Quests aus der NPC-Ansicht hinzufügen und sie automatisch an den NPC anhängen lassen. Und ich wollte einen Questgeber in der Questansicht sehen und hinzufügen können. Und ich wollte in der Lage sein, NPCs von der Standortansicht aus anzuzeigen und zu Orten hinzuzufügen.

All dies erforderte eine Menge Anhängen an Listen, und als ich anfing, dies mit schnellen Schreibvorgängen zu tun, nachdem ich das Objekt erstellt hatte, wurde mir klar, dass das nicht funktionieren würde. Ich müsste Objekte manuell herumreichen und anhängen.

Was ich wollte, war so etwas zu tun:

func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
    let realm = try! Realm()
    let thisLocation = game.locations.where { $0.name == locationName }.first!

    try! realm.write {
        npc!.locations.append(thisLocation)
    }
}

Hier kam mir etwas in den Weg, das mir als neuer Entwickler nicht ganz klar war. Ich hatte noch nie wirklich etwas mit Threading und eingefrorenen Objekten zu tun, aber ich bekam Abstürze, deren Fehlermeldungen mich glauben ließen, dass dies damit zusammenhängt. Glücklicherweise erinnerte ich mich daran, ein Codebeispiel zum Auftauen von eingefrorenen Objekten geschrieben zu haben, damit Sie in anderen Threads damit arbeiten können, also ging es zurück zu den Dokumenten – diesmal zur Threading-Seite, die sich mit eingefrorenen Objekten befasst. (Weitere Verbesserungen, die das Realm Swift SDK-Team hinzugefügt hat, seit ich MongoDB beigetreten bin – yay!)

Nach dem Besuch der Dokumente hatte ich so etwas:

func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
    let realm = try! Realm()
    Let thawedNPC = npc.thaw()
    let thisLocation = game.locations.where { $0.name == locationName }.first!

    try! realm.write {
        thawedNPC!.locations.append(thisLocation)
    }
}

Das sah richtig aus, stürzte aber immer noch ab. Aber warum? (An diesem Punkt habe ich mich dafür verflucht, dass ich kein ausführlicheres Codebeispiel in der Dokumentation bereitgestellt habe. Die Arbeit an dieser App hat definitiv zu einigen Tickets geführt, um unsere Dokumentation in einigen Bereichen zu verbessern!)

Nachdem ich in den Foren geforscht und das große Orakel Google konsultiert hatte, stieß ich auf einen Thread, in dem jemand über dieses Problem sprach. Es stellt sich heraus, dass Sie nicht nur das Objekt, an das Sie anhängen möchten, auftauen müssen, sondern auch das, was Sie anhängen möchten. Dies mag für einen erfahreneren Entwickler offensichtlich sein, aber es hat mich für eine Weile gestolpert. Also, was ich wirklich brauchte, war so etwas:

func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
    let realm = try! Realm()
    let thawedNpc = npc.thaw()
    let thisLocation = game.locations.where { $0.name == locationName     }.first!
    let thawedLocation = thisLocation.thaw()!

    try! realm.write {
        thawedNpc!.locations.append(thawedLocation)
    }
}

Toll! Problem gelöst. Jetzt konnte ich alle Funktionen erstellen, die ich brauchte, um das Anhängen (und Entfernen, wie sich herausstellte) von Objekten manuell zu handhaben.

Alles andere ist nur SwiftUI

Danach war alles andere, was ich lernen musste, um die App zu erstellen, nur SwiftUI, wie zum Beispiel, wie man filtert, wie man die Filter vom Benutzer auswählbar macht und wie man meine eigene Version von .searchable implementiert .

Es gibt definitiv einige Dinge, die ich mit der Navigation mache, die nicht optimal sind. Es gibt einige UX-Verbesserungen, die ich noch vornehmen möchte. Und das Wechseln meines @ObservedRealmObject var game: Game zurück zu @ObservedResults mit dem neuen Filtermaterial wird bei einigen dieser Verbesserungen helfen. Aber insgesamt machten die Realm Swift SDK Property Wrapper die Implementierung dieser App so einfach, dass sogar ich es schaffen könnte.

Insgesamt habe ich die App an zwei Wochenenden und einer Handvoll Wochentagen erstellt. Wahrscheinlich war ich an einem Wochenende in dieser Zeit mit dem Problem des Anhängens an Listen festgefahren, und außerdem habe ich eine Website für die App erstellt, alle Screenshots erhalten, die an den App Store gesendet werden können, und all die „geschäftlichen“ Dinge, die damit einhergehen Indie-App-Entwickler.

Aber ich bin hier, um Ihnen zu sagen, dass, wenn ich, ein weniger erfahrener Entwickler mit genau einer früheren App in meinem Namen – und das mit viel Feedback von meinem Lead – eine App wie Shattered Ring erstellen kann, Sie das auch können. Und es ist viel einfacher mit SwiftUI + den SwiftUI-Funktionen des Realm Swift SDK. Sehen Sie sich den SwiftUI-Schnellstart als gutes Beispiel an, um zu sehen, wie einfach es ist.