I am looking for a way to use CoreData Objects using MVVM (ditching #FetchRequest). After experimenting, I have arrived at the following implementation:
Package URL: https://github.com/TimmysApp/DataStruct
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: $0 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)
}
}
So far this is working like a charm, however I wanted to check if this is the best way to do it, and improve it if possible. Please note that I have been using #FetchRequest with SwiftUI for over a year now and decided to move to MVVM since I am using it in all my Storyboard projects.
For a cutting edge way to wrap the NSFetchedResultsController in SwiftUI compatible code you might want to take a look at AsyncStream.
However, #FetchRequest currently is implemented as a DynamicProperty so if you did that too it would allow access the managed object context from the #Environment in the update func which is called on the DynamicProperty before body is called on the View. You can use an #StateObject internally as the FRC delegate.
Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.
And FYI, when using Combine's ObservableObject we don't sink the pipeline or use cancellables. Instead, assign the output of the pipeline to an #Published. However, if you aren't using CombineLatest, then perhaps reconsider if you should really be using Combine at all.
Related
Is it common to have only one ViewModel to manage all CoreData entities?
For instance, in the following example, I have three Core Data entities, Car, CarService and ServiceRecord where Car has many carServices and each CarService has many serviceRecords. Everything is working fine but I feel like my CarViewModel file is growing and growing and I'm not sure if this is really a good MVVM practice.
As you can see in the following example I'm using CarViewModel to fetch data from Core Data and passing it around SwiftUI views. Again, everything is working fine but I feel like I'm missing something.
Can someone please share how you usually structure your code when using MVVM + CoreData + SwiftUI. Do you handle everything from one ViewModel as shown below or do you usually have a ViewModel for each entity? If a viewModel per each entity is the best option, what method do you use to pass viewModels around SwiftUI views?
CoreDataManager
class CoreDataManager{
static let instance = CoreDataManager()
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
func setupContainer()->NSPersistentContainer{
// code to setup container...
return container
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
CarViewModel
class CarViewModel: ObservableObject{
let manager: CoreDataManager
#Published var cars: [Car] = []
#Published var carServices: [CarService] = []
#Published var serviceRecords: [ServiceRecord] = []
init(coreDataManager: CoreDataManager = .instance){
self.manager = coreDataManager
// getCars() etc.
}
// CREATIONS
func addCar(name:String){}
func addService(name:String, cost: Double){}
func createRecord(name:String, cost: Double){}
// DELETES
func deleteCar(){}
func deleteCarService(){}
func deleteServiceRecord(){}
// UPDATES
func updateCar(){}
func updateService(){}
// GETS
func getCars(){}
func getServices(){}
func getRecords(){}
func save(){
self.manager.save()
}
}
SwiftUI Views
CarsView
struct CarsView: View {
#StateObject var carViewModel = CarViewModel()
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.cars) { car in
}
}
}
}
}
}
ServicesView
struct ServicesView: View {
#ObservedObject var carViewModel:CarViewModel
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.carServices) { service in
}
}
}
}
}
}
RecordsView
struct RecordsView: View {
#ObservedObject var carViewModel: CarViewModel
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.serviceRecords) { record in
}
}
}
}
}
}
Personally I would create a service file which holds all the functions to a given model. I would then only expose the functions in my viewModel that my viewController needs.
For example:
CarService.swift
class CarService {
func addCar(name:String) {}
func addService(name:String, cost: Double) {}
func createRecord(name:String, cost: Double) {}
// DELETES
func deleteCar() {}
func deleteCarService() {}
func deleteServiceRecord() {}
// UPDATES
func updateCar() {}
func updateService() {}
// GETS
func getCars() {}
func getServices() {}
func getRecords() {}
}
CarViewModel.swift
protocol CarViewModelType {
func addCar(name: String)
func deleteCar()
}
class CarViewModel: CarViewModelType {
var carService: CarService
init(service: CarService) {
self.carService = service
}
func addCar(name: String) {
self.carService.addCar(name: name)
}
func deleteCar() {
self.carService.deleteCar()
}
}
CarViewController.swift
class CarViewController: UIViewController {
var viewModel: CarViewModelType!
}
This is of course just one way to go about it, I believe there is no 'right' way to structure your code.
Hope it helps
Let me give you a simplified example of my problem.
My application structure is,
View <- Viewmodel <- UserRepository <- UserStorageService
in the above view and view model are pretty self-explanatory. UserRespository is an ObservableObject, which works as a single data source for the view model.
UserStorageService is another ObervableObject which handles read/write data from CoreData.
With this approach I can easily change the method I fetch data in my application. as an example if I want to fetch data from an api I have to replace the StorageService with an APIService.
Let me show you my sample code,
UserStorageService.swift
class UserStorageService: NSObject, ObservableObject {
#Published var users: [User] = []
private let userController: NSFetchedResultsController<User>
private let dataController: DataController
init(controller: DataController) {
dataController = controller
let fetchRequest = User.fetchRequest()
userController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: dataController.container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
super.init()
userController.delegate = self
}
func add(data: User) throws {
// Add implementation
}
func fetch() throws {
try userController.performFetch()
users = userController.fetchedObjects ?? []
}
}
extension UserStorageService: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let users = userController.fetchedObjects else {
return
}
self.users = users
}
}
This class will fetch data from CoreData and keeps the users property updated when it changes.
On my UserRepository.swift class I have created a #StateObject instance of UserStorageService and also a #Published property users which will be used in my view model as a source of truth for users.
class UserRepository: RepositoryProtocol, ObservableObject {
#Published var users: [User] = []
#StateObject var userStorageService: UserStorageService
init(dataController: DataController) {
let storageService = UserStorageService(controller: dataController)
_userStorageService = StateObject(wrappedValue: storageService)
}
func fetch() {
try? userStorageService.fetch()
users = userStorageService.users
}
func add(data: User) throws {
try? userStorageService.add(data: data)
}
}
My question is, whenever the storage changes it updates the users in UserStorageService class. When that happens, how do I update the users property in my UserRepository class which will eventually update the view model and updates the UI.
I am newbie, any help will be very much appreciated.
You can use Singleton pattern. Create one in your main class and use users variable to update it inside another class
static var singleton: UserStorageService = UserStorageService()
After declaration you can use it in UserRepository
#Published var users: [User] = UserStorageService.singleton.users
Also , this can be achieved with Combine framework, which implements Observer Design Pattern, where you have one Publisher and Subscribers.
Instantiate a published UserStorageService in your UserRepository then subscribe to it in the init() to pass any changes along to your users array:
#Published var userStorageService = UserStorageService()
var cancellables = Set<AnyCancellable>()
init() {
userStorageService.$users
.assign(to: \.users, on: self)
.store(in: &cancellables)
}
This requires you to import the Combine framework.
I'm getting an API Result in my viewModel as below:
class HomePageViewModel {
var apiResult: CountryDataFromAPI?
//Getting API result via viewModel
public func getAPIResult(withOffset: Int, completion: #escaping () -> Void) {
APIHandler.urlRequest(with: withOffset) { result in
self.apiResult?.data.append(contentsOf: result.data)
print("api resultdata in viewmodel is \(result.data)")
completion()
}
}
}
Getting the api works fine. In the code above, I can print the statement as print("api resultdata in viewmodel is (result.data)") and see the expected api result.
When I get the data inside my viewModel as above, I use it in my mainViewController.
My mainViewController is initialized with the ViewModel
class HomePageViewController: UIViewController {
private var viewModel : HomePageViewModel
private var countryDataArray: [CountryData]?
init(with viewModel: HomePageViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
}
Everything works fine until I try to get the data from viewModel as below.
I'm trying to set the data in ViewModel to a parameter called countryDataArray. But while doing that inside the closure, my print statement "print("data in vc is (self?.viewModel.apiResult?.data)")" prints out nil even though it gets the data in viewModel.
override func viewDidLoad() {
super.viewDidLoad()
//Getting API Result in viewDidLoad. And after getting the result, reloading the tableView.
self.viewModel.getAPIResult(withOffset: 11) { [weak self] in
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self?.countryDataArray = self?.viewModel.apiResult?.data
print("data in vc is \(self?.viewModel.apiResult?.data)")
self?.countriesListTableView.reloadData()
})
}
}
Why this might be happening?
PS: My api models are as below:
struct CountryDataFromAPI: Codable {
var data: [CountryData]
}
struct CountryData: Codable,Equatable {
let name : String
let code: String
let wikiDataId: String
}
In my opinion, the problem is the self.apiResult?.data.append(contentsOf: result.data)
You didn't assign any value for the apiResult.data. Because apiResult is nil.
You have to initialize the apiResult after you get data back from API.
I have a struct for my model and it needs to conform to a protocol that is NSObject only.
I am looking for a viable alternative to converting the model to a class. The requirements are:
Keeping the model a value type
Updating the model when photoLibraryDidChange is called
This would be the ideal implementation if PHPhotoLibraryChangeObserver would not require the implementation to be an NSObject
struct Model: PHPhotoLibraryChangeObserver {
var images:[UIImages] = []
fileprivate var allPhotos:PHFetchResult<PHAsset>?
mutating func photoLibraryDidChange(_ changeInstance: PHChange) {
let changeResults = changeInstance.changeDetails(for: allPhotos)
allPhotos = changeResults?.fetchResultAfterChanges
updateImages()
}
mutating func updateImages() {
// update self.images
...
}
}
I cannot pass the model to an external class implementing the observer protocol as then all the changes happen on the copy (its a value type...)
Any ideas? Best practices?
EDIT: Reformulated the question
EDIT 2: Progress
I have implemented a delegate as a reference type var of my model and pushed the data inside. Now photoLibraryDidChangeis not being called anymore.
This is the stripped down implementation:
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
//this neve gets executed. It used to provide high quality images
self.updateImages()
}
}
}
func startFetching(){
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
//this gets executed and fetches the thumbnails
self.updateImages()
}
fileprivate func appendImage(_ p: PHAsset) {
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
}
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] {
pkAdapter.images
}
func startFetching(){
pkAdapter.startFetching()
}
// example model implementation
mutating func select(_ image:UIImage){
// read directly from pkAdapter.images and change other local variables
}
}
I have put a breakpoint in photoLibraryDidChange and it just does not go there. I also checked that pkAdapter is always the same object and does not get reinitialised on "copy on change".
**EDIT: adding the model view **
This is the relevant part of the modelview responsible for the model management
class ModelView:ObservableObject {
#Published var model = Model()
init() {
self.model.startFetching()
}
var images:[UIImage] {
self.model.images
}
...
}
EDIT: solved the update problem
It was a bug in the simulator ... on a real device it works
I ended up with 2 possible designs:
decouple the PhotoKit interface completely from the model, let the modelview manage both and connect them, as the model view has access to the model instance.
create a PhotoKit interface as var of the model and push the mutable data that is generated by the PhotoKit interface inside it, so it can be changed with an escaping closure. The model is never called from the interface but just exposes the data inside the PhotoKit through a computer property.
I will show two sample implementation below. They are naive in many respect, they ignore performance problems by refreshing all pictures every time the PhotoLibrary is updated. Implementing proper delta updates (and other optimisation) would just clutter up the code and offer no extra insight on the solution to the original problem.
Decouple the PhotoKit interface
ModelView
class ModelView:ObservableObject {
var pkSubscription:AnyCancellable?
private var pkAdapter = PhotoKitAdapter()
#Published var model = Model()
init() {
pkSubscription = self.pkAdapter.objectWillChange.sink{ _ in
DispatchQueue.main.async {
self.model.reset()
for img in self.pkAdapter.images {
self.model.append(uiimage: img)
}
}
}
self.pkAdapter.startFetching()
}
}
Model
struct Model {
private(set) var images:[UIImage] = []
mutating func append(uiimage:UIImage){
images.append(uiimage)
}
mutating func reset(){
images = []
}
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
self.updateImages()
self.objectWillChange.send()
}
}
}
func startFetching(){
PHPhotoLibrary.requestAuthorization{status in
if status == .authorized {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
self.updateImages()
}
}
}
fileprivate func appendImage(_ p: PHAsset) {
// This actually appends multiple copies of the image because
// it gets called multiple times for the same asset.
// Proper tracking of the asset needs to be implemented
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
self.objectWillChange.send()
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
PhotoKit interface as property of the model
ModelView
class ModelView:ObservableObject {
#Published var model = Model()
}
Model
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] { pkAdapter.images }
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver{
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
startFetching()
}
// the rest of the implementation is the same as before
...
I'm trying to observe change of an NSMutableOrderedSet in my ViewModel with combine.
I want to know when some element is added or removed of NSMutableOrderedSet
Some code of my ViewModel :
class TrainingAddExerciceViewModel: ObservableObject {
#Published var exercice: Exercice?
#Published var serieHistories = NSMutableOrderedSet()
...
init(...) {
...
//Where i'm trying to observe
$serieHistories
.sink { (value) in
print(value)
}
.store(in: &self.cancellables)
}
}
This is the function I use in my ViewModel to add element to NSMutableOrderedSet :
func add(managedObjectContext: NSManagedObjectContext) {
let newSerieHistory = ExerciceSerieHistory(context: managedObjectContext)
self.serieHistories.add(newSerieHistory)
self.updateView()
}
I have some other publisher working well with an other type (custom class).
Did I miss something ?
If I correctly understood logic of your code try the following (that init not needed)
variant 1 - add force update
func updateView() {
// ... other code
self.objectWillChange.send()
}
variant 2 - recreate storage
func add(managedObjectContext: NSManagedObjectContext) {
let newSerieHistory = ExerciceSerieHistory(context: managedObjectContext)
let newStorage = NSMutableOrderedSet(orderedSet: self.serieHistories)
newStorage.add(newSerieHistory)
self.serieHistories = newStorage // << fires publisher
self.updateView()
}