20200824のiOSに関する記事は12件です。

Xcodeの開発で、ファイルを保存するだけで動的に中身が書き換えられるようにしてみた

Xcodeの開発で、通常であれば、ファイルを保存し、ビルドし、プログラムを起動するという手順をとる。プログラムを起動した状態で、終了させずに、その中身を書き換えるということは、通常はできない。だが、injectionという仕組みを使うと、できるようになる。

1. 「InjectionIII」を使ってみる

John Holdsworth氏により、「InjectionIII」というアプリが公開されている。これを使うと、Injectionを簡単に実行できるようになっている。
https://github.com/johnno1962/InjectionIII
以下、使ってみる。

1.1. アプリをインストール

InjectionIIIアプリをインストールする。
https://apps.apple.com/jp/app/injectioniii/id1380446739?mt=12

インストールされたら、実行する→Status menuにInjectionIIIアプリのアイコンが表示されたことを確認する→「Help/README」を選ぶと、以下のページに接続される。
https://github.com/johnno1962/InjectionIII

1.2. サンプルをダウンロードする

以下、Injection IIIのホームページである。
http://johnholdsworth.com/injection.html

以下より、サンプルプログラムをダウンロードできる。
http://johnholdsworth.com/GettingStarted.zip
解凍し、「~/dev/GettingStarted/」となるようにする。

1.3. Xcodeからサンプルを開く

GettingStarted.xcodeprojをダブルクリックして起動。→Open

1.4. InjectionIIIアプリから、GettingStartedを接続する

Status menuのInjectionIIIから「Open project」を選択→「~/dev/GettingStarted/」を選択→「Select Project Directory」

1.5. 実行する

Cmd-Rで実行→シミュレーターに「Master」と表示される→「+」を押すと、現在時刻が表示される。それをクリックする→現在時刻と「CHANGEME」が表示される。

Xcodeに戻る→Cmd-1→DetailViewController.swiftを選択→" CHANGEME"の個所を、たとえば" CHANGED!"に変更する→Cmd-Sでセーブとすると、即座に画面上の「CHANGEME」だったところが、「CHANGED!」に変わる。これがインジェクションである。

2. 独自のプログラムからインジェクションしてみる

2.1. なにかアプリを作る

まずは、なにかシンプルなアプリを開発する。文字が表示されるものが良い。
Xcodeを起動→Create a new Xcode Project→iOS→「Single View App」→Next→Product Name:「InjectionTest」、User Interface: Storyboard→Next→「~/dev」を指定→Create
ViewController.swiftに、以下のようにshow()を追加。viewDidLoad()から呼ばれるようにする。

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
        button.backgroundColor = .cyan
        button.setTitle("Hello, world!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        view.addSubview(button)
    }
}

まずはこの段階で実行してみる。Cmd-R→ビルドされ、実行される。Simulatorが起動して、「Hello, world!」が表示される。

ViewController.swiftに戻り、試しに"Hello, world!""Hello, Japan!"に修正してCmd-Sで保存しても、反映されない。当然である。

再度Cmd-Rすると、一旦シミュレーター上のアプリが終了し、再度立ち上げられ、今度は"Hello, Japan!"に修正されている。3秒程度で立ち上がる。通常は、このようにアプリを終了し、再読み込みするという手順で開発する。ビルドが早いので、3秒程度でこのサイクルは実行できる。実用上はあまり不満は持たれないかもしれない。

2.2. Linker Flagsを設定する

Xcodeに戻る。Cmd-1→InjectionTestのプロジェクトを選択→PROJECT: InjectionTest→Build Settings→Linking→Other Linker Flags→ここにカーソルを乗せると左に三角が表示される→クリックする→Debugの右の「+」をおす→Debug→Any Architecture | Any SDK:「-Xlinker -interposable」→リターンを押すと確定する

2.3. Bundleを追加

AppDelegate.swiftにBundleを追加する。

AppDelegate.swift
        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
        #endif

参考までに、AppDelegate.swiftの該当するメソッド全体である。

AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
        #endif
        return true
    }

2.4. injected()を追加

ViewController.swiftに、injectedというメソッドを追加する。

ViewController.swift
    @objc func injected() {
        show()
    }

参考までに、ViewController classの全体である。

ViewController.swift
class ViewController: UIViewController {
    @objc func injected() {
        show()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
        button.backgroundColor = .cyan
        button.setTitle("Hello, world!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        view.addSubview(button)
    }
}

2.5. InjectionIIIにProjectを指定する

Status menuのInjectionIIIから「Open project」を選択→「~/dev/InjectionTest」を選択→「Select Project Directory」

2.6. 起動する

Cmd-R→Simulatorが起動して、「Hello, world!」が表示される。以下のように出力される。

? Injection connected ?
? Watching /Users/eto/dev/InjectionTest/**

2.7. 編集する

ViewController.swiftで、該当する行を以下のように編集してみる。

ViewController.swift
        button.setTitle("Hello, Japan!", for: .normal)

Cmd-Sで、ファイルをセーブする。そうすると、シミュレーター上の「Hello, world!」が、即座に(1秒以内くらいで)「Hello, Japan!」に変わる。以下のように出力される。

? *** Compiling /Users/eto/dev/InjectionTest/InjectionTest/ViewController.swift ***
? Loading .dylib ...
objc[31231]: Class _TtC13InjectionTest14ViewController is implemented in both /Users/eto/Library/Developer/CoreSimulator/Devices/97670822-70F9-46B8-87F7-5545DF54E516/data/Containers/Bundle/Application/82DDD3CB-9924-4E5C-BCCC-1AE2A8A9E3AD/InjectionTest.app/InjectionTest (0x107b53b40) and /var/folders/94/shwk5bk14l5fx43cggr_n04m0000gn/T/com.johnholdsworth.InjectionIII/eval106.dylib (0x110d9c280). One of the two will be used. Which one is undefined.
? Loaded .dylib - Ignore any duplicate class warning ^
? Injected 'ViewController'
? Replacing InjectionTest.ViewController.__allocating_init(coder: __C.NSCoder) -> Swift.Optional<InjectionTest.ViewController>
? Replacing InjectionTest.ViewController.__allocating_init(nibName: Swift.Optional<Swift.String>, bundle: Swift.Optional<__C.NSBundle>) -> InjectionTest.ViewController
? Replacing InjectionTest.ViewController.viewDidLoad() -> ()
? Replacing InjectionTest.ViewController.show() -> ()
? Replacing InjectionTest.ViewController.injected() -> ()
? Class ViewController has an @objc injected() method. Injection will attempt a "sweep" of all live instances to determine which objects to message. If this crashes, subscribe to the global notification "INJECTION_BUNDLE_NOTIFICATION" to detect injections instead.

内部で起こっていることを説明すると、ViewController.swiftを常にウォッチし、編集されたのを検知したら、即座にそれをCompileし、動的にロードし、メソッドを置き換えている。その後、injected()が呼ばれ、表示が切り替わる。このようにすると、プログラムを実行している間に、動的にメソッドを書き換えできるため、プログラム開発効率が高まると考えられる。

ここまでのファイルを、以下に置く。このまま実行できるはずである。
https://github.com/eto/InjectionTest

done!

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

【Flutter, Dart, freezed】Bad state: Unexpected diagnosticsでbuildが終わらない不具合

TL;DR

pubspec.yaml に以下の2つを追加してください

pubspec.yaml
dependencies:
  analyzer: ^0.39.14 <- 追加

dev_dependencies:
  build_runner: ^1.10.0 <- 追加

環境

# fvmを使用
[✓] Flutter (Channel unknown, 1.20.2, on Mac OS X 10.15.5 19F101, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 11.6)
[✓] Android Studio (version 4.0)

症状

  • Bad state: Unexpected diagnostics: が大量に表示される
  • [INFO] 1.6s elapsed, 1/12 actions completed. が永遠に完了しない
$ fvm flutter packages pub run build_runner build --delete-conflicting-outputs

[...]

[INFO] Running build...
[INFO] Generating SDK summary...
[SEVERE] freezed:freezed on test/widget_test.dart:

Bad state: Unexpected diagnostics:
/Users/k3ntar0/fvm/versions/1.20.2/bin/cache/dart-sdk/lib/core/uri.dart:3259:39 - Expected an identifier.
[SEVERE] freezed:freezed on test/widget_test.dart:

[...]

Bad state: Unexpected diagnostics:
/Users/k3ntar0/fvm/versions/1.20.2/bin/cache/dart-sdk/lib/core/uri.dart:3259:39 - Expected an identifier.
[INFO] 1.6s elapsed, 1/12 actions completed.
[INFO] 2.6s elapsed, 1/12 actions completed.
[INFO] 3.7s elapsed, 1/12 actions completed.
[INFO] 4.7s elapsed, 1/12 actions completed.
[INFO] 5.8s elapsed, 1/12 actions completed.
[INFO] 6.9s elapsed, 1/12 actions completed.
[INFO] 8.0s elapsed, 1/12 actions completed.
[INFO] 9.0s elapsed, 1/12 actions completed.
[INFO] 10.0s elapsed, 1/12 actions completed.
[INFO] 11.1s elapsed, 1/12 actions completed.
[INFO] 12.1s elapsed, 1/12 actions completed.
[INFO] 13.2s elapsed, 1/12 actions completed.
[INFO] 14.2s elapsed, 1/12 actions completed.
[INFO] 15.2s elapsed, 1/12 actions completed.
[WARNING] No actions completed for 15.1s, waiting on:
  - freezed:freezed on lib/main.dart
  - freezed:freezed on lib/app.dart
  - freezed:freezed on lib/presentation/common/colors.dart
  - freezed:freezed on lib/presentation/pages/home/home_page.dart
  - freezed:freezed on lib/presentation/pages/index.dart
  .. and 6 more

やること

  • pubspec.yaml を変更する
  • きれいにする
  • buildし直す

※ fvmを使用されていない場合はfvmの部分を消して実行してください

pubspec.yaml を変更する

pubspec.yaml
dependencies:
  analyzer: ^0.39.14 <- 追加

dev_dependencies:
  build_runner: ^1.10.0 <- 追加

きれいにする

# pubspec.lockを削除
$ rm -rf pubspec.lock

# clean
$ fvm flutter clean
Cleaning Xcode workspace...                                         2.9s
Deleting .dart_tool...                                               3ms
Deleting Generated.xcconfig...                                       0ms
Deleting flutter_export_environment.sh...                            0ms

buildし直す

# pub get
$ fvm flutter pub get
Running "flutter pub get" in my_project...                          2.5s

# build_runnerを実行
$ fvm flutter packages pub run build_runner build --delete-conflicting-outputs
Precompiling executable...
Precompiled build_runner:build_runner.
[INFO] Generating build script...

[...]

[INFO] Succeeded after 18.8s with 1 outputs (12 actions)

fvmを使用されていない場合は記述不要です!

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

iOSアプリで音楽や動画のバックグラウンド再生の実装方法(他アプリ音声を停止しないやり方も)

AVPlayerやAVAudioPlayerなどで音声再生や動画再生の実装をする場合デフォルトのままだとアプリをバックグラウンドへ移行したりスリープをした時に再生が自動で停止します。
以下の実装を行うことで再生を継続させることができます。

簡単な手順でOKです。

手順1

  1. プロジェクトファイルを開く
  2. メニュー -> Editor -> Add Capability -> Background Modesを選択
  3. Audio, AirPlay, and Picture in PictureのチェックマークをONにする

スクリーンショット 2020-08-24 19.18.08.png

手順2

プロジェクト内の任意の場所に以下のコードを書きます

// アプリ初期化時等
do {
    try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
} catch _ {
    NSLog("audio session set category failure")
}

// 音声、動画再生時
do {
    try AVAudioSession.sharedInstance().setActive(true)
} catch _ {
    NSLog("audio session active failure")
}

注意点として、上記の実装をしているともし別のアプリで音声を再生している場合
上記のsetActiveが呼び出された時点で別アプリの音声再生が停止します。
なのでsetActiveは自分のアプリでの音声再生を開始する時点で呼ぶのが望ましいです。

他アプリの音声再生を停止せずに再生する方法

AVAudioSessionのsetCategory時にoptionを設定します

do {
    try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback,
                                                            options: [AVAudioSession.CategoryOptions.mixWithOthers])
} catch _ {
    NSLog("audio session set category failure")
}

上記はmixWithOthersを指定しましたが、duckOthersというのも存在します
違いは
- mixWithOthers → 他アプリの音声の音量を下げない
- duckOthers → 他アプリの音声の音量を下げる
場面に応じて使い分けましょう!

他のoption
- allowBluetooth → 音声再生時にはこのoption指定は無視される(bluetoothイヤホンを利用できるようになるとかそういうものではない)
- defaultToSpeaker → イヤホンやヘッドセットを利用しない場合にいわゆる電話の耳を当てる部分から音が出るのがデフォルトになっているが、それをスピーカーから音が出る状態にするoption

公式ドキュメント:
AVAudioSession

終わりです

Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp

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

[Swift] textFieldのclearButtonMode

textFieldは中身を全部消すことができるclearButtonが実装できます。
画像の右端にあるバツマークです
スクリーンショット 2020-08-24 18.36.53.png

これの表示パターンはclearButtonModeの設定で変更することができます。

.always

.alwaysはtextFieldに文字列があれば常に出現します。

textField.clearButtonMode = .always

.never

.neverはボタンを表示しません

textField.clearButtonMode = .never

.unlessEditing

.unlessEditingはtextFieldに文字列があり、textFieldが選択されていない時に出現します

textField.clearButtonMode = .unlessEditing

.whileEditing

.whileEditingはtextFieldに文字列があり、textFieldが選択されている時に出現します

textField.clearButtonMode = .whileEditing

storyboard上での実装

textFieldのClearButtonの部分を変更すれば実装できます

スクリーンショット 2020-08-24 19.01.19.png

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

【iOS14】WidgetKitのテンプレートを読み解く

はじめに

9月リリースが噂されているiOS14から、Widgetが追加されます。
表現が固定されていたホーム画面での体験が大きく変わる機能であり、
多くの人が注目しているかと思います?✨

Apple_ios14-widgets-redesigned_06222020_inline.jpg.large_2x.jpg

公式ブログ

よし!自分のアプリにもWidgetを追加しよう!と手を動かそうとしている皆さんに向けて、
既存アプリへの追加手順の紹介と、テンプレートコードの読み解きをします。

本記事は、最低限の挙動と実装を確認したい人に向けての解説記事です。
WidgetKitの利用シーンやWidgetKitの詳しい解説はこちらの記事に丁寧にまとまっているので、割愛させていただきます。
https://qiita.com/shiz/items/309349d9cdb75084e74e

※本記事はXcode12 beta5時点の情報です、beta版の開発画面のスクリーンショットはNDA締結により掲載しておりません。

1.追加手順

Xcodeを開き

  1. File > New > Target を選択
  2. Application Extension group セクションから Widget Extension を選択
  3. Extension の名前をつける

リファレンスには
4. If the widget provides user-configurable properties, check the Include Configuration Intent checkbox.
とありますが、beta5では選択肢出てこず(自分だけ…?)、デフォでuser-configurableのテンプレートが生成されました。

公式リファレンス: https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension

また、Application ExtensionごとにiOS Deployment Targetを設定できるので
iOS14未満をサポートしている場合も対応できそうです。
自分はiOS13対応アプリで正常挙動を確認しました。
(もちろんiOS13ではWidgetは利用できません、iOS12以下未検証)

CharacterWidgetと命名して追加すると
CharacterWidget.swift
CharacterWidget.intentdefinition
Assets.xcassets
Info.plist
のファイルが用意されます。
(Character画像をランダムに表示するWidgetを実装しようと思った命名)

2.テンプレートコードの読み解き

CharacterWidget.swiftのコード内に日本語コメントを含めながら
Widgetを構成する要素を説明します。

Widget

まずはWidgetを定義します。
テンプレートでは端末に基づいた時間が表示されるよう実装されていました。

@main
struct CharacterWidget: Widget {
    let kind: String = "CharacterWidget" // widgetの識別子、何をするwidgetか説明すべき

    var body: some WidgetConfiguration {
        // Configuration
        // StaticConfigration: ユーザが設定不可(ex.株価, ニュース)
        // IntentConfiguration: ユーザが設定可(ex. 場所ごとの天気, 荷物追跡): ← テンプレはこちらが実装されているが時間の表示だけであればStaticで良い
        IntentConfiguration(
            kind: kind,
            intent: ConfigurationIntent.self, // ユーザが設定可能なプロパティを定義するカスタムインテント、.intentdefinition からコード生成される
            provider: Provider() // Timeline セクションで解説
        ) { entry in // SwiftUIのViewを含むクロージャー、ProviderからTimelineEntryパラメーターが渡される
            CharacterWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("? Widget") // Widgetギャラリー(設定画面)にてユーザに表示するタイトル
        .description("This is ? widget.") // 同上メッセージ
        // サイズの設定も加えることができる、デフォルトは3つ全て .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

ConfigurationIntent
は、例えばユーザがアプリ内で選択したキャラクターのWidgetを表示したい場合などに実装します。
テンプレートではデフォルトで実装してあるが特に利用していないので、ユーザ設定が必要ない場合はStaticに書き換えてください。

        StaticConfiguration(
            kind: kind,
            provider: StaticProvider()
        ) { entry in
            CharacterWidgetEntryView(entry: entry)
        }

公式リファレンス: https://developer.apple.com/documentation/widgetkit/intentconfiguration

TimelineProvider

WidgetではTimelineという、いつどのViewを表示するか管理する仕組みを実装します。
まずはEntryを定義します。

struct SimpleEntry: TimelineEntry {
    let date: Date // 表示する時間
    let configuration: ConfigurationIntent
}

続いて、IntentTimelineProviderの実装です。
テンプレートでは1時間おきに5回、時間を更新して表示されるよう実装されていました。

struct Provider: IntentTimelineProvider {
    /// ユーザ設定可能の場合、初めて表示する時に表示されるプレースホルダー
    /// ウィジェットが何を表示するかについて一般的な考えをユーザに提供するべき
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }

    /// 表示するSnapshotを定義
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {

        let entry = SimpleEntry(date: Date(), configuration: configuration)
        // context.isPreview = true の時、widget galleryに表示されているので、サーバーからの取得がある場合はフラグを立ててローディング表示など実装するとよい
        // ex) if context.isPreview && !hasFetchedGameStatus { ... }
        completion(entry)
    }

    /// タイムラインを定義
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // 現在の時間から開始して、1時間おきに切り替わる5つのEntryで構成されるタイムラインを生成
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            // hourOffset時間後の時間を生成
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }
        // 配列に含めてTimelineにする
        let timeline = Timeline(entries: entries, policy: .atEnd)// policy: .atEnd タイムライン終了後新しいタイムラインを要求するポリシー
        completion(timeline)
    }
}

Timelineのpolicyは.adEndの他に

  • never: 新しい要求がくるまで何もしない
  • after(_ date: Date): タイムラインをリクエストする日時を指定

を選択できます。

こちらもIntentを利用しない場合はStaticに書き換えてください。

struct StaticProvider: TimelineProvider {
    typealias Entry = SimpleEntry

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        var entries: [SimpleEntry] = []

        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }
        let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
    }
}

View

最後に、実際に表示するWidgetを実装します。
テンプレートではシンプルにTextでentry.dateを表示するだけだったので、
3つのWidget family(サイズ)ごとの実装について追記しました。

/// widgetのView、SwiftUIで実装する
struct CharacterWidgetEntryView : View {
    @Environment(\.widgetFamily) var family: WidgetFamily// Widget familyをサポートする
    var entry: Provider.Entry

    @ViewBuilder // viewにバリエーションがある時宣言
    var body: some View {
        // サイズごとにViewを指定する実装
        switch family { 
        case .systemSmall: Text(entry.date, style: .time); Text("small")
        case .systemMedium: Text(entry.date, style: .time); Text("medium")
        case .systemLarge: Text(entry.date, style: .time); Text("large")
        @unknown default: fatalError()
        }
    }
}

SwiftUIで実装するのでもちろんPreviewもできます。

struct CharacterWidget_Previews: PreviewProvider {
    static var previews: some View {
        CharacterWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

以上です。大まかな流れが掴めていると幸いです?‍♂️

3.intentdefinition

CharacterWidget.intentdefinitionからテンプレートで実装されているConfigurationIntent周りのコードが自動生成されます。
Siri Shortcutsを実装したことがある方なら馴染みがあるかもしれません。

テンプレでは用意されているだけで利用していないので割愛しますが、
需要があればこちらについても解説する投稿をしようかなと思います。
https://developer.apple.com/documentation/widgetkit/making-a-configurable-widget

おわりに

Widgetのサンプルコードも用意されているのでぜひぜひ一度動かしてみて、
ユーザにとって需要のある機能実装の模索をしてみましょう?

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

iPhoneX系の画面の下のバー(Home Indicator)を非表示にする方法

IMG_7320.PNG

画面の下にいつもいるバー
Home Indicatorという名前です。
iPhoneXが発売されて数年、私は知りませんでした。
全画面でコンテンツを表示したい時にこのバーだけ表示されてしまっていると見た目が悪いですよね。

但し常に非表示にしておくということはできませんが操作をしていない場合に非表示になってくれる実装方法があります。

非表示にしたいViewControllerに以下の一行を追加します。

ViewController.swift
override var prefersHomeIndicatorAutoHidden: Bool { true }

公式ドキュメント:
prefersHomeIndicatorAutoHidden

常に表示するという状態と、操作していない場合は非表示にしたい、という二つの状態を切り替えたいというときは以下のような実装をします。

ViewController.swift
private var indicatorAutoHidden:Bool = false {
    didSet {
        self.setNeedsUpdateOfHomeIndicatorAutoHidden()
    }
}

override var prefersHomeIndicatorAutoHidden: Bool { 
    return self.indicatorAutoHidden
}

setNeedsUpdateOfHomeIndicatorAutoHiddenを呼び出すことですぐさま表示、非表示状態の更新を切り替えることができます。

公式ドキュメント:
setNeedsUpdateOfHomeIndicatorAutoHidden

終わりです

Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp

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

【iOSアプリ審査対策】Firebase Hostingを使ってApple審査員とデモ動画を共有する

概要

ようやく開発が完了し、喜び勇んでアプリを審査に提出するも2、3日後あたりに

「New Message from App Store Review Regarding...」の件名メールが。

緊張しながら、Resolution Centerを開いてみると

Guideline 2.1 - Information Neededでのリジェクト!

内容は「デモ動画を共有してくれ」で、少しだけホッとするというケース。あるかと思います。(私はあります)

新規の初回審査には、このアプリの動作を説明するデモ動画を要求されるパターンが多く

本来は審査提出前に予め準備して置いた方がベターだと思います。

Appleの審査チームへのデモ動画の共有は、Google DriveやDropboxでも良いのですが

アプリで既に、Firebaseを使っている場合では

Firebase Hostingを使った方が手っ取り早かったので、その手順のメモです。

環境

  • macOS Catalina 10.15.6
  • npm 6.14.5
  • firebase CLI 8.7.0

Firebase Hositing

Firebase Hosting

Firebase Hosting は、ウェブアプリ、静的コンテンツと動的コンテンツ、マイクロサービス向けの高速で安全性の高いホスティングを提供します。

保存容量や転送容量の制限はありますが、無料で利用出来ます。

Firebase CLIをインストール

Firebase CLI リファレンス

npmを使った方法で進めます。(npmがまだであればまずはそちらから準備)

まず、firebase-toolsをインストールします。(既に環境があればアップデート)

npm install -g firebase-tools

インストール済みでも、↑を実行して最新バージョンにしておく事が推奨されている様です。

無事に完了したら、次はFirebaseにGoogleアカウントでログインします。

Firebaseへのログイン認証

firebase login:ci

を実行すると

visit this URL on this device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=xxxxxxx...

というメッセーが表示され、デフォルトブラウザが起動。

Googleの認証ページが開くので、Firebaseで使用しているアカウントでログイン。

認証が成功した後、ターミナルへ戻るとトークンが表示されています。

✔  Success! Use this token to login on a CI server:

XXXXXXXXXXX(トークン)

このトークンをコピーしておきます。

トークンを環境変数へ保存

毎回、コマンドに指定してもいいのですが面倒なので環境変数にFIREBASE_TOKENとして保存しておきます。

export FIREBASE_TOKEN="コピーしたトークン"

一応、以下で確認

printenv

変数の一覧が表示されます。

.
.
.
FIREBASE_TOKEN=トークン

初期化

まずプロジェクトのディレクトリを作成

mkdir demo-video-hosting

作成したディレクトリに移動し

cd demo-video-hosting

初期化を実行します。

firebase init

を実行すると、以降は対話形式でのプロジェクト設定になります。

こんな感じで進みます(Versionによって変わるかも?)

? Please select an option: Use an existing project → 既存プロジェクトを使用する

? Select a default Firebase project for this directory:xxxxx  → プロジェクトを選択

? What do you want to use as your public directory?  → 公開ディレクトリ設定。publicのまま

? Configure as a single-page app (rewrite all urls to /index.html)? → SPAにするか?今回はしない

全て完了すると、現在のディレクトリに初期ファイルが作成されます。

出来上がった構成

./
├── firebase.json
└── public
    ├── 404.html
    └── index.html

ここで生成されたindex.htmlには、firebaseの初期化コードなども記述されているので

今回は不要ならば消しておく方が良いと思います。
(ファイルは残して中身を空にするなど)

デモ動画をアップロード

初期化時に指定した公開ディレクトリ(そのままであればpublic)

にデモ動画を設置して

firebase deploy --only hosting

を実行。ホスティング環境へデプロイします。

完了すると、メッセージの最後にドメインが表示されます。

Hosting URL: https://xxxx-xxxx-xxxx.web.app

なお、デプロイ前にテストする場合は

firebase serve --only hosting

を実行すると、ローカルで確認が出来ます。

ただ、手元の環境では何故か動画が再生出来ませんでした。
(index.htmlなどは表示される。動画のURLを叩くとクラッシュしてしまう。。)

デモ動画の再生を確認

ブラウザ(Safari)から表示させて、きちんと動画が再生されるか確認します。
(ここ審査員サイドなので大事です)

https://xxxx-xxxx-xxxx.web.app/demo-video.mp4

なお、Firebaseの無料プランでは、月の転送量制限が10GBなので

デモ動画のサイズが大きい場合は注意が必要です。(審査中の一時的な利用であれば問題無いはず)

終わりに

デモ動画のおかげで無事に審査承認され、リリースする事が出来ました。

最後に、目的を果たしたら動画を片付けます。

firebase hosting:disable

これで、アップロードしたファイル一式がホスティング上から削除されます。


Firebase Hosting
Firebase Pricing

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

Literal について

Image Literal

スクリーンショット 2020-08-24 16.30.27.png

Color Literal

スクリーンショット 2020-08-24 16.30.39.png

Literal

スクリーンショット 2020-08-24 16.30.06.png

あとは
・Image Literalをダブルクリックすれば、Assetフォルダ内に保存されている画像を選択できる。

・Color Literalをダブルクリックすれば、色を選択できますスクリーンショット 2020-08-24 16.31.22.png

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

【Flutter/ Dart】外部アプリを起動して画像をシェアする方法

概要

共有機能(UI」で言うとのやつ)を実装するために試行錯誤した結果、採用した方法を記します。

結論

esys_flutter_shareを使って簡単に実装できました。

ちなみに他に試したのは下記です。
- Platform Channelsを使ってネイティブのコードに処理を委譲(外部アプリを起動)する。
- shareプラグイン(画像のシェアができず断念)

設定

pubspec.yaml
dependencies:
  esys_flutter_share: [version]

flutter pub getを忘れずに

コード

impportして

import 'package:esys_flutter_share/esys_flutter_share.dart';

この2行だけで終わり!

final ByteData bytes = await rootBundle.load('assets/image.png');
      await Share.file('タイトル', 'ファイル名', bytes.buffer.asUint8List(), 'image/png', text: '本文');

複数ファイルのシェアやHTTP経由で取得した画像のシェアも簡単にできるようです。

参考

pub.dev

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

【Unity】iOSのLaunchScreenにStoryboardを使う

iOSのLaunch Screenの設定方法はいくつかありましたが、iOS13以降のApp Storeに提出されるすべてのAppは、Storyboardを使う必要があるようです。
App StoreにiPhone Appを提出する:Apple公式

UnityではすでにStoryboardに対応していて、Unity 2019.3以降では、Storyboardが自動生成できるようになりました。ただ、設定方法が少しトリッキーでハマってしまったので、設定方法をメモしておきます。

LaunchScreenにStoryboardを使う方法

まず、Unityの「Project Settings」→「Player」の「Splash Screen」の項目を開きます。
デフォルトでは色々チェックが入っていますが、チェックをすべて外して、下の状態にします。
「Use Storyboard for Launch Screen」にチェックを入れたい衝動に駆られますが、それはワナです。気をつけて。
スクリーンショット 2020-08-24 13.23.27.png
「Launch screen type」の項目で、下記のどちらかを選択するとLaunch Screenに表示する画像を指定できます。

Launch screen type 説明
Image and background(relative size) スクリーンに合わせて画像が引き伸ばされます。
Image and background(constant size) 画像サイズを指定できます。

現時点では「Custom XIB」も選択できますが、将来的には非推奨になると思われます。
スクリーンショット 2020-08-21 17.37.45.png
適当なLaunchScreen用の画像を入れました。
この設定でビルドすると、Storyboardが自動生成されます。

Image and background(relative size)

全画面を使うスプラッシュに良さそう。
スクリーンショット 2020-08-21 17.39.10.png

Image and background(constant size)

Appアイコンを使う場合は、こっちが良さそう。
スクリーンショット 2020-08-21 18.08.48.png
やり方が分かると、とても簡単ですね。

ハマりどころ

私がハマったのは、上記にあった「Use Storyboard for Launch Screen」です。
UnityがStoryboardファイルを自動生成してくれるのかと思いチェックを入れたのですが、ビルドエラーが出るようになりました。
スクリーンショット 2020-08-24 13.18.44.png

ArgumentException : source
Parameter name: The source path cannot be empty.

調べてみると、これは「Xcodeで作ったStoryboardファイルを使う」という項目で、Storyboardファイルの指定が必須だったようです。うーん、これは分かりづらい。

ハマる人が大量発生しそうなので、改善してほしいですね。

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

ViewとしてのSwiftUI

SwiftUIを今までのXibやStoryboardの代わりとして使うための方法をまとめました。
Playgroundで実行します。
ViewControllerを使わない、SwiftUIとModelだけのアーキテクチャにはまだ慣れないですが、コードレイアウトとしてのSwiftUIはメリットが大きいので、取り入れていきたいです。

登場人物と関係

クラス 説明
MyViewController SwiftUIで作ったViewを表示するためのViewController
ContentView SwiftUI
ContentViewDelegate ボタンをタップした時のdelegate
ViewModel タップ回数の変数を格納するObservableObject

それぞれの主な役割は以下の図です。

SwiftUI+ViewController.jpg

基本的なMVCです。

実装

import UIKit
import PlaygroundSupport
import SwiftUI

class ViewModel: ObservableObject {

    @Published var count: Int = 0

    func increment() {
        count += 1
    }

}

protocol ContentViewDelegate: AnyObject {
    func didTapAdd()
}

struct ContentView: View {

    @ObservedObject var viewModel: ViewModel = .init()

    weak var delegate: ContentViewDelegate?

    var body: some View {
        VStack(spacing: 16) {
            Text("count : \(self.viewModel.count)")
            Button.init("Add") {
                debugPrint("Add")
                self.delegate?.didTapAdd()
            }
        }
    }

}

class MyViewController : UIViewController {

    private var contentView = ContentView.init()

    override func loadView() {
        super.loadView()
        view.backgroundColor = .white

        contentView.delegate = self

        let hostingVC = UIHostingController.init(rootView: contentView)
        hostingVC.view.frame = view.bounds
        view.addSubview(hostingVC.view)

        debugPrint(view.subviews)

        self.view = view

    }
}

extension MyViewController: ContentViewDelegate {

    func didTapAdd() {
        debugPrint("didTapAdd")
        contentView.viewModel.increment()
    }

}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

ソース
https://gist.github.com/usk2000/394d70cba327e095f2b8002542721697

画面

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

AppleMusic連携で曲の固有のIDからMPMediaItemを取得したり曲を再生する時の知見

Media Player のFrameworkを使ってAppleMusic連携のアプリを作りたい。
しかしこんなんドキュメント見ても分からんだろっていう落とし穴があったりするので、いろいろ試してわかった知見を書く。

曲固有のID

単一の曲を表すMPMediaItemにはいろいろとプロパティがあるが、曲を区別するためのIDは次の二つがある。似ているようで役割が違う。

persistentID

自分のライブラリ内の固有のID(と思われる)
このIDがあれば、MPMediaItemは取得できる。

次のように、MPMediaPropertyPredicate,MPMediaQueryを使ってpersistentIDからMPMediaItemを取得する。
MPMediaItemPropertyPersistentIDというプロパティを使って絞り込もう。

private func findMPMediaItem(persistentIDString: String) -> MPMediaItem? {
        let predicate = MPMediaPropertyPredicate(value: persistentIDString, forProperty: MPMediaItemPropertyPersistentID)
        let songQuery = MPMediaQuery()
        songQuery.addFilterPredicate(predicate)

        if let items = songQuery.items {
            return items.first
        } else {
            return nil
        }
}

playbackStoreID

こちらはApple MusicやiTunes全体で固有のID(と思われる)

iTuneSearchAPIを例にすると、このようにAPIを叩けば曲の情報が取得できる。

$ curl "https://itunes.apple.com/lookup?id=1513201190" | jq

また、Apple Music APIでも曲の情報を取得できるが、こちらはtokenが必要だったりするので省略する。

ただしこのIDからMPMediaItemは取得できない!!?

↑わたしが調べる限り2020年8月時点では不可能っぽい。使えるようになったら教えてほしい。?

MPMediaItemPropertyPlaybackStoreIDといういかにも使えそうなプロパティはあるのだが、使うと次のようなエラーで怒られるぞ。stackoverflowにある通り、現状は取得する手段がない。

"MPMediaPropertyPredicate cannot filter using the playbackStoreID property"

曲を再生したい場合は?

persistentIDを使う場合

曲を再生する時はMPMusicPlayerControllerのこれをつかう

 open func setQueue(with itemCollection: MPMediaItemCollection)

次のようにMPMediaItemからMPMediaItemCollectionを作って、セットすれば再生できる

//itemsは↑みたいな方法で取得しとく
let mediaItemCollection = MPMediaItemCollection(items: items)
let musicPlayer: MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer
musicPlayer.setQueue(with: mediaItemCollection)

playbackStoreIDを使う場合

こちらの場合、MPMediaItemは取得できない

しかし曲の再生はできる。
MPMusicPlayerControllerのこれをつかう

open func setQueue(with storeIDs: [String])

次のように書けばok

let musicPlayer: MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer
musicPlayer.setQueue(with: ["1513201190"])

まとめ

persistentIDは自分のライブラリ固有のIDで、MPMediaItemを取得できる。
playbackStoreIDはApple MusicやiTunes全体で固有のIDで、API利用して曲の情報を取得できる。

Apple Music APIを使ったアプリを作りたい時はplaybackStoreIDを使うと良さそう。
Swiftでネイティブアプリを作りたい時は、persistentIDplaybackStoreIDどう使うか考えると良さそう。

参考

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