使用 Realm 和 SwiftUI 预览
在此页面上
Overview
SwiftUI 预览是开发过程中非常有用的工具。您可以通过以下几种方式在 SwiftUI 预览中使用 Realm 数据:
初始化各个对象以在详细视图中使用
有条件地使用对象数组代替
@ObservedResults
创建包含预览数据的 Realm
SwiftUI Preview 调试可能比较难理解,因此我们还有一些关于在 SwiftUI Preview 中调试持久化 Realm 问题的提示。
初始化详细视图的对象
在最简单的情况下,您可以将 SwiftUI 预览与一个或多个对象一起使用,这些对象使用可在初始化时直接设置的 Realm 属性。您可能希望在预览详细视图时执行此操作。考虑 DoggoDB 的 DogDetailView
:
struct DogDetailView: View { var dog: Dog var body: some View { VStack { Text(dog.name) .font(.title2) Text("\(dog.name) is a \(dog.breed)") AsyncImage(url: dog.profileImageUrl) { image in image.resizable() } placeholder: { ProgressView() } .aspectRatio(contentMode: .fit) .frame(width: 150, height: 150) Text("Favorite toy: \(dog.favoriteToy)") } } }
为模型对象创建扩展。扩展位置取决于代码库的惯例。您可以将扩展直接放在模型文件中,为示例数据设置专门的目录,或者在代码库中使用其他惯例。
在此扩展中,使用 static let
初始化一个或多个 Realm 对象:
extension Dog { static let dog1 = Dog(value: ["name": "Lita", "breed": "Lab mix", "weight": 27, "favoriteToy": "Squeaky duck", "profileImageUrl": "https://www.corporaterunaways.com/images/2021/04/lita-768x768.jpeg"]) static let dog2 = Dog(value: ["name": "Maui", "breed": "English Springer Spaniel", "weight": 42, "favoriteToy": "Wubba", "profileImageUrl": "https://www.corporaterunaways.com/images/2021/04/maui_with_leaf-768x576.jpeg"]) static let dog3 = Dog(value: ["name": "Ben", "breed": "Border Collie mix", "weight": 48, "favoriteToy": "Frisbee", "profileImageUrl": "https://www.corporaterunaways.com/images/2012/03/ben-630x420.jpg"]) }
在此示例中,我们使用一个值来初始化对象。 仅当模型包含可直接初始化的属性时,才能使用值初始化对象。 如果您的模型对象包含仅在写事务(write transaction)中可变的属性(例如List属性),则必须改为创建一个域来与SwiftUI预览一起使用。
将对象初始化为模型类的扩展后,您就可以在 SwiftUI 预览中加以使用。您可以将对象直接传递给预览中的视图:
struct DogDetailView_Previews: PreviewProvider { static var previews: some View { NavigationView { DogDetailView(dog: Dog.dog1) } } }
有条件地在列表视图中使用 observedResults
在列表视图中使用@ObservedResults时,这会隐式打开一个域并对其进行查询。 为此,在预览版中,您需要一个由 data 填充的域 。 作为替代方案,您可以在预览中有条件地使用静态大量,并仅在运行应用时使用@ObservedResults
变量。
您可以通过多种方式实现这一目的,但为了使代码更易于阅读和理解,我们将创建 EnvironmentValue
来检测应用程序是否在预览版中运行:
import Foundation import SwiftUI public extension EnvironmentValues { var isPreview: Bool { #if DEBUG return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" #else return false #endif } }
然后,可以将其用作视图中的环境值,并根据我们是否处于预览状态有条件地更改使用的变量。
此示例基于我们上面定义的 Dog 扩展而构建。 我们将在 Dog 扩展中创建一个dogArray
作为static let
,并包含已创建的列项对象:
static let dogArray = [dog1, dog2, dog3]
然后,当我们遍历列表时,如果是在预览中运行,则使用静态 dogArray
,如果不是在预览中运行,则使用 @ObservedResults
查询。
struct DogsView: View { var isPreview (\.isPreview) Dog.self) var dogs ( var previewDogs = Dog.dogArray var body: some View { NavigationView { VStack { List { if isPreview { ForEach(previewDogs) { dog in DogRow(dog: dog) } } else { ForEach(dogs) { dog in DogRow(dog: dog) }.onDelete(perform: $dogs.remove) } } ... More View code
这样做的好处是轻量,不需要持久化任何数据,但缺点是会使视图代码变得更加冗长。如果您喜欢更简洁的视图代码,则可以用预览中使用的数据创建 Realm。
使用预览数据创建 Realm
在某些情况下,在 SwiftUI 预览版中查看 Realm 数据的唯一选择是创建包含数据的 Realm。在填充只能在写事务期间填充的属性时,可以执行此操作,而不是直接使用值(例如列表或 MutableSet)进行初始化。如果视图依赖于从其他视图传入的更复杂的对象层次结构,您可能还需要执行此操作。
不过,直接使用 Realm 会将状态注入 SwiftUI 预览,而这可能带来一些弊端。无论您使用的是 Realm 还是 CoreData,有状态的 SwiftUI 预览都可能导致以下问题:
由于重复运行 Realm 文件创建步骤,导致出现意外或重复数据
更改模型时需要在“SwiftUI 预览”中执行迁移
与在视图内更改状态有关的潜在问题
与“SwiftUI 预览”中未以可见方式显示的问题相关的无法解释的崩溃或性能问题
您可以通过以下提示来避免或解决其中一些问题:
您可以在模型扩展中为 Realm 创建静态变量。 您可以在此处进行 Realm 填充工作。 在我们的示例中,我们创建一个Person
并将一些Dog
对象附加到dogs
List 属性。 此示例以上面的示例为基础,在上面的示例中,我们在 Dog 扩展中初始化了一些 Dog 对象。
我们将创建一个 Person
扩展,并在该扩展中创建一个 Person
对象。然后,我们将通过添加刚刚创建的 Person
并附加 Dog
扩展中的示例 Dog
对象来创建 previewRealm
。
为了避免多次添加这些对象,我们添加了一个检查功能,通过查询 Person 对象并检查计数是否为 1 来查看 Person 是否已存在。如果 Realm 包含 Person,我们就可以在 SwiftUI 预览版中使用它。如果没有,我们就添加数据。
static var previewRealm: Realm { var realm: Realm let identifier = "previewRealm" let config = Realm.Configuration(inMemoryIdentifier: identifier) do { realm = try Realm(configuration: config) // Check to see whether the in-memory realm already contains a Person. // If it does, we'll just return the existing realm. // If it doesn't, we'll add a Person append the Dogs. let realmObjects = realm.objects(Person.self) if realmObjects.count == 1 { return realm } else { try realm.write { realm.add(person) person.dogs.append(objectsIn: [Dog.dog1, Dog.dog2, Dog.dog3]) } return realm } } catch let error { fatalError("Can't bootstrap item data: \(error.localizedDescription)") } }
要在 SwiftUI 预览版中使用它,ProfileView 代码需要一个配置文件。这是 Person 对象的投影。在预览版中,我们可以获取 Realm,查询它的配置文件,并将其传递给视图:
struct ProfileView_Previews: PreviewProvider { static var previews: some View { let realm = Person.previewRealm let profile = realm.objects(Profile.self) ProfileView(profile: profile.first!) } }
如果您没有需要传入 Realm 对象的视图,而是使用 @ObservedResults
来查询 Realm 或以其他方式处理现有 Realm,则可以将该 Realm 作为环境值注入到视图中:
struct SomeListView_Previews: PreviewProvider { static var previews: some View { SomeListView() .environment(\.realm, Person.previewRealm) } }
在内存中使用 Realm
在可能的情况下,使用内存中的 Realm 来解决在“SwiftUI 预览”中使用数据库可能产生的一些与状态相关的问题。
在初始化域时使用 inMemoryIdentifier 配置属性。
static var previewRealm: Realm { var realm: Realm let identifier = "previewRealm" let config = Realm.Configuration(inMemoryIdentifier: identifier) do { realm = try Realm(configuration: config) // ... Add data to realm
注意
为“SwiftUI 预览”初始化域时,请勿使用 delete域IfMigrationNeeded 配置属性。由于 Apple 实现“SwiftUI 预览”的方式,使用此属性绕过迁移问题会导致“SwiftUI 预览”崩溃。
删除 SwiftUI 预览版
如果您遇到其他与状态有关的“SwiftUI 预览”问题,例如由于需要迁移而无法在“预览”中加载 Realm,您可以执行一些操作来删除缓存的“预览”数据。
Apple 建议的修复方法是关闭 Xcode,然后使用命令行删除所有现有的“SwiftUI 预览”数据。
关闭 Xcode。
在命令行中运行:
xcrun simctl --set previews delete all
运行此命令后,数据可能会持久化。这可能是由于 Xcode 由于 Preview 中的某些内容而保留了引用并且无法删除它。您还可以尝试以下步骤来解决问题:
为不同的模拟器构建
重新启动计算机并重新运行
xcrun simctl --set previews delete all
直接删除已存储的预览数据。此数据存储在
~/Library/Developer/Xcode/UserData/Previews
中。
获取有关 SwiftUI 预览版崩溃的详细信息
如果在使用 Realm 时遇到不明原因的 SwiftUI 预览版崩溃,请首先尝试在模拟器上运行应用程序。模拟器提供的错误消息和日志使查找和诊断问题变得更容易。如果可以在模拟器中调试问题,这是最简单的方法。
如果无法在模拟器中复制 SwiftUI Preview 崩溃,您可以查看 SwiftUI Preview 应用程序的崩溃日志。可在 ~/Library/Logs/DiagnosticReports/
中找到这些日志。这些日志有时会延迟出现,因此如果没有立即看到相关日志,请在崩溃后等待几分钟。
在预览中使用同步 Realm
如果您的应用程序使用 Atlas Device Sync,您可能会想知道如何在“SwiftUI 预览”中使用同步的 Realm。更好的做法是使用静态对象或本地 Realm,为“SwiftUI 预览”填充数据。
在我们的示例应用程序中,我们可以预览与 Device Sync 相关的视图 LoginView,而根本不需要使用 Realm:
struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() } }
由于我们只查看静态用户界面,因此无需担心 SyncContentView 是否包含显示 LoginView 或转到 OpenSyncedRealmView 的逻辑。我们还可以跳过预览 OpenSyncedRealmView,因为其只是处理与打开同步 Realm 和为 DogsView 填充相关的逻辑。因此我们想在“预览”中查看的下一个视图是 DogsView。
幸运的是,使用 Realm 时,使用 Realm 的代码并不关心 Realm 是否使用 Device Sync,您可以以相同的方式使用 Realm。 因此,我们可以在 SwiftUI 预览版中使用与上述示例中创建的相同的本地 Realm。
struct DogsView_Previews: PreviewProvider { static var previews: some View { let realm = Person.previewRealm DogsView() .environment(\.realm, realm) } }