swift - My Protocol and Delegate isn't working - ios

I have created a protocol called TermProtocol with a function inside called gotTerm. Whenever the user presses this button in my view(CategoryView), the delegate is supposed to get a callback and run the code that I have set. The delegate in my case would be the AppDelegate. I put a print statement in the function called gotTerm in the app delegate so that I could see if the code was really running, but I never saw the phrase I put in the print statement in the console. I don't think my delegate is getting a callback. can anyone help me, please?
Here is my code:
TermProtocol:
protocol TermProtocol: class{
func gotTerm()
}
CategoryView.Swift
struct CategoryView: View {
var foodCategory: String
let appDelegate = UIApplication.shared.delegate as? AppDelegate
var delegate: TermProtocol?
var body: some View {
NavigationView {
NavigationLink(destination: RestrauntView()) {
Image("Find Button")
.renderingMode(.original)
}.simultaneousGesture(TapGesture().onEnded{
print("didtapallow 4")
self.appDelegate!.terms = self.foodCategory
self.delegate?.gotTerm()
})
Spacer()
}
}
}
struct CategoryView_Previews: PreviewProvider {
static var previews: some View {
CategoryView(foodCategory: "Chinese Food")
}
}
AppDelegate.swift
import UIKit
import CoreData
import Moya
import Alamofire
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, TermProtocol {
func gotTerm() {
print("did get term")
let lat = self.locationService.setup(latOrLong: "lat")
let long = self.locationService.setup(latOrLong: "long")
self.locationService.lat = lat
self.locationService.long = long
self.loadBusinesses(lat: lat, long: long, theTerm: self.terms)
}
var theViewModels = [RestrauntListViewModel]()
let locationService = LocationService()
var terms = ""
var categoryView = CategoryView(foodCategory: "indian")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
categoryView.delegate = self
let dataSource = DataSource()
print("this is the view models in appDelegate: \(theViewModels)")
locationService.didChangeStatus = { [weak self] success in
print("did tap allow 1")
if success {
self?.locationService.getLocation()
}
}
locationService.newLocation = { [weak self] result in
print("did tap allow 2")
switch result {
case .success(let location):
self?.loadBusinesses(lat: location.coordinate.latitude, long: location.coordinate.longitude, theTerm: "chinese")
case .failure(let error):
assertionFailure("Error getting the users location \(error)")
}
}
locationService.setup(latOrLong: "lat")
return true
}
func loadBusinesses (lat: Double, long: Double, theTerm: String) {
let service = MoyaProvider<YelpService.BusinessProvider>()
let jsonDecoder = JSONDecoder()
let restrauntView = RestrauntView()
let appDelegate = AppDelegate()
print("The latitude of u is \(lat) and the long of you is \(long)")
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined, .restricted, .denied:
print("No access")
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
service.request(.search(lat: lat, long: long, term: theTerm)) { (result) in
switch result{
case.success(let response):
print("yaya")
let root = try? jsonDecoder.decode(Root.self, from: response.data)
let viewModels = root?.businesses.compactMap(RestrauntListViewModel.init)
let dataSource = DataSource()
dataSource.arrayOfImages.removeAll()
for image in viewModels! {
Alamofire.request(image.imageURL).responseImage { response in
if let image = response.result.value {
print("image downloadedline 59 appdelegate")
dataSource.arrayOfImages.append(image)
print(dataSource.arrayOfImages)
} else {
print("ERROR: image does not = response.result.value")
}
}
}
self.theViewModels = (root?.businesses.compactMap(RestrauntListViewModel.init))!
print(" restrauntView.theViewModels is here \(restrauntView.theViewModels)")
print("the constant theViewModels in the appdelegate has \(appDelegate.theViewModels.count) values")
case .failure(let error):
print("Error: \(error)")
}
}
#unknown default:
break
}
} else {
print("Location services are not enabled")
}
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "Actrual_Food_Circle")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

I'll start by stating that you shouldn't use delegates and closures this way in SwiftUI. Here's a pretty good intro into clean MVVM architecture using SwiftUI - https://nalexn.github.io/clean-architecture-swiftui/.
I believe you would want to add ViewModel for handling those states in your application.
Going back to answering your question - I think that in this context adding closures would be the quickest(not necessarily right) solution:
struct ContentView: View {
var gotTermCallback: (()->())?
var body: some View {
NavigationView {
NavigationLink(destination: Text("test")) {
...
}.simultaneousGesture(TapGesture().onEnded{
...
self.gotTermCallback?()
})
...
}
}
}
And then in you can add it to some kind of handler or side effect handler by simply calling the following code:
var contentView: ContentView = ContentView()
contentView.gotTermCallback = {
print("[DEBUG] - terms callback")
}

Related

How to update state from Phone view on WatchOS view?

I'm using a companion app to authorize a user with a 3rd party service. Once authorized, I update a UserDefaults variable to true. On the companion app side, the view updates correctly and shows that the user has been authenticated. However, on the watch OS side the view does not update. Would I need to use the Watch Connectivity API and send a message to the watch to update the state? Or is there a simple way?
Phone App
struct AuthenticationView: View {
#State private var startingWebAuthenticationSession = false
#AppStorage("authorized") private var authorized = false
var body: some View {
Group {
if !authorized {
VStack {
Button("Connect", action: { self.startingWebAuthenticationSession = true })
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://service.com/oauth/authorize?scope=email%2Cread_stats&response_type=code&redirect_uri=watch%3A%2F%2Foauth-callback&client_id=\(clientId)")!,
callbackURLScheme: callbackURLScheme
) { callbackURL, error in
guard error == nil, let successURL = callbackURL else {
return
}
let oAuthCode = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
guard let authorizationCode = oAuthCode?.value else { return }
let url = URL(string: "https://service.com/oauth/token")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let params = "client_id=\(clientId)&client_secret=\(clientSecret)&grant_type=authorization_code&code=\(authorizationCode)&redirect_uri=\(callbackURLScheme)://oauth-callback";
request.httpBody = params.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Error took place \(error)")
return
}
if let data = data, let response = String(data: data, encoding: .utf8) {
let accessTokenResponse: AccessTokenResponse = try! JSONDecoder().decode(AccessTokenResponse.self, from: response.data(using: .utf8)!)
let defaults = UserDefaults.standard
authorized = true
startingWebAuthenticationSession = false
defaults.set(accessTokenResponse.access_token, forKey: DefaultsKeys.accessToken) //TODO: Store securely
ConnectivityService.shared.send(authorized: true)
}
}
task.resume()
}
.prefersEphemeralWebBrowserSession(false)
}
}
}
else {
VStack {
Text("Authenticated!")
}
}
}
}
}
WatchOS
import SwiftUI
struct ConnectView: View {
#ObservedObject var connectivityService: ConnectivityService
var body: some View {
if !$connectivityService.authorized.wrappedValue {
VStack {
Text("Open the app on your primary device to connect.")
}
}
else {
//Some other view
}
}
}
EDIT:
Trying with Watch Connectivity API but the issue I'm experiencing is that when I authenticate from the phone, it'll take some time for the ConnectView to update the authorized variable. I know Watch Connectivity API doesn't update right away but at minimum I'd need some way for the watch to pick up that a secret access token has been retrieved and it can transition to the next view; whether that's through a shared state variable, UserDefaults, or whatever other mechanism.
Here is the ConnectivityService class I'm using:
import Foundation
import Combine
import WatchConnectivity
final class ConnectivityService: NSObject, ObservableObject {
static let shared = ConnectivityService()
#Published var authorized: Bool = false
override private init() {
super.init()
#if !os(watchOS)
guard WCSession.isSupported() else {
return
}
#endif
WCSession.default.delegate = self
WCSession.default.activate()
}
public func send(authorized: Bool, errorHandler: ((Error) -> Void)? = nil) {
guard WCSession.default.activationState == .activated else {
return
}
#if os(watchOS)
guard WCSession.default.isCompanionAppInstalled else {
return
}
#else
guard WCSession.default.isWatchAppInstalled else {
return
}
#endif
let authorizationInfo: [String: Bool] = [
DefaultsKeys.authorized: authorized
]
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
}
}
extension ConnectivityService: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
func session(
_ session: WCSession,
didReceiveUserInfo userInfo: [String: Any] = [:]
) {
let key = DefaultsKeys.authorized
guard let authorized = userInfo[key] as? Bool else {
return
}
self.authorized = authorized
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
self.authorized = true
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}
#endif
}
I tried doing these two lines but they have varying results:
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
In the first line, XCode will say that no watch app could be found, even though I'm connected to both physical devices through XCode; launch phone first then watch. I believe the first one is immediate and the second is more of when the queue feels like it. Sometimes if I hard close the watch app, it'll pick up the state change in the authorized variable, sometimes it won't. Very frustrating inter-device communication.
UserDefaults doesn't pick up the access token value on the watch side. Maybe I have to use App Groups?
I do see this error on the Watch side:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
So I thought to try and encapsulate the self.authorized = authorized call into something like:
DispatchQueue.main.async {
self.authorized = authorized
}
But it didn't do anything as far as solving the immediate state change issue.

Correct use of background fetch completion handler

My app uses CloudKit and I am trying to implement background fetch.
The method in App Delegate calls a method in my main view controller which checks for changes in the CloudKit database.
However, I realise that I am not calling the completion handler correctly, as the closures for the CloudKit will return asynchronously. I am really unsure how best to call the completion handler in the app delegate method once the operation is complete. Can I pass the completion handler through to the view controller method?
App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges()
completionHandler(.newData)
}
}
Main view controller method to get CloudKit changes
// Fetch zone changes (a method in main table view controller)
func getZoneChanges() {
DispatchQueue.global(qos: .userInitiated).async {
let customZone = CKRecordZone(zoneName: "Drugs")
let zoneID = customZone.zoneID
let zoneIDs = [zoneID]
let changeToken = UserDefaults.standard.serverChangeToken // Custom way of accessing User Defaults using an extension
// Look up the previous change token for each zone
var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]()
// Some other functioning code to process options
// CK Zone Changes Operation
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID)
// Closures for records changed, deleted etc.
// Closure details omitted for brevity as fully functional as expected.
// These closures change data model, Spotlight indexing, notifications and trigger UI refresh etc.
operation.recordChangedBlock = { (record) in
// Code...
}
operation.recordWithIDWasDeletedBlock = { (recordId, string) in
// Code...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
}
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
} else {
print("Changes fetched successfully!")
// Save local items
self.saveData() // Uses NSCoding
}
}
CKContainer.default().privateCloudDatabase.add(operation)
}
}
Update your getZoneChanges to have a completion parameter.
func getZoneChanges(completion: #escaping (Bool) -> Void) {
// the rest of your code
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
completion(false)
} else {
print("Changes fetched successfully!")
completion(true)
}
}
}
Then you can update the app delegate method to use it:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges { (success) in
completionHandler(success ? .newData : .noData)
}
}
}

AVSystemController_SystemVolumeDidChangeNotification not giving callback for the first time

I want to check if both the volume buttons are working fine. So I set the observer AVSystemController_SystemVolumeDidChangeNotification to check that.
NotificationCenter.default.addObserver(self, selector: #selector(volumeCallback(notification:)), name: NSNotification.Name("AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
Given is volumeCallback method:
#objc private func volumeCallback(notification: NSNotification) {
// check if app is in forground
guard UIApplication.shared.applicationState == .active else {
return
}
//get volume level
if let userInfo = notification.userInfo {
if let volumeChangeType = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String {
if volumeChangeType == "ExplicitVolumeChange" {
print("value changed")
let level = userInfo["AVSystemController_AudioVolumeNotificationParameter"] as? Float
guard let volLevel = level else {
return
}
// my work here
}
}
}
}
Now the problem is, I am not getting callback in volumeCallback for the first installation of the app. The weird thing is, this method is being called when the app is in background, but not being called in foreground.
I am using iPhone 5s (iOS 10.3.3).
I don't understand what is the problem in this code. Any help will be appreciated.
This can be easily done with key-value observer as AVAudioSession provides outputVolume property. Check here.
You can just add observer on this property and get callbacks.
Here's a simple way of doing this in Swift 5:
// Audio session object
private let session = AVAudioSession.sharedInstance()
// Observer
private var progressObserver: NSKeyValueObservation!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("cannot activate session")
}
progressObserver = session.observe(\.outputVolume) { [weak self] (session, value) in
print(session.outputVolume)
}
return true
}

How to use Ensembles Framework in Swift Project

I have a project which already uses Core Data. I have added support for the iPad, but now will need to use iCloud & Core Data to sync the data.
I came across Ensembles, it seems like an easy and robust framework to add to my project. Found here: https://github.com/drewmccormack/ensembles
However there are no Swift example projects with the Ensembles project so have attempted to do it myself. Here are the steps I have taken,
Step 1
Manually add Ensembles to iOS projects.
Step 2
Create new CoreDataStack using existing persistent store .sql file.
import UIKit
import CoreData
class CoreDataStack: NSObject, CDEPersistentStoreEnsembleDelegate {
static let defaultStack = CoreDataStack()
var ensemble : CDEPersistentStoreEnsemble? = nil
var cloudFileSystem : CDEICloudFileSystem? = nil
// MARK: - Core Data stack
lazy var storeName : String = {
return NSBundle.mainBundle().objectForInfoDictionaryKey(kCFBundleNameKey as String) as! String
}()
lazy var sqlName : String = {
return "SingleViewCoreData.sqlite"
}()
lazy var icloudStoreName : String = {
return self.storeName + "CloudStore"
}()
lazy var storeDescription : String = {
return "Core data stack of " + self.storeName
}()
lazy var iCloudAppID : String = {
return "iCloud." + NSBundle.mainBundle().bundleIdentifier!
}()
lazy var modelURL : NSURL = {
return NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")!
}()
lazy var storeDirectoryURL : NSURL = {
var directoryURL : NSURL? = nil
do {
try directoryURL = NSFileManager.defaultManager().URLForDirectory(NSSearchPathDirectory.ApplicationSupportDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
directoryURL = directoryURL!.URLByAppendingPathComponent(NSBundle.mainBundle().bundleIdentifier!, isDirectory: true)
} catch {
NSLog("Unresolved error: Application's document directory is unreachable")
abort()
}
return directoryURL!
}()
lazy var storeURL : NSURL = {
return self.storeDirectoryURL.URLByAppendingPathComponent(self.sqlName)
// return self.applicationDocumentsDirectory.URLByAppendingPathComponent(self.sqlName)
}()
lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "com.dprados.CoreDataSpike" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1]
}()
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")
return NSManagedObjectModel(contentsOfURL: modelURL!)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
let coordinator : NSPersistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
var options = [NSObject: AnyObject]()
options[NSMigratePersistentStoresAutomaticallyOption] = NSNumber(bool: true)
options[NSInferMappingModelAutomaticallyOption] = NSNumber(bool: true)
do {
try NSFileManager.defaultManager().createDirectoryAtURL(self.storeDirectoryURL, withIntermediateDirectories: true, attributes: nil)
} catch {
NSLog("Unresolved error: local database storage position is unavailable.")
abort()
}
// Create the coordinator and store
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error as! NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
lazy var managedObjectContext: NSManagedObjectContext = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return managedObjectContext
}()
// MARK: - Core Data Saving support
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
static func save() {
CoreDataStack.defaultStack.saveContext()
}
func enableEnsemble() {
CoreDataStack.defaultStack.cloudFileSystem = CDEICloudFileSystem(ubiquityContainerIdentifier: nil)
CoreDataStack.defaultStack.ensemble = CDEPersistentStoreEnsemble(ensembleIdentifier: self.storeName, persistentStoreURL: self.storeURL, managedObjectModelURL: self.modelURL, cloudFileSystem: CoreDataStack.defaultStack.cloudFileSystem)
CoreDataStack.defaultStack.ensemble!.delegate = CoreDataStack.defaultStack
}
func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
CoreDataStack.defaultStack.managedObjectContext.performBlockAndWait({ () -> Void in
CoreDataStack.defaultStack.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
})
if notification != nil {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.02 * Double(NSEC_PER_MSEC))), dispatch_get_main_queue(), {
NSLog("Database was updated from iCloud")
CoreDataStack.defaultStack.saveContext()
NSNotificationCenter.defaultCenter().postNotificationName("DB_UPDATED", object: nil)
})
}
}
func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, globalIdentifiersForManagedObjects objects: [AnyObject]!) -> [AnyObject]! {
NSLog("%#", (objects as NSArray).valueForKeyPath("uniqueIdentifier") as! [AnyObject])
return (objects as NSArray).valueForKeyPath("uniqueIdentifier") as! [AnyObject]
}
func syncWithCompletion(completion: (() -> Void)!) {
if CoreDataStack.defaultStack.ensemble!.leeched {
CoreDataStack.defaultStack.ensemble!.mergeWithCompletion({ (error:NSError?) -> Void in
if error != nil && error!.code != 103 {
NSLog("Error in merge: %#", error!)
} else if error != nil && error!.code == 103 {
self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
} else {
if completion != nil {
completion()
}
}
})
} else {
CoreDataStack.defaultStack.ensemble!.leechPersistentStoreWithCompletion({ (error:NSError?) -> Void in
if error != nil && error!.code != 103 {
NSLog("Error in leech: %#", error!)
} else if error != nil && error!.code == 103 {
self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
} else {
self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
if completion != nil {
completion()
}
}
})
}
}
}
Step 3
Update App Delegate to sync and add notifications
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let _ : CoreDataStack = CoreDataStack.defaultStack
// Value.ValueTypeInManagedObjectContext(CoreDataStack.defaultStack.managedObjectContext)
CoreDataStack.defaultStack.saveContext()
CoreDataStack.defaultStack.enableEnsemble()
// Listen for local saves, and trigger merges
NSNotificationCenter.defaultCenter().addObserver(self, selector: "localSaveOccured:", name: CDEMonitoredManagedObjectContextDidSaveNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "cloudDataDidDownload:", name:CDEICloudFileSystemDidDownloadFilesNotification, object:nil)
CoreDataStack.defaultStack.syncWithCompletion(nil);
return true
}
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
let identifier : UIBackgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
CoreDataStack.defaultStack.saveContext()
CoreDataStack.defaultStack.syncWithCompletion( { () -> Void in
UIApplication.sharedApplication().endBackgroundTask(identifier)
})
}
func applicationWillResignActive(application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
NSLog("Received a remove notification")
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
CoreDataStack.defaultStack.saveContext()
}
func localSaveOccured(notif: NSNotification) {
NSLog("Local save occured")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
func cloudDataDidDownload(notif: NSNotification) {
NSLog("Cloud data did download")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
Step 4
Add Notifications to project to refresh UI
override func viewWillAppear(animated: Bool) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "cloudDataDidDownload:", name:"DB_UPDATED", object:nil)
//cloudDataDidDownload refetches the entities and reload the table
}
Step 5
Watch the magic happen.. Unfortunately there is no magic atm. The new CoreDataStack works fine, I can save and retrieve data from the persistent store.
I have two devices logged into the same iCloud account, and neither data is shared to the other device.
When deleting the app and reinstalling the data is not retrieved from the iCloud and saved to the persistent store.
I do get the following NSLog when 'sometimes' saving data or loading up the app.
2016-04-06 13:17:37.101 APPNAME[435:152241] Cloud data did download
This is the outcome for the following appDelegate notification function
func cloudDataDidDownload(notif: NSNotification) {
NSLog("Cloud data did download")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
The notification is sent from this function in the CoreDataStack for when changes are merged.
func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
CoreDataStack.defaultStack.managedObjectContext.performBlockAndWait({ () -> Void in
CoreDataStack.defaultStack.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
})
if notification != nil {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.02 * Double(NSEC_PER_MSEC))), dispatch_get_main_queue(), {
NSLog("Database was updated from iCloud")
CoreDataStack.defaultStack.saveContext()
NSNotificationCenter.defaultCenter().postNotificationName("DB_UPDATED", object: nil)
})
}
}
So everything seems like it should be working fine. I get no errors but the data is not syncing. I cannot tell if the problem is the data backing up to iCloud or retrieving from iCloud and merging with the persistent store. All I can tell is that data is not shared between devices using the same iCloud account and the app does not actually restore the data from the iCloud when reinstalling the app.

How to make Core Data's ManagedObjectContext.ExecuteFetchRequest Synchronous and not Asynchronous

Attached is my code below.
The line that is giving me the problems is let fetchRequest = try moc.executeFetchRequest(fetchRequest) as! [AppSettings] appears to load asynchronously but I want it to load synchronously so that I can ensure it checks properly for a username record.
How do I do this?
I know it loads asynchronously because when I start and stop the program constantly it will find the entity roughly 80% of the time and randomly 20% of the time it will not. Since nothing else is changing the entity (since I'm just starting and stopping the program constantly), it would make sense that the code is being run asynchrnously so when I use the command
guard let appSettingsArrayItem = fetchRequest.first where fetchRequest.count>0 else {
print ("no entities found...")
return false
}
It fails to find any entities sometimes.
Check Login Function
func checkIfLoggedInAlready() -> Bool{
let fetchRequest = NSFetchRequest(entityName: "AppSettings")
//let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) //Deletes ALL appsettings entities
do {
let fetchRequest = try moc.executeFetchRequest(fetchRequest) as! [AppSettings]
guard let appSettingsArrayItem = fetchRequest.first where fetchRequest.count>0 else {
print ("no entities found...")
return false
}
guard let username = (appSettingsArrayItem as AppSettings).username else{
print ("username not found")
return false
}
print("number Of AppSetting Entities =\(fetchRequest.count)")
print(username)
//The following code deletes ALL the entities!
//try moc.persistentStoreCoordinator!.executeRequest(deleteRequest, withContext: moc)
//To delete just '1' entry use the code below.
//moc.deleteObject(appSettingsArrayItem)
//try moc.save()//save deletion change.
//print("deleted particular entity item")
return true
} catch{
fatalError("bad things happened \(error)")
}
}
Entire LoginViewController including Check Login Function
import UIKit
import CoreData
class LoginViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var usernameField: UITextField!
#IBOutlet weak var passwordField: UITextField!
var isLoggedIn = false
let moc = DataController().managedObjectContext
#IBAction func SignUpButtonPressed(sender: UIButton) {
print("sign up")
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
view.addGestureRecognizer(tap)
print("view loaded, check if already signed in here")
let loggedIn = checkIfLoggedInAlready() //checks database to see
if(loggedIn){
print("was logged in!")
isLoggedIn = true
self.performSegueWithIdentifier("loginSegue", sender: self)
}
}
func checkIfLoggedInAlready() -> Bool{
let fetchRequest = NSFetchRequest(entityName: "AppSettings")
//let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) //Deletes ALL appsettings entities
do {
let fetchRequest = try moc.executeFetchRequest(fetchRequest) as! [AppSettings]
guard let appSettingsArrayItem = fetchRequest.first where fetchRequest.count>0 else {
print ("no entities found...")
return false
}
guard let username = (appSettingsArrayItem as AppSettings).username else{
print ("username not found")
return false
}
print("number Of AppSetting Entities =\(fetchRequest.count)")
print(username)
//The following code deletes ALL the entities!
//try moc.persistentStoreCoordinator!.executeRequest(deleteRequest, withContext: moc)
//To delete just '1' entry use the code below.
//moc.deleteObject(appSettingsArrayItem)
//try moc.save()//save deletion change.
//print("deleted particular entity item")
return true
} catch{
fatalError("bad things happened \(error)")
}
}
func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
print("prepare seque")
}
func displayErrorMessage(errorMessage: String){
print("show error console with Error:"+errorMessage)
let alert = UIAlertController(title: "Error", message: errorMessage, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
switch(identifier){
case "loginSegue":
print("Is the user already logged in?")
if(isLoggedIn){
print("Detected as YES")
return true
}
print("Detected as NO, so checking username and password fields next...")
guard let password = passwordField.text!.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet()) where !password.isEmpty else {
displayErrorMessage("Password can not be empty!")
return false
}
guard let username = usernameField.text!.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet()) where !username.isEmpty else{
displayErrorMessage("Username can not be empty!")
return false
}
let url = "http://distribution.tech/restapi/v1/userlogin?email="+username+"&password="+password
print(url)
let json = JSON(url:url)
print(json)
if(json["status"].asInt==1){
let entity = NSEntityDescription.insertNewObjectForEntityForName("AppSettings", inManagedObjectContext: moc) as! AppSettings
entity.setValue(username, forKey: "username")
entity.setValue(password, forKey: "password")
entity.setValue(json["tokenid"].asString, forKey: "token")
entity.setValue(json["roleid"].asInt, forKey: "roleid")
entity.setValue(json["role"].asString, forKey: "role")
entity.setValue(json["companyid"].asInt , forKey: "companyid")
entity.setValue(json["isdev"].asInt, forKey: "isdev")
//save token and other details to database.
do {
try moc.save()
print("saved to entity")
}catch{
fatalError("Failure to save context: \(error)")
}
// token
// roleid int
// role
// companyid int
//
// {
// "companyid": 3,
// "userid": 2,
// "tokenid": "804febae26ddbd0292b3d2c66b30afd5028d5ba9",
// "status": 1,
// "roleId": 1,
// "role": "super_admin",
// "isdev": 0
// }
//Save to disk using our own method, as COREDATA is unreliable!
return true //login succesfull
}else{
displayErrorMessage("Incorrect Username or Email")
return false//failed
}
default:
displayErrorMessage("Unknown Error Related To Segue Not Found")
}
return false //if it gets to this point assume false
}
}
The managed object is created in the DataController its file is here below.
import UIKit
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
override init() {
// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = NSBundle.mainBundle().URLForResource("AppSettings", withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
self.managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
self.managedObjectContext.persistentStoreCoordinator = psc
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let docURL = urls[urls.endIndex-1]
/* The directory the application uses to store the Core Data store file.
This code uses a file named "DataModel.sqlite" in the application's documents directory.
*/
let storeURL = docURL.URLByAppendingPathComponent("AppSettings.sqlite")
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
}
}
Image Reference Of Entity & Console Error That Can Happen Sometimes
Image Reference Of Entity & Console When It Does Find Entity Most Of Time
ManagedObjectContext.ExecuteFetchRequest already runs synchronously but it looks like you are setting up your persistent store coordinator asynchronously in a background priority thread.
If this fetch request happens immediately when the app starts up, and you do it over and over again, it may not be finished setting up some of the times.
Okay the answer above was correct, so what I did was created a new project single view, selected core data option, and copied code from its AppDelegate over my own AppDelegate to get the proper CoreData Init Code, and in such a way that when the project terminates it saves the context correctly and so forth. The code looks like this.
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
self.saveContext()
}
// MARK: - Core Data stack
lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "com.distribution.tech.Test" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1]
}()
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("AppSettings", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("AppSettings.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
lazy var managedObjectContext: NSManagedObjectContext = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
// MARK: - Core Data Saving support
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
}
Its key when you do this that you change the reference to your own xcdatamodeld or this won't work. In my case it was changing this line to the correct sqlite based on my previous work.
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("AppSettings.sqlite")
and this line...
let modelURL = NSBundle.mainBundle().URLForResource("AppSettings", withExtension: "momd")!
which is the actual name of the xcdatamodeld file.
Hope this helps someone who had same issue as me. Oh...and apple if you are reading this...please add 'core data' option for tab based projects in the future... and not just single view.

Resources