SwiftUIでCouldKitを使ってみたシリーズ2!
CloudKitを使いたい
前回はCloudKitの簡単なセットアップとしてプロジェクトからContainerの作成まで行いました.
今回は,Dashboard上でデータベースを作成して,アプリ側でデータを受け取るところまでを取り上げます.
前回のリンクはこちらから
今回の完成イメージ
画面右上のツールバーのボタンを押すとCloudKitからデータを取得します表示したリストからは,お店のHPに遷移するLinkを設定します.
CloudKit Dashboardでデータの登録
Modelの定義
今回は下記の2店舗の情報を直接CloudKitのデータベースに書き込みます
name | street | website | phone_number | latitude | longitude |
---|---|---|---|---|---|
とみ田 | 千葉県松戸市松戸1339 高橋ビル1F | https://www.tomita-cocoro.jp/about_us.html | 047-368-8860 | 35.78200511 | 139.9009887 |
麺屋翔みなと | 東京都新宿区西新宿1-26-2 新宿野村ビル地下2階 | https://menya-sho.co.jp/shop/minato.html | 03-6304-5242 | 35.69302059 | 139.6954082 |
Dashboard上でSchemeを定義する
アプリの全ユーザーがアクセス可能なようにPublicのデータベースを作成します.
1. Recordの定義
レコードのPropertyの定義を行います.
各PropertyのNameとTypeを指定します. 今回は画像を,Assetとして指定をします.AssetはCKAsset型として扱われ,アプリ側でAsseet型をImageに変換する処理を行う形になります.
2. Indexの作成
レコードのIndexを指定します.
クエリが実行可能なように,record_nameに対して,Querableを指定します. また,今回は店舗名でソートができるようにしたいので,nameをSortableとしてIndexを指定します.
3. データの挿入
先程の表にまとめた情報をモデルの定義に沿って,入力していきます. 画像は,ファイルフォルダーから選択する形になります.LocationはLat/Lonを一括で登録する事ができます.
4. データの確認
登録が完了したらクエリを実行して登録したデータを確認してみましょう. 無事に2件分のデータが保存されてQueryの実行結果として表示されることが確認できました.
CloudKitのデータを利用するための実装
Modelの定義
先程の定義したRamenShopを扱うためのModelを定義します
下記の3つの手順を行います.
import Foundation
import CloudKit
struct RamenShop {
// ①Propertyの定義
// MARK: - Property
let ckRecordId: CKRecord.ID
let name: String
let street: String
let website: String
let phoneNumber: String
let location: CLLocation
let photo: CKAsset! // イメージではなくAsset
// ②Keyの定義
// MARK: - KeyValue
static let kName = "name"
static let kStreet = "street"
static let kWebsite = "website"
static let kPhoneNumber = "phoneNumber"
static let kLocation = "location"
static let kPhoto = "photo"
// ③Initialzierの定義
// MARK: - Init
init(record: CKRecord) {
ckRecordId = record.recordID
name = record[RamenShop.kName] as? String ?? "N/A"
street = record[RamenShop.kStreet] as? String ?? "N/A"
website = record[RamenShop.kWebsite] as? String ?? "N/A"
phoneNumber = record[RamenShop.kPhoneNumber] as? String ?? "N/A"
location = record[RamenShop.kLocation] as? CLLocation ?? CLLocation(latitude: 0, longitude: 0)
photo = record[RamenShop.kPhoto] as? CKAsset
}
}
1. Propertyを定義する
Dashboardで登録した内容と一致するように,各Propertyを定義します.
注意点としては,レコードのIDとしてCKRecord.IDを定義しています.これは,各Objectの持つユニークなIDで,CloudKitがデータの生成時に自動的に付与するものです.
2. Keyの定義
CKRecordはDictionaryなので,Keyを指定して値を取り出します.Keyは,Dashboardで設定したPropertyのNameと完全に一致するようにしなくてはなりません.
今回は,keyをStaticな定数として定義して扱うことにしました.
3. Initialzierによる初期化処理
レコードの初期化を行います.この処理はJSONのパースを行うのとどのようの手順になるかと思います.recordを値をキャストして各Propertyに割り当てています.
CloudKitManagerの作成
CloudKitに対してデータの取得処理を担うクラスを作成します.
Async/ AwaitでCloudKitに対して,先程の登録したPublickDatabaseから全件データを取得してみます.
import CloudKit
enum RecordName {
static let ramenShop = "RamenShop"
}
struct CloudKitManager {
static let shared = CloudKitManager()
private init() { }
static let container = CKContainer.default()
static func getShopInfo() async throws -> [RamenShop] {
// ① クエリの生成
let sortDescriptor = NSSortDescriptor(key: RamenShop.kName, ascending: true)
let query = CKQuery(recordType: RecordName.ramenShop, predicate: NSPredicate(value: true))
query.sortDescriptors = [sortDescriptor]
// ② データベースに対してqaueryの実行
let (matchResults, errorn) = try await container.publicCloudDatabase.records(matching: query)
// ③ 取得結果のパース処理
let records = matchResults.compactMap { _, result in try? result.get() }
return records.map(RamenShop.init)
}
}
1. クエリの生成
下記のコードではCloudKitのデータベースに対して,検索クエリを生成しています.NSSortDescriptorは,レコードのkeyに対して,降順または昇順で並べ替えを行います.
CKQueryの概要はこんな感じになります↓ 今回はnameを昇順で表示するためにsortDescriptorsを指定します.
プロパティー: 型 | 概要 |
---|---|
recordType: CKRecord.RecordType | 検索対象のレコードタイプの名前 |
predicate: NSPredicate | 検索条件 SQLでいうWHERE句にあたる |
sortDescriptors: [NSSortDescriptor]? | 並べ替えの条件を指定する |
let sortDescriptor = NSSortDescriptor(key: RamenShop.kName, ascending: true)
let query = CKQuery(recordType: RecordName.ramenShop, predicate: NSPredicate(value: true))
query.sortDescriptors = [sortDescriptor]
2. データベースに対してqaueryの実行
container内のPublicデータベースに対して,クエリを実行します.実行結果の返り値として,検索結果とErrorが返されるためTupleで結果を受け取ります.
let (matchResults, error) = try await container.publicCloudDatabase.records(matching: query)
3. 取得結果のパース処理
取得結果のmatchResultsは,レコードのIDと取得したRecordの結果をResult型で保持しています.
今回はIDは必要ないので,resultのみを取得したいと思います.resultの値はgetメソッドを実行し,失敗した場合にはtry?でnilを返すようにします.
// MARK: getの定義
func get() throws -> Success
compactMapはnilを除外するため,getメソッドが失敗した場合にはrecordは空になります.最後にRamenShopの初期化を実行して,配列として結果を返します.
let records = matchResults.compactMap { _, result in try? result.get() }
return records.map(RamenShop.init)
Viewの実装
画面に取得結果を表示するまでを確認したいので,今回は画面左上のツールバーのボタンを押すとCloudKitと通信を行うことにします.
重要な点としては,Async/Awaitで非同期処理を実行するため,Task内で今回の処理を実行する必要があります.取得結果は,shopInfoに渡すことで,リスト内にショップ名とリンクのボタンが表示されます.
import SwiftUI
struct ContentView: View {
@State private var shopInfo: [RamenShop] = []
var body: some View {
NavigationView {
List {
ForEach(shopInfo, id: \.self) { shopInfo in
HStack {
Text(shopInfo.name)
Spacer()
Link(destination: URL(string: shopInfo.website)!) {
Image(systemName: "map.fill")
}//: LINK
}//: HSTACK
}//: FROEATCH
}//: LIST
.navigationTitle("ラーメン情報")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
// CloudKitのクエリの実行を行う
// Asycに対応するためにTask内で実行する
Task {
do {
shopInfo = try await CloudKitManager.getLocation()
} catch let error {
print(error)
}
}
} label: {
Image(systemName: "globe.badge.chevron.backward")
.imageScale(.large)
}
}
}
}//: NAVIGATION
}//: BODY
}
まとめ
いかがでしたでしょうか? APIコールをするのと近いかたちで以外と実装自体はシンプルにできたのではないかと思います.
次回は,取得したCKAssetのImageへの変換処理と位置情報をMapViewへ表示する処理について取り上げたいと思います.
Discussion