Passing ManagedObject collection from fetch result to closure up to ViewController - ios

I want to perform a background fetch and pass the result to closure. Currently I'm using performBackgroundTask method from NSPersistentContainer which is giving a NSManagedObjectContext as a closure. Then using that context I'm executing fetch request. When fetch is done I'm, passing the result to the completion handler.
func getAllWorkspacesAsync(completion: #escaping ([Workspace]) -> Void) {
CoreDataStack.shared.databaseContainer.performBackgroundTask { childContext in
let workspacesFetchRequest: NSFetchRequest<Workspace> = NSFetchRequest(entityName: "Workspace")
workspacesFetchRequest.predicate = NSPredicate(format: "remoteId == %#", "\(UserDefaults.lastSelectedWorkspaceId))")
do {
let workspaces: [Workspace] = try childContext.fetch(workspacesFetchRequest)
completion(workspaces)
} catch let error as NSError {
// Handle error
}
}
}
I'm going to call this method from ViewModel and use Combine PassthroughSubject to notify the ViewController about the event.
class WorkspaceViewModel {
private var cancellables: Set<AnyCancellable> = []
let resultPassthroughObject: PassthroughSubject<[Workspace], Error> = PassthroughSubject()
private let cacheManager = WorkspaceCacheProvider.shared
static public let shared: WorkspaceViewModel = {
let instance = WorkspaceViewModel()
return instance
}()
func fetchWorkspaces() {
cacheManager.getAllWorkspacesAsync { [weak self] workspaces in
guard let self = self else { return }
self.resultPassthroughObject.send(workspaces)
}
}
}
And the ViewController code:
class WorkspaceViewController: UIViewController {
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
WorkspaceViewModel.shared.resultPassthroughObject
.receive(on: RunLoop.main)
.sink { _ in } receiveValue: { workspaces in
// update the UI
}
.store(in: &cancellables)
}
}
My question is: is it safe to pass NSManagedObject items?

No, it is not
Do not pass NSManagedObject instances between queues. Doing so can result in corruption of the data and termination of the app. When it is necessary to hand off a managed object reference from one queue to another, use NSManagedObjectID instances.
You would want something like:
func getAllWorkspacesAsync(completion: #escaping ([NSManagedObjectID]) -> Void) {
CoreDataStack.shared.databaseContainer.performBackgroundTask { childContext in
let workspacesFetchRequest: NSFetchRequest<Workspace> = NSFetchRequest(entityName: "Workspace")
workspacesFetchRequest.predicate = NSPredicate(format: "remoteId == %#", "\(UserDefaults.lastSelectedWorkspaceId))")
do {
let workspaces: [Workspace] = try childContext.fetch(workspacesFetchRequest)
completion(workspaces.map { $0.objectID } )
} catch let error as NSError {
// Handle error
}
}
}
You will need to make corresponding changes to your publisher so that it publishes [NSManagedObjectID].
Once you have received this array in your view controller you will need to call object(with:) on your view context to get the Workspace itself. This will perform a fetch in the view context for the object anyway.
You may want to consider whether the time taken to fetch your workspaces warrants the use of a background context. By default Core Data objects are retrieved as faults and the actual attribute values are only fetched when the fault is triggered by accessing an object's attributes.
Alternatively, if the purpose of the view controller is to present a list of workspaces that the user can select from, you could return an array of tuples or structs containing the workspace name and the object id. However, this all sounds like pre-optimisation to me.

Related

Accessing Core Data Stack in MVVM application

I'm writing an application using the MVVM pattern. And I'm wondering know how to create the CoreData stack so it can be accessed from various places in my app.
First approach is to create a persistent container in the AppDelegate and then inject this service to my ViewModels (simultaneously passing the managedObjectContext as an environment variable to my Views).
This way, however, accessing context throughout the app is more difficult: e.g. in decoding network responses, as they don't have access to the managedObjectContext:
protocol APIResource {
associatedtype Response: Decodable
...
}
extension APIResource {
func decode(_ data: Data) -> AnyPublisher<Response, APIError> {
Just(data)
// how can I access context here to pass it to JSONDecoder?
.decode(type: Response.self, decoder: JSONDecoder())
.mapError { error in
.parsing(description: error.localizedDescription)
}
.eraseToAnyPublisher()
}
}
The other solution I've seen is to use a singleton. I can access it from anywhere in the project but how can I create it in the right way?
What if I wan't to modify some object in the main and the background queue at the same time? Or what if both queues want to modify the same object?
You can use Core Data Singleton class
import CoreData
class CoreDataStack {
static let shared = CoreDataStack()
private init() {}
var managedObjectContext: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
var workingContext: NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.managedObjectContext
return context
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyStuff")
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
RaiseError.raise()
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext() {
self.managedObjectContext.performAndWait {
if self.managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
appPrint("Main context saved")
} catch {
appPrint(error)
RaiseError.raise()
}
}
}
}
func saveWorkingContext(context: NSManagedObjectContext) {
do {
try context.save()
appPrint("Working context saved")
saveContext()
} catch (let error) {
appPrint(error)
RaiseError.raise()
}
}
}
Core Data is not thread safe. If you write something on manageObject and don't want to save that, but some other thread save the context, then the changes that you don't want to persist will also persist.
So to avoid this situation always create working context - which is private.
When you press save, then first private context get saved and after that you save main context.
In MVVM you should have DataLayer through which your ViewModel interact with Core Data singleton class.

iOS private/public data management and storage in Swift

Is it possible to display as one array objects retrieved from network and same model but retrieved from core data. Purpose is to have same data possible to be public (then retrieved from network) or private and then this data is stored locally in coredata model. Attributes/Properties will be the same for both.
I plan to display this as swiftUI view (if that matters)
After some search I came with idea to have one struct that based on its privacy property will be translated to core data class model or if public directly connected to networking layer?
for example (some pseudo swift ;) )
struct Note {
let note: String
let isPrivate: Bool
func save(self) {
if self.isPrivate { save to CoreData }
else { send save request with use of networking }
}
}
class coreDataModel: NSManagedObject {
var note: String
let isPrivate = true
}
struct networkingModel {
var note: String
let isPrivate = false
}
class modelManager {
func joinData() {
let joinedModel: Note = coreDataModel + networkingModel
// and so on to display that model
}
}
I think you can try this, first load the ui with the existing local data. At the same time, make a call to your api on a background queue. Once the api results are available, filter for duplicates, then persist it locally. Then the last step is to notify the ui to reload.
This pseudo-code is a bit UIKit specific, however the same logic can be applied to when in SwiftUI. You won't need closure, you will compute directly to your publisher object, and ui will react based any new emits.
/// you can have one model for both api and core data
struct Note: Hashable { let uuid = UUID.init() }
class ModelManager {
private var _items: Array<Note> = []
var onChanged: ((Array<Note>) -> Void)? = nil
// you might also have an init, where you can have a timer running
func start() {
loadFromCoreData { [weak self] (items) in
guard let `self` = self else { return }
self._items.append(contentsOf: items)
self.onChanged?(self._items)
self.loadFromApi()
}
}
private func loadFromCoreData(completion: #escaping (Array<Note>) -> Void) {
// background queue
// logic to load from coredata.
let coreDataResults: Array<Note> = []
completion(coreDataResults)
}
private func loadFromApi() {
// background queue
let apiResults: Array<Note> = []
compute(contents: apiResults)
}
private func compute(contents: Array<Note>) {
let combined = zip(_items, contents).flatMap{[$0.0, $0.1] }
let newItems = Array(Set(combined)) // set doesn't allow duplicates
// save to db
// insert to new to _items
_items.append(contentsOf: newItems)
// sort maybe to place new items on top
self.onChanged?(self._items)
}
}
// usage of this object
let manager = ModelManager()
manager.start()
manager.onChanged = { items in
// [weak self] remember of retain cycles
// make sure you are on main queue when reloading
}

Where to store Decoded JSON array from server and how to access it globally in viewControllers?

Currently im creating application which parses JSON from my server. From server I can receive array with JSON models.
Data from this array must be populated in table View.
My question Is simple: where to store decoded array from server, if I want to access it from many viewControllers in my application?
Here is my JSON model, which coming from server.
import Foundation
struct MyModel: Codable {
var settings: Test?
var provider: [Provider]
}
extension MyModel {
struct setting: Codable {
var name: String
var time: Int
}
}
here is how I am decoding it
import Foundation
enum GetResourcesRequest<ResourceType> {
case success([ResourceType])
case failure
}
struct ResourceRequest<ResourceType> where ResourceType: Codable {
var startURL = "https://myurl/api/"
var resourceURL: URL
init(resourcePath: String) {
guard let resourceURL = URL(string: startURL) else {
fatalError()
}
self.resourceURL = resourceURL.appendingPathComponent(resourcePath)
}
func fetchData(completion: #escaping
(GetResourcesRequest<ResourceType>) -> Void ) {
URLSession.shared.dataTask(with: resourceURL) { data, _ , _ in
guard let data = data else { completion(.failure)
return }
let decoder = JSONDecoder()
if let jsonData = try? decoder.decode([ResourceType].self, from: data) {
completion(.success(jsonData))
} else {
completion(.failure)
}
}.resume()
}
}
This is an example of CategoriesProvider. It just stores categories in-memory and you can use them across the app. It is not the best way to do it and not the best architecture, but it is simple to get started.
class CategoriesProvider {
static let shared = CategoriesProvider()
private(set) var categories: [Category]?
private let categoryRequest = ResourceRequest<Category>(resourcePath: "categories")
private let dataTask: URLSessionDataTask?
private init() {}
func fetchData(completion: #escaping (([Category]?) -> Void)) {
guard categories == nil else {
completion(categories)
return
}
dataTask?.cancel()
dataTask = categoryRequest.fetchData { [weak self] categoryResult in
var fetchedCategories: [Category]?
switch categoryResult {
case .failure:
print("error")
case .success(let categories):
fetchedCategories = categories
}
DispatchQueue.main.async {
self?.categories = fetchedCategories
completion(fetchedCategories)
}
}
}
}
I suggest using URLSessionDataTask in order to cancel a previous task. It could happen when you call fetchData several times one after another. You have to modify your ResourceRequest and return value of URLSession.shared.dataTask(...)
Here more details about data task https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started#toc-anchor-004 (DataTask and DownloadTask)
Now you can fetch categories in CategoriesViewController in this way:
private func loadTableViewData() {
CategoriesProvider.shared.fetchData { [weak self] categories in
guard let self = self, let categories = categories else { return }
self.categories = categories
self.tableView.reloadData()
}
}
In the other view controllers, you can do the same but can check for the 'categories' before making a fetch.
if let categories = CategoriesProvider.shared.categories {
// do something
} else {
CategoriesProvider.shared.fetchData { [weak self] categories in
// do something
}
}
If you really want to avoid duplicate load data() calls, your simplest option would be to cache the data on disk (CoreData, Realm, File, etc.) after parsing it the first time.
Then every ViewController that needs the data, can just query your storage system.
Of course the downside of this approach is the extra code you'll have to write to manage the coherency of your data to make sure it's properly managed across your app.
make a global dictionary array outside any class to access it on every viewcontroller.

iOS Core Data - How to avoid crash when many context save at same time?

I try to avoid crash when many contexts save at same time.
The following class has one operation queue that operate only one work at same time. It has three context. First, defaultContext is main queue type, this is not directly updated and is only visible to the user. Other two contexts is localContext and externalContext.
LocalContext is for user's schedule addition and external Context is for external schedule update like cloud sync. Local context and external context is child of defaultContext and set it's automaticallyMergesChangesFromParent property to true. Even if user update and external update are implemented at same time. Since they run sequentially in the same queue, there is no data loss.
It works great when data input is small. But app gets slower when too much data is coming in. Is there any better way?
Here's my code.
class DataController {
static let shared = DataController()
var schedules: [Schedule] = []
var persistentContainer: NSPersistentContainer
let persistentContainerQueue = OperationQueue()
private init() {
persistentContainerQueue.maxConcurrentOperationCount = 1
persistentContainer = NSPersistentContainer(name: "CoreDataConcurrency")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
lazy var defaultContext: NSManagedObjectContext = {
[unowned self] in
self.persistentContainer.viewContext
}()
lazy var localContext: NSManagedObjectContext = {
[unowned self] in
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.defaultContext
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var externalContext: NSManagedObjectContext = {
[unowned self] in
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self.defaultContext
context.automaticallyMergesChangesFromParent = true
return context
}()
func enqueueCoreDataOperation(context: NSManagedObjectContext, changeBlock: #escaping () -> (NSManagedObjectContext)) {
persistentContainerQueue.addOperation {
let changedContext = changeBlock()
guard changedContext.hasChanges else {
return
}
changedContext.performAndWait({
do {
try changedContext.save()
if let parentContext = changedContext.parent {
do {
try parentContext.save()
} catch {
fatalError()
}
}
} catch {
fatalError()
}
})
}
}
func addSchedule(title: String, date: Date, context: NSManagedObjectContext) {
let changeBlock: () -> (NSManagedObjectContext) = {
let schedule = NSEntityDescription.insertNewObject(forEntityName: "Schedule", into: context) as! Schedule
schedule.title = title
schedule.date = date
return context
}
enqueueCoreDataOperation(context: context, changeBlock: changeBlock)
}
func updateSchedule(schedule: Schedule, modifiedTitle: String, context: NSManagedObjectContext) {
let scheduleInContext = context.object(with: schedule.objectID) as! Schedule
let changeBlock: () -> (NSManagedObjectContext) = {
scheduleInContext.title = modifiedTitle
return context
}
enqueueCoreDataOperation(context: context, changeBlock: changeBlock)
}
}
You could batch up the incoming data into smaller batches so each operation takes less time and add a priority to the operations so cloud based changes have a lower priority. Then they will no longer block the other changes. But I strongly suspect that you are doing something wrong in you import operation that is making take too long. Are you doing a fetch for each imported entity? Please share that code.

Rx Observable that gets value from other Observable

I am new to RxSwift and MVVM.
my viewModel has a method named rx_fetchItems(for:) that does the heavy lifting of fetching relevant content from backend, and returns Observable<[Item]>.
My goal is to supply an observable property of the viewModel named collectionItems, with the last emitted element returned from rx_fetchItems(for:), to supply my collectionView with data.
Daniel T has provided this solution that I could potentially use:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
The only problem here is that my current ServerAPI aka FirebaseAPI, has no such protocol method, because it is designed with a single method that fires all requests like this:
class FirebaseAPI {
private let session: URLSession
init() {
self.session = URLSession.shared
}
/// Responsible for Making actual API requests & Handling response
/// Returns an observable object that conforms to JSONable protocol.
/// Entities that confrom to JSONable just means they can be initialized with json.
func rx_fireRequest<Entity: JSONable>(_ endpoint: FirebaseEndpoint, ofType _: Entity.Type ) -> Observable<[Entity]> {
return Observable.create { [weak self] observer in
self?.session.dataTask(with: endpoint.request, completionHandler: { (data, response, error) in
/// Parse response from request.
let parsedResponse = Parser(data: data, response: response, error: error)
.parse()
switch parsedResponse {
case .error(let error):
observer.onError(error)
return
case .success(let data):
var entities = [Entity]()
switch endpoint.method {
/// Flatten JSON strucuture to retrieve a list of entities.
/// Denoted by 'GETALL' method.
case .GETALL:
/// Key (underscored) is unique identifier for each entity, which is not needed here.
/// value is k/v pairs of entity attributes.
for (_, value) in data {
if let value = value as? [String: AnyObject], let entity = Entity(json: value) {
entities.append(entity)
}
}
// Need to force downcast for generic type inference.
observer.onNext(entities as! [Entity])
observer.onCompleted()
/// All other methods return JSON that can be used to initialize JSONable entities
default:
if let entity = Entity(json: data) {
observer.onNext([entity] as! [Entity])
observer.onCompleted()
} else {
observer.onError(NetworkError.initializationFailure)
}
}
}
}).resume()
return Disposables.create()
}
}
}
The most important thing about the rx_fireRequest method is that it takes in a FirebaseEndpoint.
/// Conforms to Endpoint protocol in extension, so one of these enum members will be the input for FirebaseAPI's `fireRequest` method.
enum FirebaseEndpoint {
case saveUser(data: [String: AnyObject])
case fetchUser(id: String)
case removeUser(id: String)
case saveItem(data: [String: AnyObject])
case fetchItem(id: String)
case fetchItems
case removeItem(id: String)
case saveMessage(data: [String: AnyObject])
case fetchMessages(chatroomId: String)
case removeMessage(id: String)
}
In order to use Daniel T's solution, Id have to convert each enum case from FirebaseEndpoint into methods inside FirebaseAPI. And within each method, call rx_fireRequest... If I'm correct.
Id be eager to make this change if it makes for a better Server API design. So the simple question is, Will this refactor improve my overall API design and how it interacts with ViewModels. And I realize this is now evolving into a code review.
ALSO... Here is implementation of that protocol method, and its helper:
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
If my approach to MVVM or RxSwift or API design is wrong PLEASE do critique.
I know it is tough to start understanding RxSwift
I like to use Subjects or Variables as inputs for the ViewModel and Observables or Drivers as outputs for the ViewModel
This way you can bind the actions that happen on the ViewController to the ViewModel, handle the logic there, and update the outputs
Here is an example by refactoring your code
View Model
// Inputs
let didSelectItemCategory: PublishSubject<ItemCategory> = .init()
// Outputs
let items: Observable<[Item]>
init() {
let client = FirebaseAPI()
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
self.items = didSelectItemCategory
.withLatestFrom(fetchedItems, resultSelector: { itemCategory, fetchedItems in
switch itemCategory {
case .Local:
return fetchedItems.filter { $0.location == "Los Angeles" }
default: return []
}
})
}
ViewController
segmentedControl.rx.value
.map(ItemCategory.init(rawValue:))
.startWith(.Local)
.bind(to: viewModel.didSelectItemCategory)
.disposed(by: disposeBag)
viewModel.items
.subscribe(onNext: { items in
// Do something
})
.disposed(by: disposeBag)
I think the problem you are having is that you are only going half-way with the observable paradigm and that's throwing you off. Try taking it all the way and see if that helps. For example:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
EDIT to handle problem mentioned in comment. You now need to pass in the object that has the rx_fetchItems(for:) method. You should have more than one such object: one that points to the server and one that doesn't point to any server, but instead returns canned data so you can test for any possible response, including errors. (The view model should not talk to the server directly, but should do so through an intermediary...
The secret sauce in the above is the materialize operator that wraps error events into a normal event that contains an error object. That way you stop a network error from shutting down the whole system.
In response to the changes in your question... You can simply make the FirebaseAPI conform to ServerAPI:
extension FirebaseAPI: ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = self.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
}
You should probably change the name of ServerAPI at this point to something like FetchItemsAPI.
You run into a tricky situation here because your observable can throw an error and once it does throw an error the observable sequence errors out and no more events can be emitted. So to handle subsequent network requests, you must reassign taking the approach you're currently taking. However, this is generally not good for driving UI elements such as a collection view because you would have to bind to the reassigned observable every time. When driving UI elements, you should lean towards types that are guaranteed to not error out (i.e. Variable and Driver). You could make your Observable<[Item]> to be let items = Variable<[Item]>([]) and then you could just set the value on that variable to be the array of items that came in from the new network request. You can safely bind this variable to your collection view using RxDataSources or something like that. Then you could make a separate variable for the error message, let's say let errorMessage = Variable<String?>(nil), for the error message that comes from the network request and then you could bind the errorMessage string to a label or something like that to display your error message.

Resources