15
votes

I'm writing my first iOS app (I'm tired of missing event reminders and want to have a rule based reminder that, for example for certain calendars and certain people, will ring a different, louder tone, 10, 5 and 0 minutes before the event)

The first part is to get access to the calendar, and thanks to this great demo, I have that sort of covered. Got the app running on my iPhone and I can see my calendar events.

(well, the first part is to figure out swift, and general iOS basic, which I'm still working on)

The second part is where I wanted to ask before I spend hours researching. I have two tasks left to do

  1. either a background task to periodically check the calendar for new/updated events, or ability to programmatically subscribe to some sort of an event bus any calendar updates (new events, event changes)

  2. schedule notifications on a given time (I'll probably use this: How can I schedule local notification for the following scenario?)

How do I accomplish #1?

5
Regarding your question "or ability to programmatically subscribe to some sort of an event bus any calendar updates" Do you need particular calendar's event's updates or you need event updates from all calendars available in iOS device? May I know then purpose why do you need to get update? I think that can give clear idea what you want to do.Jayeshkumar Sojitra
@JayeshSojitra great question, for now just one calendar can be enough. Although I'd like to add more than one "rules" e.g. "for calendar X, if event is from organizer Y, use sound Z, n0, n1, n2 minutes before meeting"Eran Medan
Wow, I'm super thankful for all the answers, but now I'm in a bit of a problem... I have no clue how to rank them, I'll have to try it all, sadly I have only 3 days... thanks again to everyone...Eran Medan
Please mention whether you need this app to be submitted to app store. App review by Apple changes EVERYTHING when it comes to using background modes for doing some custom stuff.Swapnil Luktuke
@lukya sorry for the delay in replying, I'd say - not mandatory to be in the App Store, it is for personal use, but, if useful, would be super nice to share with the world :)Eran Medan

5 Answers

6
votes

I don’t think this is possible to accomplish without a design change.

Applications do not generally run in the background, and outside of very specific cases where Apple allows it (voip, music, location, etc.), your application will be suspended. There is the background refresh API, but that is not reliable enough.

Note that EKEventStoreChangedNotification notifications are only good when your process is running. If your process dies (due to memory pressure, device reboot, user killing the app, etc.), you will not be notified about changes when the app is launched, and you will still have to iterate over the meetings and look for changes, so you have to develop your own mechanism to iterate the EventKit database (perhaps within a certain timeframe).

If it’s a personal app, there are some tricks you can do to continue running in the background, such as playing a silent music file, voip background API, location, etc.). All these methods can keep your app running in the background, but will take a toll on battery life, and will not be approved by Apple upon submission. One way to have a consistent background runtime is to have a push notification service set a silent push to wake the app up periodically, and let it do its logic. This has the potential to launch the app in case it has been killed in the background (but not if the user killed it).

3
votes

There is a limit on what actions an application can carry out in the background. Background fetch normally involves fetching information from an external source so that your app can appear up to date once the user returns.

Background Refresh

When enabled, iOS monitors usage patterns to determine when to fetch new data. You are not able to decide when a background fetch is issued and it should not be used to do critical updates. In your project, the application could fetch new calendar events when the system feels it is an appropriate time. You can implement Background Fetch like so:

  1. Check the box Background Fetch in the Capabilities section of your application

  2. Add the following to application(_:didFinishLaunchingWithOptions:):

    UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
    
  3. Finally, implement this function in your AppDelegate.

    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        // Perform calendar updates here
    }
    

If you can compromise manual update scheduling, background refresh would be your best option. At the moment, there is no way to manually request a background update.

3
votes

To achieve #1 either a background task to periodically check the calendar for new/updated events, or ability to programmatically subscribe to some sort of an event bus any calendar updates (new events, event changes)

You can do following.

First of all to check new/updated calendar event we don't need to run any background task.

We can get the update for calendar events using .EKEventStoreChanged notification as followed in viewWillAppear method.

NotificationCenter.default.addObserver(self, selector: #selector(eventStoreChanged:), name: .EKEventStoreChanged, object: eventStore)

Handle calendar event changes (new / updated) EKEventStore changes as given below.

func eventStoreChanged(_ notification: Notification) {
    let ekEventStore: EKEventStore? = notification.object
    let now = Date()
    let offsetComponents = DateComponents()
    offsetComponents.day = 0
    offsetComponents.month = 4
    offsetComponents.year = 0
    let endDate: Date? = Calendar.current.date(byAddingComponents: offsetComponents, to: now, options: [])
    let ekEventStoreChangedObjectIDArray: [Any]? = (notification.userInfo?["EKEventStoreChangedObjectIDsUserInfoKey"] as? [Any])
    let predicate: NSPredicate? = ekEventStore?.predicateForEvents(withStartDate: now, endDate: endDate, calendars: nil)
    // Loop through all events in range
    ekEventStore?.enumerateEvents(matchingPredicate: predicate, usingBlock: {(_ ekEvent: EKEvent, _ stop: Bool) -> Void in
        // Check this event against each ekObjectID in notification
        (ekEventStoreChangedObjectIDArray as NSArray).enumerateObjects(usingBlock: {(_ ekEventStoreChangedObjectID: String, _ idx: Int, _ stop: Bool) -> Void in
            let ekObjectID: NSObject? = (ekEvent as? NSManagedObject)?.objectID
            if ekEventStoreChangedObjectID.isEqual(ekObjectID) {
                // EKEvent object is the object which is changed.
                stop = true
            }
        })
    })
}

So whenever there are any event changes (add/update/delete) we can get updates.

Additionally, when you create any event you get eventIdentifier from EKEvent object.

let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccess(to: .event) { (granted, error) in

    if (granted) && (error == nil) {
        print("granted \(granted)")
        print("error \(error)")

        let event:EKEvent = EKEvent(eventStore: eventStore)

        event.title = "Event"
        event.startDate = Date()
        event.endDate = Date()
        event.calendar = eventStore.defaultCalendarForNewEvents
        do {
            try eventStore.save(event, span: .thisEvent)
        } catch let error as NSError {
            print("failed to save event with error : \(error)")
        }
        print("Saved Event id : \(event.eventIdentifier)")
    }
    else{

        print("failed to save event with error : \(error) or access not granted")
    }
}

And get event using following method.

let event:EKEvent = eventStore?.event(withIdentifier: eventIdentifier)

Please let me know if you need any more clarification.

2
votes

To check the calendar database for changes there is a notification:

EKEventStoreChanged

Posted whenever changes are made to the Calendar database, including adding, removing, and changing events or reminders. Individual changes are not described. When you receive this notification, you should refetch all EKEvent and EKReminder objects you have accessed, as they are considered stale. If you are actively editing an event and do not wish to refetch it unless it is absolutely necessary to do so, you can call the refresh method on it. If the method returns true, you do not need to refetch the event.

Add an observer:

NotificationCenter.default.addObserver(self, selector: #selector(storeChanged), name: .EKEventStoreChanged, object: eventStore)

and implement the method to handle the notification

func storeChanged(_ notification: Notification) {}
2
votes

No, there is no way to build a professional and functional background task for iOS calendar at this moment (Apple Swift version 3.1).

The iOS background state is very limited (there are many reasons as to preserve the battery life etc.) and a "calendar background mode" doesn't yet exist at the actual moment (you can take a look to the official Background modes for apps available to the official Apple docs, Table 3-1). Take always present we have a mobile device with all it's physics limitations, not a linux server but for calendar we are also unlucky.

In other words the system don't communicate new calendar changes in the background state to your app and you will never get a reliable reminder using the iOS official app calendar.

You can make a background task using EKEventStoreChanged notification but what happen when your app is in the "suspended state"? You always pass through the background state to speak with your app and, as you see, there is no options to get calendars changes.

There is no way also if you made a custom calendar app (so without using the official calendar app) because you must always accompany the background state to an external server (remote notifications could be an idea..): suppose you don't have connection/internet so your push notification arrives in late. In this case your reminder don't know you have a specific appointment in that time and you could be warned too late and this could means a complete disaster.