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

Come ho scritto un'app in cima alle classifiche in una settimana con Realm e SwiftUI

Costruire un rilevatore di missioni Elden Ring

Ho adorato Skyrim. Ho trascorso felicemente diverse centinaia di ore a riprodurlo e riprodurlo. Quindi, quando recentemente ho sentito parlare di un nuovo gioco, lo Skyrim degli anni 2020 , Dovevo comprarlo. Inizia così la mia saga con Elden Ring, l'enorme RPG open world con la guida alla storia di George R.R. Martin.

Entro la prima ora di gioco, ho imparato quanto possono essere brutali i giochi di Souls. Mi sono insinuato in interessanti grotte sulla scogliera solo per morire così all'interno che non sono riuscito a recuperare il mio cadavere.

Ho perso tutte le mie rune.

Rimasi a bocca aperta con timore reverenziale mentre scendevo con l'ascensore fino al fiume Siofra, solo per scoprire che la morte orribile mi aspettava, lontano dal più vicino luogo di grazia. Sono scappato coraggiosamente prima di poter morire di nuovo.

Ho incontrato figure spettrali e NPC affascinanti che mi hanno tentato con alcune battute di dialogo... che ho subito dimenticato non appena è stato necessario.

10/10, altamente raccomandato.

Una cosa in particolare su Elden Ring mi ha infastidito:non c'era un quest tracker. Sempre bello, ho aperto un documento Notes sul mio iPhone. Ovviamente non era abbastanza.

Avevo bisogno di un'app che mi aiutasse a tenere traccia dei dettagli di gioco dei giochi di ruolo. Niente sull'App Store corrispondeva davvero a quello che stavo cercando, quindi a quanto pare avrei bisogno di scriverlo. Si chiama Shattered Ring ed è ora disponibile sull'App Store.

Scelte tecnologiche

Di giorno, scrivo documentazione per Realm Swift SDK. Di recente avevo scritto un'app modello SwiftUI per Realm per fornire agli sviluppatori un modello di base SwiftUI su cui costruire, completo di flussi di accesso. Il team di Realm Swift SDK ha distribuito costantemente funzionalità SwiftUI, il che lo ha reso - secondo la mia opinione probabilmente parziale - un punto di partenza semplicissimo per lo sviluppo di app.

Volevo qualcosa che potevo costruire molto velocemente, in parte per poter tornare a giocare a Elden Ring invece di scrivere un'app e in parte per battere altre app sul mercato mentre tutti parlano ancora di Elden Ring. Non potevo impiegare mesi per creare questa app. Lo volevo ieri. Realm + SwiftUI lo avrebbero reso possibile.

Modellazione dei dati

Sapevo che volevo tenere traccia delle missioni nel gioco. Il modello di ricerca era facile:

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

Tutto ciò di cui avevo veramente bisogno era un nome, un bool da attivare una volta completata la missione, un campo per le note e un identificatore univoco.

Mentre pensavo al mio gameplay, però, mi sono reso conto che non avevo solo bisogno di missioni, ma volevo anche tenere traccia dei luoghi. Mi sono imbattuto - e rapidamente fuori da quando ho iniziato a morire - così tanti posti fantastici che probabilmente avevano personaggi non giocanti (NPC) interessanti e bottino fantastico. Volevo essere in grado di tenere traccia di se avessi liberato una posizione o se fossi semplicemente scappato da essa, così potevo ricordarmi di tornare più tardi e controllarlo una volta che avessi avuto equipaggiamento migliore e più abilità. Quindi ho aggiunto un oggetto posizione:

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

Hmm. Somigliava molto al modello di ricerca. Avevo davvero bisogno di un oggetto separato? Poi ho pensato a uno dei primi luoghi che ho visitato - la Chiesa di Elleh - che aveva un'incudine da fabbro. In realtà non avevo ancora fatto nulla per migliorare la mia attrezzatura, ma potrebbe essere bello sapere quali luoghi avrebbero avuto l'incudine del fabbro in futuro quando volevo andare da qualche parte per fare un aggiornamento. Quindi ho aggiunto un altro bool:

@Persisted var hasSmithAnvil = false

Poi ho pensato a come quella stessa località avesse anche un commerciante. Potrei voler sapere in futuro se una località aveva un commerciante. Quindi ho aggiunto un altro bool:

@Persisted var hasMerchant = false

Grande! Posizione oggetto ordinato.

Ma c'era qualcos'altro. Continuavo a ricevere tutte queste interessanti curiosità sulla storia dagli NPC. E cosa è successo quando ho completato una missione:avrei dovuto tornare da un NPC per raccogliere una ricompensa? Ciò mi richiederebbe di sapere chi mi aveva dato la ricerca e dove si trovavano. È ora di aggiungere un terzo modello, l'NPC, che legherebbe tutto insieme:

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

Grande! Ora posso rintracciare gli NPC. Potrei aggiungere note per aiutarmi a tenere traccia di quegli interessanti bocconcini di storia mentre aspettavo di vedere cosa si sarebbe svolto. Potrei associare missioni e posizioni agli NPC. Dopo aver aggiunto questo oggetto, è diventato ovvio che questo era l'oggetto che collegava gli altri. Gli NPC sono nelle posizioni. Ma sapevo da alcune letture online che a volte gli NPC si spostano nel gioco, quindi le posizioni avrebbero dovuto supportare più voci, da qui l'elenco. Gli NPC danno missioni. Ma dovrebbe anche essere una lista, perché il primo NPC che ho incontrato mi ha dato più di una missione. Varre, appena fuori dallo Shattered Graveyard, quando entri per la prima volta nel gioco, mi ha detto di "Seguire i fili della grazia" e "vai al castello". Giusto, ordinato!

Ora posso usare i miei oggetti con i wrapper di proprietà SwiftUI per iniziare a creare l'interfaccia utente.

Viste SwiftUI + Wrapper di proprietà magiche di Realm

Dal momento che tutto è sospeso dall'NPC, inizierei con le viste dell'NPC. Il @ObservedResults il wrapper di proprietà ti offre un modo semplice per farlo.

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

Ora potevo scorrere un elenco di tutti gli NPC, avevo un onDelete automatico azione per rimuovere gli NPC e potrebbe aggiungere l'implementazione di .searchable da parte di Realm quando ero pronto per aggiungere ricerca e filtraggio. Ed era fondamentalmente una riga per collegarlo al mio modello di dati. Ho già detto che Realm + SwiftUI è fantastico? È stato abbastanza facile fare la stessa cosa con Posizioni e missioni e consentire agli utenti dell'app di immergersi nei propri dati attraverso qualsiasi percorso.

Quindi, la mia visualizzazione dei dettagli NPC potrebbe funzionare con @ObservedRealmObject wrapper delle proprietà per visualizzare i dettagli dell'NPC e semplificare la modifica dell'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()
        }
    }
}

Un altro vantaggio del @ObservedRealmObject era che potevo usare il $ notazione per avviare una scrittura rapida, quindi il campo delle note sarebbe semplicemente modificabile. Gli utenti possono attingere e aggiungere semplicemente più note e Realm salverebbe semplicemente le modifiche. Non c'è bisogno di una vista di modifica separata o di aprire una transazione di scrittura esplicita per aggiornare le note.

A questo punto, avevo un'app funzionante e avrei potuto spedirla facilmente.

Ma... ho avuto un pensiero.

Una delle cose che ho amato dei giochi di ruolo open world è stata rigiocarli come personaggi diversi e con scelte diverse. Quindi forse vorrei rigiocare Elden Ring come una classe diversa. Oppure - forse questo non era un tracker di Elden Ring in particolare, ma forse potrei usarlo per tracciare qualsiasi gioco di ruolo. E i miei giochi di D&D?

Se volevo monitorare più giochi, dovevo aggiungere qualcosa al mio modello. Avevo bisogno di un'idea di qualcosa come un gioco o un playthrough.

Iterazione sul Data Model

Avevo bisogno di un oggetto per racchiudere gli NPC, i luoghi e le missioni che facevano parte di questo playthrough, in modo da poterli tenere separati dagli altri playthrough. E se fosse un gioco?

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

Bene! Grande. Ora posso tracciare gli NPC, le posizioni e le missioni presenti in questo gioco e tenerli distinti dagli altri giochi.

L'oggetto Game è stato facile da concepire, ma quando ho iniziato a pensare al @ObservedResults secondo me, mi sono reso conto che non avrebbe più funzionato. @ObservedResults restituire tutti i risultati per un tipo di oggetto specifico. Quindi, se volessi visualizzare solo gli NPC per questo gioco, dovrei cambiare la mia visualizzazione.*

  • Swift SDK versione 10.24.0 ha aggiunto la possibilità di utilizzare la sintassi Swift Query in @ObservedResults , che ti consente di filtrare i risultati utilizzando where parametro. Sto sicuramente refactoring per usarlo in una versione futura! Il team di Swift SDK ha costantemente rilasciato nuove chicche SwiftUI.

Oh. Inoltre, avrei bisogno di un modo per distinguere gli NPC in questo gioco da quelli in altri giochi. Hmm. Ora potrebbe essere il momento di esaminare il backlinking. Dopo la speleologia nei documenti dell'SDK di Realm Swift, ho aggiunto questo al modello NPC:

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

Ora potrei eseguire il backlink degli NPC all'oggetto di gioco. Ma, ahimè, ora le mie opinioni si fanno più complicate.

Aggiornamento delle viste SwiftUI per le modifiche al modello

Dato che ora voglio solo un sottoinsieme dei miei oggetti (e questo era prima di @ObservedResults update), ho cambiato le mie visualizzazioni elenco da @ObservedResults a @ObservedRealmObject , osservando il gioco:

@ObservedRealmObject var game: Game

Ora ottengo ancora i vantaggi della scrittura rapida per aggiungere e modificare NPC, posizioni e missioni nel gioco, ma il mio codice elenco doveva essere aggiornato un po':

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

Ancora non male, ma un altro livello di relazioni da considerare. E poiché questo non utilizza @ObservedResults , non ho potuto utilizzare l'implementazione Realm di .searchable , ma dovrei implementarlo da solo. Non è un grosso problema, ma più lavoro.

Oggetti congelati e aggiunta agli elenchi

Ora, fino a questo punto, ho un'app funzionante. Potrei spedire questo così com'è. Tutto è ancora semplice con i wrapper di proprietà Realm Swift SDK che fanno tutto il lavoro.

Ma volevo che la mia app facesse di più.

Volevo poter aggiungere posizioni e missioni dalla vista NPC e averle aggiunte automaticamente all'NPC. E volevo essere in grado di visualizzare e aggiungere un donatore di missioni dalla vista missioni. E volevo essere in grado di visualizzare e aggiungere NPC alle posizioni dalla vista della posizione.

Tutto ciò ha richiesto molte aggiunte agli elenchi e quando ho iniziato a provare a farlo con scritture rapide dopo aver creato l'oggetto, mi sono reso conto che non avrebbe funzionato. Dovrei passare manualmente gli oggetti e aggiungerli.

Quello che volevo era fare qualcosa del genere:

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

È qui che qualcosa che non era del tutto ovvio per me quando un nuovo sviluppatore ha iniziato a intralciarmi. Non avevo mai avuto a che fare con il threading e gli oggetti congelati prima, ma ricevevo arresti anomali i cui messaggi di errore mi facevano pensare che fosse correlato a quello. Fortunatamente, mi sono ricordato di aver scritto un esempio di codice sullo scongelamento di oggetti congelati in modo da poter lavorare con loro su altri thread, quindi è tornato ai documenti, questa volta alla pagina Threading che copre gli oggetti congelati. (Altri miglioramenti che il team di Realm Swift SDK ha aggiunto da quando mi sono unito a MongoDB - evviva!)

Dopo aver visitato i documenti, ho avuto qualcosa del genere:

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

Sembrava giusto, ma stava ancora andando in crash. Ma perché? (Questo è il momento in cui mi sono maledetto per non aver fornito un esempio di codice più completo nei documenti. Lavorare su questa app ha sicuramente prodotto alcuni ticket per migliorare la nostra documentazione in alcune aree!)

Dopo aver speleologia nei forum e consultato il grande oracolo Google, mi sono imbattuto in un thread in cui qualcuno parlava di questo problema. Si scopre che devi scongelare non solo l'oggetto a cui stai cercando di aggiungere, ma anche la cosa che stai cercando di aggiungere. Questo può essere ovvio per uno sviluppatore più esperto, ma mi ha fatto inciampare per un po'. Quindi quello di cui avevo davvero bisogno era qualcosa del genere:

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

Grande! Problema risolto. Ora potrei creare tutte le funzioni di cui avevo bisogno per gestire manualmente l'aggiunta (e la rimozione, a quanto pare) di oggetti.

Tutto il resto è solo SwiftUI

Dopo questo, tutto il resto che ho dovuto imparare per produrre l'app è stato solo SwiftUI, come come filtrare, come rendere i filtri selezionabili dall'utente e come implementare la mia versione di .searchable .

Ci sono sicuramente alcune cose che sto facendo con la navigazione che non sono ottimali. Ci sono alcuni miglioramenti alla UX che voglio ancora apportare. E cambiare il mio @ObservedRealmObject var game: Game torna a @ObservedResults con il nuovo materiale di filtraggio aiuterà con alcuni di questi miglioramenti. Ma nel complesso, i wrapper delle proprietà dell'SDK di Realm Swift hanno reso l'implementazione di questa app abbastanza semplice da poterla fare anche io.

In totale, ho creato l'app in due fine settimana e una manciata di notti feriali. Probabilmente un fine settimana di quel tempo sono rimasto bloccato con l'aggiunta al problema degli elenchi, e anche creando un sito Web per l'app, ricevendo tutti gli screenshot da inviare all'App Store e tutte le cose "affari" che vanno insieme all'essere un sviluppatore di app indipendenti.

Ma sono qui per dirti che se io, uno sviluppatore meno esperto con esattamente un'app precedente a mio nome - e che con molti feedback dal mio vantaggio - posso creare un'app come Shattered Ring, puoi farlo anche tu. Ed è molto più semplice con SwiftUI + le funzionalità SwiftUI di Realm Swift SDK. Dai un'occhiata a SwiftUI Quick Start per un buon esempio per vedere com'è facile.