Observe a NSManagedObject in Swift 4 can cause crashes when modified in another thread? - swift

What I have:
a NSManagedObject that sets a dynamic property to true when it's deleted from CoreData
override func prepareForDeletion() {
super.prepareForDeletion()
hasBeenDeleted = true
}
And within a view, I observe this NSManagedObject with the new Observe pattern of Swift 4
// I added this to observe the OBSERVED deletion to avoid a crash similar to:
// "User was deallocated while key value observers were still registered with it."
private var userDeletionObserver: NSKeyValueObservation?
private func observeUserDeletion() {
userDeletionObserver = user?.observe(\.hasBeenDeleted, changeHandler: { [weak self] (currentUser, _) in
if currentUser.hasBeenDeleted {
self?.removeUserObservers()
}
})
}
private func removeUserObservers() {
userDeletionObserver = nil
userObserver = nil
}
private var userObserver: NSKeyValueObservation?
private var user: CurrentUser? {
willSet {
// I remove all observers in willSet to also cover the case where we try to set user=nil, I think it's safer this way.
removeUserObservers()
}
didSet {
guard let user = user else { return }
// I start observing the NSManagedObject for Deletion
observeUserDeletion()
// I finally start observing the object property
userObserver = user.observe(\.settings, changeHandler: { [weak self] (currentUser, _) in
guard !currentUser.hasBeenDeleted else { return }
self?.updateUI()
})
}
}
So now, here come one observation and the question:
Observation: Even if I don't do the observeUserDeletion thing, the app seems to work and seems to be stable so maybe it's not necessary but as I had another crash related to the observe() pattern I try to be over careful.
Question details: Do I really need to care about the OBSERVED object becoming nil at any time while being observed or is the new Swift 4 observe pattern automatically removes the observers when the OBSERVED object is 'nilled'?

Related

Coordinating access to NSManagedObjects across multiple background services

My first Swift app is a photo library manager, and after rebuilding its Core Data guts half a dozen times, I am throwing up my hands and asking for help. For each photo, there are a few "layers" of work I need to accomplish before I can display it:
Create an Asset (NSManagedObject subclass) in Core Data for each photo in the library.
Do some work on each instance of Asset.
Use that work to create instances of Scan, another NSManagedObject class. These have to-many relationships to Assets.
Look over the Scans and use them to create AssetGroups (another NSManagedObject) in Core Data. Assets and AssetGroups have many-to-many relationships.
For each photo, each layer must complete before the next one starts. I can do multiple photos in parallel, but I also want to chunk up the work so it loads into the UI coherently.
I'm really having trouble making this work gracefully; I've built and rebuilt it a bunch of different ways. My current approach uses singleton subclasses of this Service, but as soon as I call save() on the first one, the work stops.
Service.swift
class Service: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var name: String
var predicate: NSPredicate
var minStatus: AssetStatus
var maxStatus: AssetStatus
internal let queue: DispatchQueue
internal let mainMOC = PersistenceController.shared.container.viewContext
internal let privateMOC = PersistenceController.shared.container.newBackgroundContext()
internal lazy var frc: NSFetchedResultsController<Asset> = {
let req = Asset.fetchRequest()
req.predicate = self.predicate
req.sortDescriptors = [NSSortDescriptor(key: #keyPath(Asset.creationDate), ascending: false)]
let frcc = NSFetchedResultsController(fetchRequest: req,
managedObjectContext: self.mainMOC,
sectionNameKeyPath: "creationDateKey",
cacheName: nil)
frcc.delegate = self
return frcc
}()
#Published var isUpdating = false
#Published var frcCount = 0
init(name: String, predicate: NSPredicate? = NSPredicate(value: true), minStatus: AssetStatus, maxStatus: AssetStatus) {
self.name = name
self.predicate = predicate!
self.minStatus = minStatus
self.maxStatus = maxStatus
self.queue = DispatchQueue(label: "com.Ladybird.Photos.\(name)", attributes: .concurrent)
super.init()
self.fetch()
self.checkDays()
}
private func fetch() {
do {
try self.frc.performFetch()
print("\(name): FRC fetch count: \(frc.fetchedObjects!.count)")
} catch {
print("\(name): Unable to perform fetch request")
print("\(error), \(error.localizedDescription)")
}
}
func savePrivate() {
self.privateMOC.perform {
do {
try self.privateMOC.save()
}
catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
func save() {
do {
try self.privateMOC.save()
self.mainMOC.performAndWait {
do {
try self.mainMOC.save()
} catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
func checkDays() {
// Iterate over days in the photo library
if self.isUpdating { return }
self.isUpdating = true
// self.updateCount = self.frcCount
var daysChecked = 0
var day = Date()
while day >= PhotoKitService.shared.oldestPhAssetDate() {
print("\(name) checkDay \(DateFormatters.shared.key.string(from: day))")
checkDay(day)
var dc = DateComponents()
dc.day = -1
daysChecked += 1
day = Calendar.current.date(byAdding: dc, to: day)!
if daysChecked % 100 == 0 {
DispatchQueue.main.async {
self.save()
}
}
}
self.save()
self.isUpdating = false
}
func checkDay(_ date: Date) {
let dateKey = DateFormatters.shared.key.string(from: date)
let req = Asset.fetchRequest()
req.predicate = NSPredicate(format: "creationDateKey == %#", dateKey)
guard let allAssetsForDateKey = try? self.mainMOC.fetch(req) else { return }
if allAssetsForDateKey.count == PhotoKitService.shared.phAssetsCount(dateKey: dateKey) {
if allAssetsForDateKey.allSatisfy({$0.assetStatusValue >= minStatus.rawValue && $0.assetStatusValue <= maxStatus.rawValue}) {
let frcAssetsForDateKey = self.frc.fetchedObjects!.filter({$0.creationDateKey! == dateKey})
if !frcAssetsForDateKey.isEmpty {
print("\(name): Day \(dateKey) ready for proccessing.")
for a in frcAssetsForDateKey {
self.handleAsset(a)
}
}
}
}
self.save()
}
// implemented by subclasses
func handleAsset(_ asset: Asset) -> Void { }
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.frcCount = self.frc.fetchedObjects?.count ?? 0
self.checkDays()
}
}
I have a subclass of this for each of the four steps above. I want the data to flow between them nicely, and in previous implementations it did, but I couldn't chunk it the way I wanted, and it crashed randomly. This feels more controllable, but it doesn't work: calling save() stops the iteration happening in checkDays(). I can solve that by wrapping save() in an async call like DispatchQueue.main.async(), but it has bad side effects — checkDays() getting called while it's already executing. I've also tried calling save() after each Asset is finished, which makes the data move between layers nicely, but is slow as hell.
So rather than stabbing in the dark, I thought I'd ask whether my strategy of "service layers" feels sensible to others who others who have dealt with this kind of problem. It'd also be helpful to hear if my implementation via this Service superclass makes sense.
What would be most helpful is to hear from those with experience how they would approach implementing a solution to this problem: consecutive steps, applied concurrently to multiple Core Data entities, all in the background. There are so many ways to solve pieces of this in Swift — async/await, Tasks & Actors, DispatchQueues, ManagedObjectContext.perform(), container.performBackgroundTask(), Operation… I've tried each of them to mixed success, and what I feel like I need here is a trail map to get out of the forest.
Thanks y'all

fetchRecordCompletionBlock and semaphores - help understanding execution order

Context:
App with all data in CloudKit
ViewController calls a query to load the data for a tableview
tableview crashes because the array of data for the tableview hasn't
come back from CK
I've researched semaphores and have it nearly
working But can't seem to figure out where to place the
semaphore.signal() to get the exact right behaviour
within viewDidLoad, I call the function:
Week.fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
and the function:
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.async {
completion(weeks)
//Option 1: putting semaphore.signal() here means it never completes
// beyond initialization of the week records
}
//Option 2: putting semaphore.signal() here means it completes after the
// initialization of the Week items, but before completion(weeks) is done
// so the array isn't initialized in the view controller in time. so the
// VC tries to use weeks and unwraps a nil.
semaphore.signal()
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
Note: I have tested that the weeks array within the view controller is properly set eventually - so it does seem to be purely a timing issue :)
I've tested placement of .signal() and if I put it within the 'DispatchQueue.main.async' block, it never gets triggered - probably because that block itself is waiting for the signal.
However if I put it anywhere else, then the viewcontroller picks up at that point and the completion(weeks) doesn't get called in time.
Maybe it is obvious - but as my first time working with semaphores - I'm struggling to figure it out!
Update 1: It works with DispatchQueue(label: "background")
I was able to get it working once I twigged that the semaphore.wait() was never going to get called with semaphore.signal() on the main thread.
So I changed it from:
DispatchQueue.main.async
to
DispatchQueue(label: "background").async and popped the semaphore.signal() inside and it did the trick
Comments/critiques welcome!
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
NSLog("inside fetchWeeks in Week ")
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue(label: "background").async {
completion(weeks)
semaphore.signal()
}
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
}
Update 2: Trying to avoid use of semaphores
Per comment thread - we shouldn't need to use semaphores with CloudKit - so it is likely that I'm doing something stupid :)
moving fetchWeeks() to the viewController to try to isolate the issue...but it still blows up as fetchWeeks() has't completed before the code tries to execute the line after and use the weeks array
my viewController:
class ChallengeDetailViewController: UIViewController {
#IBOutlet weak var rideTableView: UITableView!
//set by the inbound segue
var challenge: Challenge?
// set in fetchWeeks based on the challenge
var weeks: [Week]?
override func viewDidLoad() {
super.viewDidLoad()
rideTableView.dataSource = self
rideTableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
rideTableView.delegate = self
fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
//This is where it blows up as weeks is nil
weeks = weeks!.sorted(by: { $0.weekSequence < $1.weekSequence })
}
//moved this to the view controller
func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.sync {
completion(weeks)
}
}
Model.currentModel.publicDB.add(operation)
}
Once again: Never use semaphores with the CloudKit API.
First of all declare data source arrays always as non-optional empty arrays to get rid of unnecessary unwrapping the optional
var weeks = [Week]()
The mistake is that you don't use the fetched data at the right place.
As the closure is asynchronous you have to proceed inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks
self?.weeks = weeks.sorted(by: { $0.weekSequence < $1.weekSequence })
}
or simpler
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
}
And if you need to reload the table view do it also inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
self?.rideTableView.reloadData()
}
To do so you have to call completion on the main thread
DispatchQueue.main.async {
completion(weeks)
}
And finally delete the ugly semaphore!
let semaphore = DispatchSemaphore(value: 0)
...
semaphore.signal()
...
semaphore.wait()

Retaining a Signal or SignalProducer?

Is it my responsibility to maintain a reference to a Signal or a SignalProducer, e.g., using an instance variable?
In other words, will they automatically disappear and stop sending events when they get deallocated?
FYI, not necessary, the Signal will be disposed and stop forwarding events.
Signalis a class type, if no one have a reference to it, it should be deinit.
However, Signal implementation introduces a tricky way to retain itself, see state property, so that there exists some memory leaks in temporary. As seen in source code, if there have some observers subscribe on the Signal, it's state does retain it in turn until all observers unsubscribed or the Signal received completed/error/interrupted event.
Here some marked code snippets.
// definition of SignalState
private struct SignalState<Value, Error: Swift.Error> {
var observers: Bag<Signal<Value, Error>.Observer> = Bag()
var retainedSignal: Signal<Value, Error>? // here is the key
}
public func observe(_ observer: Observer) -> Disposable? {
var token: RemovalToken?
state.modify {
$0?.retainedSignal = self // retain self when one observer on
token = $0?.observers.insert(observer)
}
if let token = token {
return ActionDisposable { [weak self] in
if let strongSelf = self {
strongSelf.state.modify { state in
state?.observers.remove(using: token)
if state?.observers.isEmpty ?? false {
// break retain cycle when disposed
state!.retainedSignal = nil
}
}
}
}
} else {
observer.sendInterrupted()
return nil
}
}
How about SignalProducer?
It is really intuitive, SignalProducer is just struct type, and you should not consider its lifetime.

How to reload UITableView without printing data twice?

I have a single view of SlackTextViewController which works as a UITableView. I'm switching between "states" using a public String allowing it to read 1 set of data in a certain state and another set of data in another state. The problem is when I switch back and forth to the original state it prints the data 2, 3, 4 times; as many as I go back and forth. I'm loading the data from a Firebase server and I think it may be a Firebase issue. Here is my code...
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if chatState == "ALL"
{
self.globalChat()
}else if chatState == "LOCAL"
{
self.localChat()
}
}
override func viewDidDisappear(animated: Bool) {
self.messageModels.removeAll()
tableView.reloadData()
ref.removeAllObservers()
}
func chatACTN()
{
if chatState == "ALL"
{
self.viewWillAppear(true)
}else if chatState == "LOCAL"
{
self.viewWillAppear(true)
}
}
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
let globalRef = ref.child("messages")
globalRef.keepSynced(true)
globalRef.queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
The problem is that each time you call globalChat() you're creating another new observer which results in having multiple observers adding the same items to self.messageModels. Thats why you're seeing the data as many times as you switch to the global state.
Since you want to clear the chat and load the last 100 each time you switch to global, there's no point in keeping the observer active when you switch to "Local".
Just remove the observer when you switch to Local, that should fix your problem.
From firebase docs:
- (void) removeAllObservers
Removes all observers at the current reference, but does not remove any observers at child references.
removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
So, ref.removeAllObservers() will not remove observers at ref.child("messages") level.
Using ref.child("messages").removeAllObservers at the beginning of localChat function to remove the observer you created in globalChat would be ok if you're only dealing with this one observer at this level but if you have more on the same level or you think you might add more in the future the best and safest way would be to remove the specific observer you created. To do that you should use the handle that is returned from starting an observer. Modify your code like this:
var globalChatHandle : FIRDatabaseHandle?
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
ref.child("messages").keepSynced(true)
globalChatHandle = ref.child("messages").queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
if globalChatHandle != nil {
ref.child("messages").removeObserverWithHandle(globalChatHandle)
}
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
And in viewDidDisappear method replace this
ref.removeAllObservers()
with
ref.child("messages").removeAllObservers()

Swift: possible removeObserver cyclic reference in addObserverForName's usingBlock

I'm toying around with a small Swift application. In it the user can create as many MainWindow instances as he wants by clicking on "New" in the application's menu.
The application delegate holds an array typed to MainWindowController. The windows are watched for the NSWindowWillCloseNotification in order to remove the controller from the MainWindowController array.
The question now is, if the removal of the observer is done correctly – I fear there might be a cyclic reference to observer, but I don't know how to test for that:
class ApplicationDelegate: NSObject, NSApplicationDelegate {
private let notificationCenter = NSNotificationCenter.defaultCenter()
private var mainWindowControllers = [MainWindowController]()
func newWindow() {
let mainWindowController = MainWindowController()
let window = mainWindowController.window
var observer: AnyObject?
observer = notificationCenter.addObserverForName(NSWindowWillCloseNotification,
object: window,
queue: nil) { (_) in
// remove the controller from self.mainWindowControllers
self.mainWindowControllers = self.mainWindowControllers.filter() {
$0 !== mainWindowController
}
// remove the observer for this mainWindowController.window
self.notificationCenter.removeObserver(observer!)
}
mainWindowControllers.append(mainWindowController)
}
}
In general you should always specify that self is unowned in blocks registered with NSNotificationCenter. This will keep the block from having a strong reference to self. You would do this with a capture list in front of the parameter list of your closure:
{ [unowned self] (_) in
// Block content
}