- 投稿日:2020-05-28T23:32:32+09:00
【Flutter】Flutter製アプリをリリースしてみた、あとARとかも使ってみた話
アプリ概要
ゴルフのオリンピックゲームの計算やボールとピンまでの距離のAR測定,1−18までの乱数を生成することができる、ゴルフ幹事向けのアプリです。
App Store はこちら
Google Play Store はこちら— TeppeiKikuchi??Flutter修行中 (@tpi29) May 28, 2020使用した技術
- bloc
- Provider
- rxDart
- Admob
- Firebase Analytics
- ARKit(iOSのみ)
ソースコードはこちら
https://github.com/Tetsukick/enGolf
AR技術のご紹介
iPhoneのみARKitを活用したプラグイン(arkit_flutter)が公開されていたので、そちらを活用して、タップした2点間の距離を測れるアプリを作成いたしました。
AR距離測定画面のソースコードを記載しておきます。
Sampleを80%ぐらい流用して簡単に開発できました。import 'package:arkit_plugin/arkit_plugin.dart'; import 'package:flutter/material.dart'; import 'package:vector_math/vector_math_64.dart' as vector; import 'dart:io'; class ARMeasureScreen extends StatefulWidget { @override _ARMeasureScreen createState() => _ARMeasureScreen(); } class _ARMeasureScreen extends State<ARMeasureScreen> { ARKitController arkitController; vector.Vector3 lastPosition; @override void dispose() { arkitController?.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return Platform.isIOS ? Scaffold( body: Container( child: ARKitSceneView( enableTapRecognizer: true, onARKitViewCreated: onARKitViewCreated, ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.delete), backgroundColor: Colors.lightGreen, onPressed: () { arkitController.remove('point'); arkitController.remove('text'); arkitController.remove('line'); lastPosition = null; }, ), ) : Scaffold( body: Center( child: Text('Androidには対応しておりません\n現在開発中のためしばらくお待ち下さい。'), ), ); } void onARKitViewCreated(ARKitController arkitController) { this.arkitController = arkitController; this.arkitController.onARTap = (ar) { final point = ar.firstWhere( (o) => o.type == ARKitHitTestResultType.featurePoint, orElse: () => null, ); if (point != null) { _onARTapHandler(point); } }; } void _onARTapHandler(ARKitTestResult point) { final position = vector.Vector3( point.worldTransform.getColumn(3).x, point.worldTransform.getColumn(3).y, point.worldTransform.getColumn(3).z, ); final material = ARKitMaterial( lightingModelName: ARKitLightingModel.constant, diffuse: ARKitMaterialProperty(color: Colors.blue)); final sphere = ARKitSphere( radius: 0.006, materials: [material], ); final node = ARKitNode( name: 'point', geometry: sphere, position: position, ); arkitController.add(node); if (lastPosition != null) { final line = ARKitLine( fromVector: lastPosition, toVector: position, ); final lineNode = ARKitNode( name: 'line', geometry: line ); arkitController.add(lineNode); final distance = _calculateDistanceBetweenPoints(position, lastPosition); final point = _getMiddleVector(position, lastPosition); _drawText(distance, point); } lastPosition = position; } String _calculateDistanceBetweenPoints(vector.Vector3 A, vector.Vector3 B) { final length = A.distanceTo(B); return '${(length * 100).toStringAsFixed(2)} cm'; } vector.Vector3 _getMiddleVector(vector.Vector3 A, vector.Vector3 B) { return vector.Vector3((A.x + B.x) / 2, (A.y + B.y) / 2, (A.z + B.z) / 2); } void _drawText(String text, vector.Vector3 point) { final textGeometry = ARKitText( text: text, extrusionDepth: 1, materials: [ ARKitMaterial( diffuse: ARKitMaterialProperty(color: Colors.red), ) ], ); const scale = 0.001; final vectorScale = vector.Vector3(scale, scale, scale); final node = ARKitNode( name: 'text', geometry: textGeometry, position: point, scale: vectorScale, ); arkitController.add(node); } }
- 投稿日:2020-05-28T21:24:33+09:00
iOS13でショートカットのバックアップを作成する
前置き
\デデーン!/
目当てのショートカットがなかなか見つからなかったり、割と不便なのでこの機会に整理します。iOSのバージョンによってはバックアップの復元方法が違う
iOS12での方法は既に先駆者様達が個人ブログ等でまとめてくださっているので、ここではiOS13でのバックアップ・復元方法について取り上げます。
必要なもの
・iCloud Drive
・ファイルApp
・ショートカットAppバックアップ
極論この2アクションで十分です。日時を書いたフォルダを作っておくと尚GOOD。復元
iCloudからリンクを取得するというのがミソです。DropboxのリンクではショートカットAppが開かないため復元できません。おまけ追記
バックアップ・復元ができる実用的なやつを作りました。リンク
- 投稿日:2020-05-28T16:57:06+09:00
CleanArchitecture ファイルの自動生成(iOS・Swift)
CleanArchitectureではファイルを大量に生成するので、できれば手動ではなく自動で行いたい。そのための手段をまとめた。
Xcodeのテンプレート機能
メリット
Xcodeの機能として、テンプレートファイルを用意しておいておくと、ファイル生成時に選ぶことができる。
Xcodeの機能にすぎず、特別な設定は不要でプロジェクトの中身に何ら影響がない。テンプレートファイルは~/Library/Developer/Xcode/Templatesに置く。デメリット
テンプレートファイルは何処かから拾ってくるか自分で書く・編集する必要がある。自分は下記から拾った。
参考
https://medium.com/swift2go/installing-the-clean-swift-template-in-xcode-6b4367006827
Kuri
メリット
普通に使いやすい
デメリット
ymlファイル・テンプレートファイルをプロジェクトルートに置かないといけない(プロジェクトの構成に影響がある)
参考
https://github.com/bannzai/Kuri
https://dev.classmethod.jp/articles/making-progress-ios-dev-using-kuri/
https://dev.classmethod.jp/articles/introduction-of-kuri-generate-command-option/
- 投稿日:2020-05-28T16:34:12+09:00
RxSwiftで検索+ページネーションをいい感じにする
iOSアプリの以下のような条件の検索画面を RxSwift でいい感じに実装する方法をメモします。
- キーワードを入力して検索ボタンをタップすると検索開始する
- 検索APIは結果が多い場合を想定して、ページ番号指定によるページネーションができる
- 検索結果一覧の最後のアイテムを表示するタイミングで自動で次のページを読み込む
- 最後のページまで読み込んだら終了する
実装方法
以下、検索画面
SearchView
を想定したコードです。細かい説明はコメントとして書いてあります。また、ところどころに動作確認用の print 文が入っています。
- 検索ボタンを押した時は
search(by keyword: String)
を実行する- 結果一覧の最後のアイテムを表示するタイミングで
requestNextPage()
を実行するSearchView.swift/// 検索画面 class SearchView { /// キーワードを入力して検索ボタンを押した時に実行する処理 func search(by keyword: String) { print("[A] Start search - ", keyword) // 次のページを要求するイベントを発生させるためだけの Subject を用意 let requestSubject = PublishSubject<Void>() self.nextPageRequestSubject = requestSubject // 実行中の検索をキャンセルしてから開始する searchDisposable?.dispose() searchDisposable = Observable // 次のページの要求イベントに検索キーワードを組み合わせる .combineLatest( // イベント発生のたびにページ番号をインクリメントする requestSubject.scan(Int(0), accumulator: { (page, _) -> Int in return page + 1 }), Observable.just(keyword) ) // 検索APIでキーワードとページ番号を指定して実行する .flatMap { (arg) -> Observable<[Int]> in let (page, keyword) = arg print("[B] Send API request - ", keyword, page) return API.search(by: keyword, page: page) .catchErrorJustReturn([]) .asObservable() } // 検索結果が空になったら止める .takeWhile { (ids) in !ids.isEmpty } // 前回までの検索結果に、新しい結果を足したものを .scan([]) { (ids, newIds) -> [Int] in ids + newIds } .subscribe(onNext: { (ids) in print("[C] Results - ", ids) }) // 最初のページを要求する requestSubject.onNext(()) } /// 検索結果一覧の最後のアイテムを表示する時に実行する処理 func requestNextPage() { print("[D] Request next page") // 次のページを要求するイベントを発生させるだけ nextPageRequestSubject?.onNext(()) } private var searchDisposable: Disposable? private var nextPageRequestSubject: PublishSubject<Void>? }説明用に検索APIの動きを想定した処理を次のように用意しました。
- 1ページあたり5件のIDを取得できる
- 検索結果は3ページまで存在する
API.swiftimport RxSwift /// API enum API { /// キーワードで検索する(検索結果は1ページ5件まで返す) static func search(by keyword: String, page: Int) -> Single<[Int]> { // 検索APIで3ページまで取得できるようなケース return Single.create { (observer) -> Disposable in switch page { case 1: observer(.success([1,2,3,4,5])) case 2: observer(.success([6,7,8,9,10])) case 3: observer(.success([11,12,13])) default: observer(.success([])) } return Disposables.create() } } }実行結果
実際の操作を想定して次のようなコードを実行してみます。
let searchView = SearchView() // ページ1のリクエスト searchView.search(by: "hoge") // ページ2のリクエスト searchView.requestNextPage() // ページ3のリクエスト searchView.requestNextPage() // ページ4のリクエスト(存在しない) searchView.requestNextPage() // ページ5のリクエスト(存在しない) searchView.requestNextPage()結果はこちらです。
[A] Start search - hoge [B] Send API request - hoge 1 [C] Results - [1, 2, 3, 4, 5] [D] Request next page [B] Send API request - hoge 2 [C] Results - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [D] Request next page [B] Send API request - hoge 3 [C] Results - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] [D] Request next page [B] Send API request - hoge 4 [D] Request next page [D] Request next pageうまく動いてますね。
- 検索中のキーワードや現在のページ番号といった状態の管理をしなくてよくなっている
- 取得済みの全ページの結果が出力されるので、それでテーブルビュー等を更新するだけでよい
- 投稿日:2020-05-28T14:42:53+09:00
iOSの各種シミュレーターをfzfで選んで起動する
プリミティブな方法だと
$ xcrun simctl listで、起動したいシミュレーターのUUIDを確認して
$ xcrun instruments -w 208E5166-9C5D-4CA5-A9DE-529AD562D311 # iPhone 11 Pro Maxとすると指定したシミュレーターが起動するのだが、Xcodeをアプデするとシミュレーターも更新されてしまうので、以前書き留めておいたUUIDが使えなくなる。また調べないといけなくてめんどくさい。
ということで、インクリメンタルに端末名で検索してEnterで起動するようにした。
.zshrc
に以下を追記.zshrcfunction simc() { xcrun instruments -w $(xcrun simctl list | grep -v unavailable | grep -E "^\s" | grep -v ":" | fzf | grep -oE "\((.+?)\)" | grep -oE ".{20,}" | head -n1 | perl -pe "s/(\(|\))//g" ) }fzfが入ってないなら以下を実行。
$ brew install fzf設定を読み込み直して実行。
source ~/.zshrc
- 投稿日:2020-05-28T13:28:02+09:00
ローカライズファイルをAndroidとiOSで共通管理する
はじめに
AndroidとiOSでサービスを展開している場合、同じ文言を使っている。
プラットフォームごとに管理することで文言の誤字が発生しやすく、コストも上がる。
ローカライズファイルを効率よく管理する方法を考えました前提条件
- AndroidとiOSで共通のリソースを扱う
- 差分を追いやすいこと
githubを使って管理
- オリジナルデータとなる文言とkeyはjsonにして管理
- githubを使うことで差分を追いやすくなる
- CIを使うことで自動化できる
ローカライズファイルを更新するツール
- JSONからローカライズファイルを生成するのを簡単にするためにツールを作成した
- t-osawa-009/JSONToString
使い方
1.オリジナルデータとなるJSONファイルを追加
[ { "key": "hoge", "key_android": "hoge", "key_ios": "hoge", "value_android": "hoge", "value_ios": "hoge" }, ]2.ローカライズファイルの生成を設定する
.JSONToString.yml
ファイルを追加outputs: - key: key value_key: value_ios output: Strings/Localizable.strings format: strings sort: asc - key: key value_key: value_android output: Strings/strings.xml format: xml3.コマンドを実行
$ JSONToString --json_path Strings.json4.生成したファイルを各プロジェクトに追加する
"hoge" = "hoge";<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hoge">hoge</string> </resources>まとめ
- githubで管理することで1元管理が可能になった
- 運用を考えて、ツールを自作するのは有効である
参考リンク
- 投稿日:2020-05-28T11:25:17+09:00
【Swift】選択上限のある複数選択可能なTableViewの実装+スクロールしてもチェックマークがズレないようにする
選択上限をつけた複数選択可能なTableViewの実装が調べても出てこなかったので、簡単に作ってみました。
今回実現したいTableView
- 複数選択可能
- 選択状態がスクロールによって他のCellにズレない
- 選択できる数に上限をつける
1. 複数選択を可能にする
MultipleTableView.swift// tableViewのisMultipleTouchEnabledをtrueにする isMultipleTouchEnabled = trueこれだけで複数選択が可能になる。
2. 選択状態がスクロールによって他のCellにズレない
Model
に選択状態を持って、その値を元にCell
をinit
する。MultipleTableView.swiftfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: R.reuseIdentifier.multipleTableViewCell.identifier, for: indexPath) as! MultipleTableViewCell cell.selectionStyle = .none cell.kind = kinds[indexPath.row] return cell }MultipleTableViewCell.swiftvar kind: KindModel? { didSet { accessoryType = kind?.isSelected ?? false ? .checkmark : .none initLabel() } }選択状態の
set
は次の 選択できる数に上限をつける と合わせて紹介します。3. 選択できる数に上限をつける
選択状態に上限をつけるのは、
TableView
に選択数のカウンタをつけるのが1番楽かなと思います。
もっとかっこいい方法はないかと考えたのですが、特に思いつきませんでした。。MultipleTableView.swiftvar selectedIndex: [Int] = [] func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let row = selectedIndex.filter({ $0 == indexPath.row }).first { guard let removeIndex = selectedIndex.firstIndex(of: row) else { return } selectedIndex.remove(at: removeIndex) kinds[indexPath.row].isSelected.toggle() return } // 今回は上限を3にしました if selectedIndex.count > 2 { return } kinds[indexPath.row].isSelected.toggle() selectedIndex.append(indexPath.row) }これで上限の管理は完了です!
didDeselectRowAt
使いたかったけど、なぜか呼ばれなかった。
SOFにも同じような投稿がありましたが、解決ならずだったので諦めました。。
- 投稿日:2020-05-28T07:01:06+09:00
iOSにユーザーデータを保存する方法と、そのためのコードの書き方: UserDefaults、Core Data、Key Chain、CloudKit
データの保存はiOSアプリの持つ主要な機能です。たとえば、ユーザーが指定した色などの環境設定を保存したり、ウェブサイトのトークンをアプリに保存したり、ToDoリストのアプリを作ってタスクを保存したりすることができます。データをシステムに保存する方法はいくつもあります。
目次
User Defaults
Core Data
Keychain
iCloud CloudKitこの記事では、上記の方法それぞれについて、サンプルコードを使って解説します。
User Defaults
主な用途
わずか2〜3行のコードで、
String
,Int
,Bool
, やどんなData
オブジェクトも保存・取り出しができます。通常は、ユーザーがオンボーディングを終えたかどうかや、天気アプリでのユーザーのデフォルト都市等、ユーザーのプリファレンスを保存するのに使用されます。
長所
- わずか数行のコードで簡単に実装
- App Extensions 間でも簡単に共有
- クラウドにデータを保存でき、ユーザーの所有デバイス間で同期可能( 同じApple ID でサインイン)
短所
シンプルなデータの保存のみに使います。データベーススキームはありません。また Apple によると、
UserDefaults
ストレージが512 KBに達すると警告が発せられます。そして Apple のドキュメンテーションには、ストレージが1MBに達するとアプリが終了するとあります。Currently, there is only a size limit for data stored to local user defaults on tvOS, which posts a warning notification when user defaults storage reaches 512kB in size, and terminates apps when user defaults storage reaches 1MB in size.
https://developer.apple.com/documentation/foundation/userdefaults/1617187-sizelimitexceedednotificationしかし、Stack Overflow のユーザーの一人は、1MB以上のデータを保存してもクラッシュを起こしていないと報告しています。
ですから、ユーザーのプリファレンスや文字列・Int値をいくらか保存したい程度であれば心配することはありません。
使い方
//To store a value UserDefaults.standard.set("Tokyo", forKey: "currentCity") //To read a value let currentCity = UserDefaults.standard.string(forKey: "currentCity")(クラウド同期バージョン)の使い方
iCloudの機能を有効にする
プロジェクト設定の
Signing & Capabilities
タブで、プラスボタンをクリックします。そして、
iCloud
機能を追加します
Key-value storage
をオンにしますそして、すべての
UserDefaults.standard
をNSUbiquitousKeyValueStore()
に置き換えるだけです:let store = NSUbiquitousKeyValueStore() store.set("Tokyo", forKey: "currentCity") let currentCity = store.string(forKey: "currentCity")資源
Core Data
主な用途
Core Data
には幅広い用途があります。私の個人的な開発では、Core Data
を使ってユーザーの日記、ToDoアイテム、ブックマークを保存しています。
Core Data
はユーザーの情報を既存の多くのアプリに保存するために広く使われています。長所
- 構造化データの保存(データベーススキームを設計する必要があります)
- 数分以内にクラウド同期サポートを有効にできる
短所
- データベーススキームを変更するたびに、新しいデータベースバージョンを作成する必要があります。そうしないとアプリがクラッシュしてしまいます。
使い方
新しいアプリ
新しいアプリを作成する場合は、プロジェクトの作成時に
Use Core Data
を選択できます。既存のアプリ
また、既存のアプリに
Core Data
を簡単に追加できます。最初に、データモデルファイルを作成します。キーボードのcommand-Nキーを押して、
Data Model
を検索します。新しい
Data Model
にあなたのアプリの名前をつけてください。例えば、ここでは私のアプリの名前はCoreDataDemo
なので、私はCoreDataDemo.xcdatamodeld
を作りました。そして
AppDelegate.swift
ファイルに次の行を追加します。// MARK: - Core Data stack lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "CoreDataDemo") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { print(error.userInfo) } }) return container }()この行で、
CoreDataDemo
をあなたのアプリの名前に置き換えてください。let container = NSPersistentContainer(name: "CoreDataDemo")データベース構造の作成
To-Doアイテムごとに次のプロパティを格納するTo-Doアプリを作成するとします:
- To-Doタスク名
- タスクの担当者
- タスクの期限
1.
.xcdatamodeld
ファイルを開き、Add Entity
をクリックします。作成した新規エンティティ・アイテムに
TodoItem
という名前を付けます。作成した新しいアイテムをダブルクリックし、名前を付けるだけです。この例では、
TodoItem
という名前を付けます。新しいプロパティを追加する
Properties
セクションのプラスアイコンをクリックして、新しいプロパティを追加します。ここでは、To-Doアプリに次のプロパティが必要です。
プロパティ名 タイプ todoTaskName String personName String taskDeadline Date タイプを設定するには、プロパティ名の横にあるドロップダウンメニューをクリックします:
このような感じになります。
Entity クラスのコードファイルを生成
- 作成した新しいエンティティ項目を選択します。右側のパネルで最後のセクションのアイコンを選択します :
Codegen
設定をManual/ None
に変更以前の設定:
新しい設定:
- クラス定義ファイルを生成
最上部のメニューで
Editor
をクリックし、Create NSManagedObject subclass...
をクリックしますこうしたサブクラスを生成することで、
TodoItem
を新規オブジェクトの作成に直接使用できるようになります。ファイルナビゲーターに以下のファイルが表示されます :
新規レコードを作成
func addNewToDoItem(todoTaskName: String, personName: String, taskDeadline: Date) { let appDelegate = UIApplication.shared.delegate as! AppDelegate let context = appDelegate.persistentContainer.viewContext let newItem = TodoItem(context: context) newItem.todoTaskName = todoTaskName newItem.personName = personName newItem.taskDeadline = taskDeadline do { try context.save() } catch { print(error.localizedDescription) } }既存のレコードを取得
func getAllPlannerDays() -> [TodoItem] { let appDelegate = UIApplication.shared.delegate as! AppDelegate let context = appDelegate.persistentContainer.viewContext let savedPlaceFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "TodoItem") do { let result = try context.fetch(savedPlaceFetch) if let convertedResult = result as? [TodoItem] { return convertedResult } } catch { return [] } return [] }変更
func modifyObject(_ object: TodoItem, newDeadline: Date) { object.taskDeadline = newDeadline //保存する let appDelegate = UIApplication.shared.delegate as! AppDelegate let context = appDelegate.persistentContainer.viewContext do { try context.save() } catch { print(error.localizedDescription) } }削除
func deleteObject(_ object: NSManagedObject) { let appDelegate = UIApplication.shared.delegate as! AppDelegate let context = appDelegate.persistentContainer.viewContext context.delete(object) //保存する do { try context.save() } catch { print(error.localizedDescription) } }プログラムから
TodoItem
をご自身のエンティティ名に置き換えてください。特定のレコードをクエリ中
すべての
TodoItem
に対しpersonName
をネコノヒー
としてクエリしましょうfunc getToDoItem(withPerson: String) -> [TodoItem] { let appDelegate = UIApplication.shared.delegate as! AppDelegate let context = appDelegate.persistentContainer.viewContext let savedPlaceFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "TodoItem") savedPlaceFetch.predicate = NSPredicate(format: "personName = %@", withPerson) do { let result = try context.fetch(savedPlaceFetch) return result as? [TodoItem] } catch { return [] } return [] } let allTodo = getToDoItem(withPerson: "ネコノヒー")ユーザーのコアデータをクラウドに同期する
すでにここにガイドを書いています:
https://qiita.com/MaShunzhe/items/2018455cab0803d78442
新しいデータベースバージョンの作成
データベースの新しいバージョン(プロパティの追加、変更、削除、または新しいエンティティの作成)を作成するたびに、データベースの新しいバージョンを作成する必要があります。
データベースモデルファイルを選択した状態で、「Editor エディター」をクリックし、「Add Model Version モデルバージョンの追加」をクリックします。
次に、ファイルナビゲーターで、データベースモデルファイルの左側にある三角形のアイコンをクリックします。
作成した新しいデータモデルバージョンをクリックします。
右側のパネルでファイルアイコンをクリックし、「Model Version モデルバージョン」を見つけて、作成した新しいモデルバージョンに切り替えます。
左側のパネルで、作成したバージョンに緑のチェックマークが付いていることを確認してください。
資源
KeyChain (キーホルダー)
主な用途
ユーザーの機密情報を保存する場合は、Key Chainに保存することをお勧めします。Keychainは、iOSがユーザーのウェブサイトのユーザー名とパスワードを保存する場所です。暗号化されており、ユーザーのパスコードまたはFace ID (Touch ID)でのみアクセスできます。
長所
- セキュリティー
- 保存された認証情報は、ユーザーのデバイス間で同期できます。
短所:
- ユーザーはMac OS KeychainアプリでKeychainに保存したデータを見ることができます
- 通常、開発者はkeychainに文字列を保存します。データを保存することはお勧めしません。
使い方
keychainにアクセスするのに必要なコードはいくつかありますが、私はkeychainに簡単にアクセスできるオープンソースのGithubリポジトリを見つけました。
https://github.com/evgenyneu/keychain-swift
資源
CloudKit
長所
- - データをプライベート・データベース、共有データベース、またはパブリック・データベース(すべてのユーザーが他のユーザーのレコードをフェッチできる)に保存できます。
- CloudKitはAppleの無料クラウドサービスです。
- CloudKitプライベート・データベースへの保存は、ユーザー自身のiCloudストレージスペースに対してカウントされます。
- CloudKitパブリック・データベースへの保存は、割り当て制限に対してカウントされます。
- ユーザーがアプリを削除してから再度インストールすると、CloudKitに保存されているデータをフェッチできます。
短所
- ユーザーはオンラインである必要があります。また、CloudKitデータベースの使用にはサイズ制限があります。
https://developer.apple.com/icloud/cloudkit/
実装
私は以前、「CloudKit」を使用してユーザーの情報を保存および取得する方法についてのガイドを紹介しました:
https://qiita.com/MaShunzhe/items/bc21527259c5da7f66ed
https://qiita.com/MaShunzhe/items/9291c86a3d0442ddf759
資源