あなたのアプリに iCloud Calendar API を統合する方法

著者
公開日

前回の記事では、Google Calendar APIをアプリに統合する方法を解説し、統合を機能させるためのすべての手順をハイライトしました。

Google Calendarとは異なり、iCloud Calendarの統合はそれほど簡単ではありません。ドキュメントが不足しており、AppleはiCloud Calendarをアプリケーションに統合するために開発者が行うべきことを十分に整備していないからです。

本記事では、認証、サポートされる操作、制限事項、統合を容易にするツールなど、iCloud Calendar APIをアプリケーションに統合する方法を深掘りします。

Apple iCloud Calendarはどのようなプロトコルと標準を使用していますか?

Apple iCloud Calendarは、カレンダー通信にCalDAV標準を使用しています。CalDAVはWebDAVを拡張したもので、クライアントがサーバー上でカレンダーやイベントを管理できるようにします。

イベントはICS(iCalendar)形式で表現されます。これはカレンダーデータのためのテキストベースのフォーマットです。つまり、あなたのアプリケーションはCalDAVリクエストとICSデータを使用してHTTP経由でiCloudカレンダーと通信できます。

利点として、これはAppleデバイスやオペレーティングシステム固有の実装ではないため、あらゆるプラットフォーム上のサーバーから利用できます。

残念ながらiCloudはカレンダー用のREST APIを提供していないため、iCloud Calendarをアプリに統合する唯一の方法はCalDAVです。

注: 私たちはサーバー間(server-to-server)の統合を推奨しています。そのためCalDAVが唯一の選択肢と言えます。

iCloudではどのように認証しますか?

通常、プラットフォーム(例えばGoogle Calendar)をアプリケーションに統合したい場合、そのプラットフォームで開発者アカウントを作成し、アプリケーションを登録し、スコープを設定し、テストユーザーを追加し、アプリ情報を入力して承認を申請します。

これらすべての手順を経て承認されると、エンドユーザーをプラットフォームのOAuth画面にリダイレクトできます。ユーザーはプラットフォームにログインし、アプリケーションが要求するスコープへのアクセスを明示的に許可します。エンドユーザーは、あなたがそのプラットフォームで設定したアプリ名や情報も確認できます。

Apple iCloudはこのようには機能しません。iCloudにはGoogle CalendarやOutlookのような標準的なOAuthフローがありません。ユーザーのiCloud Calendarを接続するには、SSL上のBasic認証を使用してApple IDで認証する必要があります。多くのiCloudアカウントでは二要素認証が有効になっているため、ユーザーはアプリ固有のパスワードを作成し、メインのAppleアカウントのパスワードの代わりにあなたのアプリで使用する必要があります。

アプリはユーザーにiCloudのメールアドレス/Apple IDと、この16文字のアプリ固有パスワードの入力を求めます。これらの資格情報を使ってiCloudのCalDAVサービスに接続できます。

アプリケーションがユーザーにメールアドレスとアプリ固有パスワードの入力を求める例。

apple-enter-app-specific-password.webp

上記の理由に加え、Appleはセキュリティ強化のためにアプリ固有パスワードを要求しています。Apple iCloudのパスワードをサードパーティアプリと共有するのは最善策ではありません。

内部的には、HTTPリクエストのヘッダーにBase64エンコードされたアプリ固有パスワードが次の形式で含まれます:
Authorization: Basic <app-specific-password-here>

iCloud Calendar APIでサポートされるメソッドは?

iCloudのCalDAVサービスはcaldav.icloud.comにホストされています。認証後、以下のメソッドが利用可能です。

  • ユーザーのカレンダーの一覧取得

  • イベントのCRUD

  • 特定イベントの取得

CalDAV標準の詳細については、利用可能なメソッドやフィルターなどを説明しているRFC 4791を参照してください。

以下では、iCloud Calendar統合に必要な最重要ポイントをまとめます。

サポートされるHTTP動詞

HTTP動詞CalDAVでの役割iCloudのサポート注意点
OPTIONSサーバー機能の検出デバッグに便利。本番では必須ではない。
PROPFINDプリンシパル、calendar-home-setの取得、カレンダー一覧、プロパティ取得事前に認証が必要。Depthは0または1を使用。
MKCALENDAR新しいカレンダーコレクションの作成書き込み権限が必要。Section 5.3.1を参照。
REPORTデータの検索(calendar-querycalendar-multigetfree-busy-query3つのREPORTは仕様で必須。iCloudでも利用可能。
PUT1件の.icsリソース(イベント/タスク)のアップロードまたは置換完全なVCALENDARを送信する必要あり。PATCH非対応。
DELETEイベントまたはカレンダーの削除安全のためIf-MatchのETagと併用。
COPY / MOVEカレンダー間でイベントをコピー/移動PUTと同じ前提条件が適用。
GET1件の.icsリソースを取得text/calendarとETagを返す。

カレンダーコレクションのプロパティ

プロパティ目的iCloud特有の注意点
CALDAV:calendar-description人が読める説明完全対応
CALDAV:calendar-timezoneクエリのデフォルトタイムゾーン対応
CALDAV:supported-calendar-component-set受け付けるコンポーネント(VEVENT, VTODO)イベントとタスクは別々のカレンダー
CALDAV:supported-calendar-data許可されるMIME/バージョン(通常text/calendar 2.0)iCloudのデフォルト
CALDAV:max-resource-sizeイベントあたりの最大サイズiCloudは約20 MB制限
CALDAV:min/max-date-time, max-instances, max-attendees-per-instanceさまざまなサーバー制限403/507エラーを避けるため遵守

注: OneCal Unified Calendar APIはまもなくリリース予定です。ぜひウェイトリストに参加して、リリース時に通知を受け取り、ローンチプロモーションをお楽しみください。

統合を簡単にするために使用できるライブラリは?

CalDAV、ICS、XML、そしてiCloud Calendar特有の注意点を扱うのは理想的とは言えません。各メソッドを理解し、XMLをJSONに変換し、アプリ内で利用するのに多くの時間を費やすことになります。

より快適に統合を行うために、次のライブラリを推奨します。

  • tsdav: JavaScript/TypeScriptを使用する場合の必需品です。tsdavを使うと、CalDAV特有の構文や用語を使用せずにiCloudサーバーと簡単に通信できます。すべてのHTTP動詞とXMLを高水準のTypeScript APIでラップしています(PROPFIND、REPORT、MKCALENDAR、PUT、DELETEなど)。詳細はtsdavドキュメントをご覧ください。

  • ical-generator: CalDAVでiCloud Calendarを統合する場合、イベントを作成または更新する際に_完全な .icsファイルをアップロードまたは置換_する必要があります。これらのファイルを手書きするのはエラーの元で、すべてのVEVENTに正しいヘッダー、UID、DTSTART/DTEND、RRULE、タイムゾーンなどが必要です。ical-generatorはこれらの問題を解決します。

  • ical.js: Mozilla Calendarチームが作成した純粋なJavaScriptのパーサ/エンジンで、icsレスポンスをJSクラスに変換します。

以下の表は、iCloud Calendar統合におけるtsdavの役割を説明します。

スタックでの役割tsdavが行うことiCloudで重要な理由
CalDAV / WebDAVクライアントすべてのHTTP動詞とXMLをラップした高水準TypeScript APIを提供生のXML生成やmultistatusレスポンス解析ではなくビジネスロジックに集中できる
ディスカバリヘルパーcreateDAVClient()がCalDAVのディスカバリフローを自動実行し、calendar-home-setを解決してpXX-caldav.icloud.comのベースURLを保存iCloud固有の2ステップディスカバリを自動化し、ボイラープレートを排除
認証ラッパーBasicOAuth 2のビルトインヘルパー。iCloudでは{ username: 'user@icloud.com', password: '<app-specific-pw>', authMethod: 'Basic' }を渡すだけ資格情報のBase64エンコードやヘッダー注入が不要
一般タスク向け型付きヘルパーfetchCalendars(), fetchCalendarObjects(), createCalendarObject(), updateCalendarObject(), deleteCalendarObject()がXMLではなくプレーンJSオブジェクトを返却/受理RFC 4791のXML構文を気にせず高速にCRUDを実装
SyncトークンサポートsyncCollection()がCalDAVのsync-collection REPORTをラップし、トークンを追跡して変更/削除アイテムのみ返すプッシュのないiCloudでポーリング実装を1行で実現
Browser + Node互換fetchのアイソモーフィック実装でサーバー(Node)でもブラウザでも動作ブラウザ拡張機能やSPAの一部として動作させる場合に便利
型付きモダンTSプロジェクト完全な型定義、ツリーシェイカブルなESモジュール、最小依存モダンなビルドパイプラインに簡単に統合

警告: tsdav iCalendarを生成しません。イベントペイロード文字列を生成するにはical-generator(または任意のICSビルダー)と併用してください。

tsdav + TypeScriptを使ったiCloud Calendar統合例

ここでは、すでにユーザーのユーザー名とAppleのアプリ固有パスワードを取得していると仮定します。

createClient — DAVClientを作成するメソッド:

import { DAVCalendar, DAVClient, DAVNamespaceShort, DAVObject } from "tsdav";

const APPLE_DAV_URL = "https://caldav.icloud.com";

function createClient({
username,
password,
}: {
username: string;
password: string;
}) {
 const client = new DAVClient({
 serverUrl: APPLE_DAV_URL,
 credentials: {
username,
password,
},
 authMethod: "Basic",
 defaultAccountType: "caldav",
});

 return client;
}

constants — 定数を含むファイル

export const PROD_ID = {
company: "your-company-name-here",
product: "your-product-name-here",
};

getCalendars — すべてのカレンダーを取得するメソッド

async getCalendars(
...params: Parameters<typeof DAVClient.prototype.fetchCalendars>
): Promise<DAVCalendar[]> {

// この初期化を別メソッドに抽象化することもできます。 // 簡潔にするため、各メソッドでクライアントを初期化しています。 const client = createClient({
 username: <email-here>,
 password: <password-here>,
});

 return client.fetchCalendars(...params);
}

getCalendarById — IDでカレンダーを取得するメソッド

async getCalendarById(calendarUrl: string): Promise<DAVCalendar> {
 const calendars = await getCalendars();
 const calendar = calendars.find((el) => el.url === calendarUrl);
 if (!calendar) {
 throw new Error(`Apple Calendar with id ${calendarUrl} not found`);
}

 return calendar;
}

getCalendarEvents — カレンダーイベントを一覧取得するメソッド

async getCalendarEvents(
calendarUrl: string,
query: GetCalendarEventsQuery = {}
) {
 const client = createClient({
 username: <email-here>,
 password: <password-here>,
});

 const calendar = await getCalendarById(calendarUrl);

 const events = await client.fetchCalendarObjects({
calendar,
 timeRange: query.dateRange ?? undefined,
})

 return { events, nextSyncToken: calendar.syncToken };
}

getEventById — IDでイベントを取得するメソッド

async getEventById(calendarUrl: string, eventId: string) {
 const client = createClient({
 username: <email-here>,
 password: <password-here>,
});

 const eventUrl = new URL(`${eventId}.ics`, calendarUrl).pathname;

 const responses = await client.calendarMultiGet({
 url: calendarUrl,
 props: {
[`${DAVNamespaceShort.DAV}:getetag`]: {},
[`${DAVNamespaceShort.CALDAV}:calendar-data`]: {},
},
 objectUrls: [eventUrl],
 depth: "1",
})


 if (responses.length === 0) {
 throw new Error(
 `Received no response while fetching ${eventUrl}`,
 null
);
} else if (responses[0].status >= 400) {
 throw new Error(
 `Failed to get Apple event by id. Status: ${responses[0].statusText}`,
responses[0]
);
}

 const response = responses[0];
 const calendarObject: DAVObject = {
 url: new URL(response.href ?? "", calendarUrl).href,
 etag: `${response.props?.getetag}`,
 data:
response.props?.calendarData?._cdata ?? response.props?.calendarData,
};

 try {
 return calendarObject;
} catch (err: any) {
 this.logger.error("Failed to process Apple Response", {
 message: err.message,
 event: calendarObject,
});
 return [];
}
}

createEvent — イベントを作成するメソッド

async createEvent(calendarUrl: string, data: AppleEvent) {
 const client = createClient({
 username: <email-here>,
 password: <password-here>,
});

 const eventId = data.id ?? <generate-id-here>

 const calendar = ical({
 prodId: PROD_ID,
 method: ICalCalendarMethod.REQUEST,
});

 const event = calendar.createEvent({ ...data, id: eventId });

 const response = await client.createCalendarObject({
 calendar: {
 url: calendarUrl,
},
 filename: `${event.id()}.ics`,
 iCalString: calendar.toString(),
})

 if (!response.ok) {
 throw new Error(
 `Failed to create Apple event: ${response.statusText}`,
response
);
}

 return { id: event.id(), eventWithExceptions };
}

updateEvent — イベントを更新するメソッド

async updateEvent(calendarUrl: string, eventId: string, data: AppleEvent) {
 const client = createClient({
 username: <email-here>,
 password: <password-here>,
});

 const originalEventData = await getEventById(calendarUrl, eventId);

 const calendar = ical({
 prodId: PROD_ID,
 method: ICalCalendarMethod.REQUEST,
});

 for (let event of originalEventData) {
 if (event.status === ICalEventStatus.CANCELLED) continue;

 if (event.id === eventId) {
calendar.createEvent({ ...event, ...data, id: eventId, url: null });
} else {
calendar.createEvent({ ...event, id: eventId, url: null });
}
}

 const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);

 const response = await
client.updateCalendarObject({
 calendarObject: {
 url: calendarObjectUrl.href,
 data: calendar.toString(),
},
})

 if (!response.ok) {
 throw new Error(
 `Failed to update Apple event: ${response.statusText}`,
response
);
}

 return getEventById(calendarUrl, eventId);
}

deleteEvent — IDでイベントを削除するメソッド

async deleteEvent(calendarUrl: string, eventId: string) {
 const client = createClient({
 username: <email-here>,
 password: <password-here>,
});

 const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);

 const response = await client.deleteCalendarObject({
 calendarObject: {
 url: calendarObjectUrl.href,
},
})

 if (!response.ok) {
 throw new Error(
 `Failed to delete Apple event: ${response.statusText}`,
response
);
}

 return { id: eventId };
}

Apple iCloud Calendarの制限事項

  1. アプリ固有パスワードによるBasic認証: 前述のとおり、iCloudは標準的なOAuth 2.0を採用していないため、認証と通信にはアプリ固有パスワードを使用する必要があります。

  2. Webhook/プッシュ通知の非対応: Google CalendarやOutlookとは異なり、iCloudには最高のカレンダーAPIと言えるような機能がなく、カレンダーの変更を通知するWebhookを登録できません。サードパーティアプリはリアルタイム更新を購読できないため、定期的にポーリングし、sync-collection REPORTを使って効率的に変更を取得する必要があります。

  3. PATCHメソッド非対応: イベントを部分更新するPATCHメソッドを使用できず、完全なPUTでイベントを更新する必要があります。

  4. 招待の制御不可: iCloud Calendarは会議招待を自動処理します。出席者付きのイベントを作成または変更すると、iCloud Calendarが招待状を送信し、出席者のステータスを更新します。CalDAVのscheduling Outbox/Inboxを使用して招待を手動で制御することはできません。

iCloud Calendarをアプリケーションに統合する簡単な方法はありますか?

iCloud Calendarをアプリに統合するのは容易ではありません。標準的なカレンダー規約に従っておらず、多くのCalDAVメソッドやフィルタが機能しないためです。

より簡単な方法として、Unified Calendar APIを利用する手があります。

Unified Calendar API Example

Unified Calendar API製品を利用すると、次のメリットがあります。

  • 最新の標準に従った、十分にドキュメント化されテストされたAPIでiCloud Calendarを統合できる。

  • 開発と保守にかかる時間を大幅に削減できる。 統合に関するAPI、クライアント、エッジケースなどがサービス側で解決されているため、実装と保守に費やす時間を節約できます。

  • iCloud Calendar以外のカレンダープロバイダーも追加作業なく統合できる。 多くのUnified Calendar API製品はGoogle CalendarやOutlookなど主要なカレンダープロバイダーにも対応しています。

  • 独自のポーリングソリューションを実装せずにプッシュ通知/Webhookを利用できる。 iCloud用にポーリングシステムを構築するのは難しく、キューや新しいサーバーを立ち上げる必要があります。

Unified Calendar APIを使ってiCloud Calendarをアプリに統合しよう

OneCal Unified Calendar APIのリリース準備がほぼ完了しました。上記のUnified Calendar APIのメリットをすべて享受できます。

ウェイトリストに参加して、ローンチの通知を受け取り、リリース時の割引やプロモーションをぜひご利用ください。