I use MagicalRecord for my project and in my database I have CDSong entity, which can be voted by multiple CDVoter entities.
The voters are added and deleted in background using NSManagedObjectContext.performAndWait(block:) called from a serial dispatch queue. I have an NSFetchedResultsController which fetches CDSongs and displays their voters (in this simple scenario it only prints the voters' names).
Everything would be fine, but I receive crashes occassionally in NSFetchedResultsControllerDelegate's controllerDidChangeContent method :-/ According to my analysis it seems like some invalid empty CDVoter (name = nil, votedSong = nil) objects appear in the CDSong.voters relationships. These empty voters are not returned from CDVoter.mr_findAll().
This is the code that simulates the crash (usually after <20 button clicks the app crashes because a CDVoter's name is nil). Am I doing something wrong with contexts and saving? Putting whole test code here with database and frc initialization if somebody wants to try it out, but the problematic part is in controllerDidChangeContent and buttonPressed methods. Thanks for your help :)
import UIKit
import CoreData
import MagicalRecord
class MRCrashViewController : UIViewController, NSFetchedResultsControllerDelegate {
var frc: NSFetchedResultsController<NSFetchRequestResult>!
let dispatchQueue = DispatchQueue(label: "com.testQueue")
override func viewDidLoad() {
super.viewDidLoad()
self.initializeDatabase()
self.initializeFrc()
}
func initializeDatabase() {
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.error)
MagicalRecord.setupCoreDataStack()
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.warn)
if CDSong.mr_findFirst() == nil {
for i in 1...5 {
let song = CDSong.mr_createEntity()!
song.id = Int16(i)
}
}
NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait()
}
func initializeFrc() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CDSong")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil)
self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.mr_default(), sectionNameKeyPath: nil, cacheName: nil)
self.frc!.delegate = self
try! self.frc!.performFetch()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
for song in controller.fetchedObjects! {
print((song as! CDSong).voters!.reduce("", { $0 + ($1 as! CDVoter).name! }))
}
print("----");
}
#IBAction func buttonPressed(_ sender: Any) {
for _ in 1...10 {
self.dispatchQueue.async {
let moc = NSManagedObjectContext.mr_()
moc.performAndWait {
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
moc.mr_saveToPersistentStoreAndWait()
}
}
}
}
}
Note:
I tried to use MagicalRecord.save(blockAndWait:) with no success.
Ok, so I found the reason of the crashes: Although mr_saveToPersistentStoreAndWait waits until the changes are saved into rootSavingContext, it doesn't wait until they are merged into defaultContext (if they were made by a private queue context). If the rootSavingContext has been changed by another save before the main queue context merges the old changes on main thread, the merge is then corrupted (changes in NSManagedObjectContextDidSave notification don't correspond to current context state of rootSavingContext in rootContextDidSave: internal method of MagicalRecord).
Explanation of my proposed solution:
DatabaseSavingManager contains a private queue saving context, which will be used for all saves in the application (maybe a drawback if you want to use multiple saving contexts, but it's enough for my needs - saves happen in background and consistency is maintained). As #Sneak commented, there's no reason to use a background serial queue which creates multiple contexts and waits for them to finish (that's what I originally did), because NSManagedObjectContext has its own serial queue, so now I used one context which is created on main thread and thus must be always called from main thread (using perform(block:) to avoid main thread blocking).
After saving to persistent store the saving context waits for the NSManagedObjectContextObjectsDidChange notification from defaultContext, so that it knows that the defaultContext has merged the changes. That's why no other saves than using DatabaseSavingManager's saving context are allowed, because they could confuse the waiting process.
Here is the code of DatabaseSavingManager:
import Foundation
import CoreData
class DatabaseSavingManager: NSObject {
static let shared = DatabaseSavingManager()
fileprivate let savingDispatchGroup = DispatchGroup()
fileprivate var savingDispatchGroupEntered = false
fileprivate lazy var savingContext: NSManagedObjectContext = {
if !Thread.current.isMainThread {
var context: NSManagedObjectContext!
DispatchQueue.main.sync {
context = NSManagedObjectContext.mr_()
}
return context
}
else {
return NSManagedObjectContext.mr_()
}
}()
override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(defaultContextDidUpdate(notification:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: NSManagedObjectContext.mr_default())
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func save(block: #escaping (NSManagedObjectContext) -> ()) {
guard Thread.current.isMainThread else {
DispatchQueue.main.async {
self.save(block: block)
}
return
}
let moc = self.savingContext
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
func saveAndWait(block: #escaping (NSManagedObjectContext) -> ()) {
if Thread.current.isMainThread {
self.savingContext.performAndWait {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
else {
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
group.leave()
}
}
group.wait()
}
}
fileprivate func saveToPersistentStoreAndWait() {
if self.savingContext.hasChanges {
self.savingDispatchGroupEntered = true
self.savingDispatchGroup.enter()
self.savingContext.mr_saveToPersistentStoreAndWait()
self.savingDispatchGroup.wait()
}
}
#objc fileprivate func defaultContextDidUpdate(notification: NSNotification) {
if self.savingDispatchGroupEntered {
self.savingDispatchGroup.leave()
self.savingDispatchGroupEntered = false
}
}
}
And example how to use it (no NSFetchedResultsController crashes anymore; can be called from any thread, also very frequently):
DatabaseSavingManager.shared.save { (moc) in
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
}
Of course, this is not the most elegant solution for sure, just the first that came to my mind, so other approaches are welcome
Related
I started learning programming and I decided to try out my first Note Taking App.
My Goal is to create an App similar to the iPhone's NoteApp. Therefore, I wanted the note's title be set when the User writes in the TextView as the first line. Therefore, I created a NoteViewController, which contains a TextView and a NoteIndexViewController, which is a TableViewController, both embedded in a NavigationController.
I'm also using Core Data to store the data.
The problem is that I don't know how I can commit those changes to the DataBase without using a button. I know how to create an instance of the NSManagedObject - in NoteIndexViewController to create new notes in the TableView using a Button:
#IBAction func addNotePressed(_ sender: UIBarButtonItem) {
let newNoteIndex = NoteIndex(context: self.context)
newNoteIndex.name = "Temporal Name"
notesArray.append(newNoteIndex)
saveNoteIndex()
performSegue(withIdentifier: K.segueToNote, sender: self)
}
But I'm completely lost if I want to commit the changes without a "Save Button" to create the instance and also committing changes. This is the code I got so far. Notice that I did not set any Note() object.
class NoteViewController: UIViewController {
var noteArray = [Note]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var selectedNote: NoteIndex? {
didSet {
loadData()
}
}
var firstLine: String?
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
if !textView.text.isEmpty {
if let newLine = textView.text.firstIndex(of: "\n") {
let firstLetter = textView.text.startIndex
let lineBrake = textView.text.index(before: newLine)
let lettersTillPosition = textView.text.distance(from: firstLetter, to: lineBrake)
firstLine = (textView.text as NSString).substring(to: lettersTillPosition)
} else {
if textView.text.count >= 30{
firstLine = (textView.text as NSString).substring(to: 30)
} else {
firstLine = (textView.text as NSString).substring(to: textView.text.count)
}
}
selectedNote!.name = firstLine
saveCurrentNote()
}
}
//MARK: - Data Manipulation Methods
func saveCurrentNote() {
do {
try context.save()
} catch {
print("Error saving cateogry \(error)")
}
}
func loadData(with request: NSFetchRequest<Note> = Note.fetchRequest()) {
// goToIndex is the relationship between the IndexNote entity and Note. And when Back button is pressed the code tend also to break in this part.
request.predicate = NSPredicate(format: "goToIndex.name MATCHES %#", selectedNote!.name!)
do {
noteArray = try context.fetch(request)
} catch {
print("This is a load error: \(error)")
}
}
}
extension NoteViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
saveCurrentNote()
}
}
Here is a possible solution for your question. You can use Notification Center to monitor if the user is interrupted and if so you can do a quick save.
Place these in the scene delegate
func sceneWillResignActive(_ scene: UIScene) {
let notificationName = NSNotification.Name(ReuseIdentifier.pause)
NotificationCenter.default.post(name: notificationName , object: nil)
}
func sceneDidDisconnect(_ scene: UIScene) {
let notificationName = NSNotification.Name(ReuseIdentifier.quit)
NotificationCenter.default.post(name: notificationName, object: nil)
}
Place something like this where the user data is being saved.
/// Monitors application state for major changes.
/// - Pause Observer: Adds observer that notifies application if application is no longer active (enters foreground).
/// - Quit Observer: Adds observer that notifies application if terminated.
private func checkForPauseOrQuit(){
NotificationCenter.default.addObserver(self,
selector: #selector(autoSave),
name: NSNotification.Name(ReuseIdentifier.pause),
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(autoSave),
name: NSNotification.Name(ReuseIdentifier.quit),
object: nil)
}
And then for your selector method you create your NSManagedObject and capture whatever values the user may have started typing.
On startup you do the reverse, and make sure to erase the values. This should function only as a temporary holding container not your main entity. Check out my note application for reference:
https://github.com/victis23/AwesomeNote/tree/WorkingBranch/AwesomeNote
I am trying to do some simple Core Data unit tests.
But from what I can see there are two persistent stores created (both sql type), so my setup(), tearDown() unit test methods are called twice, reseting the NSManagedObjectContext thus causing some issues.
Here is my code to load the stores:
func createContainer(storeType: StoreType, completion: #escaping (NSPersistentContainer) -> ()) {
let container = NSPersistentContainer(name: "Gym")
container.loadPersistentStores { description, error in
guard error == nil else {
fatalError("Failed to load store \(error!)")
}
description.type = storeType.type
completion(container)
}
}
And this is my unit test code:
import XCTest
import CoreData
#testable import Gym
class GymTests: XCTestCase {
var context: NSManagedObjectContext!
override func setUp() {
super.setUp()
createContainer(storeType: .inMemory) { container in
self.context = container.viewContext
}
}
func testAddingExercise() {
// Given
context.performChanges {
_ = Exercise.insert(into: self.context, name: "Bench Press", groups: Set(["Chest", "Triceps", "Shoulders"]))
_ = Exercise.insert(into: self.context, name: "Squats", groups: Set(["Legs"]))
_ = Exercise.insert(into: self.context, name: "Deadlifts", groups: Set(["Back", "Legs", "Arms"]))
}
// When
let exercises = Exercise.fetch(in: context)
// Then
XCTAssertEqual(exercises.count, 3)
}
override func tearDown() {
context = nil
super.tearDown()
}
}
The second time it runs my test, the context was reset in tearDown() so context will be nil when adding exercises.
Not sure exactly how is this happening. Why is it creating two stores ?
Motivation is to get a trigger for recalculation upon changes on values of Entity.
My quick solution cited below works, but it has drawbacks. It is inefficient.
In actual App, there are tens of entities. Changes on any of them will cause unnecessary notifications. Those could be avoided, if possible.
In this example, the only EmployeeMO is interested. No other entity needs to be observed.
What is your thoughts?
let n = NotificationCenter.default
n.addObserver(self, selector: #selector(mocDidChange(notification:)),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: managedObjectContext)
#objc func mocDidChange(notification n: Notification) {
if n.isRelatedTo(as: EmployeeMO.self) {
// do recalculation
}
}
And an extension to check if the notification is related to a given managed object:
extension Notification {
public func isRelatedTo<T>(as t: T.Type) -> Bool where T: NSManagedObject {
typealias S = Set<T>
let d = userInfo as! [String : Any]
return d[NSInsertedObjectsKey] is S ||
d[NSUpdatedObjectsKey] is S ||
d[NSDeletedObjectsKey] is S ||
d[NSRefreshedObjectsKey] is S ||
d[NSInvalidatedObjectsKey] is S
}
}
Xcode 9 Beta, Swift 4
Thanks.
The is a built in object that does exactly this already - NSFetchedResultsController. It is designed to work with a tableview or collectionView, but can work fine without one. It is lightweight enough that it is safe to use for just one object.
Thanks to #jon, now my source code has been greatly improved.
class myBaseArrayController: NSArrayController, NSFetchedResultsControllerDelegate {
// https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller
var frc: NSFetchedResultsController<NSManagedObject>?
func setupObserver() {
frc = NSFetchedResultsController(fetchRequest: defaultFetchRequest() as! NSFetchRequest<NSManagedObject>,
managedObjectContext: managedObjectContext!,
sectionNameKeyPath: nil, cacheName: nil)
frc?.delegate = self
do {
try frc?.performFetch()
}
catch {
fatalError("...")
}
}
}
Every ArrayController for its corresponding entity simply implements controllerDidChangeContent().
class myOneOfThemArrayController: myBaseArrayController {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("controllerDidChangeContent: \(controller.fetchRequest.entityName)")
// do some work
}
}
No more comparison to find which is what for. :-)
Caveat - I read the few questions about testing threads but may have missed the answer so if the answer is there and I missed it, please point me in the right direction.
I want to test that a tableView call to reloadData is executed on the main queue.
This should code should result in a passing test:
var cats = [Cat]() {
didSet {
DispatchQueue.main.async { [weak self] in
tableView.reloadData()
}
}
}
This code should result in a failing test:
var cats = [Cat]() {
didSet {
tableView.reloadData()
}
}
What should the test look like?
Note to the testing haters: I know this is an easy thing to catch when you run the app but it's also an easy thing to miss when you're refactoring and adding layers of abstraction and multiple network calls and want to update the UI with some data but not other data etc etc... so please don't just answer with "Updates to UI go on the main thread" I know that already. Thanks!
Use dispatch_queue_set_specific function in order to associate a key-value pair with the main queue
Then use dispatch_queue_get_specific to check for the presence of key & value:
fileprivate let mainQueueKey = UnsafeMutablePointer<Void>.alloc(1)
fileprivate let mainQueueValue = UnsafeMutablePointer<Void>.alloc(1)
/* Associate a key-value pair with the Main Queue */
dispatch_queue_set_specific(
dispatch_get_main_queue(),
mainQueueKey,
mainQueueValue,
nil
)
func isMainQueue() -> Bool {
/* Checking for presence of key-value on current queue */
return (dispatch_get_specific(mainQueueKey) == mainQueueValue)
}
I wound up taking the more convoluted approach of adding an associated Bool value to UITableView, then swizzling UITableView to redirect reloadData()
fileprivate let reloadDataCalledOnMainThreadString = NSUUID().uuidString.cString(using: .utf8)!
fileprivate let reloadDataCalledOnMainThreadKey = UnsafeRawPointer(reloadDataCalledOnMainThreadString)
extension UITableView {
var reloadDataCalledOnMainThread: Bool? {
get {
let storedValue = objc_getAssociatedObject(self, reloadDataCalledOnMainThreadKey)
return storedValue as? Bool
}
set {
objc_setAssociatedObject(self, reloadDataCalledOnMainThreadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
dynamic func _spyReloadData() {
reloadDataCalledOnMainThread = Thread.isMainThread
_spyReloadData()
}
//Then swizzle that with reloadData()
}
Then in the test I updated the cats on the background thread so I could check if they were reloaded on the main thread.
func testReloadDataIsCalledWhenCatsAreUpdated() {
// Checks for presence of another associated property that's set in the swizzled reloadData method
let reloadedPredicate = NSPredicate { [controller] _,_ in
controller.tableView.reloadDataWasCalled
}
expectation(for: reloadedPredicate, evaluatedWith: [:], handler: nil)
// Appends on the background queue to simulate an asynchronous call
DispatchQueue.global(qos: .background).async { [weak controller] in
let cat = Cat(name: "Test", identifier: 1)
controller?.cats.append(cat)
}
// 2 seconds seems excessive but NSPredicates only evaluate once per second
waitForExpectations(timeout: 2, handler: nil)
XCTAssert(controller.tableView.reloadDataCalledOnMainThread!,
"Reload data should be called on the main thread when cats are updated on a background thread")
}
Here is an updated version of the answer provided by Oleh Zayats that I am using in some tests of Combine publishers.
extension DispatchQueue {
func setAsExpectedQueue(isExpected: Bool = true) {
guard isExpected else {
setSpecific(key: .isExpectedQueueKey, value: nil)
return
}
setSpecific(key: .isExpectedQueueKey, value: true)
}
static func isExpectedQueue() -> Bool {
guard let isExpectedQueue = DispatchQueue.getSpecific(key: .isExpectedQueueKey) else {
return false
}
return isExpectedQueue
}
}
extension DispatchSpecificKey where T == Bool {
static let isExpectedQueueKey = DispatchSpecificKey<Bool>()
}
This is an example test using Dispatch and Combine to verify it is working as expected (you can see it fail if you remove the receive(on:) operator).:
final class IsExpectedQueueTests: XCTestCase {
func testIsExpectedQueue() {
DispatchQueue.main.setAsExpectedQueue()
let valueExpectation = expectation(description: "The value was received on the expected queue")
let completionExpectation = expectation(description: "The publisher completed on the expected queue")
defer {
waitForExpectations(timeout: 1)
DispatchQueue.main.setAsExpectedQueue(isExpected: false)
}
DispatchQueue.global().sync {
Just(())
.receive(on: DispatchQueue.main)
.sink { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
completionExpectation.fulfill()
} receiveValue: { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
valueExpectation.fulfill()
}.store(in: &cancellables)
}
}
override func tearDown() {
cancellables.removeAll()
super.tearDown()
}
var cancellables = Set<AnyCancellable>()
}
I'm currently struggling to find an easy-to-use programming approach/design pattern, which solves the following problem:
I've got an REST API where the iOS app can request the required data. The data is needed in different ViewControllers. But the problem is, that the data should "always" be up to date. So I need to set up a timer which triggers a request every 5-20 seconds, or sth like that. Everytime the data changes, the view needs to be updated (at the current viewcontroller, which is displayed).
I tried some stuff with delegation and MVC Pattern, but it's kind a messy. How is it done the right way?
In my current implementation I only can update the whole UICollectionView, not some specific cells, because I don't know how the data changed. My controller keeps track of the data from the api and updates only if the hash has changed (if data changed on the server). My models always holds the last fetched data.
It's not the perfect solution, in my opinion..
I also thought about models, that keep themselves up to date, to abstract or virtualise my Rest-API. In this case, my controller doesn't even know, that it isn't directly accessible data.
Maybe someone can help me out with some kind of programming model, designpattern or anything else. I'm happy about anything!
UPDATE: current implementation
The Controller, which handles all the data
import Foundation
import SwiftyJSON
import SwiftyTimer
class OverviewController {
static let sharedInstance = OverviewController()
let interval = 5.seconds
var delegate : OverviewControllerUpdateable?
var model : OverviewModel?
var timer : NSTimer!
func startFetching() -> Void {
self.fetchData()
timer = NSTimer.new(every: interval) {
self.fetchData()
}
timer.start(modes: NSRunLoopCommonModes)
}
func stopFetching() -> Void {
timer.invalidate()
}
func getConnections() -> [Connection]? {
return model?.getConnections()
}
func getConnectionsSlave() -> [Connection]? {
return model?.getConnectionsSlave()
}
func getUser() -> User? {
return model?.getUser()
}
func countConnections() -> Int {
if let count = model?.getConnections().count {
return count
}
return 0
}
func countConnectionsSlave() -> Int {
if let count = model?.getConnectionsSlave().count {
return count
}
return 0
}
func fetchData() {
ApiCaller.doCall(OverviewRoute(), completionHandler: { (data, hash) in
if let actModel = self.model {
if (actModel.getHash() == hash) {
//no update required
return
}
}
var connections : [Connection] = []
var connectionsSlave : [Connection] = []
for (_,connection):(String, JSON) in data["connections"] {
let connectionObj = Connection(json: connection)
if (connectionObj.isMaster == true) {
connections.append(connectionObj)
} else {
connectionsSlave.append(connectionObj)
}
}
let user = User(json: data["user"])
//model needs update
let model = OverviewModel()
model.setUser(user)
model.setConnections(connections)
model.setConnectionsSlave(connectionsSlave)
model.setHash(hash)
self.model = model
//prevent unexpectedly found nil exception
if (self.delegate != nil) {
self.delegate!.reloadView()
}
}, errorHandler: { (errors) in
}) { (progress) in
}
}
}
protocol OverviewControllerUpdateable {
func reloadView()
}
The model, which holds the data:
class OverviewModel {
var user : User!
var connections : [Connection]!
var connectionsSlave : [Connection]!
var connectionRequests : [ConnectionRequest]!
var hash : String!
...
}
And in the ViewController, I use it like this:
class OverviewVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, OverviewControllerUpdateable {
let controller = OverviewController.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
self.controller.delegate = self
self.controller.startFetching()
}
//INSIDE THE UICOLLECTIONVIEW DELEGATE METHODS
...
if let user : User = controller.getUser() {
cell.intervalTime = interval
cell.nameLabel.text = "Ihr Profil"
}
...
func reloadView() {
self.userCollectionView.reloadData()
}
}
You could use a Singleton object to fetch your data periodically, then post notifications (using NSNotificationCenter) when the data is updated. Each view controller dependent on the data would listen for these notifications, then reload UI based on the updated data.