sql >> Database teknologi >  >> NoSQL >> MongoDB

Hvordan jeg skrev en Chart-Topping-app på en uge med Realm og SwiftUI

Opbygning af en Elden Ring Quest Tracker

Jeg elskede Skyrim. Jeg brugte gladeligt flere hundrede timer på at spille og afspille det. Så da jeg for nylig hørte om et nyt spil, 2020'ernes Skyrim , jeg var nødt til at købe det. Således begynder min saga med Elden Ring, det massive open-world RPG med historievejledning fra George R.R. Martin.

Inden for den første time af spillet lærte jeg, hvor brutale Souls-spil kan være. Jeg sneg mig ind i interessante klippehuler for kun at dø så langt inde, at jeg ikke kunne hente mit lig.

Jeg mistede alle mine runer.

Jeg gabede i ærefrygt undren, da jeg kørte med elevatoren ned til Siofra-floden, blot for at opdage, at den grusomme død ventede mig, langt fra det nærmeste nådested. Jeg løb modigt væk, før jeg kunne dø igen.

Jeg mødte spøgelsesagtige skikkelser og fascinerende NPC'er, som fristede mig med et par linjers dialog... som jeg straks glemte, så snart det var nødvendigt.

10/10, stærkt anbefalet.

Især én ting ved Elden Ring irriterede mig - der var ingen quest tracker. Nogensinde den gode sport åbnede jeg et Notes-dokument på min iPhone. Det var selvfølgelig ikke nær nok.

Jeg havde brug for en app til at hjælpe mig med at spore RPG-afspilningsdetaljer. Intet i App Store matchede virkelig det, jeg ledte efter, så jeg skulle åbenbart skrive det. Den hedder Shattered Ring, og den er tilgængelig i App Store nu.

Tekniske valg

Om dagen skriver jeg dokumentation til Realm Swift SDK. Jeg havde for nylig skrevet en SwiftUI-skabelonapp til Realm for at give udviklere en SwiftUI-startskabelon at bygge videre på, komplet med login-flows. Realm Swift SDK-teamet har støt leveret SwiftUI-funktioner, hvilket har gjort det - efter min sandsynligvis partiske mening - til et dødssimpelt udgangspunkt for app-udvikling.

Jeg ville have noget, jeg kunne bygge superhurtigt – dels så jeg kunne komme tilbage til at spille Elden Ring i stedet for at skrive en app, og dels for at slå andre apps på markedet, mens alle stadig taler om Elden Ring. Jeg kunne ikke tage måneder at bygge denne app. Jeg ville have det i går. Realm + SwiftUI skulle gøre det muligt.

Datamodellering

Jeg vidste, at jeg ville spore quests i spillet. Quest-modellen var let:

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

Det eneste, jeg virkelig havde brug for, var et navn, en bool til at skifte, når missionen var fuldført, et notefelt og en unik identifikator.

Mens jeg tænkte over mit gameplay, indså jeg dog, at jeg ikke kun havde brug for quests - jeg ville også holde styr på lokationer. Jeg faldt ind i - og hurtigt ud af, da jeg begyndte at dø - så mange fede steder, der sandsynligvis havde interessante ikke-spillerkarakterer (NPC'er) og fantastisk bytte. Jeg ville gerne være i stand til at holde styr på, om jeg havde ryddet en placering, eller bare løb væk fra den, så jeg kunne huske at gå tilbage senere og tjekke det ud, når jeg havde fået bedre gear og flere evner. Så jeg tilføjede et placeringsobjekt:

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

Hmm. Det lignede quest-modellen meget. Havde jeg virkelig brug for et separat objekt? Så tænkte jeg på et af de tidlige steder, jeg besøgte - Elleh-kirken - som havde en smed-ambolt. Jeg havde faktisk ikke gjort noget for at forbedre mit udstyr endnu, men det kunne være rart at vide, hvilke steder der havde smith ambolten i fremtiden, når jeg ville hen for at lave en opgradering. Så jeg tilføjede endnu en bool:

@Persisted var hasSmithAnvil = false

Så tænkte jeg på, hvordan det samme sted også havde en købmand. Jeg vil måske gerne vide i fremtiden, om et sted havde en købmand. Så jeg tilføjede endnu en bool:

@Persisted var hasMerchant = false

Store! Placeringsobjekt sorteret.

Men... der var noget andet. Jeg blev ved med at få alle disse interessante historier fra NPC'er. Og hvad skete der, da jeg fuldførte en opgave - skulle jeg gå tilbage til en NPC for at modtage en belønning? Det ville kræve, at jeg ved, hvem der havde givet mig missionen, og hvor de befandt sig. Tid til at tilføje en tredje model, NPC, der ville binde alt sammen:

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 = ""
}

Store! Nu kunne jeg spore NPC'er. Jeg kunne tilføje noter for at hjælpe mig med at holde styr på de interessante historier, mens jeg ventede på, hvad der ville udfolde sig. Jeg kunne knytte quests og lokationer til NPC'er. Efter at have tilføjet dette objekt, blev det tydeligt, at dette var det objekt, der forbandt de andre. NPC'er er på lokationer. Men jeg vidste fra noget læsning på nettet, at nogle gange bevæger NPC'er sig rundt i spillet, så steder skal understøtte flere poster - deraf listen. NPC'er giver quests. Men det burde også være en liste, for den første NPC, jeg mødte, gav mig mere end én quest. Varre, lige uden for Shattered Graveyard, da du første gang gik ind i spillet, sagde til mig at "Følg nådens tråde" og "gå til slottet." Okay, sorteret!

Nu kunne jeg bruge mine objekter med SwiftUI-egenskabsindpakninger til at begynde at oprette brugergrænsefladen.

SwiftUI Views + Realm's Magical Property Wrappers

Da alt hænger af NPC'en, ville jeg starte med NPC-visningerne. @ObservedResults ejendomsindpakning giver dig en nem måde at gøre dette på.

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)
        }
    }
}

Nu kunne jeg gentage en liste over alle NPC'erne, havde en automatisk onDelete handling for at fjerne NPC'er og kunne tilføje Realms implementering af .searchable da jeg var klar til at tilføje søgning og filtrering. Og det var dybest set en linje at tilslutte det til min datamodel. Fik jeg nævnt, at Realm + SwiftUI er fantastisk? Det var nemt nok at gøre det samme med Locations og Quests og gøre det muligt for appbrugere at dykke ned i deres data gennem en hvilken som helst sti.

Så kunne min NPC-detaljevisning fungere med @ObservedRealmObject egenskabsindpakning for at vise NPC-detaljerne og gøre det nemt at redigere NPC'en:

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()
        }
    }
}

En anden fordel ved @ObservedRealmObject var, at jeg kunne bruge $ notation for at starte en hurtig skrivning, så notefeltet ville bare være redigerbart. Brugere kunne trykke ind og bare tilføje flere noter, og Realm ville bare gemme ændringerne. Intet behov for en separat redigeringsvisning eller at åbne en eksplicit skrivetransaktion for at opdatere noterne.

På dette tidspunkt havde jeg en fungerende app, og jeg kunne nemt have sendt den.

Men... jeg havde en tanke.

En af de ting, jeg elskede ved open world RPG-spil, var at afspille dem som forskellige karakterer og med forskellige valg. Så måske ville jeg gerne genspille Elden Ring som en anden klasse. Eller - måske var dette ikke specifikt en Elden Ring-tracker, men måske kunne jeg bruge den til at spore et hvilket som helst RPG-spil. Hvad med mine D&D-spil?

Hvis jeg ville spore flere spil, var jeg nødt til at tilføje noget til min model. Jeg havde brug for et koncept af noget som et spil eller et gennemspil.

Iteration på datamodellen

Jeg havde brug for et eller andet objekt til at omfatte de NPC'er, Locations og Quests, der var en del af dette gennemspilning, så jeg kunne holde dem adskilt fra andre gennemspilninger. Så hvad hvis det var et spil?

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>()
}

I orden! Store. Nu kan jeg spore de NPC'er, Locations og Quests, der er i dette spil, og holde dem adskilt fra andre spil.

Spilobjektet var let at forestille sig, men da jeg begyndte at tænke på @ObservedResults efter min mening indså jeg, at det ikke ville fungere længere. @ObservedResults returnere alle resultater for en bestemt objekttype. Så hvis jeg kun ville vise NPC'erne for dette spil, skulle jeg ændre mine synspunkter.*

  • Swift SDK version 10.24.0 tilføjede muligheden for at bruge Swift Query-syntaks i @ObservedResults , som giver dig mulighed for at filtrere resultater ved hjælp af where parameter. Jeg overvejer bestemt at bruge dette i en fremtidig version! Swift SDK-teamet har konstant frigivet nye SwiftUI-godbidder.

Åh. Jeg har også brug for en måde at skelne NPC'erne i dette spil fra dem i andre spil. Hrm. Nu er det måske tid til at se nærmere på backlinking. Efter at have spillet i Realm Swift SDK Docs, føjede jeg dette til NPC-modellen:

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

Nu kunne jeg backlinke NPC'erne til spilobjektet. Men desværre bliver mine synspunkter mere komplicerede.

Opdatering af SwiftUI-visninger for modelændringerne

Da jeg kun vil have en delmængde af mine objekter nu (og dette var før @ObservedResults update), skiftede jeg min listevisning fra @ObservedResults til @ObservedRealmObject , observerer spillet:

@ObservedRealmObject var game: Game

Nu får jeg stadig fordelene ved hurtigskrivning for at tilføje og redigere NPC'er, Locations og Quests i spillet, men min listekode skulle opdateres en lille smule:

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

Stadig ikke dårligt, men et andet niveau af forhold at overveje. Og da dette ikke bruger @ObservedResults , jeg kunne ikke bruge Realm-implementeringen af ​​.searchable , men skulle selv implementere det. Ikke en big deal, men mere arbejde.

Frosne objekter og tilføjelse til lister

Nu, indtil dette tidspunkt, har jeg en fungerende app. Jeg kunne sende den som den er. Alt er stadig enkelt med Realm Swift SDK-ejendomsindpakningerne, der gør alt arbejdet.

Men jeg ville have min app til at gøre mere.

Jeg ønskede at være i stand til at tilføje Locations og Quests fra NPC-visningen og få dem automatisk tilføjet til NPC'en. Og jeg ønskede at være i stand til at se og tilføje en quest-giver fra quest-visningen. Og jeg ønskede at være i stand til at se og tilføje NPC'er til placeringer fra placeringsvisningen.

Alt dette krævede en masse tilføjelse til lister, og da jeg begyndte at prøve at gøre dette med hurtige skrivninger efter at have oprettet objektet, indså jeg, at det ikke ville fungere. Jeg bliver nødt til manuelt at sende objekter rundt og tilføje dem.

Det, jeg ville, var at gøre sådan noget:

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)
    }
}

Det var her, noget, der ikke var helt indlysende for mig som ny udvikler, begyndte at komme i vejen for mig. Jeg havde aldrig rigtig skullet gøre noget med trådning og frosne genstande før, men jeg fik nedbrud, hvis fejlmeddelelser fik mig til at tro, at det var relateret til det. Heldigvis huskede jeg at skrive et kodeeksempel om optøning af frosne objekter, så du kan arbejde med dem på andre tråde, så det var tilbage til docs - denne gang til Threading-siden, der dækker Frozen Objects. (Flere forbedringer, som Realm Swift SDK-teamet har tilføjet, siden jeg blev medlem af MongoDB - yay!)

Efter at have besøgt dokumenterne, havde jeg noget som dette:

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)
    }
}

Det så rigtigt ud, men styrtede stadig ned. Men hvorfor? (Det var her, jeg forbandede mig selv for ikke at give et mere grundigt kodeeksempel i dokumenterne. Arbejdet med denne app har bestemt givet nogle billetter til at forbedre vores dokumentation på nogle få områder!)

Efter at have talt i fora og konsulteret det store orakel Google, stødte jeg på en tråd, hvor nogen talte om dette problem. Det viser sig, at du ikke kun skal tø den genstand, du forsøger at føje til, men også den ting, du forsøger at tilføje. Dette kan være indlysende for en mere erfaren udvikler, men det slog mig i et stykke tid. Så hvad jeg virkelig havde brug for var noget som dette:

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)
    }
}

Store! Problem løst. Nu kunne jeg oprette alle de funktioner, jeg havde brug for til manuelt at håndtere tilføjelsen (og fjernelse, som det viser sig) af objekter.

Alt andet er bare SwiftUI

Herefter var alt andet, jeg skulle lære for at producere appen, kun SwiftUI, som hvordan man filtrerer, hvordan man gør filtrene brugervalgbare, og hvordan man implementerer min egen version af .searchable .

Der er helt sikkert nogle ting, jeg laver med navigation, som er mindre end optimale. Der er nogle UX-forbedringer, jeg stadig gerne vil lave. Og skifter mit @ObservedRealmObject var game: Game tilbage til @ObservedResults med de nye filtreringsting vil hjælpe med nogle af disse forbedringer. Men overordnet set gjorde Realm Swift SDK-ejendomsindpakningerne implementeringen af ​​denne app så enkel, at selv jeg kunne gøre det.

I alt byggede jeg appen på to weekender og en håndfuld ugenætter. Sandsynligvis en weekend af den tid gik jeg i stå med spørgsmålet om tilføjelse til lister, og jeg lavede også en hjemmeside til appen, fik alle skærmbilleder til at sende til App Store og alle de "forretningsting", der hører med til at være en indie app udvikler.

Men jeg er her for at fortælle dig, at hvis jeg, en mindre erfaren udvikler med præcis én tidligere app til mit navn - og det med en masse feedback fra mit leder - kan lave en app som Shattered Ring, kan du også. Og det er meget nemmere med SwiftUI + Realm Swift SDK's SwiftUI-funktioner. Tjek SwiftUI Quick Start for et godt eksempel for at se, hvor nemt det er.


  1. Mongodb concat int og streng

  2. Hvordan implementerer man en strøm af futures til et blokerende opkald ved hjælp af futures.rs og Redis PubSub?

  3. Redis lister

  4. Det lykkedes ikke at forbinde Mongoose med Atlas