20201022のSwiftに関する記事は12件です。

iOSアプリ開発:タイマーアプリ(6.設定画面の作成)

記事

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、アラーム音のオン/オフやプログレスバーのオン/オフを含む設定画面の作成方法について掲載します。

環境

  • OS: macOS 10.15.7 (Catalina)
  • エディタ: Xcode 12.1
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

手順

  1. 設定項目をリストアップする
  2. SettingView を作成する
  3. SettingView にリストを作成する
  4. SettingView のリストに設定項目を追加する
  5. TimeManager に各種設定に必要なプロパティを追加する
  6. SettingView の設定項目を TimeManager のプロパティと関連づける
  7. SettingView をモーダルにする
  8. MainView に設定ボタンを追加し、設定画面をモーダルで表示する

手順詳細

1. 設定項目をリストアップする

設定画面を作成していきます。今まで作成してきたPickerViewやTimerView、ButtonsViewはすべてMainViewに配置され、タイマーアプリにはMainView以外の画面がない状態でした。

今回はMainViewとは別に、設定画面を1つ作成していきます。
この設定画面には以下の項目を表示することにします。

  • アラーム音を鳴らすかどうかのトグルスイッチ
  • バイブレーションを有効にするかどうかのトグルスイッチ
  • アラーム音をリストから選択
  • プログレスバーを表示するかどうかのトグルスイッチ
  • エフェクトアニメーションを表示するかどうかのトグルスイッチ
  • 設定画面を閉じるボタン

2. SettingViewを作成する

SwiftUIテンプレートから、SettingViewという名前で新しくファイルを作成します。

ここでも、最終的にTimeManagerクラスのプロパティに設定情報を反映することになるので、先に@EnvironmentObject プロパティラッパーをつけて、TimeManagerのインスタンスを作成します。

SettingView.swift
import 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.swift
struct 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.swift
struct 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.swift
class 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.swift
struct 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.swift
struct 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.swift
import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
    }
}

ボタンアイコンは SF Symbols から "ellipsis.circle.fill" を使います。

SettingButtonView.swift
import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
    }
}

設定ボタンは、スタート/一時停止ボタンやリセットボタンより少し小さめのサイズにするので、frame()モディファイアで縦、横のサイズを入れておきます。

SettingButtonView.swift
import 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.swift
class TimeManager: ObservableObject {
    //(他のプロパティ省略)

    //設定画面の表示/非表示
    @Published var isSetting: Bool = false

    //(メソッド省略)
}

そして最後にSettingButtonViewに戻り、.onTapGestureを追加して、クロージャ{}内に TimeManagerの isSetting プロパティが trueになるようにします。

SettingButtonView.swift
import 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.swift
struct 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の画面を横向きにしたときのレイアウトの調整などで柔軟にアレンジできるのが理想です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.swift
import UIKit
import PromiseKit

class ViewController: UIViewController {

        override func viewDidLoad() {

            let promise = Promise<String> { value in
                // 処理
            }.done { value in
                // 成功した時に実行
            }.catch { error in
                // 失敗した時に実行
            }.finally {
                // 最後に実行
            }
        }

}

コード例①

ViewController.swift
import 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.swift
import 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.swift
import 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.swift
import 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]:処理を終了します

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebView内のテキストフィールドタップ時のキーボード設定

WebView内のテキストフィールドを選択した時に、表示されるキーボードのタイプは選択可能なのか調べました。今回はSFSafariViewで試していますがWKWebViewでも同じはずです。

可能です

htmlのtypeの指定によって表示されるキーボードの種類が変わります。

// 普通のキーボード
<input type="text" size="20" maxlength="30">

// PhonePad 
<input type="tel" size="20" maxlength="30">

これ以外にもtypeにurlemailを指定することで、それぞれに応じたキーボードを表示させることができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebView内のテキストフィールドタップ時のキーボード

WebView内のテキストフィールドを選択した時に、表示されるキーボードのタイプは何によって決まるのか調べました。今回はSFSafariViewで試していますがWKWebViewでも同じはずです。

結論

htmlのtypeの指定によって表示されるキーボードの種類が変わります。

// 普通のキーボード
<input type="text" size="20" maxlength="30">

// PhonePad 
<input type="tel" size="20" maxlength="30">

これ以外にもtypeにurlemailを指定することで、それぞれに応じたキーボードを表示させることができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARKit+SceneKit+Metal で Webブラウジング

スマホやARグラスで仮想オブジェクトにUIKitを使いたいこともあるだろうと考え、UIView を SceneKit のシーン内に表示する方法を試してみた。
動作確認がしやすいのでUIViewとしては WKWebView を使用。
demo.gif
参考情報:
 ・こちらのSwiftUIでの動作のツイート

シーン内でのUIViewの表示方法

UIViewをキャプチャしMTLTexutre に変換
②①をSceneKitのSCNNode のマテリアルに設定

これだけ。それっぽく動いているが、いくつかハマったところを以下に記載。

どうやってWebViewをスクロールさせるか

仮想オブジェクトとなっているWebViewでスクロールやタップ処理をさせるには前面に ARSCNView がいる状態で、タッチイベントを任意のWebViewに渡す必要がある。
ここでtouchesBegan() 等の UIResponder のイベントを ARSCNViewで受け取って任意のWebViewの touchesBegan() 等に流す方法を思いついたが、これだとスクロールもタップも全く動作せず。そもそもUIEventを渡したところでスクロールをさせたりボタンタップをさせる、といったUI部品固有のアクションをさせることはできるのか?という気がしてきた。この辺り知識不足。。。ご存知の方がいたら教えて欲しいです。
プログラムからスクロールさせる方法をググると UIScrollViewcontentOffset で設定するやりかたはでてくるが、これだと心地よい慣性スクロールはできないはず。
で、結局、良い方法が見つからず次の方法で実現。

ARSCNViewと同じサイズ同じ位置に WebView を配置
ARSCNView は前面にしておき isUserInteractionEnabled は falseにする
・ヒットテストを行い、画面の中央にある WebView の isUserInteractionEnabled をtrueにする

これで任意のWebViewのスクロールは可能になるが、この方法には次の欠点がある。

・スクロールさせる場合、WebView を任意のアスペクト比にできない。画面サイズに合わせる必要がある。
・タップされた位置をシーン内のWebViewの位置に補正できないので、ボタンクリック等のイベント処理はできない(使い物にならない)

上記より、この方法が使えるケースが使えるのは、画面内の仮想オブジェクトにおいて、何かしらの情報を表示のみ、操作はスクロールのみ、というケースに限定されると思われる。
前述のツイートのようにSwiftUIでどのようにできるかまた試す予定。

画面キャプチャが遅くてレンダリングが追いつかない

キャプチャのタイミングを SCNSceneRendererDelegaterenderer(_: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
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参照:https://stackoverflow.com/questions/24023639/xcode-command-usr-bin-codesign-failed-with-exit-code-1-errsecinternalcomponen

この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_CERT

4.Fastlaneで中間証明書をキーチェーンに登録します。

fastlane/Fastlane
import_certificate(
  keychain_name: ENV["MATCH_KEYCHAIN_NAME"],
  keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"],
  certificate_path: 'AppleWWDRCAG3.cer',
  certificate_password: ''
)

これでうまく行きました!

まとめ

このバグに丸1日はまりました。。
CircleCIかfastlaneが新しい中間証明書を読み込む方法を知ってる方がいたらぜひ教えていただきたいです(>_<)

この対応すごくがんばったのでよかったらLGTMお願いしますᕦ(ò_óˇ)ᕤ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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)のことです。

ちなみにDataFoundationで定義されている型なので、実際のコードには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。当然NSStringSwift.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にできないか?

できます。
UTF8ViewCollectionプロトコルに準拠しておりElementUInt8なので、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.swift
public 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にならないということも起き得ます。


  1. https://developer.apple.com/documentation/swift/stringprotocol/3126754-data 

  2. 実際にはNSStringは抽象クラスとして定義され、NSConcreteStringなどのサブクラスが文字列操作を担当するという、いわゆる「クラスクラスタ」を形成しています。 

  3. 2020年10月現在: https://w3techs.com/technologies/overview/character_encoding 

  4. https://github.com/apple/swift/blob/a353176e1eb570a56809cf4202f5f30aa8905840/stdlib/public/Darwin/Foundation/NSStringAPI.swift#L791-L811 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の部分は、ご自身の作りたいものに合わせて適用させてください。

自分のメモ用であり、誰か一人でもいいのでお役に立てれば思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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はこちらになります

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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は非推奨というだけなので使えるには使えるのですが、折を見て変更していきたいところです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.swift
import 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.swift
let jsonValue = JSON(json)
let tonesScore = jsonValue["document_tone"]["tones"][self.count]["score"].float

まず、感情分析結果を代入している値jsonJSONとしてjsonValueを作成します。(ほんまやいこしい言い方ですいません笑)

次に解析をおこないます。
jsonValue["document_tone"]["tones"][self.count]["score"].floatこのように指定することで値を取得でします。ほんとシンプルでかわかりやすいですよね?

注意点として

①分析結果であるjsonValueの配列の構造を理解していないと取得できない。
②今回使用しているself.countメンバ変数countを定義しているとする。なお、self.はクロージャー内なので記述している。

これでscoreの取得はできているはずです。
ただし、このままではscoreの値が冗長なので、小数点を切り上げて取得します。

ViewController.swift
let 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.swift
let tonesName = jsonValue["document_tone"]["tones"][self.count]["tone_name"].string
let tone_name = tonesName

これで、"document_tone"の配列"tones"の中にある"score"と、"tone_name"を取得できたと思いますのでprint()で確認してみましょう。

ViewController.swift
print("=====個別取得の確認=====")
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はシンプルに記述できるので必須のライブラリですね。

是非、参考にしてください!
最後までご覧いただいてありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む