Xamarin.iOS 中的 EventKit

iOS 内置了两个与日历相关的应用程序:日历应用程序和提醒应用程序。 日历应用程序管理日历数据的方式很容易理解,但提醒应用程序相对复杂。 提醒其实可以与日期关联,比如何时到期和何时完成。因此,iOS 将所有日历数据(无论是日历事件还是提醒)存储在一个位置,称为“日历数据库”。

EventKit 框架提供了一种方法来访问日历日历事件和日历数据库存储的提醒数据。 虽然从 iOS 4 开始就提供了对日历和日历事件的访问权限,但对提醒数据的访问权限是 iOS 6 新提供的。

本指南将介绍:

  • EventKit 基础知识–将通过主要类介绍 EventKit 的基本部分,并提供对其用法的理解。 在处理文档的下一部分之前,需要先阅读此部分。
  • 常见任务–常见任务部分旨在快速参考如何执行常见操作,例如:枚举日历,创建、保存和检索日历事件和提醒,以及使用内置控制器来创建和修改日历事件。 此部分无需从头到尾阅读,因为它只是特定任务的参考资料。

本指南中的所有任务均在配套示例应用程序中提供:

The companion sample application screens

要求

EventKit 在 iOS 4.0 中引入,但对提醒数据的访问权限是在 iOS 6.0 中引入的。 因此,若要进行常规 EventKit 开发,需要针对至少版本 4.0 和 6.0 的提醒。

此外,提醒应用程序在模拟器上不可用,这意味着提醒数据也将不可用,除非先添加它们。 此外,访问请求仅会在实际设备上向用户显示。 因此,EventKit 开发最好在设备上进行测试。

EventKit 基础知识

使用 EventKit 时,掌握其常见类及其用法至关重要。 所有这些类都可以在 EventKitEventKitUI 中找到(适用于 EKEventEditController)。

EventStore

EventStore 类是 EventKit 中最重要的类,因为它需要在 EventKit 中执行任何操作。 对于所有 EventKit 数据,都可以将其视为持久性存储或数据库引擎。 从 EventStore 可以访问日历应用程序中的日历和日历事件,以及提醒应用程序中的提醒。

由于 EventStore 类似于数据库引擎,因此它应有较长的生存期,这意味着应在应用程序实例的生存期内尽可能少地创建和销毁它。 事实上,建议在应用程序中创建 EventStore 的一个实例后,除非确定不再需要它,否则应将该引用保留至整个应用程序的生命周期结束。 此外,所有调用都应转到单个 EventStore 实例。 因此,建议使用 Singleton 模式来保持单个实例可用。

创建事件存储

以下代码演示了创建 EventStore 类的单个实例并使其在应用程序中静态可用的有效方法:

public class App
{
    public static App Current {
            get { return current; }
    }
    private static App current;

    public EKEventStore EventStore {
            get { return eventStore; }
    }
    protected EKEventStore eventStore;

    static App ()
    {
            current = new App();
    }
    protected App () 
    {
            eventStore = new EKEventStore ( );
    }
}

上面的代码使用 Singleton 模式在应用程序加载时实例化 EventStore 的实例。 然后,可以从应用程序中对 EventStore 进行全局访问,如下所示:

App.Current.EventStore;

请注意,此处的所有示例都使用此模式,因此它们通过 App.Current.EventStore 引用 EventStore

请求访问日历和提醒数据

在允许通过 EventStore 访问任何数据之前,应用程序必须首先请求访问日历事件数据或提醒数据,具体取决于所需的数据。 为便于执行此操作,EventStore 公开一种称为 RequestAccess 的方法,在调用该方法时会向用户显示警报视图,告知用户应用程序请求访问日历数据或提醒数据,具体取决于传递给它的 EKEntityType。 由于它会引发警报视图,因此调用是异步的,并将调用作为 NSAction (或 Lambda)传递给它的完成处理程序,这将接收两个参数;一个决定是否授予访问权限的布尔值,以及一个 NSError,如果其不为 null,则该布尔值将包含请求中的任何错误信息。 例如,以下编码将请求访问日历事件数据,并在未授予请求时显示警报视图。

App.Current.EventStore.RequestAccess (EKEntityType.Event, 
    (bool granted, NSError e) => {
            if (granted)
                    //do something here
            else
                    new UIAlertView ( "Access Denied", 
"User Denied Access to Calendar Data", null,
"ok", null).Show ();
            } );

授予请求后,只要应用程序安装在设备上,它就会被记住,并且不会向用户弹出警报。 但是,仅会授予对资源类型的访问权限,即授予日历事件或提醒。 如果应用程序需要同时访问两者,则应请求此两者。

由于记住了权限,因此每次发出请求相对更便宜,因此最好在执行操作之前始终请求访问权限。

此外,由于完成处理程序是在单独的(非 UI)线程上调用的,因此完成处理程序中的任何更新都应通过 InvokeOnMainThread 进行调用,否则将引发异常,而如果未捕获,应用程序将崩溃。

EKEntityType

EKEntityType 是描述 EventKit 项或数据的类型的枚举。 它具有两个值:Event 和提醒。 它用于多种方法,包括 EventStore.RequestAccess 用于告知 EventKit 应访问或检索哪种类型的数据。

EKCalendar

EKCalendar 表示包含一组日历事件的日历。 日历可以存储在很多不同的位置,例如本地、iCloud、第三方提供商位置(例如 Exchange ServerGoogle等)。很多时候,EKCalendar 用于告知 EventKit 应查找事件的位置或保存事件的位置。

EKEventEditController

EKEventEditController 可在 EventKitUI 命名空间中找到,它是可用于编辑或创建日历事件的内置控制器。 与内置相机控制器一样,EKEventEditController 会在显示 UI 和处理保存方面为你承担繁重的工作。

EKEvent

EKEvent 表示日历事件。 EKEventEKReminder 都继承自 EKCalendarItem,并具有 TitleNotes 等字段。

EKReminder

EKReminder 表示提醒项。

EKSpan

EKSpan 是一个枚举,用于描述修改可递归的事件时的事件范围,并具有两个值:ThisEventFutureEventsThisEvent 意味着任何更改都只会发生于所引用的序列中的特定事件,而 FutureEvents 则将影响该事件和所有将来的重复事件。

任务

为方便使用,已将 EventKit 用法分解为常见任务,如以下部分所述。

枚举日历

若要枚举用户在设备上配置的日历,请在 EventStore 上调用 GetCalendars,并传递要接收的日历类型(提醒或事件):

EKCalendar[] calendars = 
App.Current.EventStore.GetCalendars ( EKEntityType.Event );

使用内置控制器添加或修改事件

如果要创建或编辑与使用日历应用程序时向用户显示的相同 UI 的事件,EKEventEditViewController 会执行大量工作:

The UI that is presented to the user when using the Calendar Application

若要使用它,需要将其声明为类级变量,以便在方法中声明时不会进行垃圾回收:

public class HomeController : DialogViewController
{
        protected CreateEventEditViewDelegate eventControllerDelegate;
        ...
}

然后,若要启动它:对其进行实例化,为它提供对 EventStore 的引用,连接 EKEventEditViewDelegate 委托,然后使用 PresentViewController 显示它:

EventKitUI.EKEventEditViewController eventController = 
        new EventKitUI.EKEventEditViewController ();

// set the controller's event store - it needs to know where/how to save the event
eventController.EventStore = App.Current.EventStore;

// wire up a delegate to handle events from the controller
eventControllerDelegate = new CreateEventEditViewDelegate ( eventController );
eventController.EditViewDelegate = eventControllerDelegate;

// show the event controller
PresentViewController ( eventController, true, null );

(可选)如果要预填充事件,则可以创建全新的事件(如下所示),也可以检索已保存的事件:

EKEvent newEvent = EKEvent.FromStore ( App.Current.EventStore );
// set the alarm for 10 minutes from now
newEvent.AddAlarm ( EKAlarm.FromDate ( DateTime.Now.AddMinutes ( 10 ) ) );
// make the event start 20 minutes from now and last 30 minutes
newEvent.StartDate = DateTime.Now.AddMinutes ( 20 );
newEvent.EndDate = DateTime.Now.AddMinutes ( 50 );
newEvent.Title = "Get outside and exercise!";
newEvent.Notes = "This is your reminder to go and exercise for 30 minutes.”;

如果要预填充 UI,请确保在控制器上设置事件属性:

eventController.Event = newEvent;

若要使用现有事件,请参阅后面的“按 ID 检索事件”部分。

委托应重写 Completed 方法,当用户完成对话框时由控制器进行调用:

protected class CreateEventEditViewDelegate : EventKitUI.EKEventEditViewDelegate
{
        // we need to keep a reference to the controller so we can dismiss it
        protected EventKitUI.EKEventEditViewController eventController;

        public CreateEventEditViewDelegate (EventKitUI.EKEventEditViewController eventController)
        {
                // save our controller reference
                this.eventController = eventController;
        }

        // completed is called when a user eith
        public override void Completed (EventKitUI.EKEventEditViewController controller, EKEventEditViewAction action)
        {
                eventController.DismissViewController (true, null);
                }
        }
}

(可选)在委托中,可以在 Completed 方法中检查操作,以修改事件并重新保存,或者执行其他操作(如果已取消,等等):

public override void Completed (EventKitUI.EKEventEditViewController controller, EKEventEditViewAction action)
{
        eventController.DismissViewController (true, null);

        switch ( action ) {

        case EKEventEditViewAction.Canceled:
                break;
        case EKEventEditViewAction.Deleted:
                break;
        case EKEventEditViewAction.Saved:
                // if you wanted to modify the event you could do so here,
// and then save:
                //App.Current.EventStore.SaveEvent ( controller.Event, )
                break;
        }
}

以编程方式创建事件

若要在代码中创建事件,请在 EKEvent 类上使用 FromStore 工厂方法,并在其中设置任何数据:

EKEvent newEvent = EKEvent.FromStore ( App.Current.EventStore );
// set the alarm for 10 minutes from now
newEvent.AddAlarm ( EKAlarm.FromDate ( DateTime.Now.AddMinutes ( 10 ) ) );
// make the event start 20 minutes from now and last 30 minutes
newEvent.StartDate = DateTime.Now.AddMinutes ( 20 );
newEvent.EndDate = DateTime.Now.AddMinutes ( 50 );
newEvent.Title = "Get outside and do some exercise!";
newEvent.Notes = "This is your motivational event to go and do 30 minutes of exercise. Super important. Do this.";

必须设置要保存事件的日历,但如果没有首选项,则可以使用默认值:

newEvent.Calendar = App.Current.EventStore.DefaultCalendarForNewEvents;

若要保存事件,请对 EventStore 调用 SaveEvent 方法:

NSError e;
App.Current.EventStore.SaveEvent ( newEvent, EKSpan.ThisEvent, out e );

保存后,EventIdentifier 属性将使用唯一标识符进行更新,稍后即可使用该标识符检索事件:

Console.WriteLine ("Event Saved, ID: " + newEvent.CalendarItemIdentifier);

EventIdentifier 是字符串格式的 GUID。

以编程方式创建提醒

在代码中创建提醒的方法与创建日历事件大致相同:

EKReminder reminder = EKReminder.Create ( App.Current.EventStore );
reminder.Title = "Do something awesome!";
reminder.Calendar = App.Current.EventStore.DefaultCalendarForNewReminders;

若要保存,请对 EventStore 调用 SaveReminder 方法:

NSError e;
App.Current.EventStore.SaveReminder ( reminder, true, out e );

按 ID 检索事件

若要按 ID 检索事件,请使用 EventStore 上的 EventFromIdentifier 方法,并向其传递从事件中提取的 EventIdentifier

EKEvent mySavedEvent = App.Current.EventStore.EventFromIdentifier ( newEvent.EventIdentifier );

对于事件,还有两个其他标识符属性,但 EventIdentifier 是唯一适用于此属性的标识符属性。

按 ID 检索提醒

若要检索提醒,请使用 EventStore上的 GetCalendarItem 方法,并将其传递给 CalendarItemIdentifier

EKCalendarItem myReminder = App.Current.EventStore.GetCalendarItem ( reminder.CalendarItemIdentifier );

由于 GetCalendarItem 会返回 EKCalendarItem,因此如果需要访问提醒数据或使用实例作为 EKReminder,则必须将其强制转换为 EKReminder

请勿对日历事件使用 GetCalendarItem,就像在撰写时一样,它不会工作。

删除事件

若要删除日历事件,请在 EventStore 上调用 RemoveEvent 并传递对事件的引用,以及相应的 EKSpan

NSError e;
App.Current.EventStore.RemoveEvent ( mySavedEvent, EKSpan.ThisEvent, true, out e);

但是,在删除事件后,事件引用将为 null

删除提醒

若要删除提醒,请对 EventStore 调用 RemoveReminder 并传递对提醒的引用:

NSError e;
App.Current.EventStore.RemoveReminder ( myReminder as EKReminder, true, out e);

请注意,在上面的代码中,有一个强制转换是 EKReminder,因为使用 GetCalendarItem 对它进行了检索

搜索事件

若要搜索日历事件,则必须通过 EventStore 上的 PredicateForEvents 方法创建 NSPredicate 对象。 NSPredicate 是 iOS 用于查找匹配项的查询数据对象:

DateTime startDate = DateTime.Now.AddDays ( -7 );
DateTime endDate = DateTime.Now;
// the third parameter is calendars we want to look in, to use all calendars, we pass null
NSPredicate query = App.Current.EventStore.PredicateForEvents ( startDate, endDate, null );

创建 NSPredicate 后,对 EventStore 使用 EventsMatching 方法:

// execute the query
EKCalendarItem[] events = App.Current.EventStore.EventsMatching ( query );

请注意,查询是同步的(阻塞),可能需要很长时间,具体取决于查询,因此可能需要启动新的线程或任务来执行此操作。

搜索提醒

搜索提醒类似于事件;它需要谓词,但调用已是异步的,因此你不必担心会阻止线程:

// create our NSPredicate which we'll use for the query
NSPredicate query = App.Current.EventStore.PredicateForReminders ( null );

// execute the query
App.Current.EventStore.FetchReminders (
        query, ( EKReminder[] items ) => {
                // do someting with the items
        } );

总结

本文档概述了 EventKit 框架的重要部分和一些最常见的任务。 但是,EventKit 框架非常大且功能强大,并且包括尚未在此处引入的功能,例如:批处理更新、配置警报、配置事件定期、注册和侦听日历数据库上的更改、设置 GeoFences 等。 有关详细信息,请参阅 Apple 的“日历和提醒编程指南”。