Today extension: syncing data with container app - ios

Context
I've been playing around with Today Extensions using this example project.
The app is quite simple:
In the containing app, you have a list of todo items, which you can mark completed
In the Today widget, you see the same list, but you can switch between completed, and incomplete items using a segmented control.
My goal is the following: whenever there is a data change, either in the container app, or the widget, I want both to reflect the changes:
If I mark an item as completed in the container app, then pull down the Notification Center, the widget should be updated
When I do the same in the widget, then return to the app, the app's state should be updated
The implementation
I understand, that the container app, and the extension run in their separate processes, which means two constraints:
NSUserDefaultsDidChangeNotification is useless.
Managing the model instances in memory is useless.
I also know, that in order to access a shared container, both targets must opt-in to the App Groups entitlements under the same group Id.
The data access is managed by an embedded framework, TodoKit. Instead of keeping properties in memory, it goes straight to NSUserDefaults for the appropriate values:
public struct ShoppingItemStore: ShoppingStoreType {
private let defaultItems = [
ShoppingItem(name: "Coffee"),
ShoppingItem(name: "Banana"),
]
private let defaults = NSUserDefaults(suiteName: appGroupId)
public init() {}
public func items() -> [ShoppingItem] {
if let loaded = loadItems() {
return loaded
} else {
return defaultItems
}
}
public func toggleItem(item: ShoppingItem) {
let initial = items()
let updated = initial.map { original -> ShoppingItem in
return original == item ?
ShoppingItem(name: original.name, status: !original.status) : original
}
saveItems(updated)
}
private func saveItems(items: [ShoppingItem]) {
let boxedItems = items.map { item -> [String : Bool] in
return [item.name : item.status]
}
defaults?.setValue(boxedItems, forKey: savedDataKey)
defaults?.synchronize()
}
private func loadItems() -> [ShoppingItem]? {
if let loaded = defaults?.valueForKey(savedDataKey) as? [[String : Bool]] {
let unboxed = loaded.map { dict -> ShoppingItem in
return ShoppingItem(name: dict.keys.first!, status: dict.values.first!)
}
return unboxed
}
return nil
}
}
The problem
Here's what works:
When I modify the list in my main app, then stop the simulator, and then launch the Today target from Xcode, it reflects the correct state. This is true vice-versa.
This verifies, that my app group is set up correctly.
However, when I change something in the main app, then pull down the Notification Center, it is completely out of sync. And this is the part, which I don't understand.
My views get their data straight from the shared container. Whenever a change happens, I immediately update the data in the shared container.
What am I missing? How can I sync up these two properly? My data access class is not managint any state, yet I don't understand why it doesn't behave correctly.
Additional info
I know about MMWormhole. Unfortunately this is not an option for me, since I need to reach proper functionality without including any third party solutions.
This terrific article, covers the topic, and it might be possible, that I need to employ NSFilePresenter, although it seems cumbersome, and I don't completely understand the mechanism yet. I really hope, there is an easier solution, than this one.

Well, I have learned two things here:
First of all, Always double check your entitlements, mine somehow got messed up, and that's why the shared container behaved so awkwardly.
Second:
Although viewWillAppear(_:) is not called, when you dismiss the notification center, it's still possible to trigger an update from your app delegate:
func applicationDidBecomeActive(application: UIApplication) {
NSNotificationCenter.defaultCenter().postNotificationName(updateDataNotification, object: nil)
}
Then in your view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserverForName(updateDataNotification, object: nil, queue: NSOperationQueue.mainQueue()) { (_) -> Void in
self.tableView.reloadData()
}
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Updating your Today widget is simple: each time the notification center is pulled down, viewWillAppear(:_) is called, so you can query for new data there.
I'll update the example project on GitHub shortly.

Related

Core Data sometimes loses data

We are running an App through Citrix Secure Hub, it seems that sometimes there is a rollback with loosing some Data in CoreData.
As i understand, CoreData is having something like an working copy of all the objects, and sometimes its tries to persist that on the filesystem.
Well tried to simulate the behavior but without any success, we could not find out any data loss or rollbacked data in our test environment.
So is there a way to force iOS to write the current "working copy" on the disk to prevent any data loss when using too much memory (and maybe crash)? We call our save function after
As we already found out:
We were NOT using:
func applicationWillResignActive(_ application: UIApplication) {
print("applicationWillResignActive")
}
to save the context, could this be a problem (we are already saving the context after every created object) ?
At the Moment we dont really handle problems when the context could not be saved, are there any recommendations how to handle that in a productive environment? And is it a good thing to maybe crash to app to prevent the user from struggeling with data loss?
Edit: this is the used Core Data Handler:
import Foundation
import CoreData
let context = CoreDataManager.shared.managedObjectContext
func saveContext(_ completion: (() -> Void)? = nil) {
CoreDataManager.shared.save(completion)
}
func saveContextSync() {
CoreDataManager.shared.saveSync()
}
class CoreDataManager: NSObject {
static let shared = CoreDataManager()
lazy var managedObjectContext: NSManagedObjectContext = {
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
return managedObjectContext
}()
And our save functionality:
#objc func save(_ completion: (() -> Void)?) {
saveAsync(completion)
}
func saveAsync(_ completion: (() -> Void)?) {
func save() {
context.perform {
do { try context.save() }
catch {
// HERE WE NEED TO HANDLE IT FOR A PRODUCTIVE ENVIRONMENT
}
completion?()
}
}
if Thread.isMainThread {
save()
} else {
DispatchQueue.main.async {
save()
}
}
}
func saveSync() {
func save() {
context.performAndWait {
do { try context.save() }
catch { print(error)
// TRY TO REPRODUCE MEMORY LOSS APP TO SEE WHAT HAPPENS
abort()
}
}
}
if Thread.isMainThread {
save()
} else {
DispatchQueue.main.sync {
save()
}
}
}
Edit 2: This question in Objective C should be very similar:
Core Data reverts to previous state without apparent reason
Edit 3: It seems that there is no crash, some users telling me that they are adding data, then just press the home button and after a couple of hours the data from the last "task" is lost.
There are three possible causes.
Write Conflicts
Core data generally wants writes to be done in a single synchronous way. If you write in multiple ways at the same time to the same object (even if they are touching different properties and don't strictly conflict), it will be a merge conflict. You can set a merge policy (by default the value is 'error' - meaning don't apply the changes) but that is really a bad solution because you are tell core-data to lose information silently. see NSPersistentContainer concurrency for saving to core data for a setup to prevent merge conflicts.
Closing the app with unsaved data
If you setup your core-data correctly this shouldn't happen. The correct way to setup core data to only read from the 'viewContext' and write in a single synchronous way. Each writing is done in a single atomic block and the UI is only updated after it is saved. If you are displaying information from a context that is not saved to disk this can be a problem. For example it appears that your app only uses a single main-thread context for both reading and writing. Making changes to that context and not calling save will leave the app in a state where there are major changes that are only in memory and not on disk.
There is an error saving to disk
This is by far the rarest event, but there are users that have really really full disks. If this happens there is NOTHING that you can do to save. There is physically no room left on the disk. Generally the correct thing to do is to tell the user and leave it at that.
Without knowing more about your particular setup it hard to say for certain what your problem is. I would recommend the following setup:
use NSPersistentContainer
only read from the viewContext and never write to it.
make an operation queue for writing to core data
for every operation, create a context, make changes to it, and then save. Do not pass any managed object into or out of these blocks.
This should deal with all but the third problem.

Dispose (cancel) observable. SubscribeOn and observeOn different schedulers

Reformed question
I have reformed my question. To the common case.
I want to generate items with RxSwift in background thread (loading from disk, long-running calculations, etc.), and observe items in MainThread. And I want to be sure that no items will be delivered after dispose (from main thread).
According to documentation (https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#disposing):
So can this code print something after the dispose call is executed? The answer is: it depends.
If the scheduler is a serial scheduler (ex. MainScheduler) and dispose is called on the same serial scheduler, the answer is no.
Otherwise it is yes.
But in case of using subscribeOn and observerOn with different schedulers - we cannot guarantee that nothing will be emitted after dispose (manual or by dispose bag, it does not matter).
How should I generate items (images, for example) in background and be sure that result will not be used after the dispose?
I made workaround in real project, but I want to solve this problem and to understand how should we avoid it in the same cases.
In my test project I have used small periods - they demonstrate the problem perfectly!
import RxSwift
class TestClass {
private var disposeBag = DisposeBag()
private var isCancelled = false
init(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
assert(Thread.isMainThread)
load(longRunningTaskDuration: longRunningTaskDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + cancelAfter) { [weak self] in
self?.cancel()
}
}
private func load(longRunningTaskDuration: TimeInterval) {
assert(Thread.isMainThread)
// We set task not cancelled
isCancelled = false
DataService
.shared
.longRunngingTaskEmulation(sleepFor: longRunningTaskDuration)
// We want long running task to be executed in background thread
.subscribeOn(ConcurrentDispatchQueueScheduler.init(queue: .global()))
// We want to process result in Main thread
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { [weak self] (result) in
assert(Thread.isMainThread)
guard let strongSelf = self else {
return
}
if !strongSelf.isCancelled {
print("Should not be called! Task is cancelled!")
} else {
// Do something with result, set image to UIImageView, for instance
// But if task was cancelled, this method will set invalid (old) data
print(result)
}
}, onError: nil)
.disposed(by: disposeBag)
}
// Cancel all tasks. Can be called in PreapreForReuse.
private func cancel() {
assert(Thread.isMainThread)
// For test purposes. After cancel, old task should not make any changes.
isCancelled = true
// Cancel all tasks by creating new DisposeBag (and disposing old)
disposeBag = DisposeBag()
}
}
class DataService {
static let shared = DataService()
private init() { }
func longRunngingTaskEmulation(sleepFor: TimeInterval) -> Single<String> {
return Single
.deferred {
assert(!Thread.isMainThread)
// Enulate long running task
Thread.sleep(forTimeInterval: sleepFor)
// Return dummy result for test purposes.
return .just("Success")
}
}
}
class MainClass {
static let shared = MainClass()
private init() { }
func main() {
Timer.scheduledTimer(withTimeInterval: 0.150, repeats: true) { [weak self] (_) in
assert(Thread.isMainThread)
let longRunningTaskDuration: TimeInterval = 0.050
let offset = TimeInterval(arc4random_uniform(20)) / 1000.0
let cancelAfter = 0.040 + offset
self?.executeTest(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
}
}
var items: [TestClass] = []
func executeTest(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
let item = TestClass(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
items.append(item)
}
}
Call MainClass.shared.main() somewhere to start.
We call method to load some data and later we call cancel (all from Main Thread). After cancel we sometimes receive the result (in main thread too), but it is old already.
In real project TestClass is a UITableViewCell subclass and cancel method is called in prepareForReuse. Then cell is being reused and new data is set to the cell. And later we get the result of OLD task. And old image is set to the cell!
ORIGINAL QUESTION (OLD):
I would like to load image with RxSwift in iOS. I want to load image in background, and to use it in main thread. So I subscribeOn background thread, and observeOn main thread. And function will look like this:
func getImage(path: String) -> Single<UIImage> {
return Single
.deferred {
if let image = UIImage(contentsOfFile: path) {
return Single.just(image)
} else {
return Single.error(SimpleError())
}
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
}
But I get problems with cancelation. Because different schedulers are used to create items and to call dispose (disposing from main thread), subscription event can be raised after dispose is called. So in my case of using in UITableViewCell I receive invalid (old) image.
If I create item (load image) in the same scheduler that observes (Main thread), everything works fine!
But I would like to load images in background and I want it will be canceled after disposing (in prepareForReuse method or in new path set method). What is the common template for this?
EDIT:
I have created a test project, where I can emulate the problem when the event is received after dispose.
And I have one simple solution that works. We should emit items in the same scheduler. So we should capture scheduler and emit items there (after long running task completes).
func getImage2(path: String) -> Single<UIImage> {
return Single
.create(subscribe: { (single) -> Disposable in
// We captrure current queue to execute callback in
// TODO: It can be nil if called from background thread
let callbackQueue = OperationQueue.current
// For async calculations
OperationQueue().addOperation {
// Perform any long-running task
let image = UIImage(contentsOfFile: path)
// Emit item in captured queue
callbackQueue?.addOperation {
if let result = image {
single(.success(result))
} else {
single(.error(SimpleError()))
}
}
}
return Disposables.create()
})
.observeOn(MainScheduler.instance)
}
But it is not in Rx way. And I think this is not the best solution.
May be I should use CurrentThreadScheduler to emit items, but I cannot understand how. Is there any tutorial or example of items generation with schedulers usage? I did not find any.
Interesting test case. There is a small bug, it should be if strongSelf.isCancelled instead of if !strongSelf.isCancelled. Apart from that, the test case shows the problem.
I would intuitively expect that it is checked whether a dispose has already taken place before emitting, if it happens on the same thread.
I found additionally this:
just to make this clear, if you call dispose on one thread (like
main), you won't observe any elements on that same thread. That is a
guarantee.
see here: https://github.com/ReactiveX/RxSwift/issues/38
So maybe it is a bug.
To be sure I opened an issue here:
https://github.com/ReactiveX/RxSwift/issues/1778
Update
It seems it was actually a bug. Meanwhile, the fine people at RxSwift have confirmed it and fortunately fixed it very quickly. See the issue link above.
Testing
The bug was fixed with commit bac86346087c7e267dd5a620eed90a7849fd54ff. So if you are using CocoaPods, you can simply use something like the following for testing:
target 'RxSelfContained' do
use_frameworks!
pod 'RxAtomic', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
pod 'RxSwift', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
end

OBEXFileTransferServices doesn't connect

I'm trying to write a macOS app that would connect to already paired the bluetooth phone and retrieves the list of address book entries and call records. This information should be available via standard OBEX interface. I'm relatively new to macOS development (although have enough experience with iOS development) and I have a feeling that I'm doing something wrong on a very basic level.
Here are snippets of my code:
First I'm finding particular paired Bluetooth device by its address
let paired = IOBluetoothDevice.pairedDevices()
let device = paired?.first(where: { (device) -> Bool in
return (device as? IOBluetoothDevice)?.addressString == "some_address"
}) as? IOBluetoothDevice
This actually works fine and I'm getting back valid object. Next, I'm picking up address book service and creating BluetoothOBEXSession for it
let service = device!.getServiceRecord(for: IOBluetoothSDPUUID(uuid32:kBluetoothSDPUUID16ServiceClassPhonebookAccess.rawValue))
let obexSession = IOBluetoothOBEXSession(sdpServiceRecord: service!)
This also works fine, I'm getting proper service object and session is created.
Next step (I would assume) is to create an OBEXFileTransfer session and do something (like checking current directory or retrieving the content of telecom/cch which supposed to have the list of combined outgoing and incoming calls:
let ftp = OBEXFileTransferServices(obexSession: obexSession!)
ftp!.delegate = self
if ftp!.connectToFTPService() == 0 {
NSLog("\(ftp!.currentPath())") // -- empty
ftp!.changeCurrentFolderForward(toPath: "telecom/cch")
NSLog("\(ftp!.currentPath())") // -- empty
ftp!.retrieveFolderListing()
}
I have added the following delegate's method to my view controller (to receive callbacks from OBEX FTS but they never get called:
override func fileTransferServicesRetrieveFolderListingComplete(_ inServices: OBEXFileTransferServices!, error inError: OBEXError, listing inListing: [Any]!) {
NSLog("Listing complete...")
}
override func fileTransferServicesConnectionComplete(_ inServices: OBEXFileTransferServices!, error inError: OBEXError) {
NSLog("Connection complete...")
}
override func fileTransferServicesDisconnectionComplete(_ inServices: OBEXFileTransferServices!, error inError: OBEXError) {
NSLog("Disconnect complete...")
}
override func fileTransferServicesAbortComplete(_ inServices: OBEXFileTransferServices!, error inError: OBEXError) {
NSLog("Abort complete...")
}
What am I doing wrong here?
I also could not find any good Bluetooth examples for macOS either, if somebody has good links, please do share.

Why does Example app using Photos framework use stopCachingImagesForAllAssets after each change?

So I'm looking at the Photos API and Apple's sample code here
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
and its conversion to swift here
https://github.com/ooper-shlab/SamplePhotosApp-Swift
I have integrated the code into my project so that a collectionView is successfully updating itself form the library as I take photos. There is one quirk: Sometimes cells are blank, and it seems to be connected to stopCachingImagesForAllAssets which Apple calls each time the library is updated at the end of photoLibraryDidChange delegate method.
I can remove the line and it fixes the problem, but surely there is a reason Apple put it there in the first place? I am concerned with memory usage.
// MARK: - PHPhotoLibraryChangeObserver
func photoLibraryDidChange(changeInstance: PHChange) {
// Check if there are changes to the assets we are showing.
guard let
assetsFetchResults = self.assetsFetchResults,
collectionChanges = changeInstance.changeDetailsForFetchResult(assetsFetchResults)
else {return}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue()) {
// Get the new fetch result.
self.assetsFetchResults = collectionChanges.fetchResultAfterChanges
let collectionView = self.pictureCollectionView!
if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
// Reload the collection view if the incremental diffs are not available
collectionView.reloadData()
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
collectionView.performBatchUpdates({
if let removedIndexes = collectionChanges.removedIndexes
where removedIndexes.count > 0 {
collectionView.deleteItemsAtIndexPaths(removedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let insertedIndexes = collectionChanges.insertedIndexes
where insertedIndexes.count > 0 {
collectionView.insertItemsAtIndexPaths(insertedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let changedIndexes = collectionChanges.changedIndexes
where changedIndexes.count > 0 {
collectionView.reloadItemsAtIndexPaths(changedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
}, completion: nil)
}
self.resetCachedAssets() //perhaps prevents memory warning but causes the empty cells
}
}
//MARK: - Asset Caching
private func resetCachedAssets() {
self.imageManager?.stopCachingImagesForAllAssets()
self.previousPreheatRect = CGRectZero
}
I was having the same result.
Here's what fixed the issue for me:
Since performBatchUpdates is asynchronous, the resetCachedAssets gets executed possibly while the delete/insert/reload is happening, or even between those.
That didn't sound nice to me. So I moved the line:
self.resetCachedAssets()
to the first line of the dispatch_async block.
I hope this helps you too.

Swift Problems - NSUserDefault and Settings Bundle not responding

So I had set up a settings bundle to just do one thing. Allow the Users to choose between the The TouchUI and GestureUI in my app and for some odd reason, I am unable to get the Settings Bundle to control it. It stays with one and doesn't switch even when I have the If else Statement. originally I had var touchCheck = userDefaults.boolForKey("myGestureEnabledDisabled") but it didn't change boolean at all when i keep closing app(Multitask > SwipeUp app) and re running the app via springboard. The Settings App could have the bundle at NO but log says gestures are on. After watching a Tutorial, i changed boolForKey to valueForKey which causes the build to fail and there is no error in the code the way i have it below.
override func viewDidLoad() {
var userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.synchronize()
var touchCheck = Bool(userDefaults.valueForKey("myGestureEnabledDisabled"))
if touchCheck {
whenGuestureIsEnabled()
} else {
whenGestureIsDisabled()
self.navBar.hidden = false
}
}
func whenGuestureIsEnabled() {
NSLog("Gesture is suppose to be on")
}
func whenGestureIsDisabled() {
NSLog("Gesture is OFF")
}
Maybe from what I was thinking, I shouldn't do this in UIViewController but I had seen this in action in a youtube tutorial and it was in OBJ-C.
You should cast to bool, not to create a new one:
//notice (Bool) cast in the beginning
var touchCheck = (Bool)userDefaults.valueForKey("myGestureEnabledDisabled")

Resources