NotionSwiftを使ってアプリ作ってみた話
「NotionAPIでアプリ作ってみたい」と思い立って、公式より簡単に作れないかなーと方法を探していたところ、良い感じのライブラリを見つけました。
使ってみよう
READMEを読んだ感じ、使いやすそうだ
作ったアプリ
Notionで作成した習慣トラッカーの入力を簡単にするアプリを作りました。(よかったら使っていただけると嬉しいです!)
ライブラリを使用した箇所は以下になります。
- NotionDB作成
- 既存のNotionDBとの連携
- NotionDBにページを追加
- 特定のページのプロパティを更新
実装
Wrapperクラスの作成
NotionClient
のインスタンスとAPI処理を持つclass。
NotionClient
のインスタンスを生成するにはintegrationToken
とdatabaseId
が必要なのですが、漏れたらまずい情報のためKeyChain
に保存しておりインスタンス生成時にKeyChain
から取得するようにしています。
class KeyChainHelper {
static let shared = KeyChainHelper()
private init() {}
// App Bundle Id
let service = "com.xxxxxxx.xxxxxx"
enum AccountString: String {
case integrationToken = "integrationToken"
case databaseId = "databaseId"
}
func save(account: AccountString, data: Data) -> Bool {
let query = [
kSecValueData: data,
kSecClass: kSecClassGenericPassword,
kSecAttrService: self.service,
kSecAttrAccount: account.rawValue,
] as [CFString : Any] as CFDictionary
let matchingStatus = SecItemCopyMatching(query, nil)
switch matchingStatus {
case errSecItemNotFound:
// データが存在しない場合は保存
let status = SecItemAdd(query, nil)
return status == noErr
case errSecSuccess:
// データが存在する場合は更新
SecItemUpdate(query, [kSecValueData as String: data] as CFDictionary)
return true
default:
Logger.error("Failed to save data to keychain")
return false
}
}
func read(account: AccountString) -> String? {
let query = [
kSecAttrService: self.service,
kSecAttrAccount: account.rawValue,
kSecClass: kSecClassGenericPassword,
kSecReturnData: true
] as [CFString : Any] as CFDictionary
var result: AnyObject?
SecItemCopyMatching(query, &result)
if let data = (result as? Data) {
return String(data: data, encoding: .utf8)
} else {
return nil
}
}
}
ライブラリのAPIはcallbackで実装されていましたが、SwiftUIで扱う場合async/await
で使えた方が便利だなーと感じ、Wrapperクラスを作ることにしました。なくても良いです!
import Foundation
import NotionSwift
class NotionClientWrapper: ObservableObject {
@Published var notionClinent: NotionClient?
@Published var databaseId: Database.Identifier?
init(){
if let accessKeyString = KeyChainHelper.shared.read(account: .integrationToken),
let databaseIdString = KeyChainHelper.shared.read(account: .databaseId) {
self.notionClinent = NotionClient(accessKeyProvider: StringAccessKeyProvider(accessKey: accessKeyString))
self.databaseId = Database.Identifier(databaseIdString)
} else {
Logger.debug("accessKeyStringかdatabaseIdStringが存在しない")
self.notionClinent = nil
self.databaseId = nil
}
}
// DBの作成
func createDatabaseQueryAsync(
request: DatabaseCreateRequest
) async throws -> Database {
guard let notionClinent = self.notionClinent else {
let error = "notionClinentがnil"
Logger.debug(error)
throw NotionClientError.builderError(message: error)
}
return try await withCheckedThrowingContinuation { continuation in
notionClinent.databaseCreate(
request: request
) { result in
switch result {
case let .success(database):
Logger.debug("databaseId: \(database.id) の作成に成功")
continuation.resume(returning: (database))
case let .failure(error):
Logger.debug("updatePageProperties_error:\(error)")
continuation.resume(throwing: error)
}
}
}
}
// 以下その他API
}
withCheckedThrowingContinuation
でラップするとasync/await
で扱えるようになります。
API部分
画面から扱いやすいようにNotionAPIのメソッドをまとめたclassを作成しました。
class NotionClientStore: ObservableObject {
@Published var notionClinent = NotionClientWrapper()
// 以下APIメソッド
}
APIを投げる時にNotionのページのURLからdatabaseIdを取得する必要があるのですが、URL全文から以下のコードでidを取得できます。
func createIdByPageUrl(urlString: String) -> String {
let pattern = #"(?<=https:\/\/www\.notion\.so\/)[\w\d]+"#
if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
let nsrange = NSRange(urlString.startIndex..<urlString.endIndex, in: urlString)
if let match = regex.firstMatch(in: urlString, options: [], range: nsrange) {
return String(urlString[Range(match.range, in: urlString)!])
}
}
return ""
}
API実装例_DBの作成
ライブラリではNotionを様々な方法で操作できるのですが、本記事では一例としてDB作成の実装を紹介します。
Request
の作成
func createDatabaseCreateRequest(
pageId: UUIDv4,
databaseTitle: String
) -> DatabaseCreateRequest {
let parentPageId = Page.Identifier(pageId)
let request = DatabaseCreateRequest(
parent: .pageId(parentPageId),
icon: nil,
cover: nil,
title: [.init(string: databaseTitle)],
properties: ["名前": .title,
"運動": .checkbox,
"勉強": .checkbox,
"カレンダー": .createdTime]
)
return request
}
api
func createDatabaseQueryAsync(
request: DatabaseCreateRequest
) async throws -> Database {
guard let notionClinent = self.notionClinent else {
let error = "notionClinentがnil"
Logger.debug(error)
throw NotionClientError.builderError(message: error)
}
return try await withCheckedThrowingContinuation { continuation in
notionClinent.databaseCreate(
request: request
) { result in
switch result {
case let .success(database):
Logger.debug("databaseId: \(database.id) の作成に成功")
continuation.resume(returning: (database))
case let .failure(error):
Logger.debug("updatePageProperties_error:\(error)")
continuation.resume(throwing: error)
}
}
}
}
ボタンを押した時などに呼ぶ
private func createDatabase() {
let pageId = self.notionClientStore.createIdByPageUrl(urlString: pegeUrlText)
let request = self.notionClientStore.createDatabaseCreateRequest(pageId: pageId,
databaseTitle: databaseNameText)
Task {
do {
let database = try await notionClientStore.notionClinent.createDatabaseQueryAsync(request: request)
// 作成したDBの情報を使っての処理はここに書く
} catch let error as NotionClientError {
// NotionApiエラー
} catch {
// その他エラー
}
}
}
注意点
timezone対応していないため、「作成日時」「日付」プロパティで特定の日付が入力されているページを取得・操作したい場合は日本時間に変換する必要があります。
func dateFormatterJa() -> DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ja_JP")
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
return dateFormatter
}
それでも日付判定がうまくいかない場合があります。
ライブラリのissueは立っているので対応してくれるのを願うばかりです、、
最後に
ライブラリを使った感想としてはNotionと連携したiOSアプリを作る場合は、導入すると結構楽に実装ができたので選択肢としては全然ありかなと思います。もし活用される際は本記事をご参考にしていただけると幸いです。
私自身の未熟さもあり、記載のコードが「これは...」となられたかもしれませんが、どうかご容赦ください。ご指摘あればよろしくお願いいたします。
アプリ使ってもらえると喜びます!
Discussion