sql >> Databáze >  >> NoSQL >> MongoDB

Jak jsem za týden napsal špičkovou aplikaci s Realm a SwiftUI

Sestavení pátrání po Elden Ringu

Miloval jsem Skyrim. S radostí jsem strávil několik set hodin hraním a přehráváním. Takže když jsem nedávno slyšel o nové hře, Skyrim roku 2020 , musel jsem si to koupit. Tak začíná moje sága s Elden Ringem, masivním RPG s otevřeným světem s příběhovým vedením od George R. R. Martina.

Během první hodiny hry jsem poznal, jak brutální Souls hry mohou být. Vplížil jsem se do zajímavých jeskyní na útesech, ale zemřel jsem tak hluboko uvnitř, že jsem nemohl získat svou mrtvolu.

Ztratil jsem všechny své runy.

Zíral jsem v úžasu, když jsem jel výtahem dolů k řece Siofra, jen abych zjistil, že mě čeká strašlivá smrt, daleko od nejbližšího místa milosti. Statečně jsem utekl, než jsem mohl znovu zemřít.

Potkal jsem strašidelné postavy a fascinující NPC, které mě lákaly několika řádky dialogu... na které jsem okamžitě zapomněl, jakmile to bylo potřeba.

10/10, vysoce doporučeno.

Jedna věc mě na Elden Ringovi rozčilovala – nebyl tam žádný quest tracker. Vždy dobrý sport, otevřel jsem si dokument Notes na svém iPhonu. To samozřejmě nestačilo.

Potřeboval jsem aplikaci, která by mi pomohla sledovat podrobnosti o přehrávání RPG. Nic v App Store skutečně neodpovídalo tomu, co jsem hledal, takže to zřejmě budu muset napsat. Jmenuje se Shattered Ring a je nyní k dispozici v App Store.

Tech Choices

Přes den píšu dokumentaci pro Realm Swift SDK. Nedávno jsem napsal šablonovou aplikaci SwiftUI pro Realm, abych vývojářům poskytl startovací šablonu SwiftUI, na které lze stavět, včetně přihlašovacích toků. Tým Realm Swift SDK neustále dodává funkce SwiftUI, díky čemuž se z něj – podle mého pravděpodobně neobjektivního názoru – stal smrtelně jednoduchý výchozí bod pro vývoj aplikací.

Chtěl jsem něco, co bych mohl postavit super rychle – částečně, abych se mohl vrátit k hraní Elden Ring místo psaní aplikace, a částečně porazit ostatní aplikace na trhu, zatímco všichni stále mluví o Elden Ring. Sestavení této aplikace mi nemohlo trvat měsíce. Chtěl jsem to včera. Realm + SwiftUI to mělo umožnit.

Modelování dat

Věděl jsem, že chci ve hře sledovat questy. Model hledání byl snadný:

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

Vše, co jsem opravdu potřeboval, bylo jméno, bool, který se dal přepnout, když byl úkol dokončen, pole s poznámkami a jedinečný identifikátor.

Když jsem přemýšlel o své hře, uvědomil jsem si, že nepotřebuji jen questy – chtěl jsem také sledovat lokace. Narazil jsem - a rychle jsem se z toho dostal, když jsem začal umírat - tolik skvělých míst, která pravděpodobně měla zajímavé nehráčské postavy (NPC) a úžasnou kořist. Chtěl jsem mít možnost sledovat, zda jsem místo vyčistil, nebo z něj jen utekl, abych si mohl zapamatovat, že se vrátím později a prověřím to, až budu mít lepší vybavení a více schopností. Takže jsem přidal objekt umístění:

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

Hmm Vypadalo to hodně jako model hledání. Opravdu jsem potřeboval samostatný objekt? Pak jsem přemýšlel o jednom z prvních míst, které jsem navštívil - kostel Elleh - který měl kovářskou kovadlinu. Vlastně jsem ještě neudělal nic pro vylepšení své výbavy, ale mohlo by být fajn vědět, na kterých místech byla v budoucnu kovářská kovadlina, když jsem chtěl jít někam vylepšit. Tak jsem přidal další bool:

@Persisted var hasSmithAnvil = false

Pak jsem přemýšlel o tom, že na stejném místě byl také obchodník. Možná budu chtít v budoucnu vědět, zda nějaké místo mělo obchodníka. Tak jsem přidal další bool:

@Persisted var hasMerchant = false

Skvělý! Umístění objektu seřazeno.

Ale… bylo tu ještě něco. Neustále jsem od NPC dostával všechny tyhle zajímavé příběhy. A co se stalo, když jsem dokončil quest – musel bych se vrátit k NPC, abych získal odměnu? To by vyžadovalo, abych věděl, kdo mi dal úkol a kde se nachází. Je čas přidat třetí model, NPC, který by vše spojil dohromady:

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

Skvělý! Nyní jsem mohl sledovat NPC. Mohl jsem přidávat poznámky, které by mi pomohly udržet si přehled o těch zajímavých historkách, zatímco jsem čekal, co se vyvine. Mohl bych přidružit questy a místa k NPC. Po přidání tohoto objektu bylo zřejmé, že to byl objekt, který spojoval ostatní. NPC jsou na místech. Ale z nějakého čtení online jsem věděl, že někdy se NPC ve hře pohybují, takže místa by musela podporovat více položek – proto ten seznam. NPC dávají úkoly. Ale to by měl být také seznam, protože první NPC, které jsem potkal, mi dalo více než jeden quest. Varre, hned před Shattered Graveyard, když jste poprvé vstoupili do hry, mi řekl, „Následujte vlákna milosti“ a „jděte do hradu“. Dobře, seřazeno!

Nyní jsem mohl použít své objekty s obálkami vlastností SwiftUI a začít vytvářet uživatelské rozhraní.

SwiftUI Views + Realm's Magical Property Wrappers

Protože vše visí mimo NPC, začal bych pohledy NPC. @ObservedResults property wrapper vám nabízí snadný způsob, jak toho dosáhnout.

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

Nyní jsem mohl procházet seznam všech NPC, měl jsem automatické onDelete akce k odstranění NPC a mohla by přidat implementaci Realmu .searchable když jsem byl připraven přidat vyhledávání a filtrování. A v podstatě to byl jeden řádek, který jsem to připojil k mému datovému modelu. Zmínil jsem se, že Realm + SwiftUI je úžasný? Bylo dost snadné udělat totéž s umístěními a úkoly a umožnit uživatelům aplikace ponořit se do svých dat jakoukoli cestou.

Potom by můj detailní pohled NPC mohl fungovat s @ObservedRealmObject vlastnost wrapper pro zobrazení podrobností o NPC a usnadnění úpravy NPC:

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

Další výhoda @ObservedRealmObject bylo, že mohu použít $ zápis pro zahájení rychlého zápisu, takže pole poznámek bude pouze upravitelné. Uživatelé by mohli klepnout a přidat další poznámky a Realm by jen uložil změny. Není potřeba samostatné zobrazení úprav nebo otevření explicitní transakce zápisu pro aktualizaci poznámek.

V tuto chvíli jsem měl funkční aplikaci a mohl jsem ji snadno odeslat.

Ale... napadlo mě.

Jedna z věcí, kterou jsem na RPG hrách s otevřeným světem miloval, bylo hrát je jako různé postavy a s různými možnostmi. Možná bych si tedy chtěl znovu zahrát Elden Ring jako jinou třídu. Nebo – možná to nebyl konkrétně tracker Elden Ring, ale možná bych ho mohl použít ke sledování jakékoli RPG hry. A co moje hry D&D?

Pokud jsem chtěl sledovat více her, potřeboval jsem do svého modelu něco přidat. Potřeboval jsem koncept něčeho jako hry nebo playthrough.

Iterace na datovém modelu

Potřeboval jsem nějaký předmět, který by zahrnoval NPC, Místa a Úkoly, které byly součástí tohoto playthrough, abych je mohl držet odděleně od ostatních playthrough. Tak co když to byla hra?

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

V pořádku! Skvělý. Nyní mohu sledovat NPC, lokace a úkoly, které jsou v této hře, a odlišit je od ostatních her.

Objekt hry bylo snadné si představit, ale když jsem začal přemýšlet o @ObservedResults podle mého názoru jsem si uvědomil, že to už nebude fungovat. @ObservedResults vrátit všechny výsledky pro konkrétní typ objektu. Takže pokud bych chtěl zobrazit pouze NPC pro tuto hru, musel bych změnit své pohledy.*

  • Swift SDK verze 10.24.0 přidala možnost používat syntaxi Swift Query v @ObservedResults , která umožňuje filtrovat výsledky pomocí where parametr. Rozhodně to předělám, abych to použil v budoucí verzi! Tým Swift SDK neustále vydává nové vychytávky SwiftUI.

Ach. Také bych potřeboval způsob, jak odlišit NPC v této hře od těch v jiných hrách. Hrm. Možná je čas podívat se na zpětné odkazy. Po hláskování v Realm Swift SDK Docs jsem do modelu NPC přidal toto:

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

Nyní jsem mohl zpětně propojit NPC s herním objektem. Ale bohužel, teď jsou mé názory složitější.

Aktualizace zobrazení SwiftUI pro změny modelu

Protože teď chci pouze podmnožinu svých objektů (a to bylo před @ObservedResults update), přepnul jsem zobrazení seznamu z @ObservedResults na @ObservedRealmObject , sledující hru:

@ObservedRealmObject var game: Game

Nyní stále využívám výhody rychlého psaní pro přidávání a úpravu NPC, lokací a úkolů ve hře, ale můj kód seznamu se musel trochu aktualizovat:

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

Stále to není špatné, ale je třeba zvážit další úroveň vztahů. A protože toto nepoužívá @ObservedResults , nemohl jsem použít implementaci Realm .searchable , ale musel bych to implementovat sám. Není to velký problém, ale více práce.

Zmrazené objekty a přidávání do seznamů

Až do tohoto bodu mám funkční aplikaci. Mohl bych to poslat tak, jak je. Vše je stále jednoduché, přičemž veškerou práci odvádějí obaly vlastností Realm Swift SDK.

Ale chtěl jsem, aby moje aplikace uměla víc.

Chtěl jsem mít možnost přidávat lokace a úkoly z pohledu NPC a nechat je automaticky přidávat k NPC. A chtěl jsem mít možnost zobrazit a přidat quest-giver z pohledu questu. A chtěl jsem mít možnost prohlížet a přidávat NPC do umístění z pohledu umístění.

To vše vyžadovalo spoustu připojování k seznamům, a když jsem se o to začal pokoušet rychlým zápisem po vytvoření objektu, uvědomil jsem si, že to nebude fungovat. Musel bych ručně předávat objekty a přidávat je.

Co jsem chtěl, bylo udělat něco takového:

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

Tady mi začalo překážet něco, co mi jako novému vývojáři nebylo úplně zřejmé. Nikdy předtím jsem s vytvářením vláken a zmrazenými objekty nemusel nic dělat, ale docházelo ke zhroucení, jehož chybové zprávy mě přiměly si myslet, že to souvisí s tím. Naštěstí jsem si vzpomněl, jak jsem napsal příklad kódu o rozmrazování zmrazených objektů, abyste s nimi mohli pracovat v jiných vláknech, takže jsem se vrátil k dokumentům – tentokrát na stránku Threading, která pokrývá zmrazené objekty. (Další vylepšení, která přidal tým Realm Swift SDK od doby, kdy jsem se připojil k MongoDB – yay!)

Po návštěvě dokumentů jsem měl něco takového:

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

Vypadalo to správně, ale stále to padalo. Ale proč? (To je, když jsem se proklínal, že jsem v dokumentech neposkytnul důkladnější příklad kódu. Práce na této aplikaci rozhodně přinesla několik lístků ke zlepšení naší dokumentace v několika oblastech!)

Po hláskování ve fórech a konzultaci s velkým orákulem Google jsem narazil na vlákno, kde někdo mluvil o tomto problému. Ukázalo se, že musíte rozmrazit nejen předmět, ke kterému se pokoušíte připojit, ale také věc, kterou se pokoušíte připojit. Zkušenějšímu vývojáři to může být jasné, ale mě to na chvíli podrazilo. Takže to, co jsem opravdu potřeboval, bylo něco takového:

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

Skvělý! Problém je vyřešen. Nyní jsem mohl vytvořit všechny funkce, které jsem potřeboval k ručnímu zpracování přidávání (a odstraňování, jak se ukázalo) objektů.

Všechno ostatní je jen SwiftUI

Poté jsem se všechno ostatní, co jsem se musel naučit, abych vytvořil aplikaci, bylo jen SwiftUI, jako jak filtrovat, jak nastavit filtry tak, aby si je mohl uživatel vybrat, a jak implementovat svou vlastní verzi .searchable .

Určitě jsou některé věci, které dělám s navigací, které nejsou optimální. Stále chci provést některá vylepšení UX. A přepnutí mé @ObservedRealmObject var game: Game zpět na @ObservedResults s novým filtrováním pomůže s některými z těchto vylepšení. Celkově však obálky vlastností Realm Swift SDK udělaly implementaci této aplikace natolik jednoduchou, že bych to dokázal i já.

Celkově jsem aplikaci postavil za dva víkendy a pár nocí v týdnu. Pravděpodobně jsem jeden víkend v té době uvízl u problému s připojováním k seznamům a také jsem pro aplikaci vytvořil webovou stránku, získal všechny snímky obrazovky, které jsem mohl odeslat do App Store, a všechny „obchodní“ věci, které s tím souvisí. nezávislý vývojář aplikací.

Ale jsem tu, abych vám řekl, že pokud já, méně zkušený vývojář s přesně jednou předchozí aplikací na své jméno – a to se spoustou zpětné vazby od mého vedoucího – dokážu vytvořit aplikaci jako Shattered Ring, můžete to udělat i vy. A je to sakra mnohem jednodušší s SwiftUI + funkcemi SwiftUI Realm Swift SDK. Podívejte se na SwiftUI Quick Start, kde najdete dobrý příklad, abyste viděli, jak snadné to je.


  1. replika Set mongo docker-compose

  2. Pracovat se dvěma samostatnými instancemi redis s sidekiq?

  3. Operátoři MongoDB $gt/$lt s cenami uloženými jako řetězce

  4. Proč bylo v tomto programu gevent provedeno pouze jedno připojení k redis?