20201022のiOSに関する記事は11件です。

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で続きを読む

モバイル向け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=ja

Fileの扱いについて

そもそもパッケージが違うよ Webではファイルパスでファイルに直接アクセスできないからon memoryで扱う
dart:io
dart:html

Networkから画像が取得できる??

できるけど、AuthHeaderが効かないから画像が取得できない場合もある
https://github.com/flutter/flutter/issues/57187

カメラ使える?

まだ非対応です。
https://github.com/flutter/flutter/issues/45297

WebサーバーないけどWebで公開して確認したい!

Github Page(https://pages.github.com/)使えます。
プライベートリポジトリでは無料プランだと使えません。

Bitbucketならプライベートリポジトリでもつかえます
https://support.atlassian.com/bitbucket-cloud/docs/publishing-a-website-on-bitbucket-cloud/

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

MonacaでiOSデバッグビルドを実行するまで

Monacaを使えばMacがなくてもiOSアプリのビルドが実行できますが、Apple Developer Programへのアカウント登録と、アカウントを利用して証明書やプロビジョニング・プロファイルを発行する必要があります。

この辺がガチのアプリ開発初心者だと公式マニュアル見ただけだと難しかったので手順を残しました。

Apple Developer Programへの登録

事前にApple Developer Programに登録するか、会社やチームでやってる場合ならそのアカウントに招待してもらうか、証明書とプロビジョニング・プロファイルの発行までやってもらう必要があると思います。

秘密鍵とCSRの生成

Monaca IDEの「iOSビルド設定」から「秘密鍵とCSRの生成」を行います。
image.png

Apple Developer Programに登録しているApple IDアカウントの名前とメールアドレスと設定します。
image.png

これでCSR(certificate signing request:証明書署名要求)が作成され、ダウンロードできるようになります。
image.png
これは証明書ではなく、Appleに証明書を要求するための情報になります。

CSRをApple Developer Programに登録してCER証明書を発行

Apple Developer Programから「Certificates, IDs & Profiles」を選択、プラスマークを押してCertificatesを追加します。
image.png

デバックビルドなので「iOS App Development」を選択
image.png

先ほど作成したCSRをアップロード
image.png

CER証明書が発行されるのでダウンロードします。
image.png

なんでこんな手順を踏むのかというと、Appleアカウントとビルドに使われたMac(Monacaの場合ビルドサーバのMac)を紐づけるために必要みたいです。

App IDsを登録

アプリの識別子になるBundle IDを決定します。
Apple Developer Programから「Certificates, IDs & Profiles」を選択、プラスマークを押してIdentifiersを追加します。

image.png

image.png

image.png

image.png

登録したBundle IDをApp IDとしてMonaca IDEの「iOSアプリ設定」で設定します。
image.png

UDIDを登録

UDID(Unique Device IDentifier)、iOS端末に割り当てられた固有IDをApple Developer Programに登録します。
デバッグビルドしたアプリはここで登録した端末にしかインストールできません。
image.png

プロビジョニング・プロファイルの作成

プロビジョニング・プロファイル(Provisioning Profile)はこれまで作った「CER証明書」と「Bundle ID」、「UDID」の紐づけになるものです。

image.png

image.png

紐づけるBundle IDを選択します。
image.png

紐づけるCER証明書を選択します。
image.png

紐づけるUDIDを選択します。
image.png

名前を付けてプロビジョニング・プロファイルを生成します。
image.png

生成したファイルをダウンロード。
image.png

ビルド・インストール

CER証明書とプロビジョニング・プロファイルをMonacaにアップロードします。
「証明書のアップロード」⇒「プロファイルのアップロード」の順でアップロードします。
image.png
成功すれば証明書がMonacaに登録されます。

いよいよビルドです。
設定した証明書を選択してビルドを開始します。
image.png

しばらく待ってアプリのビルドエラーがなければ、ビルド結果が表示されます。
image.png

以上でデバッグビルド完了です。
IPAファイルのダウンロードと、UDIDを登録した端末ならQRコードからアプリがインストールできます。

参考

[iPhone] iOSアプリを登録、申請して公開するまで

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

[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で続きを読む

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を作成

ここら辺の設定はこちらの記事を参考にさせていただきました

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

SwiftUI プログラムへの Core Data データベース機能の追加

本記事では、

  • Core Data データベースを既存の SwiftUI アプリケーションに追加する方法を紹介します。
  • NSMergePolicy を使用したコアデータデータベースのマージ競合の管理方法についても説明します。

Core Dataモデルファイルとクラス定義の作成

モデルファイルとクラス定義用のコードファイルの作成の方法はこちらの通りです。これに関しては過去記事の1つで紹介済みです。

詳細に関してはこちらをクリックしてください

最初に、データモデルファイルを作成します。キーボードのcommand-Nキーを押して、Data Model を検索します。

Screen Shot 2020-05-27 at 2.37.09 PM.png

新しい Data Model にあなたのアプリの名前をつけてください。例えば、ここでは私のアプリの名前は CoreDataDemo なので、私は CoreDataDemo.xcdatamodeld を作りました。

データベース構造の作成

To-Doアイテムごとに次のプロパティを格納するTo-Doアプリを作成するとします:

  • To-Doタスク名
  • タスクの担当者
  • タスクの期限
1. .xcdatamodeld ファイルを開き、Add Entity をクリックします。

Screen Shot 2020-05-27 at 2.42.46 PM.png

作成した新規エンティティ・アイテムに TodoItem という名前を付けます。

作成した新しいアイテムをダブルクリックし、名前を付けるだけです。この例では、 TodoItem という名前を付けます。

新しいプロパティを追加する

Properties セクションのプラスアイコンをクリックして、新しいプロパティを追加します。

Screen Shot 2020-05-27 at 2.44.30 PM.png

ここでは、To-Doアプリに次のプロパティが必要です。

プロパティ名 タイプ
todoTaskName String
personName String
taskDueTime Date

タイプを設定するには、プロパティ名の横にあるドロップダウンメニューをクリックします:

Screen Shot 2020-05-27 at 2.47.09 PM.png

このような感じになります。

Screen Shot 2020-05-27 at 2.48.28 PM.png

Entity クラスのコードファイルを生成
  1. 作成した新しいエンティティ項目を選択します。右側のパネルで最後のセクションのアイコンを選択します :

Screen Shot 2020-05-27 at 3.05.23 PM.png

  1. Codegen 設定を Manual/ None に変更

以前の設定:

Screen Shot 2020-05-27 at 3.08.57 PM.png

新しい設定:

Screen Shot 2020-05-27 at 3.10.38 PM.png

  1. クラス定義ファイルを生成

最上部のメニューで Editor をクリックし、Create NSManagedObject subclass... をクリックします

こうしたサブクラスを生成することで、TodoItem を新規オブジェクトの作成に直接使用できるようになります。

Screen Shot 2020-05-27 at 3.11.37 PM.png

ファイルナビゲーターに以下のファイルが表示されます :

Screen Shot 2020-05-27 at 3.13.31 PM.png

データベースマネージャーを作成する

現在、コアデータベースに関連する関数を含む新規ファイルを作成できるようになっています。

このファイルで、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 関数に追加してください。SwiftUICoreData フレームワークのインポートをお忘れなく。

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)

また、変数 viewContextToDoItemCreate ビューに追加する必要があります。

@Environment(\.managedObjectContext) private var viewContext

データベースのコンフリクトへの対応

データベースが自身に変更を書き込もうとする際には、コンフリクトが発生することがあります。(例えばオブジェクトの重複など。)

デフォルトで、Core Data フレームワークは例外を発生させます。

viewContextmergePolicy プロパティーを設定することで、コンフリクトの解消を試みることができます。

self.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)

以下は一部の結合ポリシーの説明です。

Name Explanation
NSErrorMergePolicyType デフォルト。try viewContext.save() を呼び出すと、例外が報告されます。
NSMergeByPropertyStoreTrumpMergePolicyType メモリ内の変更は外部変更を置き換えます
NSMergeByPropertyObjectTrumpMergePolicyType 外部変更はメモリ内の変更を置き換えます。
NSOverwriteMergePolicyType メモリ内のオブジェクト全体が永続ストアにプッシュされます。
NSRollbackMergePolicyType 対立する変更されたオブジェクトについて、すべての状態が破棄されます

通常、NSMergeByPropertyObjectTrumpMergePolicy を使用すると、Core Data はデータベースのデータバージョンを新しい変更で自動的に上書きします。


:relaxed: Twitter@MaShunzhe

私の公開されているiOSの記事のリストをカテゴリー別にご覧いただけます (39)

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