- 投稿日: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-22T22:07:26+09:00
ARKit+SceneKit+Metal で Webブラウジング
スマホやARグラスで仮想オブジェクトにUIKitを使いたいこともあるだろうと考え、UIView を SceneKit のシーン内に表示する方法を試してみた。
動作確認がしやすいのでUIViewとしてはWKWebView
を使用。
参考情報:
・こちらのSwiftUIでの動作のツイートシーン内でのUIViewの表示方法
①
UIView
をキャプチャしMTLTexutre
に変換
②①をSceneKitのSCNNode
のマテリアルに設定これだけ。それっぽく動いているが、いくつかハマったところを以下に記載。
どうやってWebViewをスクロールさせるか
仮想オブジェクトとなっているWebViewでスクロールやタップ処理をさせるには前面に
ARSCNView
がいる状態で、タッチイベントを任意のWebViewに渡す必要がある。
ここでtouchesBegan()
等のUIResponder
のイベントをARSCNView
で受け取って任意のWebViewのtouchesBegan()
等に流す方法を思いついたが、これだとスクロールもタップも全く動作せず。そもそもUIEventを渡したところでスクロールをさせたりボタンタップをさせる、といったUI部品固有のアクションをさせることはできるのか?という気がしてきた。この辺り知識不足。。。ご存知の方がいたら教えて欲しいです。
プログラムからスクロールさせる方法をググるとUIScrollView
のcontentOffset
で設定するやりかたはでてくるが、これだと心地よい慣性スクロールはできないはず。
で、結局、良い方法が見つからず次の方法で実現。・
ARSCNView
と同じサイズ同じ位置に WebView を配置
・ARSCNView
は前面にしておきisUserInteractionEnabled
は falseにする
・ヒットテストを行い、画面の中央にある WebView のisUserInteractionEnabled
をtrueにするこれで任意のWebViewのスクロールは可能になるが、この方法には次の欠点がある。
・スクロールさせる場合、WebView を任意のアスペクト比にできない。画面サイズに合わせる必要がある。
・タップされた位置をシーン内のWebViewの位置に補正できないので、ボタンクリック等のイベント処理はできない(使い物にならない)上記より、この方法が使えるケースが使えるのは、画面内の仮想オブジェクトにおいて、何かしらの情報を表示のみ、操作はスクロールのみ、というケースに限定されると思われる。
前述のツイートのようにSwiftUIでどのようにできるかまた試す予定。画面キャプチャが遅くてレンダリングが追いつかない
キャプチャのタイミングを
SCNSceneRendererDelegate
のrenderer(_:updateAtTime:)
としているが、メインスレッドで画面をキャプチャ&SceneKitに渡す必要があるため、このメソッドのなかでメインキューにキャプチャ処理をエンキューしている。画面キャプチャが十分に高速であればこれだけでも良いかもしれないが、ARでトラッキングをしながら3Dレンダリングしながらキャプチャをするという過酷な処理となっているため、急速にメインキューが膨らんで画面が固まるという現象に遭遇した。キャプチャが終わるまで次のキャプチャをしないように対策。func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // メインキューでの画面キャプチャが終わっていない場合はキャプチャしないようにする // キャプチャ処理は遅いのでこれがないとキューがはけず固まる guard !isCaptureWaiting else { return } isCaptureWaiting = trueソースコード全体
class ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet weak var scnView: ARSCNView! @IBOutlet weak var webView1: WKWebView! @IBOutlet weak var webView2: WKWebView! @IBOutlet weak var webView3: WKWebView! private var device = MTLCreateSystemDefaultDevice()! private var planeNode1: SCNNode! private var planeNode2: SCNNode! private var planeNode3: SCNNode! private var node1Texture: MTLTexture? private var node2Texture: MTLTexture? private var node3Texture: MTLTexture? private var viewWidth: Int = 0 private var viewHeight: Int = 0 private var isCaptureWaiting = false override func viewDidLoad() { super.viewDidLoad() scnView.scene = SCNScene(named: "art.scnassets/sample.scn")! planeNode1 = scnView.scene.rootNode.childNode(withName: "plane1", recursively: true) planeNode2 = scnView.scene.rootNode.childNode(withName: "plane2", recursively: true) planeNode3 = scnView.scene.rootNode.childNode(withName: "plane3", recursively: true) // UIイベントはいったん、受け付けないようにする scnView.isUserInteractionEnabled = false setUIEnable(webView1: false, webView2: false, webView3: false) // ARSCNViewは常に前面に表示 self.view.bringSubviewToFront(scnView) // AR Session 開始 self.scnView.delegate = self let configuration = ARWorldTrackingConfiguration() self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking]) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if node1Texture == nil || node2Texture == nil || node3Texture == nil { viewWidth = Int(view.bounds.width) viewHeight = Int(view.bounds.height) // テクスチャバッファを確保 // WebViewのサイズ = self.viewのサイズ としている let desc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: viewWidth, height: viewHeight, mipmapped: false) node1Texture = device.makeTexture(descriptor: desc)! node2Texture = device.makeTexture(descriptor: desc)! node3Texture = device.makeTexture(descriptor: desc)! // サイト読み込み webView1.load(URLRequest(url: URL(string:"https://qiita.com")!)) webView2.load(URLRequest(url: URL(string:"https://www.apple.com")!)) webView3.load(URLRequest(url: URL(string:"https://stackoverflow.com")!)) } } // 各WebViewの isUserInteractionEnabled 設定 func setUIEnable(webView1: Bool, webView2: Bool, webView3: Bool) { self.webView1.isUserInteractionEnabled = webView1 self.webView2.isUserInteractionEnabled = webView2 self.webView3.isUserInteractionEnabled = webView3 } func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { guard let _node1Texture = node1Texture, let _node2Texture = node2Texture, let _node3Texture = node3Texture else { return } // メインキューでの画面キャプチャが終わっていない場合はキャプチャしないようにする // キャプチャ処理は遅いのでこれがないとキューがはけず固まる guard !isCaptureWaiting else { return } isCaptureWaiting = true DispatchQueue.main.async { // ノードのヒットテストを行い画面中央にあるWebView(ノード)を見つける let bounds = self.scnView.bounds let screenCenter = CGPoint(x: bounds.midX, y: bounds.midY) let options: [SCNHitTestOption: Any] = [ .boundingBoxOnly: true, // boundingBoxでテスト .firstFoundOnly: true // いちばん手前のオブジェクトのみ返す ] if let hitResult = self.scnView.hitTest(screenCenter, options: options).first, let nodeName = hitResult.node.name { // 画面中央にあるノードに対応するWebViewの isUserInteractionEnabled を true にする switch nodeName { case "plane1": self.setUIEnable(webView1: true, webView2: false, webView3: false) case "plane2": self.setUIEnable(webView1: false, webView2: true, webView3: false) case "plane3": self.setUIEnable(webView1: false, webView2: false, webView3: true) default: self.setUIEnable(webView1: false, webView2: false, webView3: false) } } else { self.setUIEnable(webView1: false, webView2: false, webView3: false) } // 画面中央にあるノードに対応するWebViewのみキャプチャしてノードのマテリアルを更新 let setNodeMaterial: (UIView, SCNNode, MTLTexture) -> () = { captureView, node, texture in let material = SCNMaterial() material.diffuse.contents = captureView.takeTextureSnapshot(device: self.device, textureWidth: self.viewWidth, textureHeight: self.viewHeight, textureBuffer: texture) node.geometry?.materials = [material] } if self.webView1.isUserInteractionEnabled { setNodeMaterial(self.webView1, self.planeNode1, _node1Texture) } else if self.webView2.isUserInteractionEnabled { setNodeMaterial(self.webView2, self.planeNode2, _node2Texture) } else if self.webView3.isUserInteractionEnabled { setNodeMaterial(self.webView3, self.planeNode3, _node3Texture) } self.isCaptureWaiting = false } } } extension UIView { // 任意のUIViewの画面キャプチャをして MTLTexture に変換 // 参考URL : https://stackoverflow.com/questions/61724043/render-uiview-contents-into-mtltexture func takeTextureSnapshot(device: MTLDevice, textureWidth: Int, textureHeight: Int, textureBuffer: MTLTexture) -> MTLTexture? { guard let context = CGContext(data: nil, width: textureWidth, height: textureHeight, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil } // 上下反転の回避 context.translateBy(x: 0, y: CGFloat(textureHeight)) context.scaleBy(x: 1, y: -1) guard let data = context.data else { return nil } layer.render(in: context) textureBuffer.replace(region: MTLRegionMake2D(0, 0, textureWidth, textureHeight), mipmapLevel: 0, withBytes: data, bytesPerRow: context.bytesPerRow) return textureBuffer } }
- 投稿日:2020-10-22T21:43:03+09:00
CircleCI×fastlaneで証明書を更新したらExit status: 65エラーでこける。
経緯
今の現場では、developブランチにコミットされたタイミングでcircleCIが動き、Fastlaneが動き、deployGateにアップされるという仕組みになっているのですが、
証明書を更新したらなぜかfastlaneがExit status: 65
で落ちてしまいました。CircleCIにssh接続してログを見たところどうやら codesignで
errsecinternalcomponent
エラーで落ちているようでした。やったこと
circleCIに登録するp12ファイルに権限追加
キーチェーンアプリで対象の秘密鍵をダブルクリック→アクセス制御→常に許可するリストに /usr/bin/codesign と xcode を追加する。
参照:https://qiita.com/sekitaka_1214/items/61d68d603ee1c1b7adf1
キーチェーンをunlockする
CircleCIにssh接続して↓のコマンドを叩く。
security unlock-keychain login.keychain
この2つでは解決しませんでした。。
解決
いろいろ考えた結果、どうやら新しい証明書は今までの証明書と中間証明書(Apple Worldwide Developer Relations Certification)が変わっており、CircleCIが新しい中間証明書を読み込めなかったのが問題でした。
対応内容
1.中間証明書(.cer)をダウンロードします。
2.中間証明書をbase64エンコードし、CircleCIに環境変数として設定します。
base64 -i (中間証明書)| pbcopy
(今回は、APPLE_RELATION_CERTとして設定しました。)
3.CircleCI上で中間証明書をデコードします。.circleci/config.yml- run: name: Decode releation certificates command: base64 -D -o AppleWWDRCAG3.cer \<<< $APPLE_RELATION_CERT4.Fastlaneで中間証明書をキーチェーンに登録します。
fastlane/Fastlaneimport_certificate( keychain_name: ENV["MATCH_KEYCHAIN_NAME"], keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"], certificate_path: 'AppleWWDRCAG3.cer', certificate_password: '' )これでうまく行きました!
まとめ
このバグに丸1日はまりました。。
CircleCIかfastlaneが新しい中間証明書を読み込む方法を知ってる方がいたらぜひ教えていただきたいです(>_<)この対応すごくがんばったのでよかったらLGTMお願いしますᕦ(ò_óˇ)ᕤ
- 投稿日:2020-10-22T17:45:08+09:00
[Swift] string.data(using: .utf8)ってnilになるの?
結論
絶対になりません。
※ただしSwift 5以降に限る。説明
data(using:)
ってどんなメソッド?シグネチャ
StringProtocol
で規定されているdata(using:allowLossyConversion:)
というメソッドに由来します1。実際には、func data(using encoding: String.Encoding, allowLossyConversion: Bool = false) -> Data?というように
allowLossyConversion
にデフォルト引数が設定されているため、そちらは省略できます。即ち、string.data(using: .utf8)
はstring.data(using: .utf8, allowLossyConversion: false)
のことです。ちなみに
Data
はFoundation
で定義されている型なので、実際のコードにはimport Foundation
が必要です。返り値
上記で見た通り返り値の型は
Data?
とオプショナル型になっています。メソッドは、指定したエンコーディングでエンコードされたデータを返すことになりますが、変換できない文字が含まれているとnil
となります。
たとえば、最近流行の(?)絵文字"?"をUS-ASCIIにしようとするとnil
が返ってきます:import Foundation let string: String = "?" let data: Data? = string.data(using: .ascii) print(data.debugDescription) // -> nil
String
は内部でどのように文字列データを保持しているか?実はここが大事なところです。
Swift 4までは基本的にUTF-16でデータが保持されていました。これは、Swiftが当初からObjective-CのFoundationをそのまま活用しようとしていたことから来ています。歴史の長いNSString
は原則UTF-16で文字列データを保持していました2。当然NSString
とSwift.String
を"toll-free"で相互変換することを考えるとUTF-16を内部データ型に選択するのは当然だったでしょう。しかし、世の中どの程度UTF-16が使われているでしょうか?ほとんどUTF-8が使われているのではないでしょうか。W3Techs - World Wide Web Technology Surveysによれば、今やWebサイトのうち95%以上がUTF-8だそうです3。この統計はWebサイトだけですが、それ以外の分野でも似たようなものではないでしょうか。
そうなると、内部データをUTF-16で保持していても、実際にデータを読み込んだりデータとして出力したりする場合、UTF-8⇄UTF-16の変換コストを伴うことがほとんどということになります。
であれば、最初から内部データはUTF-8として保持しておくほうがいい、となるのは自然なことでしょう。というわけで、Swift 5からは
String
の内部データがUTF-8になったのでした。詳細は https://swift.org/blog/utf8-string/ に書かれています。内部のUTF-8についてはvar utf8: Self.UTF8View { get }
を通してアクセス(read-only)できます。UTF-8のデータをそのまま
Data
にできないか?できます。
UTF8View
はCollection
プロトコルに準拠しておりElement
はUInt8
なので、let data = Data(string.utf8)
とするだけで、UTF-8でエンコードされた文字列のデータを得られます。
ここで注目すべきはData(string.utf8)
は必ず成功するということです(∵failable initializerではない)。
つまり、次の例で言うとlet data_1: Data? = string.data(using: .utf8) let data_2: Data = Data(string.utf8)
data_1
はオプショナル型ですが、data_2
は必ずData
型です。
では、常にData(string.utf8)
を使えば良いのではないか、と思いませんか。正解です。
data(using:)
の実装を見てみようSwiftはオープンソースなので、どのように実装しているか見放題です。"NSStringAPI.swift"に実装を見ることができます。該当箇所4を引用してみます:
NSStringAPI.swiftpublic func data( using encoding: String.Encoding, allowLossyConversion: Bool = false ) -> Data? { switch encoding { case .utf8: return Data(self.utf8) default: return _ns.data( using: encoding.rawValue, allowLossyConversion: allowLossyConversion) } }なんと!
encoding
が.utf8
のときはData(self.utf8)
をそのまま返しています(ちなみに、Gitで変更履歴を辿ってもらえるとわかるかと思いますが、Swift 4以前はどんなencoding
であろうとNSString
にデータへの変換をお願いする形になっていました)。
これで答えが出ましたね。最後にもう一度結論
Q.
string.data(using: .utf8)
ってnil
になるの?
A. 絶対になりません。 ※ただしSwift 5以降に限る。おまけ(注意点)
var utf8: Self.UTF8View { get }
は内部にUTF-8として不正なバイト列を持ってしまっていても、そのままそのバイト列を返してきます。従って、Swift 4ではnil
になっていたstring.data(using: .utf8)
がSwift 5ではnil
にならないということも起き得ます。
https://developer.apple.com/documentation/swift/stringprotocol/3126754-data ↩
実際には
NSString
は抽象クラスとして定義され、NSConcreteString
などのサブクラスが文字列操作を担当するという、いわゆる「クラスクラスタ」を形成しています。 ↩2020年10月現在: https://w3techs.com/technologies/overview/character_encoding ↩
https://github.com/apple/swift/blob/a353176e1eb570a56809cf4202f5f30aa8905840/stdlib/public/Darwin/Foundation/NSStringAPI.swift#L791-L811 ↩
- 投稿日:2020-10-22T17:07:06+09:00
Firestoreでドキュメント(document)削除したけど、サブコレクション(subcollection)消えないって人へ
Firebase/Firestoreを使って、SNSの投稿機能を実装しようとしていました。
※違うところはご指摘いただけると幸いです。
ところが、投稿(posts/{postId})を削除したのにサブコレクション(comments/{comment}が消えないじゃないか...
documentとsubcollectionの表記が雑ですみません。
そこで色々調べてみましたが...
・CloudFuctionsを使って削除する
・別途commentsのdocumentを取ってきて削除するメソッド書く
という記事ばかり。CloudFuctionsは、あまり動作が重くなる等の理由で非推奨みたいです。
なのでTutorialを見たりしつつ熟慮の結果、
func delete(collection: CollectionReference, batchSize: Int = 100, completion: @escaping (Error?) -> ()) { collection.limit(to: batchSize).getDocuments { (docset, error) in guard let docset = docset else { completion(error) return } guard docset.count > 0 else { completion(nil) return } let batch = collection.firestore.batch() docset.documents.forEach {batch.deleteDocument($0.reference)} batch.commit { (batchError) in if let batchError = batchError { completion(batchError) } else { self.delete(collection: collection, batchSize: batchSize, completion: completion) } } } }この関数でうまく動きました。
completionの部分は、ご自身の作りたいものに合わせて適用させてください。自分のメモ用であり、誰か一人でもいいのでお役に立てれば思います。
- 投稿日: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-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-22T10:11:54+09:00
[Swift, ARKit]非推奨になったhitTestからraycastQueryに移行する
iOS14が正式にリリースされました。
これに伴いARKitも3.5から4.0に移行しましたが、その中でARSCNViewに存在していたopen func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]という関数が非推奨(deprecated)となりました。
この関数の使いどころとしては、ARSCNView内に設置した3Dオブジェクトを移動させる時に、指先でなぞった場所の位置を取得することだと思います。
例えば以下のようなコードです。@IBOutlet var scnView: ARSCNView! override func viewDidLoad() { super.viewDidLoad() let panGesture = UIPanGestureRecognizer(target: self, action: #selector(pan)) self.scnView.addGestureRecognizer(panGesture) } @objc func pan(_ sender: UIPanGestureRecognizer) { //こちらのhitTestは[SCNHitTestResult]を返すもので非推奨となった関数とは別もの guard let node = scnView.hitTest(sender.location(in: scnView), options: nil).first?.node else { return } //こちらが非推奨となった関数 guard let transform = scnView.hitTest(sender.location(in: scnView), types: .existingPlaneUsingExtent).first?.worldTransform else { return } node.setWorldTransform(SCNMatrix4(transform)) }コードによってはsenderのstateによってswitchしたり、guardではなくif letを使っていたりと多少の違いはあるかと思います。
これをraycastQueryに書き直すと以下のような形になります。
//変数宣言とviewDidLoad()は省略 @objc func pan(_ sender: UIPanGestureRecognizer) { guard let node = scnView.hitTest(sender.location(in: scnView), options: nil).first?.node else { return } //変更したところ guard let raycast = scnView.raycastQuery(from: sender.location(in: scnView), allowing: .estimatedPlane, alignment: .any), let result = scnView.session.raycast(raycast).first? else { return } node.setWorldTransform(SCNMatrix4(result.transform)) }ARSCNView本体から取得したARRaycastQueryを、今度はARSCNViewのsessionに送って、そこから[ARRaycastResult]を改めて取得しています。
ARSCNViewのタップ箇所をそのまま使うのではなく、間にsessionを挟んでいますね。ARRaycastResult自体は変更は無いので、hitTestで取得した時と同じ使い方で問題ありません。
次にraycastQueryの中身を確認していきます。
open func raycastQuery(from point: CGPoint, allowing target: ARRaycastQuery.Target, alignment: ARRaycastQuery.TargetAlignment) -> ARRaycastQuery?第一引数はそのままですね。
第二引数と第三引数はどちらもARRaycastQueryにあるenumです。第二引数のenumはこちら。
public enum Target : Int { case existingPlaneGeometry = 0 case existingPlaneInfinite = 1 case estimatedPlane = 2 }ポイントに存在するものが何なのかを判断するためのものです。
大きさの確定したPlaneが欲しい場合はexistingPlaneInfiniteを、大雑把でいいのならestimatedPlaneを選択します。次は第三引数のenumについて。
public enum TargetAlignment : Int { case horizontal = 0 case vertical = 1 case any = 2 }こちらは該当箇所が垂直なのか、水平なのか、どちらでも構わないのかという3つの選択肢を列挙しているものです。
第二引数と第三引数はどちらも新しく登場したものですが、hitTestで使っていたARHitTestResult.ResultTypeを分解し、組み合わせを自由にできるようになったと考えていいのではと思います。
hitTestは非推奨というだけなので使えるには使えるのですが、折を見て変更していきたいところです。
- 投稿日: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はシンプルに記述できるので必須のライブラリですね。是非、参考にしてください!
最後までご覧いただいてありがとうございました!