我如何在一周内用 Realm 和 SwiftUI 写出一个排行榜冠军的应用程序
构建 Elden Ring Quest Tracker
我喜欢天际。我很高兴地花了几百个小时玩和重播它。因此,当我最近听说有一款新游戏《2020 年代的天际》时,我不得不购买它。因此,我的传奇故事开始于 Elden Ring,这是一款由 George R.R. Martin 提供故事指导的大型开放世界 RPG。
在游戏的第一个小时内,我就了解了灵魂游戏的残酷程度。我爬进了有趣的悬崖边洞穴,结果死在里面太深了,以至于我无法取回我的尸体。
我失去了所有的符文。
当我乘电梯下到赛弗拉河时,我惊讶地目瞪口呆,却发现可怕的死亡在等着我,远离最近的恩典之地。在我再次死去之前,我勇敢地逃跑了。
我遇到了幽灵般的人物和迷人的 NPC,他们用几行对话来诱惑我......我一需要就马上忘记了。
10/10,强烈推荐。
关于 Elden Ring 的一件事特别惹恼了我 - 没有任务追踪器。永远的好运动,我在我的 iPhone 上打开了一个 Notes 文档。当然,这还远远不够。
我需要一个应用程序来帮助我跟踪 RPG 游戏的详细信息。 App Store 上的任何内容都没有真正符合我的要求,所以显然我需要编写它。它被称为 Shattered Ring,它现在在 App Store 上可用。
[中排名第一的破碎环](https://res.cloudinary.com/practicaldev/image/fetch/s--cmRAdiqK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https: //dev-to-uploads.s3.amazonaws.com/uploads/articles/22ea74u4ahd0hzfbisv8.jpeg)
技术选择
白天,我为Realm Swift SDK编写文档。我最近为 Realm 编写了一个 SwiftUI 模板应用程序,为开发人员提供了一个 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 = ""
}
进入全屏模式 退出全屏模式
我真正需要的只是一个名称、一个在任务完成时切换的布尔值、一个注释字段和一个唯一标识符。
然而,当我考虑我的游戏玩法时,我意识到我不仅需要任务——我还想跟踪位置。我偶然发现了——当我开始死亡时很快就离开了——这么多很酷的地方,可能有有趣的非玩家角色 (NPC) 和很棒的战利品。我希望能够跟踪我是否已经清除了一个位置,或者只是逃离了它,这样我就可以记得在我有更好的装备和更多的能力后回去检查它。所以我添加了一个位置对象:
class Location: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var isCleared = false
@Persisted var notes = ""
}
进入全屏模式 退出全屏模式
唔。这看起来很像任务模型。我真的需要一个单独的对象吗?然后我想到了我参观的早期地点之一——埃莱教堂——那里有一个铁砧。我实际上还没有做任何事情来改进我的装备,但是当我想去某个地方进行升级时,知道将来哪些地方有史密斯铁砧可能会很好。所以我添加了另一个布尔值:
@Persisted var hasSmithAnvil = false
然后我想到了同一个地方怎么也有一个商人。我将来可能想知道某个位置是否有商家。所以我添加了另一个布尔值:
@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 给了我不止一个任务。 Varre,当你第一次进入游戏时,就在破碎的墓地外面,告诉我“跟随恩典的线索”和“去城堡”。对了,整理!
现在我可以将我的对象与 SwiftUI 属性包装器一起使用来开始创建 UI。
SwiftUI 视图 + Realm 的神奇属性包装器
由于一切都与 NPC 无关,我将从 NPC 视图开始。@ObservedResults
属性包装器为您提供了一种简单的方法来执行此操作。
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 游戏呢?
如果我想跟踪多个游戏,我需要在我的模型中添加一些东西。我需要一个游戏或通关之类的概念。
迭代数据模型
我需要一些对象来包含 this 通关中的 NPC、地点和任务,这样我就可以将它们与其他通关区分开来。那么如果那是一个游戏呢?
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在
@ObservedResults
中添加了使用 Swift Query 语法的功能,这允许您使用where
参数过滤结果。我肯定会重构以在未来的版本中使用它! Swift SDK 团队一直在稳步发布新的 SwiftUI 好东西。
哦。另外,我需要一种方法来区分这个游戏中的 NPC 和其他游戏中的 NPC。人力资源管理系统。现在可能是研究反向链接的时候了。在深入了解Realm Swift SDK Docs之后,我将其添加到 NPC 模型中:
@Persisted(originProperty: "npcs") var npcInGame: LinkingObjects<Game>
现在我可以将 NPC 反向链接到 Game 对象。但是,唉,现在我的观点变得更加复杂了。
更新模型更改的 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
,我不能使用.searchable
的 Realm 实现,但必须自己实现它。没什么大不了的,但更多的工作。
冻结对象并附加到列表
现在,到目前为止,我有一个工作应用程序。我可以按原样发货。使用 Realm Swift SDK 属性包装器完成所有工作,一切仍然很简单。
但我希望我的应用能够做得更多。
我希望能够从 NPC 视图中添加位置和任务,并将它们自动附加到 NPC。我希望能够从任务视图中查看和添加任务给予者。我希望能够从位置视图查看 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)
}
}
进入全屏模式 退出全屏模式
这就是我作为一个新开发人员并不完全清楚的事情开始妨碍我的地方。我以前从来没有真正需要对线程和冻结对象做任何事情,但是我遇到了崩溃,其错误消息让我认为这与此有关。幸运的是,我记得写了一个关于解冻冻结对象的代码示例,这样你就可以在其他线程上使用它们,所以它回到了文档——这次是线程页面,它涵盖了 Frozen Objects。 (自从我加入 MongoDB 以来,Realm Swift SDK 团队添加了更多改进 - 耶!)
访问文档后,我有这样的事情:
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)
}
}
进入全屏模式 退出全屏模式
看起来不错,但仍然崩溃。但为什么? (这是我诅咒自己没有在文档中提供更全面的代码示例的时候。开发这个应用程序肯定会产生一些票来改进我们在一些领域的文档!)
在论坛中探索并咨询了伟大的 oracle 谷歌之后,我遇到了一个线程,其中有人在谈论这个问题。事实证明,你不仅要解冻你要附加的对象,还要解冻你要附加的东西。对于更有经验的开发人员来说,这可能是显而易见的,但它让我绊倒了一段时间。所以我真正需要的是这样的:
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
版本。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--nNKVAj5c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// dev-to-uploads.s3.amazonaws.com/uploads/articles/0ygv6xprzql72sksikdp.png)
肯定有一些事情我正在做的导航不是最佳的。我仍然想做一些用户体验改进。使用新的过滤器将我的@ObservedRealmObject var game: Game
切换回@ObservedResults
将有助于其中的一些改进。但总的来说,Realm Swift SDK 属性包装器让这个应用程序的实现变得非常简单,即使我也能做到。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--Y1r9PR-Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https ://dev-to-uploads.s3.amazonaws.com/uploads/articles/lh7wr8ulnx0bg3d5mxeb.png)
总的来说,我在两个周末和几个工作日的晚上构建了这个应用程序。可能是那个时候的一个周末,我被附加到列表的问题困住了,还为这个应用程序制作了一个网站,把所有的截图提交到 App Store,以及所有伴随着成为一个“商业”的东西。独立应用开发者。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--xoFkwXkl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:/ /dev-to-uploads.s3.amazonaws.com/uploads/articles/towygxvozomuifm9t8ph.png)
但我在这里告诉你,如果我,一个经验不足的开发人员,我的名字之前只有一个应用程序 - 并且我的领导有很多反馈 - 可以制作像Shattered Ring这样的应用程序,你可以, 也。使用 SwiftUI +Realm Swift SDK 的 SwiftUI 功能会容易得多。查看SwiftUI 快速入门以获得一个很好的示例,看看它是多么容易。
更多推荐
所有评论(0)