- 投稿日:2020-08-24T21:02:10+09:00
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/InjectionIII1.2. サンプルをダウンロードする
以下、Injection IIIのホームページである。
http://johnholdsworth.com/injection.html以下より、サンプルプログラムをダウンロードできる。
http://johnholdsworth.com/GettingStarted.zip
解凍し、「~/dev/GettingStarted/
」となるようにする。1.3. Xcodeからサンプルを開く
GettingStarted.xcodeproj
をダブルクリックして起動。→Open1.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.swiftclass 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.swiftfunc 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.swiftclass 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.swiftbutton.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/InjectionTestdone!
- 投稿日:2020-08-24T19:28:03+09:00
【Flutter, Dart, freezed】Bad state: Unexpected diagnosticsでbuildが終わらない不具合
TL;DR
pubspec.yaml
に以下の2つを追加してくださいpubspec.yamldependencies: 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.yamldependencies: 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... 0msbuildし直す
# 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を使用されていない場合は記述不要です!
- 投稿日:2020-08-24T19:23:00+09:00
iOSアプリで音楽や動画のバックグラウンド再生の実装方法(他アプリ音声を停止しないやり方も)
AVPlayerやAVAudioPlayerなどで音声再生や動画再生の実装をする場合デフォルトのままだとアプリをバックグラウンドへ移行したりスリープをした時に再生が自動で停止します。
以下の実装を行うことで再生を継続させることができます。簡単な手順でOKです。
手順1
- プロジェクトファイルを開く
- メニュー -> Editor -> Add Capability -> Background Modesを選択
- Audio, AirPlay, and Picture in PictureのチェックマークをONにする
手順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
- 投稿日:2020-08-24T19:04:10+09:00
[Swift] textFieldのclearButtonMode
textField
は中身を全部消すことができるclearButton
が実装できます。
画像の右端にあるバツマークです
これの表示パターンは
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 = .whileEditingstoryboard上での実装
textFieldのClearButtonの部分を変更すれば実装できます
- 投稿日:2020-08-24T18:58:18+09:00
【iOS14】WidgetKitのテンプレートを読み解く
はじめに
9月リリースが噂されているiOS14から、Widgetが追加されます。
表現が固定されていたホーム画面での体験が大きく変わる機能であり、
多くの人が注目しているかと思います?✨よし!自分のアプリにもWidgetを追加しよう!と手を動かそうとしている皆さんに向けて、
既存アプリへの追加手順の紹介と、テンプレートコードの読み解きをします。本記事は、最低限の挙動と実装を確認したい人に向けての解説記事です。
WidgetKitの利用シーンやWidgetKitの詳しい解説はこちらの記事に丁寧にまとまっているので、割愛させていただきます。
https://qiita.com/shiz/items/309349d9cdb75084e74e※本記事はXcode12 beta5時点の情報です、beta版の開発画面のスクリーンショットはNDA締結により掲載しておりません。
1.追加手順
Xcodeを開き
- File > New > Target を選択
- Application Extension group セクションから Widget Extension を選択
- 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のサンプルコードも用意されているのでぜひぜひ一度動かしてみて、
ユーザにとって需要のある機能実装の模索をしてみましょう?
- 投稿日:2020-08-24T18:00:11+09:00
iPhoneX系の画面の下のバー(Home Indicator)を非表示にする方法
画面の下にいつもいるバー
Home Indicatorという名前です。
iPhoneXが発売されて数年、私は知りませんでした。
全画面でコンテンツを表示したい時にこのバーだけ表示されてしまっていると見た目が悪いですよね。但し常に非表示にしておくということはできませんが操作をしていない場合に非表示になってくれる実装方法があります。
非表示にしたいViewControllerに以下の一行を追加します。
ViewController.swiftoverride var prefersHomeIndicatorAutoHidden: Bool { true }公式ドキュメント:
prefersHomeIndicatorAutoHidden常に表示するという状態と、操作していない場合は非表示にしたい、という二つの状態を切り替えたいというときは以下のような実装をします。
ViewController.swiftprivate var indicatorAutoHidden:Bool = false { didSet { self.setNeedsUpdateOfHomeIndicatorAutoHidden() } } override var prefersHomeIndicatorAutoHidden: Bool { return self.indicatorAutoHidden }setNeedsUpdateOfHomeIndicatorAutoHiddenを呼び出すことですぐさま表示、非表示状態の更新を切り替えることができます。
公式ドキュメント:
setNeedsUpdateOfHomeIndicatorAutoHidden終わりです
Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp
- 投稿日:2020-08-24T17:42:14+09:00
【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 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これで、アップロードしたファイル一式がホスティング上から削除されます。
- 投稿日:2020-08-24T16:44:54+09:00
Literal について
- 投稿日:2020-08-24T15:12:32+09:00
【Flutter/ Dart】外部アプリを起動して画像をシェアする方法
概要
共有機能(UI」で言うと
<
のやつ)を実装するために試行錯誤した結果、採用した方法を記します。結論
esys_flutter_share
を使って簡単に実装できました。ちなみに他に試したのは下記です。
- Platform Channelsを使ってネイティブのコードに処理を委譲(外部アプリを起動)する。
- shareプラグイン(画像のシェアができず断念)設定
pubspec.yamldependencies: 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経由で取得した画像のシェアも簡単にできるようです。
参考
- 投稿日:2020-08-24T13:59:33+09:00
【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」にチェックを入れたい衝動に駆られますが、それはワナです。気をつけて。
「Launch screen type」の項目で、下記のどちらかを選択するとLaunch Screenに表示する画像を指定できます。
Launch screen type 説明 Image and background(relative size) スクリーンに合わせて画像が引き伸ばされます。 Image and background(constant size) 画像サイズを指定できます。 現時点では「Custom XIB」も選択できますが、将来的には非推奨になると思われます。
適当なLaunchScreen用の画像を入れました。
この設定でビルドすると、Storyboardが自動生成されます。Image and background(relative size)
Image and background(constant size)
Appアイコンを使う場合は、こっちが良さそう。
やり方が分かると、とても簡単ですね。ハマりどころ
私がハマったのは、上記にあった「Use Storyboard for Launch Screen」です。
UnityがStoryboardファイルを自動生成してくれるのかと思いチェックを入れたのですが、ビルドエラーが出るようになりました。
ArgumentException : source Parameter name: The source path cannot be empty.調べてみると、これは「Xcodeで作ったStoryboardファイルを使う」という項目で、Storyboardファイルの指定が必須だったようです。うーん、これは分かりづらい。
ハマる人が大量発生しそうなので、改善してほしいですね。
- 投稿日:2020-08-24T10:27:06+09:00
ViewとしてのSwiftUI
SwiftUIを今までのXibやStoryboardの代わりとして使うための方法をまとめました。
Playgroundで実行します。
ViewControllerを使わない、SwiftUIとModelだけのアーキテクチャにはまだ慣れないですが、コードレイアウトとしてのSwiftUIはメリットが大きいので、取り入れていきたいです。登場人物と関係
クラス 説明 MyViewController SwiftUIで作ったViewを表示するためのViewController ContentView SwiftUI ContentViewDelegate ボタンをタップした時のdelegate ViewModel タップ回数の変数を格納する ObservableObject
それぞれの主な役割は以下の図です。
基本的な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
- 投稿日:2020-08-24T02:06:38+09:00
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でネイティブアプリを作りたい時は、persistentID
とplaybackStoreID
どう使うか考えると良さそう。参考