Cloudkit error handling not being called if qualityOFService is missing - swift

I am trying to handle Cloudkit errors. I read this post: CloudKit - full and complete error handling example
but i have a few questions:
1.Why is the error handling not working if i dont set .qualityOFService? and is .userInitiated correct, as on the post mentioned it is set to .background?. Also, do i have to set it for the
loadDataAgain()?
2.How can i make the error handling reusable that will take an error and another parameter(according to the viewcontroller that is
called from)?
#objc func loadData() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: myRecordType.type, predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 5
//if the below line is missing errors will not be handled
queryOperation.qualityOfService = .userInitiated
queryOperation.recordFetchedBlock = { item in
self.array.append(item)
}
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil{
if let ckerror = error as? CKError {
if ckerror.code == CKError.requestRateLimited {
let retryInterval = ckerror.userInfo[CKErrorRetryAfterKey] as? TimeInterval
DispatchQueue.main.async {
Timer.scheduledTimer(timeInterval: retryInterval!, target: self, selector: #selector(self.loadData), userInfo: nil, repeats: false)
}
} else if ckerror.code == CKError.zoneBusy {
let retryInterval = ckerror.userInfo[CKErrorRetryAfterKey] as? TimeInterval
DispatchQueue.main.async {
Timer.scheduledTimer(timeInterval: retryInterval!, target: self, selector: #selector(self.loadData), userInfo: nil, repeats: false)
}
} else if ckerror.code == CKError.limitExceeded {
let retryInterval = ckerror.userInfo[CKErrorRetryAfterKey] as? TimeInterval
DispatchQueue.main.async {
Timer.scheduledTimer(timeInterval: retryInterval!, target: self, selector: #selector(self.loadData), userInfo: nil, repeats: false)
}
} else if ckerror.code == CKError.notAuthenticated {
//present relevant alert with action to reload ViewController
} else if ckerror.code == CKError.networkFailure {
//present relevant alert with action to reload ViewController
} else if ckerror.code == CKError.networkUnavailable {
//present relevant alert with action to reload ViewController
} else if ckerror.code == CKError.quotaExceeded {
//present relevant alert with action to reload ViewController
} else if ckerror.code == CKError.partialFailure {
//present relevant alert with action to reload ViewController
} else if (ckerror.code == CKError.internalError || ckerror.code == CKError.serviceUnavailable) {
//present relevant alert with action to reload ViewController
}
}
}
else{
if cursor != nil {
self.loadDataAgain(cursor!)
}
}
OperationQueue.main.addOperation {
self.tableView.reloadData()
}
}
func loadDataAgain(_ cursor: CKQueryOperation.Cursor) {
let queryOperation = CKQueryOperation(cursor: cursor)
queryOperation.resultsLimit = 5
queryOperation.recordFetchedBlock = { item in
self.array.append(item)
}
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil{
//have the same error handling as above
}
else{
if cursor != nil {
self.loadDataAgain(cursor!)
}
}
OperationQueue.main.addOperation {
self.tableView.reloadData()
}
}
Database.share.publicDB.add(queryOperation)
}

With respect to your first question, see this answer that suggests errors aren't reported for lower quality of service settings:
it's because [the] system is waiting for a better network condition, while [if] you set .default or .userInitiated the system expects a "real time" response
If you have a look at Apple's Energy Efficiency Guide for iOS Apps documentation section titled "About CloudKit and Quality of Service," Apple seems to confirm this approach, saying
CKOperation is a subclass of the NSOperation class. Although the NSOperation class has a default QoS level of NSQualityOfServiceBackground, CKOperation objects have a default QoS level of NSQualityOfServiceUtility. At this level, network requests are treated as discretionary when your app isn’t in use. On iPhones, discretionary operations are paused when Low Power Mode is enabled.
You'll need to explicitly set a different QOS for every CKOperation you create if you don't want the default .utility. If you find yourself doing this a lot with related operations, I'd suggest looking at CKOperationGroup and its default configuration property - if you set a QOS in the configuration it'll override the default in any CKOperations in the group.

Related

having trouble trying to keep users logged in with addStateDidChangeListener()

So my goal for now is to successfully keep users logged in and show a certain viewController depending if they're logged in or not. I've read a lot of the Stack questions that showed up first on Google searches about this same topic and they said use addStateDidChangeListener() and that's exactly what I did.
I didn't know how to approach this, so I watched a Youtube video and copied the exact code the guy had, his project did what I wanted mine to do, so I gave it a shot. Unfortunately when I run the simulator, sign in, exit the simulator and simulate again, nothing changes. I will add my code and it's location.
This is the code in my AppDelegate.swift in the didFinishLaunchingWithOptions method
let storyboard = UIStoryboard.init(name: "Main", bundle: Bundle.main)
let auth = Auth.auth()
auth.addStateDidChangeListener { (_, user) in
switch user {
case nil:
guard self.activeViewController! is StudentSegmentedTableViewController else { return }
let nonLoggedInViewController = storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.GothereMainMenuStoryboardID) as! GothereMainMenuViewController
self.navigationController.setViewControllers([nonLoggedInViewController], animated: false)
self.navigationController.popToViewController(nonLoggedInViewController, animated: true)
self.activeViewController = nonLoggedInViewController
default:
guard self.activeViewController! is GothereMainMenuViewController else { return }
let alreadyLoggedInViewController = storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.StudentEventDashboardStoryboardID) as! StudentSegmentedTableViewController
self.navigationController.setViewControllers([alreadyLoggedInViewController], animated: false)
self.navigationController.popToViewController(alreadyLoggedInViewController, animated: true)
self.activeViewController = alreadyLoggedInViewController
}
}
let nonLoggedInViewController = storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.GothereMainMenuStoryboardID) as! GothereMainMenuViewController
let alreadyLoggedInViewController = storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.StudentEventDashboardStoryboardID) as! StudentSegmentedTableViewController
activeViewController = nonLoggedInViewController
switch Auth.auth().currentUser != nil {
case true:
activeViewController = alreadyLoggedInViewController
default:
break
}
navigationController = UINavigationController.init(rootViewController: activeViewController)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
I tried just this alone at first, and it didn't work so then I implemented a state listener in reasonable spots in my app.
First I added one that enables right after successful log in/signup and the segue is performed .
func enableAuth() {
authListener = Auth.auth().addStateDidChangeListener { (_, user) in
print("State Listener activated")
}
}
This is what I call in the viewDidLoad() of the segued viewController right after login/signup. To remove it, I simply call it when the logout button is pressed..
func disableAuthState() {
Auth.auth().removeStateDidChangeListener(self.authListener!)
print("State Listener Deactivated")
}
func studentLogoutSelected() {
var text = UITextField()
let alert = UIAlertController(title: "Logout", message: "Are you sure you want to logout?", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in
self.dismiss(animated: true, completion: nil)
}
let logoutAction = UIAlertAction(title: "Logout", style: .default) { (logoutAction) in
let firebaseAuth = Auth.auth()
do {
try firebaseAuth.signOut()
self.disableAuthState()
self.performSegue(withIdentifier: Constants.Segues.studentLogout, sender: self)
} catch let signOutError as NSError {
print("There was an error signing the user out. \(signOutError)")
}
}
alert.addAction(cancelAction)
alert.addAction(logoutAction)
present(alert, animated: true, completion: nil)
}
After all these functions and implementations, the shown blocks of code still don't do what I expected them to do. If anybody can point out issues or suggestions, that would be great, thanks.
First of all are you add FirebaseApp.configure() on your didFinishLaunchingWithOptions function in appdelegate? Then, Can you try call enableAuth in viewWillAppear()

ReplayKit Won't Record Twice in One Session

I am using ReplayKit in an app to record the visible screen with some text and a video playing. The issue I am facing is that ReplayKit is working just fine for the first screen recording, but if I am to record again in the same session (ie without closing the app) it runs into this error:
MyViewController[423:39346] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}
In this scenario, I am actually trying to screen record on the same ViewController (only with a different video being played and some text content altered). Below is my recording code:
#objc func startRecording() {
let recorder = RPScreenRecorder.shared()
recorder.startRecording{ [unowned self] (error) in
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
print("NOT Recording")
} else {
self.video.play()
print("Recording")
self.isRecording = true
}
}
recordIcon.isHidden = true
ring.isHidden = true
}
#objc func stopRecording() {
let recorder = RPScreenRecorder.shared()
recorder.stopRecording( handler: { previewViewController, error in
if let error = error {
print("\(error.localizedDescription)")
}
// Handling iPads
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad {
previewViewController?.modalPresentationStyle = UIModalPresentationStyle.popover
previewViewController?.popoverPresentationController?.sourceRect = CGRect.zero
previewViewController?.popoverPresentationController?.sourceView = self.view
}
if previewViewController != nil {
self.previewViewController = previewViewController
previewViewController?.previewControllerDelegate = self
}
self.present(previewViewController!, animated: true, completion: nil)
})
isRecording = false
recordIcon.isHidden = false
ring.isHidden = false
return
}
func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
dismiss(animated: true)
}
Any help on this is greatly appreciated. I'd hate to force users to have to reopen the app before recording again.
It might be that your app keeps the screen recording longer than it should. If this is the case, try implementing the discardRecording(handler: #escaping () -> Void) function. Here are more details on the discardRecording.

AVPlayer SeekToTime causes AVLayer to disappear

I currently am using AVAssetWriter to capture my video and data. Once done, I playback my AVPlayerItem. The problem is after the first initial play and once DidItemFinishPlaying notification is called, I try to seek it back to the beginning. When I do this, the avlayer disappears instead of showing the poster frame in a paused state. Another thing I tried was doing SeektoTime when I am about to start replaying the video but I can see my live preview from my camera for about 1-2 seconds before it starts playing the video again. Here is my code.
let outputFileURL = NSURL(fileURLWithPath: self.outputVidFilePath)
appDel.currCamData = NSFileManager.defaultManager().contentsAtPath(outputFileURL.path!)
appDel.currCamDataType = "Video"
appDel.currCamDataURL = outputFileURL
self.vidPlayer.replaceCurrentItemWithPlayerItem(AVPlayerItem(URL: outputFileURL))
self.avLayer.hidden = false
self.playerTimer = NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: #selector(CameraViewController.checkPlayerStatus), userInfo: nil, repeats: true)
func checkPlayerStatus()
{
if playerTimer == nil
{
return
}
if vidPlayer.status == AVPlayerStatus.ReadyToPlay
{
if vidPlayer.currentItem != nil
{
let currentPlayItem = vidPlayer.currentItem!
if currentPlayItem.status == AVPlayerItemStatus.ReadyToPlay
{
playerTimer!.invalidate()
playerTimer = nil
if (!playerNotificationStarted) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(CameraViewController.itemDidFinishPlaying(_:)), name: AVPlayerItemDidPlayToEndTimeNotification, object: nil)
playerNotificationStarted = true
vidPlayer.play()
isVideoPlaying = true
}
}
}
}
}
func itemDidFinishPlaying(notification :NSNotification)
{
self.vidPlayer.seekToTime(kCMTimeZero)
self.playButton.hidden = false
self.isVideoPlaying = false
}

Reload data in swift, of a tableview every 30 seconds

So I have a table that takes some data from my data base and displays it in his cells. I made a refresh button that reloads my code so the user can be able to see the new entries that somebody added into the data base, when you tap it.
This is how the code of my refresh button looks like:
#IBAction func refreshButton(sender: AnyObject) {
textArray.removeAllObjects()
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Conversation", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
publicCloudDatabase?.performQuery(query, inZoneWithID: nil,
completionHandler: ({results, error in
if (error != nil) {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Cloud Access Error",
message: error.localizedDescription)
}
} else {
if results.count > 0 {
var record = results[0] as! CKRecord
self.currentRecord = record
dispatch_async(dispatch_get_main_queue()) {
for x in results{
self.textArray.addObject(x.objectForKey("message") as! String)
self.tableView.reloadData()
}
}
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("No Match Found",
message: "No record matching the address was found")
}
}
}
}))
}
I'd like to remove that button and make a refresh of the table every 30 seconds (in the background of the app - i don't want the user to be aware of it), it is that possible ? If yes, how can it be done? Thank you!
Using timers is one way you can achieve,
In ViewDidload
self.myTimer = NSTimer(timeInterval: 30.0, target: self, selector: "refresh", userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(self.myTimer, forMode: NSDefaultRunLoopMode)
func refresh() {
// a refresh of the table
}
1.performQuery every 30s.
2.compare the different between old datas and new datas.
3.Insert the different part by using this Api
(void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
rather than reloadData

How to get contact list from device in ios swift?

I have tried to get the contact list from device but something went wrong so I did not get the contact list.
This is my code what am I doing wrong?
let addressBook = APAddressBook()
override func viewDidLoad() {
super.viewDidLoad()
self.registerCellClass(AddressTableViewCell.self, forModelClass: APContact.self)
self.addressBook.fieldsMask = APContactField.Default | APContactField.Thumbnail
self.addressBook.sortDescriptors = [NSSortDescriptor(key: "firstName", ascending: true),
NSSortDescriptor(key: "lastName", ascending: true)]
self.addressBook.filterBlock = {(contact: APContact!) -> Bool in
return contact.phones.count > 0
}
self.addressBook.loadContacts({ (contacts: [AnyObject]!, error: NSError!) in
if (contacts != nil) {
self.memoryStorage().addItems(contacts)
} else if (error != nil) {
let alert = UIAlertView(title: "Error", message: error.localizedDescription, delegate: nil, cancelButtonTitle: "OK")
alert.show()
}
})
}
I assume that you're hitting the addItems line, but that you're not seeing it reflected in the table view? If that's the case, the issue is likely that loadContacts runs asynchronously, so you have to call reloadData on your table view when it's done. Otherwise your table view will never get updated after APAddressBook finishes loading the contacts.