EKEventViewController wont show Edit Button on iOS 13 - ios

i got a simple app where i want to present an event within an EKEventViewController.
// the button action which validates if the event store access is granted and presents the given alert if true
#IBAction func actionButtonShowPopover(_ sender: Any) {
eventStore.requestAccess(to: .event) { (granted, _) in
guard granted else { return }
let event = self.generateAndSaveEvent()
self.presentEventViewController(withEvent: event)
}
}
// creates and tries to save an sample even and returns it
private func generateAndSaveEvent() -> EKEvent {
let event = EKEvent(eventStore: eventStore)
event.title = "Event Title"
event.startDate = Date()
event.endDate = Date().addingTimeInterval(1800)
event.calendar = eventStore.defaultCalendarForNewEvents
do {
try eventStore.save(event, span: .thisEvent)
} catch(let error) {
print(error)
}
return event
}
// displays an EKEventViewController with our newly created event within an popover
private func presentEventViewController(withEvent event: EKEvent) {
DispatchQueue.main.async {
let eventVC = EKEventViewController()
eventVC.event = event
eventVC.allowsEditing = true
eventVC.modalPresentationStyle = .popover
eventVC.popoverPresentationController?.sourceView = self.buttonShowPopover
eventVC.popoverPresentationController?.sourceRect = self.buttonShowPopover.frame.offsetBy(dx: 0, dy: -10)
eventVC.popoverPresentationController?.backgroundColor = .white
eventVC.popoverPresentationController?.permittedArrowDirections = .up
self.present(eventVC, animated: false, completion: nil)
}
}
i created an event as shown in the code above and simply displaying it within the popover view controller. since ios 13 i got a different result:
iOS 12.4 with edit button
iOS 13 without edit button
is there any chance i'm missing changes from iOS12 -> iOS13?
thanks upfront - i'm grateful for any advice!

Edit button has moved to navigation bar in iOS 13. You need to present it without popover style.

In my app, that's been around for a while, I was experiencing the same problem with the edit button no longer appearing in iOS13. Unlike other users, my EKEventViewController was already wrapped in a navigation controller, so it wasn't that issue.
Several hours of going down rabbit holes, I've found a fix. Here's where the problems were occurring in my app:
Debugging just before opening the view, the EKEvent object I was trying to edit didn't have a value set for .eventIdentifier. Reading into this, it seems this property is lazy loaded, so not being able to retrieve the value here suggests the link between the EKEvent and the EKStore I used to fetch it has been lost somewhere in the application lifecycle. This is a change that been introduced somewhere along the iOS/Swift upgrade journey - I can't pin down the change that has caused this.
By accessing the EKEvent.eventIdentifier at the time I first retrieved the EKEvent, I now had this identifier for later use
Before presenting the EKEventViewController, I fetch a fresh copy of this event and use the fresh event in the controller:
let freshEvent = store.event(withIdentifier: staleEvent.eventIdentifier)
eventViewController.event = freshEvent

Related

iOS ShareContext tapping on Suggestion Intent property of extensionContext is nil

I have a ShareExtension in my iOS app. I am trying to use Suggestions. I can successfully 'donate' the intent using the following code from the apple developer website:
let groupName = INSpeakableString(spokenPhrase: "Juan Chavez")
let sendMessageIntent = INSendMessageIntent(recipients: nil,
content: nil,
speakableGroupName: groupName,
conversationIdentifier: "sampleConversationIdentifier",
serviceName: nil,
sender: nil)
// Add the user's avatar to the intent.
let image = INImage(named: "Juan Chavez")
sendMessageIntent.setImage(image, forParameterNamed: \.speakableGroupName)
// Donate the intent.
let interaction = INInteraction(intent: sendMessageIntent, response: nil)
interaction.donate(completion: { error in
if error != nil {
// Add error handling here.
} else {
// Do something, e.g. send the content to a contact.
}
})
This works fine and I am able to see my app icon in the suggestion row on the top for each conversation. However when I click on the suggestion, the intent property of the extentsionContext is nil:
override func viewDidLoad() {
super.viewDidLoad()
// Populate the recipient property with the metadata in case the user tapped a suggestion from the share sheet.
let intent = self.extensionContext?.intent as? INSendMessageIntent
if intent != nil { // this is nil despite selecting suggestion
let conversationIdentifier = intent!.conversationIdentifier
self.recipient = recipient(identifier: conversationIdentifier!)
}
}
My ShareExtension plist is as follows:
The other odd behaviour is that I'm able to do the donate from the main app but not from the app extension. In the main app the only relevant entry in the plist is the same NSUserActivityTypes entry. Not the NSExtension entries.
My understanding is that tapping on the suggestion, the extensionContext should contain the intent.

Firebase Phone Authentication - Long Delay & Multiple OTPs

I'm working on an iOS app project that involves Firebase's phone authentication. I have it working fine on simulator, my iPhone, and my iPad. However, now that I am in the TestFlight stage , my external testers are experiencing long delays in receiving their OTPs as well as receiving duplicates when they reach the ViewController where they enter the OTP code (This is probably due to them hitting the button multiple times).
I also have APNs enabled and working properly.
I don't have much code to share as I followed Firebase's documentation.
What could be some reasons for a long delay in receiving the OTP code from Firebase? I will be including an activity spinner in the project when users tap the sign-in button. However, I also don't want it to be spinning for a minute as users wait for their OTP.
#objc func phoneSignIn() {
guard let phoneNumber = startVerificationView.phoneNumberTextField.text else { return }
let completePhoneNumber = "+1\(phoneNumber)"
Auth.auth().settings?.isAppVerificationDisabledForTesting = isVerificationDisabled
PhoneAuthProvider.provider().verifyPhoneNumber(completePhoneNumber, uiDelegate: nil) { (verificationId, error) in
if error == nil {
guard let verifyId = verificationId else { return }
UserDefaults.standard.set(verifyId, forKey: "verificationId")
let vc = CheckVerificationViewController()
vc.modalPresentationStyle = .fullScreen
vc.completePhoneNumber = completePhoneNumber
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
Also isVerificationDisabled is set to false.

UITest cases to handle with location services alert

I am writing UI test cases for my project.
My project flow is as below:
Login Screen. User enters credentials and press login.
Home Screen. There is location requirement so system as for user's permission. I allow it.
Logout.
So when I do fresh install of application this flow is recorded in test case and works if I perform on new fresh build.
But problem is when I test on old build there is no alert for location permission and the test's gets fail. How can I handle this cases or ask user for permission every time when I run tests?
For resetting credentials of user I am passing launchArguments to XCUIApplication() and handle in AppDelegate.
I have implemented code let me know if its correct way:
addUIInterruptionMonitor(withDescription: "Allow “APP” to access your location?") { (alert) -> Bool in
alert.buttons["Only While Using the App"].tap()
return true
}
The above code works for both if alert comes or not.
Using an interruption monitor is the correct way. However, it's safer to check if the alert being displayed is the alert you're expecting before you interact with the alert:
addUIInterruptionMonitor(withDescription: "Allow “APP” to access your location?") { (alert) -> Bool in
let button = alert.buttons["Only While Using the App"]
if button.exists {
button.tap()
return true // The alert was handled
}
return false // The alert was not handled
}
Try this
let app2 = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let button = app2.alerts.firstMatch.buttons["Allow While Using App"]
button.waitForExistence(timeout: 10)
button.tap()
I use the following code to allow user's location:
// MARK: - Setup
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
alert.buttons["Allow Once"].tap()
return true
}
}
In this setup, I "register" the interruption monitor for tapping the allow button, so in this case I can dismiss that modal. Now, there's my test:
// MARK: - Test change mall
func testChangeMall() {
let selectorChangeButton = app.buttons["change_mall_button"]
XCTAssert(selectorChangeButton.exists, "Selector change button does not exist")
selectorChangeButton.tap()
app.navigationBars.firstMatch.tap()
let cell = app.staticTexts["Shopping Centre"]
XCTAssert(cell.exists, "There's no cell with this title")
cell.tap()
sleep(1)
let label = app.staticTexts["Shopping Centre"]
XCTAssert(label.exists, "Nothing changes")
}
In this test, simply I go to a view controller with a list sorted by location. First, I need to dismiss the location's system alert. So, first I dismiss that modal and then I tap a cell from my TableView. Then, I need to show it in my main view controller so I dismiss my view controller and I expect the same title.
Happy Coding!

NSFetchedResultsController with > now() doesn't refresh expired entries in swift

I have a UITableview I'm keeping updated with recent items. (That is, added to my CoreData within the last 5 minutes.) I have a field in my Item entity called 'expire_date' which is a Date type. When I download a new item in the background, I add it to CoreData, setting the expire_date to NSDate() plus 5 minutes:
item.expire_date = NSDate(timeIntervalSince1970: NSDate().timeIntervalSince1970+5*60);
My setup for the NSFetchedResultsController looks like:
let fetchRequest=NSFetchRequest(entityName: "Item")
fetchRequest.predicate = NSPredicate(format: "expire_date > now()")
let sortDescriptor = NSSortDescriptor(key: "expire_date")
fetchRequest.sortDescriptors=[sortDescriptor]
myFRC=NSFetchedResultsController(fetchRequest:fetchRequest, managedObjectContext:myMOC)
do {
try myFRC!.performFetch()
} catch {
print(error)
}
The NSFetchedResultsControllerDelegate code is the standard boilerplate for this kind of thing when using a UITableView.
This all works great when starting the app: only existing items that haven't 'timed out' show up. It also works great when new items are added.
The problem is that when an item in the list times out, it doesn't get removed from the results
Anyone have any idea how to accomplish this?
I had the possibility of one or more Items being selected in the UITableView, and I didn't want to lose those selections so I couldn't use W.K.S.'s answer unfortunately. It was strictly speaking correct from the information I had given, although it didn't update the list immediately after any item expires. However, Wain gave me the spark of an idea that worked the way I wanted.
It seems the Item is re-examined by the query when that item is updated, so the trick is to update the item in question when it has expired. I added an NSTimer variable:
var timeoutTimer:NSTimer?
I added a function, addMinimumTimeout() that (re)sets the timeout timer:
func addMinimumTimeout() {
if let _timeoutTimer=timeoutTimer {
_timeoutTimer.invalidate();
timeoutTimer=nil;
}
if myFRC?.fetchedObjects?.count==0 {
return;
}
if let firstItem = myFRC?.fetchedObjects?[0] as? Item {
let timeout=(firstItem.expire_date!.timeIntervalSince1970-NSDate().timeIntervalSince1970)+1.0;
timeoutTimer=NSTimer.scheduledTimerWithTimeInterval(timeout, target: self, selector: #selector(itemTimedOut), userInfo: firstItem, repeats: false);
}
}
My itemTimedOut code looks like this:
func itemTimedOut(timer:NSTimer) {
guard let item=timer.userInfo as? Item else {
return;
}
item.expire_date! = item.expire_date!;
do {
try item.managedObjectContext!.save();
} catch {
let saveError = error as NSError;
print("Error saving: \(saveError)")
return;
}
}
Then in my viewDidLoad, right after I perform my fetch, I call:
addMinimumTimeout();
and I also add it at the end of the boilerplate controller(controller, didChangeObject, atIndexPath, forChangeType, newIndexPath) function.
This way, if there's no items in the list, there's no timeout timer created, but as soon as one is added, the timeout timer is created. When an item is removed for whatever reason, the timer is updated, and if the last one is timed out or removed, there's no timeout timer running.

Set a reminder in iOS Swift

I am trying to set a simple EKReminder in my swift application to remind users to catch the bus. However, when I try to save my reminder, I always get a error (no error is reported, the app just crashes). I have the code below.
public class func createReminder(reminderTitle: String, timeInterval: NSDate) {
var calendarDatabase = EKEventStore()
calendarDatabase.requestAccessToEntityType(EKEntityTypeReminder,
completion: nil)
let reminder = EKReminder(eventStore: calendarDatabase)
reminder.title = reminderTitle
let alarm = EKAlarm(absoluteDate: timeInterval)
reminder.addAlarm(alarm)
reminder.calendar = calendarDatabase.defaultCalendarForNewReminders()
var error: NSError?
calendarDatabase.saveReminder(reminder, commit: true, error: &error)
}
The following should work in Swift 4.2
func AddReminder() {
eventStore.requestAccess(to: EKEntityType.reminder, completion: {
granted, error in
if (granted) && (error == nil) {
print("granted \(granted)")
let reminder:EKReminder = EKReminder(eventStore: self.eventStore)
reminder.title = "Must do this!"
reminder.priority = 2
// How to show completed
//reminder.completionDate = Date()
reminder.notes = "...this is a note"
let alarmTime = Date().addingTimeInterval(1*60*24*3)
let alarm = EKAlarm(absoluteDate: alarmTime)
reminder.addAlarm(alarm)
reminder.calendar = self.eventStore.defaultCalendarForNewReminders()
do {
try self.eventStore.save(reminder, commit: true)
} catch {
print("Cannot save")
return
}
print("Reminder saved")
}
})
}
info.plist requires appropriate privacy settings as well.
I haven't used anything like this before, but looking at your code I can see that you call the requestAccessToEntity-method, without handling the response. That method will most likely show the user a prompt, asking them to accept that your app has access to "Reminders". With your code, you ask for the permission, but the rest of your code will execute immediately after asking, without 'waiting' for the response. The very first time this code runs, the user will be asked, and your reminder will be denied, because it tries to save right away.
Even if your user clicks "allow", your code has already run without permission.
Now, if the user clicked allow one time, and then tries to do the same again, then maybe it will work, I don't know. But if your user clicked "Cancel" on the prompt, your code will never work until they go into Settings and allow your app to show reminders.
You should not create your reminder before you know if the user allows it, so you should really split this function into two separate functions. And do not pass nil for completion in that function; handle the response.
try the following:
EKEntityTypeReminder -> EKEntityType.Reminder

Resources