核心数据与 SwiftUI MVVM 反馈
Core Data with SwiftUI MVVM Feedback
我正在寻找一种使用 MVVM 来使用 CoreData 对象的方法(放弃 @FetchRequest
)。经过试验,我得出了这个实现:
Datable.swift:
protocol Datable {
associatedtype Object: NSManagedObject
//MARK: - Mapping
static func map(from object: Object) -> Self
func map(from object: Object) -> Self
//MARK: - Entity
var object: Object {get}
//MARK: - Fetching
static var modelData: ModelData<Self> {get}
//MARK: - Writing
func save()
}
extension Datable {
static var modelData: ModelData<Self> {
return ModelData()
}
func map(from object: Object) -> Self {
return Self.map(from: object)
}
func save() {
_ = object
let viewContext = PersistenceController.shared.container.viewContext
do {
try viewContext.save()
}catch {
print(String(describing: error))
}
}
}
extension Array {
func model<T: Datable>() -> [T] {
return self.map({T.map(from: [=10=] as! T.Object)})
}
}
ModelData.swift:
class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var publishedData = CurrentValueSubject<[T], Never>([])
private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
override init() {
let fetchRequest = T.Object.fetchRequest()
fetchRequest.sortDescriptors = []
fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchController.delegate = self
do {
try fetchController.performFetch()
publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
}catch {
print(String(describing: error))
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let data = controller.fetchedObjects as? [T.Object] else {return}
self.publishedData.value = data.model()
}
}
Attempt.swift:
struct Attempt: Identifiable, Hashable {
var id: UUID?
var password: String
var timestamp: Date
var image: Data?
}
//MARK: - Datable
extension Attempt: Datable {
var object: AttemptData {
let viewContext = PersistenceController.shared.container.viewContext
let newAttemptData = AttemptData(context: viewContext)
newAttemptData.password = password
newAttemptData.timestamp = timestamp
newAttemptData.image = image
return newAttemptData
}
static func map(from object: AttemptData) -> Attempt {
return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
}
}
ViewModel.swift:
class HomeViewModel: BaseViewModel {
@Published var attempts = [Attempt]()
required init() {
super.init()
Attempt.modelData.publishedData.eraseToAnyPublisher()
.sink { [weak self] attempts in
self?.attempts = attempts
}.store(in: &cancellables)
}
}
到目前为止,这很有效,但是我想检查一下这是否是最好的方法,并在可能的情况下进行改进。请注意,我已经将 @FetchRequest
与 SwiftUI 一起使用一年多了,并决定转向 mvvm,因为我在我的所有 Storyboard 项目中都在使用它。
对于在 SwiftUI 兼容代码中包装 NSFetchedResultsController
的前沿方法,您可能需要查看 AsyncStream。
但是,@FetchRequest
目前作为 DynamicProperty
实现,因此如果您也这样做,它将允许从 update
中的 @Environment
访问托管对象上下文在 View
上调用 body
之前在 DynamicProperty
上调用的 func。您可以在内部使用 @StateObject
作为 FRC 委托。
谨慎使用 MVVM,因为它使用对象,而 SwiftUI 旨在与值类型一起使用,以消除对象可能导致的一致性错误。请参阅文档 Choosing Between Structures and Classes。如果您在 SwiftUI 之上构建一个 MVVM 对象层,您将面临重新引入这些错误的风险。您最好按照设计的方式使用 View
数据结构,并在编写遗留视图控制器时留下 MVVM。但老实说,如果你学习了子视图控制器模式并理解了响应链,那么就根本不需要 MVVM 视图模型对象了。
仅供参考,在使用 Combine 的 ObservableObject
时,我们不会 sink
管道或使用 cancellables
。相反,assign
the output of the pipeline to an @Published
. However, if you aren't using CombineLatest
,然后重新考虑是否真的应该使用 Combine。
我正在寻找一种使用 MVVM 来使用 CoreData 对象的方法(放弃 @FetchRequest
)。经过试验,我得出了这个实现:
Datable.swift:
protocol Datable {
associatedtype Object: NSManagedObject
//MARK: - Mapping
static func map(from object: Object) -> Self
func map(from object: Object) -> Self
//MARK: - Entity
var object: Object {get}
//MARK: - Fetching
static var modelData: ModelData<Self> {get}
//MARK: - Writing
func save()
}
extension Datable {
static var modelData: ModelData<Self> {
return ModelData()
}
func map(from object: Object) -> Self {
return Self.map(from: object)
}
func save() {
_ = object
let viewContext = PersistenceController.shared.container.viewContext
do {
try viewContext.save()
}catch {
print(String(describing: error))
}
}
}
extension Array {
func model<T: Datable>() -> [T] {
return self.map({T.map(from: [=10=] as! T.Object)})
}
}
ModelData.swift:
class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var publishedData = CurrentValueSubject<[T], Never>([])
private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
override init() {
let fetchRequest = T.Object.fetchRequest()
fetchRequest.sortDescriptors = []
fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchController.delegate = self
do {
try fetchController.performFetch()
publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
}catch {
print(String(describing: error))
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let data = controller.fetchedObjects as? [T.Object] else {return}
self.publishedData.value = data.model()
}
}
Attempt.swift:
struct Attempt: Identifiable, Hashable {
var id: UUID?
var password: String
var timestamp: Date
var image: Data?
}
//MARK: - Datable
extension Attempt: Datable {
var object: AttemptData {
let viewContext = PersistenceController.shared.container.viewContext
let newAttemptData = AttemptData(context: viewContext)
newAttemptData.password = password
newAttemptData.timestamp = timestamp
newAttemptData.image = image
return newAttemptData
}
static func map(from object: AttemptData) -> Attempt {
return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
}
}
ViewModel.swift:
class HomeViewModel: BaseViewModel {
@Published var attempts = [Attempt]()
required init() {
super.init()
Attempt.modelData.publishedData.eraseToAnyPublisher()
.sink { [weak self] attempts in
self?.attempts = attempts
}.store(in: &cancellables)
}
}
到目前为止,这很有效,但是我想检查一下这是否是最好的方法,并在可能的情况下进行改进。请注意,我已经将 @FetchRequest
与 SwiftUI 一起使用一年多了,并决定转向 mvvm,因为我在我的所有 Storyboard 项目中都在使用它。
对于在 SwiftUI 兼容代码中包装 NSFetchedResultsController
的前沿方法,您可能需要查看 AsyncStream。
但是,@FetchRequest
目前作为 DynamicProperty
实现,因此如果您也这样做,它将允许从 update
中的 @Environment
访问托管对象上下文在 View
上调用 body
之前在 DynamicProperty
上调用的 func。您可以在内部使用 @StateObject
作为 FRC 委托。
谨慎使用 MVVM,因为它使用对象,而 SwiftUI 旨在与值类型一起使用,以消除对象可能导致的一致性错误。请参阅文档 Choosing Between Structures and Classes。如果您在 SwiftUI 之上构建一个 MVVM 对象层,您将面临重新引入这些错误的风险。您最好按照设计的方式使用 View
数据结构,并在编写遗留视图控制器时留下 MVVM。但老实说,如果你学习了子视图控制器模式并理解了响应链,那么就根本不需要 MVVM 视图模型对象了。
仅供参考,在使用 Combine 的 ObservableObject
时,我们不会 sink
管道或使用 cancellables
。相反,assign
the output of the pipeline to an @Published
. However, if you aren't using CombineLatest
,然后重新考虑是否真的应该使用 Combine。