- 投稿日:2020-10-22T23:30:54+09:00
iOSアプリ開発:タイマーアプリ(6.設定画面の作成)
記事
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、アラーム音のオン/オフやプログレスバーのオン/オフを含む設定画面の作成方法について掲載します。環境
- OS: macOS 10.15.7 (Catalina)
- エディタ: Xcode 12.1
- 言語: Swift
- 主な使用ライブラリ: SwiftUI
手順
- 設定項目をリストアップする
- SettingView を作成する
- SettingView にリストを作成する
- SettingView のリストに設定項目を追加する
- TimeManager に各種設定に必要なプロパティを追加する
- SettingView の設定項目を TimeManager のプロパティと関連づける
- SettingView をモーダルにする
- MainView に設定ボタンを追加し、設定画面をモーダルで表示する
手順詳細
1. 設定項目をリストアップする
設定画面を作成していきます。今まで作成してきたPickerViewやTimerView、ButtonsViewはすべてMainViewに配置され、タイマーアプリにはMainView以外の画面がない状態でした。
今回はMainViewとは別に、設定画面を1つ作成していきます。
この設定画面には以下の項目を表示することにします。
- アラーム音を鳴らすかどうかのトグルスイッチ
- バイブレーションを有効にするかどうかのトグルスイッチ
- アラーム音をリストから選択
- プログレスバーを表示するかどうかのトグルスイッチ
- エフェクトアニメーションを表示するかどうかのトグルスイッチ
- 設定画面を閉じるボタン
2. SettingViewを作成する
SwiftUIテンプレートから、SettingViewという名前で新しくファイルを作成します。
ここでも、最終的にTimeManagerクラスのプロパティに設定情報を反映することになるので、先に@EnvironmentObject プロパティラッパーをつけて、TimeManagerのインスタンスを作成します。
SettingView.swiftimport SwiftUI struct SettingView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { Text("Hello, World!") } }3. SettingViewにリストを作成する
SwiftUIで、主にリスト表示に使われるコンポーネントは2種類あります。1つはList、もう一つはFormです。
一般的なiOSアプリの設定画面はFormが用いられていますので、今回はFormを利用します。
ちなみに、Listはいくつかのセクションをタイトルをつけて区切っても、そのセクション区切りを含めてすべて罫線で仕切られた表示になるため、どちらかというとリマインダーなどのアイテムをずらりと並べるのに向いています。
アラーム音の選択の項目については、設定画面からさらにサウンド選択画面に遷移させたいので、Form コンポーネントを NavigationView の中に入れます。こうすることで、Form 内からさらに別の List や Form へ遷移するような構成にすることができます。
SettingView.swiftstruct SettingView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { NavigationView { Form { } } } }4. SettingViewのリストに設定項目を追加する
画面構成としては、以下のように想定しています。
セクション1:アラーム関連
- アラーム音オン/オフのトグルスイッチ
- バイブレーションオン/オフのトグルスイッチ
- アラーム音選択セクション2:アニメーション関連
- プログレスバー表示オン/オフのトグルスイッチ
- エフェクトアニメーションオン/オフのトグルスイッチ
※セクション2の実際の実装は設定画面作成ができてからやっていきます。セクション3:設定画面を閉じる
- 設定画面を閉じるボタンFormの中に、Sectionを3つ入れていきます。
1つ目のSectionには、Toggle2つとNavigationLinkを1つ入れます。
二つ目のSectionには、Toggleを1つ入れます。
三つ目のSectionには、Buttonを1つ入れます。Sectionの引数headerには、そのセクションのタイトルを Textで入れておくと何のセクションかわかりやすくなります。
SettingView.swiftstruct SettingView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { NavigationView { Form { Section(header: Text("Alarm:")) { Toggle(isOn: ) { } Toggle(isOn: ) { } NavigationLink(destination: )) { } } Section(header: Text("Animation:")) { Toggle(isOn: ) { } Toggle(isOn: ) { } } Section(header: Text("Save:")) { Button(action: ) { } } } } } }さて、ここでToggleコンポーネントのオン/オフを反映するためのプロパティが必要になります。そのプロパティは、設定画面からMainViewの各コンポーネントに反映される必要があります。例えば、設定画面でアラームをオンにしたら、その設定が実際に発動するのはMainViewのほうです。
ですので、各設定情報を格納するプロパティは、TimeManagerクラスに@Publishedのプロパティラッパーをつけて追加する必要があります。
5. TimeManagerに各種設定に必要なプロパティを追加する
TimeManagerにトグルスイッチの設定情報を格納するためのプロパティを4つ追加します。追加するのは以下の4つになります。
- アラーム音オン/オフの設定
- バイブレーションオン/オフの設定
- プログレスバー表示オン/オフの設定
- エフェクトアニメーション表示オン/オフの設定
TimeManager.swiftclass TimeManager: ObservableObject { //(他のプロパティ省略) //soundIDプロパティの値に対応するサウンド名を格納 @Published var soundName: String = "Beat" //アラーム音オン/オフの設定 @Published var isAlarmOn: Bool = true //バイブレーションオン/オフの設定 @Published var isVibrationOn: Bool = true //プログレスバー表示オン/オフの設定 @Published var isProgressBarOn: Bool = true //エフェクトアニメーション表示オン/オフの設定 @Published var isEffectAnimationOn: Bool = true //1秒ごとに発動するTimerクラスのpublishメソッド var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() //(メソッド省略) }6. SettingViewの設定項目をTimeManagerのプロパティと関連づける
ToggleメソッドのisOn引数に設定を格納したいTimeManagerのプロパティを指定します。このとき$記号を頭につける必要があります。また、クロージャ内にはForm内の設定項目として表示したい名前をTextで入れます。
Toggle(isOn: $timeManager.isAlarmOn) { Text("Alarm Sound") }このようにして、ひとまずすべての Toggle の引数、クロージャを記述していきます。
アラーム音選択の項目については、選択画面がまだないので、いったんコメントアウトしておきます。
最後の保存ボタンのラベルは、テキストとチェックマークアイコンで用意します。水平方向の中央に配置したいので、HStackで囲み、左右からSpacer()を配置することで中央にもってきます。ボタンをタップしたときのアクションはまだ空白です。
SettingView.swiftstruct SettingView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { NavigationView { Form { Section(header: Text("Alarm:")) { Toggle(isOn: $timeManager.isAlarmOn) { Text("Alarm Sound") } Toggle(isOn: $timeManager.isVibrationOn) { Text("Vibration") } // NavigationLink(destination: ) { // // } } Section(header: Text("Animation:")) { Toggle(isOn: $timeManager.isProgressBarOn) { Text("Progress Bar") } Toggle(isOn: $timeManager.isEffectAnimationOn) { Text("Effect Animation") } } Section(header: Text("Save:")) { Button(action: ) { HStack { Spacer() Text("Done") Image(systemName: "checkmark.circle") Spacer() } } } } } } }7. SettingViewをモーダルにする
SettingViewをモーダルで表示させます。モーダルというのは、モーダルウインドウを縮めた名前で、そのウインドウが開いている間は他の操作が不可になるようなものです。
SettingViewに、@Environment(.presentationMode) というプロパティラッパーをつけた変数を用意します。
次に設定画面を閉じるときにタップする Save ボタンの action引数のクロージャ内にモーダルを閉じるためのコードを記述します。
self.presentationMode.wrappedValue.dismiss()
SettingView.swiftstruct SettingView: View { //モーダルシートを利用するためのプロパティ @Environment(\.presentationMode) var presentationMode @EnvironmentObject var timeManager: TimeManager var body: some View { NavigationView { Form { //(他のSection省略) Section(header: Text("Save:")) { Button(action: { //タップしたらモーダルを閉じる self.presentationMode.wrappedValue.dismiss() }) { HStack { Spacer() Text("Done") Image(systemName: "checkmark.circle") Spacer() } } } } } } }今度は反対に設定画面を開くためのボタンを MainView に用意していきます。ボタン一つですが、わかりやすく View を作成します。では、新規にSettingButtonという名前の新規SwiftUIファイルを作成します。
SettingButtonView.swiftimport SwiftUI struct SettingButtonView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { } }ボタンアイコンは SF Symbols から "ellipsis.circle.fill" を使います。
SettingButtonView.swiftimport SwiftUI struct SettingButtonView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { Image(systemName: "ellipsis.circle.fill") } }設定ボタンは、スタート/一時停止ボタンやリセットボタンより少し小さめのサイズにするので、frame()モディファイアで縦、横のサイズを入れておきます。
SettingButtonView.swiftimport SwiftUI struct SettingButtonView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { Image(systemName: "ellipsis.circle.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) } }ここで、モーダルを表示/非表示を示す、Bool型のプロパティを TimeManagerクラスに作成しておきます。名前は isSetting としておきます。
TimeManager.swiftclass TimeManager: ObservableObject { //(他のプロパティ省略) //設定画面の表示/非表示 @Published var isSetting: Bool = false //(メソッド省略) }そして最後にSettingButtonViewに戻り、.onTapGestureを追加して、クロージャ{}内に TimeManagerの isSetting プロパティが trueになるようにします。
SettingButtonView.swiftimport SwiftUI struct SettingButtonView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { Image(systemName: "ellipsis.circle.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) .onTapGesture { self.timeManager.isSetting = true } }8. MainViewに設定ボタンを追加し、設定画面をモーダルで表示する
それでは ManiView に SettingButtonView を追加していきます。
追加する箇所は、PickerViewやTimerViewより下の、スタート/一時停止、リセットボタンと同じ画面下端です。そのため、ZStack{} で ButtonsView と SettingButtonView をレイヤー状に前後に重ねます。どちらが前、後ろでも構いません。
垂直方向の位置が全てのボタンで揃ったほうが美しいので、SettingBottonView も ButtonsView 同様、padding(.bottom) モディファイアを追加します。
そして、モーダルウインドウを表示するための .sheet() モディファイアを追加します。 isPresented 引数には、true のときにモーダルが表示される Bool 型プロパティを指定します。つまり、先に用意しておいた TimeManager クラスの isSetting プロパティです。
クロージャ {} 内にはモーダルで表示したい View を記述します。ここでは SettingView() です。このとき必ずプロパティの .environmentObject(self.timeManager) も記述しておきます。
MainView.swiftstruct MainView: View { @EnvironmentObject var timeManager: TimeManager var body: some View { ZStack { if timeManager.timerStatus == .stopped { PickerView() } else { TimerView() } VStack { Spacer() //ButtonsViewとSettingButtonViewをレイヤー状に重ねる ZStack { ButtonsView() .padding(.bottom) //設定ボタンを追加 SettingButtonView() .padding(.bottom) //isSettingプロパティがtrueになったらSettingViewをモーダルで表示 .sheet(isPresented: $timeManager.isSetting) { SettingView() .environmentObject(self.timeManager) } } } } //(.osReceiveモディファイア部分省略) } }今回は設定ボタンだけの View を SettingButtonView として作成しましたが、先に作成した ButtonsView にまとめてしまっても良いかと思います。個人的には、iPhoneの画面を横向きにしたときのレイアウトの調整などで柔軟にアレンジできるのが理想です。
- 投稿日:2020-10-22T22:36:04+09:00
[Swift]PromiseKit を利用した非同期処理
書くこと
Promise を利用した非同期処理
※忘れた時に確認するための備忘録開発環境
PC MacBook Air(13-inch,2017) PC OS macOS Catalina(ver 10.15.6) IDE Xcode(ver 12.0.1) iPhone SE(2nd Generation) iPhone OS ver 14.0.1 Swift 5.3 前提条件
① MacBook Air と iPhone は USBケーブルで接続する
② Xcode を利用してデスクトップ上にSample
というプロジェクトアプリを作成する
③Sample
は既にCocoaPods
利用してPromiseKit
を導入している状態である
④ 今回は、Sample
にあるViewController.swift
にコードを記述する基本的なコード
ViewController.swiftimport UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { let promise = Promise<String> { value in // 処理 }.done { value in // 成功した時に実行 }.catch { error in // 失敗した時に実行 }.finally { // 最後に実行 } } }コード例①
ViewController.swiftimport UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { print("----- promise ここから -----") let promise = Promise<String> { value in // 処理 DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { value.fulfill("sample1") print("\(Date()) [1]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { value.fulfill("sample2") print("\(Date()) [2]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { value.fulfill("sample3") print("\(Date()) [3]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { value.fulfill("sample4") print("\(Date()) [4]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { value.fulfill("sample5") print("\(Date()) [5]:\(value)") } print("\(Date()) [6]:\(value)") }.done { value in // 成功した時に実行 print("\(Date()) [7]:\(value)") }.catch { error in // 失敗した時に実行 print("\(Date()) [8]:\(error.localizedDescription)") }.finally { // 最後に実行 print("\(Date()) [9]:処理を終了します") } print("----- promise ここまで -----") } }コード例① 実行結果
Xcodeログ----- promise ここから ----- 2020-10-22 13:16:28 +0000 [6]:PromiseKit.Resolver<Swift.String> ----- promise ここまで ----- 2020-10-22 13:16:29 +0000 [5]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:16:29 +0000 [7]:sample5 2020-10-22 13:16:29 +0000 [9]:処理を終了します 2020-10-22 13:16:30 +0000 [4]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:16:31 +0000 [3]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:16:32 +0000 [2]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:16:33 +0000 [1]:PromiseKit.Resolver<Swift.String>コード例②
ViewController.swiftimport UIKit import PromiseKit enum SampleError: Error { case error1 case error2 case error3 } class ViewController: UIViewController { override func viewDidLoad() { print("----- promise ここから -----") let promise = Promise<String> { value in // 処理 value.reject(SampleError.error1) DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { value.fulfill("sample1") print("\(Date()) [1]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { value.fulfill("sample2") print("\(Date()) [2]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { value.fulfill("sample3") print("\(Date()) [3]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { value.fulfill("sample4") print("\(Date()) [4]:\(value)") } DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { value.fulfill("sample5") print("\(Date()) [5]:\(value)") } print("\(Date()) [6]:\(value)") }.done { value in // 成功した時に実行 print("\(Date()) [7]:\(value)") }.catch { error in // 失敗した時に実行 print("\(Date()) [8]:\(error.localizedDescription)") }.finally { // 最後に実行 print("\(Date()) [9]:処理を終了します") } print("----- promise ここまで -----") } }コード例② 実行結果
Xcodeログ----- promise ここから ----- 2020-10-22 13:26:04 +0000 [6]:PromiseKit.Resolver<Swift.String> ----- promise ここまで ----- 2020-10-22 13:26:04 +0000 [8]:The operation couldn’t be completed. (Sample.SampleError error 0.) 2020-10-22 13:26:04 +0000 [9]:処理を終了します 2020-10-22 13:26:05 +0000 [5]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:26:06 +0000 [4]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:26:07 +0000 [3]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:26:08 +0000 [2]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:26:09 +0000 [1]:PromiseKit.Resolver<Swift.String>コード例③
ViewController.swiftimport UIKit import PromiseKit enum SampleError: Error { case error1 case error2 case error3 } class ViewController: UIViewController { override func viewDidLoad() { print("----- promise ここから -----") let promise = Promise<String> { value in // 処理 value.fulfill("sample1") print("\(Date()) [1]:\(value)") value.fulfill("sample2") print("\(Date()) [2]:\(value)") value.fulfill("sample3") print("\(Date()) [3]:\(value)") value.fulfill("sample4") print("\(Date()) [4]:\(value)") value.fulfill("sample5") print("\(Date()) [5]:\(value)") print("\(Date()) [6]:\(value)") }.done { value in // 成功した時に実行 print("\(Date()) [7]:\(value)") }.catch { error in // 失敗した時に実行 print("\(Date()) [8]:\(error.localizedDescription)") }.finally { // 最後に実行 print("\(Date()) [9]:処理を終了します") } print("----- promise ここまで -----") } }コード例③ 実行結果
Xcodeログ----- promise ここから ----- 2020-10-22 13:29:03 +0000 [1]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:29:03 +0000 [2]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:29:03 +0000 [3]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:29:03 +0000 [4]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:29:03 +0000 [5]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:29:03 +0000 [6]:PromiseKit.Resolver<Swift.String> ----- promise ここまで ----- 2020-10-22 13:29:03 +0000 [7]:sample1 2020-10-22 13:29:03 +0000 [9]:処理を終了しますコード例④
ViewController.swiftimport UIKit import PromiseKit enum SampleError: Error { case error1 case error2 case error3 } class ViewController: UIViewController { override func viewDidLoad() { print("----- promise ここから -----") let promise = Promise<String> { value in // 処理 value.reject(SampleError.error1) value.fulfill("sample1") print("\(Date()) [1]:\(value)") value.fulfill("sample2") print("\(Date()) [2]:\(value)") value.fulfill("sample3") print("\(Date()) [3]:\(value)") value.fulfill("sample4") print("\(Date()) [4]:\(value)") value.fulfill("sample5") print("\(Date()) [5]:\(value)") print("\(Date()) [6]:\(value)") }.done { value in // 成功した時に実行 print("\(Date()) [7]:\(value)") }.catch { error in // 失敗した時に実行 print("\(Date()) [8]:\(error.localizedDescription)") }.finally { // 最後に実行 print("\(Date()) [9]:処理を終了します") } print("----- promise ここまで -----") } }コード例④ 出力結果
Xcodeログ----- promise ここから ----- 2020-10-22 13:33:08 +0000 [1]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:33:08 +0000 [2]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:33:08 +0000 [3]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:33:08 +0000 [4]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:33:08 +0000 [5]:PromiseKit.Resolver<Swift.String> 2020-10-22 13:33:08 +0000 [6]:PromiseKit.Resolver<Swift.String> ----- promise ここまで ----- 2020-10-22 13:33:08 +0000 [8]:The operation couldn’t be completed. (Sample.SampleError error 0.) 2020-10-22 13:33:08 +0000 [9]:処理を終了します
- 投稿日:2020-10-22T22:27:14+09:00
WebView内のテキストフィールドタップ時のキーボード設定
WebView内のテキストフィールドを選択した時に、表示されるキーボードのタイプは選択可能なのか調べました。今回は
SFSafariView
で試していますがWKWebView
でも同じはずです。可能です
htmlの
type
の指定によって表示されるキーボードの種類が変わります。// 普通のキーボード <input type="text" size="20" maxlength="30"> // PhonePad <input type="tel" size="20" maxlength="30">これ以外にもtypeに
url
や
- 投稿日:2020-10-22T22:27:14+09:00
WebView内のテキストフィールドタップ時のキーボード
WebView内のテキストフィールドを選択した時に、表示されるキーボードのタイプは何によって決まるのか調べました。今回は
SFSafariView
で試していますがWKWebView
でも同じはずです。結論
htmlの
type
の指定によって表示されるキーボードの種類が変わります。// 普通のキーボード <input type="text" size="20" maxlength="30"> // PhonePad <input type="tel" size="20" maxlength="30">これ以外にもtypeに
url
や
- 投稿日:2020-10-22T17:54:37+09:00
モバイル向けFlutterアプリをWeb対応するときに注意したいこと
Platform判定はできない
Platform.isIOSなどで判定しているところは動かないので、その前段階でkIsWebでWebかどうかを判定する
モバイル向けのsecure_strageのパッケージは非対応
アプリ内にログイントークンなど持ちたいからflutter_secure_storageプラグイン使うと思うけど、Webでは非対応なのでshared_preferencesプラグインを使う
APIRequestがエラーになる
CROSサポート(参考)を有効にする必要がある。
開発でテストするなら以下のChrome extensionをインストールしたら解決できる
https://chrome.google.com/webstore/detail/cors-unblock/lfhmikememgdcahcdlaciloancbhjino/related?hl=jaFileの扱いについて
そもそもパッケージが違うよ Webではファイルパスでファイルに直接アクセスできないからon memoryで扱う
dart:io
dart:htmlNetworkから画像が取得できる??
できるけど、AuthHeaderが効かないから画像が取得できない場合もある
https://github.com/flutter/flutter/issues/57187カメラ使える?
まだ非対応です。
https://github.com/flutter/flutter/issues/45297WebサーバーないけどWebで公開して確認したい!
Github Page(https://pages.github.com/)使えます。
プライベートリポジトリでは無料プランだと使えません。Bitbucketならプライベートリポジトリでもつかえます
https://support.atlassian.com/bitbucket-cloud/docs/publishing-a-website-on-bitbucket-cloud/
- 投稿日:2020-10-22T14:23:56+09:00
iOS14のSwiftUIでSKStoreReviewController実装メモ
環境
- Xcode: 12.0
- Swift5
実装例
import StoreKit if let windowScene = UIApplication.shared.windows.first?.windowScene { SKStoreReviewController.requestReview(in: windowScene) }SwiftUIでもこれだけでOK
この書き方はiOS14.0からですので注意してください。
https://developer.apple.com/documentation/storekit/skstorereviewcontroller/3566727-requestreview備考
アプリ公開しました!よろしければインストールお願いします。
とらんぽTwitter始めました!よろしければフォローお願いします。
@yajima_tohshu
- 投稿日:2020-10-22T13:14:13+09:00
MonacaでiOSデバッグビルドを実行するまで
Monacaを使えばMacがなくてもiOSアプリのビルドが実行できますが、Apple Developer Programへのアカウント登録と、アカウントを利用して証明書やプロビジョニング・プロファイルを発行する必要があります。
この辺がガチのアプリ開発初心者だと公式マニュアル見ただけだと難しかったので手順を残しました。
Apple Developer Programへの登録
事前にApple Developer Programに登録するか、会社やチームでやってる場合ならそのアカウントに招待してもらうか、証明書とプロビジョニング・プロファイルの発行までやってもらう必要があると思います。
秘密鍵とCSRの生成
Monaca IDEの「iOSビルド設定」から「秘密鍵とCSRの生成」を行います。
Apple Developer Programに登録しているApple IDアカウントの名前とメールアドレスと設定します。
これでCSR(certificate signing request:証明書署名要求)が作成され、ダウンロードできるようになります。
これは証明書ではなく、Appleに証明書を要求するための情報になります。CSRをApple Developer Programに登録してCER証明書を発行
Apple Developer Programから「Certificates, IDs & Profiles」を選択、プラスマークを押してCertificatesを追加します。
デバックビルドなので「iOS App Development」を選択
なんでこんな手順を踏むのかというと、Appleアカウントとビルドに使われたMac(Monacaの場合ビルドサーバのMac)を紐づけるために必要みたいです。
App IDsを登録
アプリの識別子になるBundle IDを決定します。
Apple Developer Programから「Certificates, IDs & Profiles」を選択、プラスマークを押してIdentifiersを追加します。登録したBundle IDをApp IDとしてMonaca IDEの「iOSアプリ設定」で設定します。
UDIDを登録
UDID(Unique Device IDentifier)、iOS端末に割り当てられた固有IDをApple Developer Programに登録します。
デバッグビルドしたアプリはここで登録した端末にしかインストールできません。
プロビジョニング・プロファイルの作成
プロビジョニング・プロファイル(Provisioning Profile)はこれまで作った「CER証明書」と「Bundle ID」、「UDID」の紐づけになるものです。
ビルド・インストール
CER証明書とプロビジョニング・プロファイルをMonacaにアップロードします。
「証明書のアップロード」⇒「プロファイルのアップロード」の順でアップロードします。
成功すれば証明書がMonacaに登録されます。いよいよビルドです。
設定した証明書を選択してビルドを開始します。
しばらく待ってアプリのビルドエラーがなければ、ビルド結果が表示されます。
以上でデバッグビルド完了です。
IPAファイルのダウンロードと、UDIDを登録した端末ならQRコードからアプリがインストールできます。参考
- 投稿日:2020-10-22T11:36:38+09:00
fastlaneでiOSアプリがアップロードできなくなった(2020/10,version 2.150.0)
fastlaneでiOSアプリがアップロードできなくなりました。
結論からいうと、2.160.0
以上のバージョンにアップデートすると解消されました。現象
fastlaneでiOSアプリをアップロードしようとすると、途中で処理が止まってタイムアウトになる。
[11:06:42]: Loading './fastlane/metadata/zh-Hans/release_notes.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hans/support_url.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hans/marketing_url.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hans/promotional_text.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hans/name.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hans/subtitle.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hans/privacy_url.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/description.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/keywords.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/release_notes.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/support_url.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/marketing_url.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/promotional_text.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/name.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/subtitle.txt'... [11:06:42]: Loading './fastlane/metadata/zh-Hant/privacy_url.txt'... [11:06:42]: Loading './fastlane/metadata/copyright.txt'... [11:06:42]: Loading './fastlane/metadata/primary_category.txt'... [11:06:42]: Loading './fastlane/metadata/secondary_category.txt'... [11:06:42]: Loading './fastlane/metadata/primary_first_sub_category.txt'... [11:06:42]: Loading './fastlane/metadata/primary_second_sub_category.txt'... [11:06:42]: Loading './fastlane/metadata/secondary_first_sub_category.txt'... [11:06:42]: Loading './fastlane/metadata/secondary_second_sub_category.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/first_name.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/last_name.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/phone_number.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/email_address.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/demo_user.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/demo_password.txt'... [11:06:42]: Loading './fastlane/metadata/review_information/notes.txt'...
必ずここで処理が止まりタイムアウトになります。
解決
fastlaneのバージョンを2.160.0以上にあげることで解決します。
修正PRはこちらになります
- 投稿日:2020-10-22T09:46:05+09:00
[Swift5]ライブラリ 'SwiftyJSON' を使ってJSONから配列(個別)の情報を取得する
SwiftyJSONとは
JSONを簡単に扱うためのライブラリです。JSONを解析する場合などに、ライブラリを用いることでよりシンプルなコードで実装できます。
ドキュメントを添付していますので参考にしててください。
https://github.com/SwiftyJSON/SwiftyJSON投稿のポイント
今回は、前回投稿した記事[Swift5]"IBM Watson ToneAnalyzer"で取得した分析結果をJSONに変換するで取得したJSONを参考に解析をおこないと思います。まだ前回記事を見ていないという方は先にそちらをご覧ください。
▼前回記事
https://qiita.com/nkekisasa222/items/3304cb77d78d9adfe8acライブラリのインストール
まず、
Podfile
にpodをインストールします。podfile.# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'アプリ名' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for アプリ名 pod 'SwiftyJSON' end次にターミナルで
$ pod install
を実行し、アプリケーションのSwiftyJSONを使いたいControllerにインポート
します。ViewController.swiftimport UIKit import SwiftyJSON //ここを記述今回取得する値の確認
前回記事で分析結果をJSON形式に変換しました。下記がその内容です。
JSON.JSON: { "sentences_tone" : [ { "sentence_id" : 0, "text" : "Team, I know that times are tough!", "tones" : [ { "score" : 0.80182699999999996, "tone_id" : "analytical", "tone_name" : "Analytical" } ] }, { "sentence_id" : 1, "text" : "Product sales have been disappointing for the past three quarters.", "tones" : [ { "score" : 0.77124099999999995, "tone_id" : "sadness", "tone_name" : "Sadness" }, { "score" : 0.68776800000000005, "tone_id" : "analytical", "tone_name" : "Analytical" } ] }, { "sentence_id" : 2, "text" : "We have a competitive product, but we need to do a better job of selling it!", "tones" : [ { "score" : 0.50676299999999996, "tone_id" : "analytical", "tone_name" : "Analytical" } ] } ], "document_tone" : { "tones" : [ { "score" : 0.61650000000000005, "tone_id" : "sadness", "tone_name" : "Sadness" }, { "score" : 0.82988799999999996, "tone_id" : "analytical", "tone_name" : "Analytical" } ] } }今回は
"document_tone"
の配列"tones"
の中にある"score"
と、"tone_name"
の値を取得したいと思います。JSON解析の実行
ViewController.swiftlet jsonValue = JSON(json) let tonesScore = jsonValue["document_tone"]["tones"][self.count]["score"].floatまず、感情分析結果を代入している
値json
をJSON
としてjsonValue
を作成します。(ほんまやいこしい言い方ですいません笑)次に解析をおこないます。
jsonValue["document_tone"]["tones"][self.count]["score"].float
このように指定することで値を取得でします。ほんとシンプルでかわかりやすいですよね?注意点として
①分析結果である
jsonValue
の配列の構造を理解していないと取得できない。
②今回使用しているself.count
はメンバ変数count
を定義しているとする。なお、self.
はクロージャー内なので記述している。これで
score
の取得はできているはずです。
ただし、このままではscoreの値が冗長なので、小数点を切り上げて取得します。ViewController.swiftlet jsonValue = JSON(json) let tonesScore = jsonValue["document_tone"]["tones"][self.count]["score"].float //tonesScoreの小数点を切り上げて取得 let decimal = tonesScore let decimalPoint = ceil(decimal! * 100)/100 let tone_score = decimalPoint続いて
tone_name
を取得します。ViewController.swiftlet tonesName = jsonValue["document_tone"]["tones"][self.count]["tone_name"].string let tone_name = tonesNameこれで、
"document_tone"
の配列"tones"
の中にある"score"
と、"tone_name"
を取得できたと思いますのでprint()
で確認してみましょう。ViewController.swiftprint("=====個別取得の確認=====") print("document_tone.score : \(tone_score)") print("document_tone.tone_name: \(tone_name)")結果...
=====個別取得の確認===== document_tone.score : 0.62 document_tone.tone_name: Optional("Sadness")しっかりと小数点も切り上げて取得できていますね。ちなみに
tone_name
の"Sadness"
は悲しみ
という意味なので感情分析もおこなえているのがわかります。成功です!
最後に
今回は、JSONを扱いやすくするライブラリ
SwiftyJSON
について投稿しました。
swiftを学んでいるとJSONと触れ合う機会が多いなーと最近は思います。
そういう意味でもSwiftyJSONはシンプルに記述できるので必須のライブラリですね。是非、参考にしてください!
最後までご覧いただいてありがとうございました!
- 投稿日:2020-10-22T09:09:11+09:00
FlutterでPDFを取得したい君に
始めに
Flutterで個人開発のアプリを作っているのですが、その機能の中にPDFをアップロードするという機能がありまして、そのためにどうやってFlutterからPDFを取得できるのかを調べたので、メモがわりに書きました。
ありがたいことにfile_pickerのというパッケージを使うとめっちゃ簡単に実装することができました
環境
Dart:
2.9.1
Flutter:1.20.2
file_picker:2.0.9+1
実装
Future<File> getPdfFile() async { FilePickerResult result = await FilePicker.platform.pickFiles(); if (result != null) { File file = File(result.files.single.path); return file; } else { throw Exception('can not pick file'); } }実装自体はパッケージから関数を呼ぶだけなのですが、今回はPDFだけを取得したいので引数に色々渡す必要があります
それらを渡したのが次のコード
Future<File> getPdfFile() async { FilePickerResult result = await FilePicker.platform.pickFiles( type: FileType.custom, // ←引数を追加 allowedExtensions: ['pdf'], // ←引数を追加 ); if (result != null) { File file = File(result.files.single.path); return file; } else { throw Exception('can not pick file'); } }引数の
type
でファイルのタイプでcustom
を指定する
第二引数のallowedExtensions
で選択したいファイルの拡張子を配列で渡す(今回の場合は.pdfのみだが、.pngや.docを追加できたりもする)そうすることによってPDFのファイルが取得できるので、後は好きな形に変形して終わりです
iOSの設定
今回はiOSの方でしか動作確認をしてないので、Androidでは動かない可能性があります。。。
XcodeでCapabilityを追加
XcodeでTARGETSの「Signing & Capability」の左上のCapabilityのプラスボタンを選択。
そこから「iCloud」を追加TARGETSの「Signing & Capability」に「iCloud」の項目が追加されていることを確認します。
iCloud項目の中のServices項目で
- Key-value storage
- iCloud Documents
- CloudKitとある中で「iCloud Documents」だけにチェックを入れる。
Services項目の下にあるContainers項目に自分のアプリのBundle IDのContainerを作成
ここら辺の設定はこちらの記事を参考にさせていただきました
- 投稿日:2020-10-22T06:08:43+09:00
SwiftUI プログラムへの Core Data データベース機能の追加
本記事では、
- Core Data データベースを既存の SwiftUI アプリケーションに追加する方法を紹介します。
NSMergePolicy
を使用したコアデータデータベースのマージ競合の管理方法についても説明します。Core Dataモデルファイルとクラス定義の作成
モデルファイルとクラス定義用のコードファイルの作成の方法はこちらの通りです。これに関しては過去記事の1つで紹介済みです。
詳細に関してはこちらをクリックしてください
最初に、データモデルファイルを作成します。キーボードのcommand-Nキーを押して、
Data Model
を検索します。新しい
Data Model
にあなたのアプリの名前をつけてください。例えば、ここでは私のアプリの名前はCoreDataDemo
なので、私はCoreDataDemo.xcdatamodeld
を作りました。データベース構造の作成
To-Doアイテムごとに次のプロパティを格納するTo-Doアプリを作成するとします:
- To-Doタスク名
- タスクの担当者
- タスクの期限
1.
.xcdatamodeld
ファイルを開き、Add Entity
をクリックします。作成した新規エンティティ・アイテムに
TodoItem
という名前を付けます。作成した新しいアイテムをダブルクリックし、名前を付けるだけです。この例では、
TodoItem
という名前を付けます。新しいプロパティを追加する
Properties
セクションのプラスアイコンをクリックして、新しいプロパティを追加します。ここでは、To-Doアプリに次のプロパティが必要です。
プロパティ名 タイプ todoTaskName String personName String taskDueTime Date タイプを設定するには、プロパティ名の横にあるドロップダウンメニューをクリックします:
このような感じになります。
Entity クラスのコードファイルを生成
- 作成した新しいエンティティ項目を選択します。右側のパネルで最後のセクションのアイコンを選択します :
Codegen
設定をManual/ None
に変更以前の設定:
新しい設定:
- クラス定義ファイルを生成
最上部のメニューで
Editor
をクリックし、Create NSManagedObject subclass...
をクリックしますこうしたサブクラスを生成することで、
TodoItem
を新規オブジェクトの作成に直接使用できるようになります。ファイルナビゲーターに以下のファイルが表示されます :
データベースマネージャーを作成する
現在、コアデータベースに関連する関数を含む新規ファイルを作成できるようになっています。
このファイルで、
NSPersistentCloudKitContainer
をご利用のデータベース名で初期化することになります。import CoreData struct PersistenceController { static let shared = PersistenceController() let container: NSPersistentCloudKitContainer init() { container = NSPersistentCloudKitContainer(name: "CoreDataDemo") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { print(error.localizedDescription) } }) } }その後、メインのSwiftUIアプリファイル(
@main
というコードから始まるファイル)で、PersistenceController
の共有インスタンスを初期化できます。@main struct SimpleTODOApp: App { let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { ContentView() } } }
SceneDelegate
を使っている場合には、SceneDelegate.swift
ファイルにおいて、コードをscene
関数に追加してください。SwiftUI
とCoreData
フレームワークのインポートをお忘れなく。func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let context = PersistenceController.shared.container.viewContext let contentView = ContentView() if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } }
.environment
を追加これで、
PersistenceController.shared
がすでに初期化されているので、ルートSwiftUIビューの環境変数を設定する必要があります。まず、コンテンツビューを初期化するファイルにこの行を追加します。
ContentView() .environment(\.managedObjectContext, persistenceController.container.viewContext)そして、この変数をコンテンツビュー
ContentView
の最上部に追加します:@Environment(\.managedObjectContext) private var viewContextこれで、コンテンツビューがデータベースにアクセスできるようになります。
FetchRequestを追加する
データベースからコンテンツをフェッチするには、
FetchRequest
を作成します。@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \TodoItem.taskDueTime, ascending: true)], animation: .default) private var items: FetchedResults<TodoItem>上記のコード例では、
TodoItem.dueTime
に基づいて項目をソートしています。それを自分のプロパティ名に変更する必要があります。
keyPath
変数のフォーマットを[データベースエンティティの名前].[選択したエンティティの変数名]にする必要があります。これで、アイテムをテーブルに表示できます。データベース内の項目が変更されると、テーブルは自動的に更新されます。
その他のSwiftUIビューの環境の準備
初期化する各ビューに対してこのステップを繰り返して環境を準備することが重要です。
例えば、
ToDoItemCreate
というビューがあるなら、ToDoItemCreate
ビューを初期化する際にはこの行を追加する必要があります。ToDoItemCreate(isShown: $showTodoCreateView) .environment(\.managedObjectContext, self.viewContext)また、変数
viewContext
をToDoItemCreate
ビューに追加する必要があります。@Environment(\.managedObjectContext) private var viewContextデータベースのコンフリクトへの対応
データベースが自身に変更を書き込もうとする際には、コンフリクトが発生することがあります。(例えばオブジェクトの重複など。)
デフォルトで、
Core Data
フレームワークは例外を発生させます。
viewContext
のmergePolicy
プロパティーを設定することで、コンフリクトの解消を試みることができます。self.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)以下は一部の結合ポリシーの説明です。
Name Explanation NSErrorMergePolicyType デフォルト。 try viewContext.save()
を呼び出すと、例外が報告されます。NSMergeByPropertyStoreTrumpMergePolicyType メモリ内の変更は外部変更を置き換えます NSMergeByPropertyObjectTrumpMergePolicyType 外部変更はメモリ内の変更を置き換えます。 NSOverwriteMergePolicyType メモリ内のオブジェクト全体が永続ストアにプッシュされます。 NSRollbackMergePolicyType 対立する変更されたオブジェクトについて、すべての状態が破棄されます 通常、
NSMergeByPropertyObjectTrumpMergePolicy
を使用すると、Core Data
はデータベースのデータバージョンを新しい変更で自動的に上書きします。