MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Jak w ciągu tygodnia napisałem aplikację z listą przebojów za pomocą Realm i SwiftUI?

Tworzenie trackera zadań Elden Ring

Kochałem Skyrima. Szczęśliwie spędziłem kilkaset godzin grając i odtwarzając to. Kiedy więc niedawno usłyszałem o nowej grze, Skyrim z lat 2020 , musiałem to kupić. Tak zaczyna się moja saga z Elden Ring, masywną grą RPG z otwartym światem z przewodnikiem fabularnym od George'a R.R. Martina.

W ciągu pierwszej godziny gry dowiedziałem się, jak brutalne mogą być gry Souls. Zakradłem się do interesujących jaskiń na klifach, by umrzeć tak głęboko w środku, że nie mogłem odzyskać mojego trupa.

Straciłem wszystkie runy.

Gapiłam się w zdumieniu, kiedy jechałam windą do rzeki Siofra, tylko po to, by stwierdzić, że czeka mnie makabryczna śmierć, daleko od najbliższego miejsca łaski. Dzielnie uciekłam, zanim mogłam ponownie umrzeć.

Spotkałem upiorne postacie i fascynujących NPC, którzy kusili mnie kilkoma linijkami dialogu… o których natychmiast zapomniałem, gdy tylko było to potrzebne.

10/10, gorąco polecam.

Jedna rzecz szczególnie w Elden Ring irytowała mnie - nie było quest trackera. Kiedykolwiek był dobry sport, otworzyłem dokument Notatki na moim iPhonie. Oczywiście to nie wystarczyło.

Potrzebowałem aplikacji, która pomogłaby mi śledzić szczegóły rozgrywki RPG. Nic w App Store tak naprawdę nie pasowało do tego, czego szukałem, więc najwyraźniej musiałbym to napisać. Nazywa się Shattered Ring i jest już dostępny w App Store.

Opcje techniczne

Na co dzień piszę dokumentację dla Realm Swift SDK. Niedawno napisałem aplikację szablonową SwiftUI dla Realm, aby zapewnić programistom szablon startowy SwiftUI do budowania, wraz z przepływami logowania. Zespół Realm Swift SDK stale udostępnia funkcje SwiftUI, co sprawiło, że – w mojej prawdopodobnie stronniczej opinii – jest to martwy, prosty punkt wyjścia do tworzenia aplikacji.

Chciałem czegoś, co mógłbym zbudować bardzo szybko – częściowo po to, by móc wrócić do grania w Elden Ring zamiast pisać aplikację, a częściowo, aby pobić inne aplikacje na rynku, podczas gdy wszyscy wciąż mówią o Elden Ring. Stworzenie tej aplikacji nie mogło zająć miesięcy. Chciałem tego wczoraj. Realm + SwiftUI miał to umożliwić.

Modelowanie danych

Wiedziałem, że chcę śledzić questy w grze. Model questowy był prosty:

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

Wszystko, czego naprawdę potrzebowałem, to nazwa, bool do przełączenia po zakończeniu zadania, pole notatek i unikalny identyfikator.

Jednak gdy myślałem o mojej rozgrywce, zdałem sobie sprawę, że potrzebuję nie tylko zadań - chciałem również śledzić lokalizacje. Natknąłem się – i szybko wyszedłem, kiedy zacząłem umierać – tak wiele fajnych miejsc, które prawdopodobnie miały interesujące postacie niezależne (NPC) i niesamowite łupy. Chciałem być w stanie śledzić, czy wyczyściłem miejsce, czy po prostu z niego uciekłem, żebym mógł wrócić później i sprawdzić to, gdy będę miał lepszy sprzęt i więcej umiejętności. Dodałem więc obiekt lokalizacji:

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

Hmm. To bardzo przypominało model questowy. Czy naprawdę potrzebowałem osobnego przedmiotu? Potem pomyślałem o jednym z pierwszych miejsc, które odwiedziłem - Kościele Elleh - w którym znajdowało się kowadło kowalskie. Właściwie nie zrobiłem jeszcze nic, aby ulepszyć mój sprzęt, ale może fajnie byłoby wiedzieć, w których lokalizacjach znajdowało się kowadło kowalskie w przyszłości, kiedy chciałem gdzieś się ulepszyć. Więc dodałem kolejny bool:

@Persisted var hasSmithAnvil = false

Potem pomyślałem o tym, że w tej samej lokalizacji był też kupiec. Mogę chcieć wiedzieć w przyszłości, czy w danej lokalizacji był kupiec. Więc dodałem kolejny bool:

@Persisted var hasMerchant = false

Świetny! Obiekt lokalizacji posortowany.

Ale… było coś jeszcze. Ciągle dostawałem te wszystkie ciekawe ciekawostki fabularne od NPC. A co się stało, gdy wykonałem zadanie - czy musiałbym wrócić do NPC, aby odebrać nagrodę? To wymagałoby ode mnie wiedzy, kto zlecił mi zadanie i gdzie oni się znajdowali. Czas dodać trzeci model, NPC, który połączy wszystko razem:

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

Świetny! Teraz mogłem śledzić NPC. Mógłbym dodawać notatki, które pomogłyby mi śledzić te ciekawe ciekawostki z historii, czekając, aby zobaczyć, co się wydarzy. Mogłem powiązać zadania i lokacje z NPC. Po dodaniu tego obiektu stało się oczywiste, że jest to obiekt, który łączył pozostałe. NPC są w lokacjach. Ale z lektury online wiedziałem, że czasami NPC poruszają się w grze, więc lokacje musiałyby obsługiwać wiele wpisów - stąd lista. NPC dają misje. Ale to też powinna być lista, ponieważ pierwszy NPC, którego spotkałem, dał mi więcej niż jedno zadanie. Varre, tuż za Strzaskanym Cmentarzem, kiedy po raz pierwszy wchodzisz do gry, powiedział mi, abym „podążał za nićmi łaski” i „idź do zamku”. Dobrze, posortowane!

Teraz mogłem użyć moich obiektów z opakowaniami właściwości SwiftUI, aby rozpocząć tworzenie interfejsu użytkownika.

Widoki SwiftUI + opakowania magicznych właściwości Realm

Ponieważ wszystko wisi na NPC, zacząłbym od widoków NPC. @ObservedResults opakowanie właściwości zapewnia łatwy sposób na zrobienie tego.

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

Teraz mogłem iterować po liście wszystkich NPC, miałem automatyczne onDelete akcja usunięcia NPC i może dodać implementację Realm .searchable kiedy byłem gotowy, aby dodać wyszukiwanie i filtrowanie. I była to w zasadzie jedna linia, która łączyła go z moim modelem danych. Czy wspomniałem, że Realm + SwiftUI jest niesamowity? Łatwo było zrobić to samo z lokalizacjami i zadaniami i umożliwić użytkownikom aplikacji dostęp do swoich danych dowolną ścieżką.

Wtedy mój widok szczegółów NPC mógłby działać z @ObservedRealmObject opakowanie właściwości, aby wyświetlić szczegóły NPC i ułatwić edycję 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()
        }
    }
}

Kolejna zaleta @ObservedRealmObject było to, że mogę użyć $ notacji, aby zainicjować szybki zapis, aby pole notatek było edytowalne. Użytkownicy mogą dotknąć i po prostu dodać więcej notatek, a Realm po prostu zapisze zmiany. Nie ma potrzeby oddzielnego widoku edycji ani otwierania wyraźnej transakcji zapisu w celu aktualizacji notatek.

W tym momencie miałem działającą aplikację i mogłem ją łatwo wysłać.

Ale… pomyślałem.

Jedną z rzeczy, które uwielbiałem w grach RPG z otwartym światem, było odtwarzanie ich jako różnych postaci i z różnymi wyborami. Więc może chciałbym odtworzyć Elden Ring jako inna klasa. Albo - może to nie był konkretnie tracker Elden Ring, ale może mógłbym go użyć do śledzenia dowolnej gry RPG. A co z moimi grami D&D?

Jeśli chciałem śledzić wiele gier, musiałem coś dodać do mojego modelu. Potrzebowałem koncepcji czegoś w rodzaju gry lub rozgrywki.

Iteracja na modelu danych

Potrzebowałem jakiegoś obiektu, który obejmowałby NPC, Lokacje i Zadania, które były częścią tego rozgrywkę, abym mógł je oddzielić od innych rozgrywek. A jeśli to była gra?

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

W porządku! Świetny. Teraz mogę śledzić NPC, Lokalizacje i Zadania, które są w tej grze i odróżniać je od innych gier.

Obiekt Game był łatwy do wyobrażenia, ale kiedy zacząłem myśleć o @ObservedResults w moich poglądach zdałem sobie sprawę, że to już nie zadziała. @ObservedResults zwrócić wszystkie wyniki dla określonego typu obiektu. Więc gdybym chciał wyświetlać tylko NPC w tej grze, musiałbym zmienić swoje poglądy.*

  • Swift SDK w wersji 10.24.0 dodał możliwość używania składni Swift Query w @ObservedResults , który umożliwia filtrowanie wyników za pomocą where parametr. Zdecydowanie przeprowadzam refaktoryzację, aby użyć tego w przyszłej wersji! Zespół Swift SDK stale wypuszcza nowe gadżety SwiftUI.

Oh. Potrzebuję też sposobu na odróżnienie NPC w tej grze od tych z innych gier. Hrm. Teraz może nadszedł czas, aby przyjrzeć się linkowaniu wstecznemu. Po spelunkowaniu w Realm Swift SDK Docs, dodałem to do modelu NPC:

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

Teraz mogłem połączyć NPC z obiektem gry. Ale, niestety, teraz moje poglądy stają się bardziej skomplikowane.

Aktualizowanie widoków SwiftUI dla zmian modelu

Ponieważ chcę teraz tylko podzbiór moich obiektów (i to było przed @ObservedResults aktualizacja), zmieniłem widoki listy z @ObservedResults do @ObservedRealmObject , obserwując grę:

@ObservedRealmObject var game: Game

Teraz nadal korzystam z szybkiego pisania, aby dodawać i edytować NPC, Lokacje i Misje w grze, ale mój kod listy musiał się trochę zaktualizować:

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

Wciąż nieźle, ale inny poziom relacji do rozważenia. A ponieważ to nie jest użycie @ObservedResults , nie mogłem użyć implementacji Realm .searchable , ale sam musiałbym to zaimplementować. Nie jest to wielka sprawa, ale więcej pracy.

Zamrożone obiekty i dołączanie do list

Teraz do tego momentu mam działającą aplikację. Mogę wysłać to tak, jak jest. Wszystko jest nadal proste, ponieważ opakowania właściwości Realm Swift SDK wykonują całą pracę.

Ale chciałem, żeby moja aplikacja robiła więcej.

Chciałem móc dodawać Lokacje i Zadania z widoku NPC i mieć je automatycznie dodawane do NPC. Chciałem mieć możliwość przeglądania i dodawania zleceniodawcy z widoku zadań. Chciałem mieć możliwość przeglądania i dodawania NPC do lokalizacji z widoku lokalizacji.

Wszystko to wymagało dużo dopisywania do list, a kiedy zacząłem próbować robić to za pomocą szybkich zapisów po utworzeniu obiektu, zdałem sobie sprawę, że to nie zadziała. Musiałbym ręcznie przekazywać obiekty i dołączać je.

Chciałem zrobić coś takiego:

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

W tym momencie coś, co nie było dla mnie do końca oczywiste jako nowego programisty, zaczęło mi przeszkadzać. Nigdy wcześniej nie musiałem robić nic z wątkami i zamrożonymi obiektami, ale dostawałem awarie, których komunikaty o błędach sprawiły, że pomyślałem, że jest to związane z tym. Na szczęście przypomniałem sobie o napisaniu przykładowego kodu o rozmrażaniu zamrożonych obiektów, aby można było z nimi pracować w innych wątkach, więc wróciłem do dokumentacji - tym razem do strony Threading, która obejmuje zamrożone obiekty. (Więcej ulepszeń, które zespół Realm Swift SDK dodał odkąd dołączyłem do MongoDB - tak!)

Po wizycie w dokumentacji miałem coś takiego:

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

Wyglądało to dobrze, ale nadal się zawieszało. Ale dlaczego? (To wtedy przeklinałem siebie, że nie przedstawiłem dokładniejszego przykładu kodu w dokumentacji. Praca nad tą aplikacją z pewnością zaowocowała kilkoma biletami, które poprawią naszą dokumentację w kilku obszarach!)

Po przejrzeniu forów i skonsultowaniu się z wielką wyrocznią Google, natknąłem się na wątek, w którym ktoś mówił o tym problemie. Okazuje się, że musisz rozmrozić nie tylko przedmiot, do którego próbujesz dołączyć, ale także rzecz, do której próbujesz dołączyć. Może to być oczywiste dla bardziej doświadczonego programisty, ale na chwilę mnie zmyliło. Więc to, czego naprawdę potrzebowałem, to coś takiego:

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

Świetny! Problem rozwiązany. Teraz mogłem stworzyć wszystkie funkcje potrzebne do ręcznego dodawania (i usuwania, jak się okazuje) obiektów.

Wszystko inne to tylko SwiftUI

Potem wszystko, czego musiałem się nauczyć, aby tworzyć aplikację, to tylko SwiftUI, na przykład jak filtrować, jak ustawić filtry do wyboru przez użytkownika i jak zaimplementować moją własną wersję .searchable .

Zdecydowanie są rzeczy, które robię z nawigacją, które nie są optymalne. Jest kilka ulepszeń UX, które nadal chcę wprowadzić. I przełączam moją grę @ObservedRealmObject var game: Game powrót do @ObservedResults z nowymi funkcjami filtrowania pomoże w niektórych z tych ulepszeń. Ale ogólnie rzecz biorąc, opakowania właściwości Realm Swift SDK sprawiły, że implementacja tej aplikacji była na tyle prosta, że ​​nawet ja mogłem to zrobić.

W sumie zbudowałem aplikację w dwa weekendy i kilka nocy w tygodniu. Prawdopodobnie w jeden weekend tego czasu utknąłem z problemem dołączania do list, a także tworzeniem strony internetowej dla aplikacji, pobieraniem wszystkich zrzutów ekranu do przesłania do App Store i wszystkimi rzeczami „biznesowymi”, które wiążą się z byciem niezależny programista aplikacji.

Ale jestem tutaj, aby powiedzieć, że jeśli ja, mniej doświadczony programista z dokładnie jedną wcześniejszą aplikacją na moje nazwisko – i to z wieloma opiniami od mojego lidera – mogę stworzyć aplikację taką jak Shattered Ring, ty też możesz. I jest to o wiele łatwiejsze dzięki funkcjom SwiftUI + Realm Swift SDK SwiftUI. Sprawdź Szybki start SwiftUI, aby zobaczyć dobry przykład, aby zobaczyć, jakie to proste.


  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB $mergeObjects

  2. Atrybut Mongoengine creation_time w dokumencie

  3. MongoDB grupuj według godziny

  4. Znajdź w podwójnie zagnieżdżonej tablicy MongoDB

  5. MongoDB $ostatni operator potoku agregacji