MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Как написах приложение за най-висока класация за една седмица с Realm и SwiftUI

Създаване на Elden Ring Quest Tracker

Обичах Skyrim. С удоволствие прекарах няколкостотин часа в игра и преиграване. Така че, когато наскоро чух за нова игра, Skyrim от 2020-те , трябваше да го купя. Така започва моята сага с Elden Ring, масивната RPG с отворен свят с напътствия от Джордж Р. Р. Мартин.

В рамките на първия час от играта научих колко брутални могат да бъдат Souls игрите. Промъкнах се в интересни пещери край скалите, само за да умра толкова вътре, че не можах да извадя трупа си.

Изгубих всичките си руни.

Зяпнах в страхопочитан учуд, докато слизах с асансьора надолу към река Сиофра, само за да открия, че ужасната смърт ме очаква, далеч от най-близкото място за благодат. Смело избягах, преди да успея да умра отново.

Срещнах призрачни фигури и завладяващи NPC, които ме изкушиха с няколко реда диалог... които веднага забравих веднага щом се наложи.

10/10, силно препоръчително.

Едно нещо по-специално за Elden Ring ме дразнеше - нямаше quest tracker. Винаги добър спорт, отворих документ за бележки на моя iPhone. Разбира се, това не беше почти достатъчно.

Имах нужда от приложение, което да ми помогне да проследявам подробности за възпроизвеждането на RPG. Нищо в App Store не отговаряше на това, което търсех, така че очевидно ще трябва да го напиша. Нарича се Shattered Ring и вече е наличен в App Store.

Технически избор

През деня пиша документация за Realm Swift SDK. Наскоро бях написал приложение за шаблони SwiftUI за Realm, за да предоставя на разработчиците начален шаблон за SwiftUI, върху който да надграждат, допълнен с потоци за влизане. Екипът на Realm Swift SDK непрекъснато доставя функции на SwiftUI, което го превърна - според вероятно предубеденото ми мнение - мъртва проста отправна точка за разработка на приложения.

Исках нещо, което бих могъл да създам супер бързо – отчасти, за да мога да се върна към игра на Elden Ring, вместо да пиша приложение, и отчасти да победя други приложения на пазара, докато всички все още говорят за Elden Ring. Не можех да отнеме месеци, за да създам това приложение. Вчера го исках. Realm + SwiftUI щяха да направят това възможно.

Моделиране на данни

Знаех, че искам да проследявам куестове в играта. Моделът на мисията беше лесен:

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

Всичко, от което наистина се нуждаех, беше име, bool за превключване, когато мисията приключи, поле за бележки и уникален идентификатор.

Докато мислех за моя геймплей обаче, осъзнах, че не се нуждая само от куестове - исках също да следя местоположенията. Натъкнах се на – и бързо излязох, когато започнах да умирам – на толкова много страхотни места, които вероятно имаха интересни герои без играч (NPC) и страхотна плячка. Исках да мога да следя дали съм изчистил местоположение или просто избягах от него, за да мога да се сетя да се върна по-късно и да го проверя, след като имам по-добра екипировка и повече способности. Така че добавих обект за местоположение:

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

Хмм Това много приличаше на модела на мисията. Наистина ли имах нужда от отделен обект? Тогава си помислих за едно от първите места, които посетих – църквата Еле – която имаше ковашка наковалня. Всъщност все още не бях направил нищо, за да подобря екипировката си, но може да е хубаво да знам кои места са имали ковашка наковалня в бъдеще, когато исках да отида някъде, за да направя ъпгрейд. Така че добавих още един bool:

@Persisted var hasSmithAnvil = false

Тогава си помислих как на същото място има и търговец. Може би искам да знам в бъдеще дали дадено местоположение има търговец. Така че добавих още един bool:

@Persisted var hasMerchant = false

Страхотен! Обектът за местоположение е сортиран.

Но… имаше нещо друго. Непрекъснато получавах всички тези интересни истории от NPC. И какво се случи, когато завърших една мисия - трябва ли да се върна при NPC, за да взема награда? Това ще изисква от мен да знам кой ми е дал мисията и къде се намират. Време е да добавим трети модел, NPC, който ще свърже всичко заедно:

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

Страхотен! Сега можех да проследявам NPC. Бих могъл да добавя бележки, които да ми помогнат да следя тези интересни истории, докато чаках да видя какво ще се развие. Мога да свържа куестове и локации с NPC. След добавянето на този обект стана очевидно, че това е обектът, който свързва другите. NPC са на места. Но от някакво четене онлайн знаех, че понякога NPC се движат в играта, така че местата ще трябва да поддържат множество вписвания - оттук и списъкът. NPC дават куестове. Но това също трябва да бъде списък, защото първият NPC, който срещнах, ми даде повече от един куест. Варе, точно извън Shattered Graveyard, когато влезете за първи път в играта, ми каза да „Следвам нишките на благодатта“ и „отправете се в замъка“. Добре, подредено!

Сега мога да използвам моите обекти с обвивки на свойства на SwiftUI, за да започна да създавам потребителския интерфейс.

SwiftUI Views + Magical Property Wrappers на Realm

Тъй като всичко зависи от NPC, бих започнал с изгледите на NPC. @ObservedResults property wrapper ви дава лесен начин да направите това.

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

Сега можех да преглеждам списък с всички NPC, имах автоматично onDelete действие за премахване на NPC и може да добави реализацията на Realm на .searchable когато бях готов да добавя търсене и филтриране. И това беше основно един ред, за да го свържа с моя модел на данни. Споменах ли, че Realm + SwiftUI е невероятен? Беше достатъчно лесно да се направи същото с Locations и Quests и да се даде възможност на потребителите на приложения да се гмурнат в своите данни по всякакъв път.

Тогава моят подробен изглед на NPC може да работи с @ObservedRealmObject обвивка на свойства за показване на подробностите за NPC и улесняване на редактирането на 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()
        }
    }
}

Друго предимство на @ObservedRealmObject беше, че мога да използвам $ нотация, за да започне бързо писане, така че полето за бележки просто ще може да се редактира. Потребителите могат да докоснат и просто да добавят още бележки, а Realm просто ще запази промените. Няма нужда от отделен изглед за редактиране или от отваряне на изрична транзакция за запис, за да актуализирате бележките.

В този момент имах работещо приложение и лесно можех да го изпратя.

Но… имах мисъл.

Едно от нещата, които обичах в RPG игрите с отворен свят, беше да ги преигравам като различни герои и с различен избор. Така че може би бих искал да преиграя Elden Ring като различен клас. Или - може би това не беше конкретно тракер на Elden Ring, но може би бих могъл да го използвам за проследяване на всяка RPG игра. Какво ще кажете за моите D&D игри?

Ако исках да проследя няколко игри, трябваше да добавя нещо към моя модел. Имах нужда от концепция за нещо като игра или игра.

Итерация на модела на данни

Имах нужда от някакъв обект, който да обхване NPC, местоположения и мисии, които бяха част от това playthrough, за да мога да ги държа отделно от другите игри. И така, ако това беше Игра?

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

Добре! Страхотен. Сега мога да проследявам NPC, местоположенията и мисии, които са в тази игра, и да ги различавам от другите игри.

Обектът Game беше лесен за замисляне, но когато започнах да мисля за @ObservedResults според моите виждания разбрах, че това вече няма да работи. @ObservedResults връща всички резултати за конкретен тип обект. Така че, ако исках да показвам само NPC за тази игра, ще трябва да променя изгледите си.*

  • Swift SDK версия 10.24.0 добави възможността за използване на синтаксис на Swift Query в @ObservedResults , което ви позволява да филтрирате резултатите с помощта на where параметър. Определено преработвам, за да използвам това в бъдеща версия! Екипът на Swift SDK непрекъснато пуска нови екстри на SwiftUI.

ох. Освен това ще ми трябва начин да разгранича NPC в тази игра от тези в други игри. Хрм Сега може би е време да разгледаме обратните връзки. След като се запознах с Realm Swift SDK Docs, добавих това към NPC модела:

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

Сега можех да свържа обратно NPC към обекта на играта. Но, уви, сега вижданията ми стават по-сложни.

Актуализиране на изгледите на SwiftUI за промените в модела

Тъй като сега искам само подмножество от моите обекти (а това беше преди @ObservedResults актуализиране), превключих изгледите на списъка си от @ObservedResults до @ObservedRealmObject , наблюдавайки играта:

@ObservedRealmObject var game: Game

Сега все още получавам предимствата на бързото писане за добавяне и редактиране на NPC, местоположения и мисии в играта, но кодът на моя списък трябваше да се актуализира малко:

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

Все още не е лошо, но друго ниво на взаимоотношения, което трябва да разгледате. И тъй като това не използва @ObservedResults , не можах да използвам реализацията на Realm на .searchable , но ще трябва да го прилагам сам. Не е голяма работа, но повече работа.

Замразени обекти и добавяне към списъци

Сега, до този момент, имам работещо приложение. Мога да изпратя това както е. Всичко все още е просто с обвивките за свойства на Realm Swift SDK, които вършат цялата работа.

Но исках приложението ми да направи повече.

Исках да мога да добавя локации и мисии от изгледа на NPC и да ги добавя автоматично към NPC. И исках да мога да виждам и добавям quest-giver от изгледа на quest. И исках да мога да преглеждам и добавям NPC към местоположения от изгледа за местоположение.

Всичко това изискваше много добавяне към списъци и когато започнах да се опитвам да правя това с бързи записи след създаване на обекта, разбрах, че това няма да работи. Ще трябва ръчно да предавам обекти и да ги добавя.

Това, което исках, беше да направя нещо подобно:

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

Това е мястото, където нещо, което не беше съвсем очевидно за мен като нов разработчик, започна да ми пречи. Никога преди не ми се е налагало да правя нищо с нишки и замразени обекти, но получавах сривове, чиито съобщения за грешка ме накараха да мисля, че това е свързано с това. За щастие си спомних как написах пример за код за размразяване на замразени обекти, за да можете да работите с тях в други нишки, така че се върнах към документите - този път към страницата Threading, която обхваща замразени обекти. (Още подобрения, които екипът на Realm Swift SDK добави, откакто се присъединих към MongoDB – да!)

След като посетих документите, имах нещо подобно:

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

Това изглеждаше правилно, но все още се срива. Но защо? (Тогава се проклех, че не предоставих по-подробен пример за код в документите. Работата по това приложение определено доведе до някои билети за подобряване на нашата документация в няколко области!)

След като се разпитах във форумите и се консултирах с великия оракул Google, попаднах на тема, в която някой говореше по този проблем. Оказва се, че трябва да размразите не само обекта, към който се опитвате да добавите, но и нещото, което се опитвате да добавите. Това може да е очевидно за по-опитен разработчик, но ме препъна за известно време. Така че това, което наистина ми трябваше, беше нещо подобно:

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

Страхотен! Проблема решен. Сега можех да създам всички необходими функции, за да се справя ръчно с добавянето (и премахването, както се оказа) на обекти.

Всичко останало е просто SwiftUI

След това всичко останало, което трябваше да науча, за да създам приложението, беше просто SwiftUI, като например как да филтрирам, как да направя филтрите избираеми от потребителя и как да внедря моя собствена версия на .searchable .

Определено има някои неща, които правя с навигацията, които не са оптимални. Има някои подобрения на UX, които все още искам да направя. И превключване на моята @ObservedRealmObject var game: Game обратно към @ObservedResults с новите неща за филтриране ще помогне за някои от тези подобрения. Но като цяло обвивките за свойства на Realm Swift SDK направиха внедряването на това приложение достатъчно лесно, че дори аз мога да го направя.

Като цяло създадох приложението за два уикенда и няколко делнични вечери. Вероятно един уикенд от онова време бях заседнал с проблема с добавянето към списъците, а също и като направих уебсайт за приложението, получавах всички екранни снимки за изпращане в App Store и всички „бизнес“ неща, които съпътстват това, че съм инди разработчик на приложения.

Но съм тук, за да ви кажа, че ако аз, по-малко опитен разработчик с точно едно предишно приложение на моето име – и това с много обратна връзка от моя водещ – мога да направя приложение като Shattered Ring, вие също можете. И е много по-лесно с SwiftUI + SwiftUI функциите на Realm Swift SDK. Вижте бързия старт на SwiftUI за добър пример, за да видите колко е лесно.


  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB:изведете 'id' вместо '_id'

  2. Как да моделираме система за гласуване с харесвания с MongoDB

  3. Най-добра практика за поддържане на mgo сесия

  4. root потребител на MongoDB

  5. Актуализиране на полето със стойност на друго поле в документа