20200518のSwiftに関する記事は13件です。

【Swift】Optional型を安全に扱う5つの方法

Optional型とは

nilを代入できる型のこと。

let a: Int? = Int("42") // Optionalで宣言
let b: Optional<Int> = Int("42") // 上と同義

ただし他の型の変数に代入したりする場合、unwrapという処理が必要になる。
その際、nilが代入されている可能性があるので慎重に扱わなければなりません。
ここではOptional型を安全にunwrapして扱うための5つの方法をまとめました。

1. Force Unwrapping

変数の中身がなんであれとにかくunwrapする。
もしnilだった場合はruntime errorとなってしまうため、nilでないことが確かな場合に使うと良い。
"Unconditional Unwrapping"とも言うらしい。

let myOptioal: String? // Optional<String>で宣言
myOptional = "Angela" // 文字列を代入
print(myOptional!) // "Angela"が出力される
let myOptioal: String? // Optional<String>で宣言
myOptional = nil // nilを代入
print(myOptional!) // runtime errorとなる

2. Check for nil value

if statementでunwrapしようとしている変数の中身がnilかどうか判定し、trueの場合の処理でunwrapをする。

let myOptioal: String? // Optional<String>で宣言
myOptional = nil // nilを代入

if myOptional != nil {
  print(myOptional!) // nilでない場合のみunwrapするので安全
} else {
  print("myOptional was found to be nil") // nilだった場合はこちらの処理が実行される
}

unwrapする前にnilかどうか判定するので安全な一方で、毎回unwrapしなくてはならないので面倒。そんなときは次の"Optional Binding"が有効。

let myOptioal: String? // Optional<String>で宣言
myOptional = nil // nilを代入

if myOptional != nil {
  let text1: String = myOptional!
  let text2: String = myOptional!
  let text3: String = myOptional! // 毎回unwrapする必要がある
} else {
  print("myOptional was found to be nil")
}

3. Optional Binding

if statementで新たな定数にOptional型の変数を代入する。nilでない場合はif let safeOptional = myOptionalの判定はtrueになり、nilの場合は判定の結果がfalseになる。ブロックの中では非OptionalのString型であるsafeOptionalが使えるのでunwrapは不要となるので便利。

let myOptioal: String? // Optional<String>で宣言
myOptional = nil // nilを代入

if let safeOptional = myOptional { 
  // myOptionalがnilでない場合はtrueになりここの処理が実行される
  let text1: String = safeOptional
  let text2: String = safeOptional
  let text3: String = safeOptional //条件の箇所で宣言したsafeOptionalは非OptionalのStringなのでunwrapは不要
} else {
  // myOptionalがnilの場合はfalseになりここの処理が実行される
  print("myOptional was found to be nil")
}

4. Nil Coalescing Operator

Optional型の変数の中身がnilの場合にデフォルト値を与えたい場合に有効です。

COALESCEといえばSQLの関数にもありますね。"NULL以外の最初の引数を返す"というものです。
SwiftのNil Coalescing Operatorという演算子も同様の考え方が使えます。

optional ?? defaultValue // optionalがnilでない場合はoptinalを、nilの場合はdefaultValueを返す
let myOptioal: String? // Optional<String>で宣言
myOptional = "Angela" // 文字列を代入

let text: String = myOptional ?? "I am the default value"
print(text) // "Angela"が出力される
let myOptioal: String? // Optional<String>で宣言
myOptional = nil // nilを代入

let text: String = myOptional ?? "I am the default value"
print(text) // "I am the default value"が出力される

5. Optional Chaining

これまではOptional型のStringなどを見てきましたがstructのインスタンスの場合はどうでしょうか。Optional型のユーザー定義のstructを格納する変数や定数が宣言されている場合にも安全にunwrapしたいです。これにはまた別の方法が用意されています。

struct MyOptional {
  var property = 123
  func method() {
    print("I am the struct's method.")
  }
}

let myOptional: MyOptional? // Optional<MyOptional>で宣言
myOptional = MyOptional() // MyOptionalのインスタンスを代入

print(myOptional?.property) // 123が出力される
myOptional?.method() // "I am the struct's method."が出力される
struct MyOptional {
  var property = 123
  func method() {
    print("I am the struct's method.")
  }
}

let myOptional: MyOptional? // Optional<MyOptional>で宣言
myOptional = nil // nilを代入

print(myOptional?.property) // myOptionalはnilなのでpropertyにはアクセスしない
myOptional?.method() // myOptionalはnilなのでmethodは実行しない

まとめ

Optional型はSwiftの便利な点であると同時に混乱することが多い箇所だと思うので、これらを状況によって使い分けることが大事ですね。

参考

https://developer.apple.com/documentation/swift/optional
https://www.udemy.com/course/ios-13-app-development-bootcamp/

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

【Swift】Realm Listのシャッフル

他は知らないがSwiftのRealmはshuffleがサポートされていない。

自分でシャッフルする。
それだけの記事。

func shuffle() {
    for _ in array {
        let swap1 = Int.random(in: 0..<array.count)
        let swap2 = Int.random(in: 0..<array.count)
        try! realm.write {
            list.swapAt(swap1, swap2)
        }
    }
}

まぁswapする回数はお好み。

どうやってシャッフルすればいいか結局小一時間悩んだ。
クソ時間の無駄ですね!!!

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

Swift the realm is already in a write transaction

the realm is already in a write transaction と言われたとき。

realmの処理が落ちきっていないんですかね。処理自体は完遂されていると思うんですが。
僕にはわからん。

try! realm.write {
    /* ... */
    realm.cancelWrite() // 処理を止めて
    realm.beginWrite() // また始める
    /* ... */
}

これでエラーが出なくなった。
エラーが起こる原因もこれで通る理由もよくわかっていないのは釈然しないがどうしてもというときは。

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

【Swift】Error, failed to read Core Data data model from...でつまづいた話

技術は熱いうちに打て | 【Swift】Error, failed to read Core Data data model from…でつまづいた話

概要

掲題の通りですが、一度追加したCoreDataの定義ファイルの名前を変更したかったので削除し、
ファイル名を変えたものをもう一度追加した時に発生しました。

直し方

https://stackoverflow.com/questions/25589138/compilation-failed-for-data-model-at-path

こちらが非常に参考になりました。

xcdatamodeldのあるフォルダを直接Finderで表示して、
元ファイルを削除したら解決しました。

誰かのお役に立てば。

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

[iOS] UIWebView APIが含まれているライブラリを調べる方法

前置き

本記事執筆時点の2020年5月現在、
新規アプリはUIWebView APIが含まれているとすでに受付停止されており、
アップデートは2020年12月から受付停止です。

参考リンク:
[iOS] UIWebViewがいよいよヤバいらしい("ITMS-90809: Deprecated API Usage"メールが届いた件)
[速報] [iOS] UIWebViewが使えなくなる最終期限が告知されました

使用ライブラリの中にUIWebView APIが含まれているかどうかの調査、
特に、手動で導入しているサードパーティ製ライブラリ(すなわちGitHub等でPublicな情報が出ていないもの)の調査は少々面倒かと思います。

以下、私たちのチームでUIWebView APIが含まれているライブラリを調査した方法をシェアします

本題

Macのターミナルで、以下のコマンドを実行

nm {ライブラリ導入フォルダ}/{ライブラリ名}.framwork/{ライブラリ名} | grep UIWeb
および(または)
nm {ライブラリ導入フォルダ}/{ライブラリ名}.a | grep UIWeb

そうすると、該当ライブラリのシンボル内にUIWebViewに関連する箇所がある場合には以下の様に出力されます。

U OBJC_CLASS$_UIWebView

UIWebView APIが含まれていなければ、何も出力されません。

以上、簡単ですが、これから同様の調査をされる方に、何かの参考になればと思います。

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

iOS13以降のUIActivityViewControllerでUIImageのプレビューをなんとかしたい

UIActivityViewControllerでUIImageのプレビューをなんとかしたい

動機

自作アプリでUIActivityViewControllerを使ってみたけど、サムネイルがアプリアイコンになってしまう。
「メモ」アプリみたいに、シェアする画像をサムネイルに使いたい。

https://qiita.com/ezura/items/6036c6e100599b601482

を読んでも、コードがよく分からなかったので、

https://stackoverflow.com/questions/57850483/ios13-share-sheet-how-to-set-preview-thumbnail-when-sharing-uiimage

を参考にしてやってみた。

やり方

LinkPresentation frameworkをインポート

import LinkPresentation

UIViewControllerに変数を宣言する

var shareImage: UIImage? //シェアするイメージ

UIActivityViewControllerの呼び出し

[shareImage, self]をactivityItemsにセットする
selfをセットするのは、ViewControllerにUIActivityItemSourceを実装してあげるといいらしいので.
shareImage = シェアするイメージ
let share = UIActivityViewController(activityItems: [shareImage, self], applicationActivities: nil)
present(share, animated: true, completion: nil)

UIViewControllerに、UIActivityItemSourceのメソッドを実装してあげる

extension viewController : UIActivityItemSource {
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return nil
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let imageProvider = NSItemProvider(object: shareImage!)
let metadata = LPLinkMetadata()
metadata.imageProvider = imageProvider
return metadata
}
}

これで、サムネイルがうまく表示される。
UIActivityItemSourceを扱うclassを作れるとさらに良さげ。

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

【Alamofire】Module 'Alamofire' has no member named 'request' というエラーが出る

Alamofire.request(route).responseJSONDecodable { response in }

これを書いて普通にAlamofireを使おうと思ったら

Module 'Alamofire' has no member named 'request'

そんなん無いよと怒られました。

Alamofireは5系からの変更に注意

Alamofireは5系からメソッドの命名とかが結構変わっています。
こういう変更は勘弁してもらいたい。

基本的に「Alamofire」と記述していた部分は「AF」に変わったみたいです。
上記の例だと

AF.request(route).responseJSONDecodable { response in }

こうすれば動くようになります。

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

Flutterで演奏アプリを作った

音楽の知識がなくても誰でも曲を演奏したり作曲したりできるアプリ(伴奏のみ)をFlutterで作成しました。

技術的に共有できる部分を紹介していきたいと思います。

使い方

基本的な使い方としては並んでいる大きなボタンをタップするだけです。これで選択したコードが勝手に再生されます。

今回のアプリで重視したのは、音楽について何もわからない人でも、子どもでも誰でも曲が演奏&作曲出来てしまうという部分です。実際にうちの7歳児も適当にボタンを押しながら左右にリズムをとって何やらオリジナルの曲を口ずさんでいたので恐らくこの部分は達成できているのではないかと思います。

並んでいるコードは一つの曲を演奏するのに必要な最低限のコードです。これさえあれば基本的な曲は大体演奏できますし、作ることも出来ます。メイン、サブはどんな順番に使ってもだいたいいい感じの流れになるようになっています。右下の上下ボタンでこの曲調は変更もできます。

演奏の保存

いい感じに演奏ができたら下に「直前の演奏を保存する」ボタンがありますのでそれを実行すれば保存し、後でいつでも再生することが出来ます。

image.png

事前に録音ボタンを押してスタートする必要もないため、ふと思いついた時に必ずすぐ保存することが出来ます。

テンポやストロークの変更

テンポはスライダーでいつでもすぐ変更できます。

image.png

ストロークもいくつかの基本的なストロークパターンから選択することが出来ます。

image.png

あとは跳ねるパターンなど、いくつか追加予定です。最終的にはユーザーが自分で作れるようにできると良いかなと思っています。

作り方

作り方を説明していきます。

音の再生

Flutterでは音の再生はできません。iOS、Androidのネイティブ側と連携させて作成する必要があります。これは普通にドキュメントにもあります。

Writing custom platform-specific code - Flutter

あとは普段使われているFlutterのプラグイン等でネイティブの機能を使っているものなども基本的には同様の方法で作られています。誰もがよく使うネイティブの機能はこのようにプラグインとして作成されて、みんながDart側だけの開発でアプリが作れてしまうようになっています。

ネイティブとの連携方法

具体的には、Dart側にはこのように書きます。MethodChannelに識別子を指定し、invokeMethodでネイティブ側に命令するだけです。

class Player {
  static const platform =
      const MethodChannel('com.example.anyone-composer/midi');

  Future setTempo(int tempo) async {
    await platform.invokeMethod('setTempo', {'tempo': tempo});
  }
}

以下はAndroid、iOSでの受け取り方ですが、これもシンプルです。ドキュメントにあるように型も色々使えます。

Android側の処理

Android側で上記の処理を受け取る例です。

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    GeneratedPluginRegistrant.registerWith(flutterEngine);

    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.anyone-composer/midi").setMethodCallHandler {
        call, result ->
        if (call.method == "setTempo") {
            val tempo: Int? = call.argument<Int>("tempo")
            // nanikasuru
            result.success(true)
        }
    }
}

iOS側の処理

iOS側で処理を受け取る例です。

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let midiChannel = FlutterMethodChannel(name: "com.example.anyone-composer/midi",
                                              binaryMessenger: controller.binaryMessenger)
    midiChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "setTempo" {
        if let map = call.arguments as? Dictionary<String, Int>,
          let tempo = map["tempo"] {
          // nanikasuru
        }
        result(true)
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

音声の再生

sf2という拡張子のサウンドフォントファイルを利用してMidiを再生しています。実はFlutterのプラグインには元々Midiを再生するためのプラグインがあったります。

flutter_midi/FlutterMidiPlugin.java at master · rodydavis/flutter_midi

しかしこれだと今回作成したい機能としては不足していました。例えば音を設定したり、音程を指定したり、音を一斉に停止したりする機能が実装されていなかったりしました。そのためこれを参考に独自に作成していくことにしました。ですので基本的な再生の仕組みについては上記のネイティブ側のコードを見るとわかります。使われている機能のドキュメントやサンプルはほとんどなかったので拡張は苦労しましたが……。

注意点

ネイティブ側に入るとスレッドの概念も出てきます。例えば

重い処理を行う場合だったり……

Flutterのネイティブで重い処理をさせる方法(Android) - Crieit

タイマーなどで非同期処理を行う場合だったり……

Swift マルチスレッドでの同期処理(synchronized)

というのを気をつける必要があります。

再生アニメーションの描画

演奏中、再生中はストローク中の今どこを再生しているかをアニメーションで表示しています。赤いラインが右に流れていくだけのアニメーションです。

image.png

これはFlutterにあるCustomPaintという機能を利用しています。CustomPaintはCustomPainterというクラスをオーバーライドした独自の描画を作成したクラスを渡すことで、Canvasに何でも自由に描画することができるウィジェットです。必要なUIを実現するためのウィジェットが見つからない時はこれで何でも作成できます。

ウィジェットの利用は例えばこんな感じでシンプルです。

child: CustomPaint(
  painter: StrokeAndLinePainter(
    context,
    Data.strokeSets[playerStore.strokeIndex],
    playerStore.tempo,
    timeStore.time,
  ),
),

実際のPainterクラスの描画例です。色々処理が入っているのでごちゃごちゃしていますが、要するに canvas.drawなんちゃら のようなものを使って描画するだけです。

  @override
  void paint(Canvas canvas, Size size) {
    strokes.asMap().forEach((index, velocity) {
      final paint = Paint();
      paint.color = Theme.of(context).accentColor;
      final rect = Rect.fromLTWH(
        horizontalMargin +
            (size.width - horizontalMargin * 2) / strokes.length * index,
        margin,
        barWidth,
        size.height - margin * 2,
      );
      canvas.drawRect(rect, paint);
    });
  }

アニメーションの方法

CustomPaintはあくまでも独自の描画を行うための仕組みであり、アニメーションの機能を提供しているわけではありません。Flutter自体がそもそも状態に応じた描画をするためのものですので、単なる描画ウィジェットでは対応ができません。そのため、アニメーションの処理は自分で作る必要はあります。

具体的にはTimerを使ってstateを更新していっています。まずはタイマーを作成します。

      Timer.periodic(Duration(milliseconds: 16), _onTimer);

色々省略していますが、更新しているところです。時間はネイティブ側から受け取り、それをセットしています。

  Future _onTimer(Timer timer) async {
    final timeStore = Provider.of<TimeStore>(context, listen: false);
    final time = await _player.getTime();
    timeStore.setTime(time);
  }

これにより高頻度で画面全体を更新してしまわないように、専用のStoreを作り、アニメーション部分だけを再描画するようにしています。

  Widget _buildStrokePaint() {
    return Consumer2<PlayerStore, TimeStore>(builder: (
      context,
      playerStore,
      timeStore,
      _,
    ) {
      return CustomPaint(
        painter: StrokeAndLinePainter(
          context,
          Data.strokeSets[playerStore.strokeIndex],
          playerStore.tempo,
          timeStore.time,
        ),
      );
    });
  }

電池消費を抑えられたりと、恐らくユーザーに優しいはず……。他にも色々と同様のことを行う方法はあると思います。

演奏データの保存

演奏データの保存は、実際に音声を保存しているわけではなく、単にコードを変更したタイミングとコードを保存しているだけです。そのため非常にデータとしても軽量で、SQLiteに保存しています。わざわざ録音スタートボタンを押さなくてもいい、というのもとりあえず毎回演奏データをstate上に保存しておけるからです。

再生時にはそのタイミングで音を変えたりしているだけです。ですので、再生時に違うテンポや違うストロークで再生することも可能です。

今回はこんな感じでシンプルなベースモデルを作成し、全てのモデルでこれを使うようにしてみました。

abstract class Model {
  final String table = '';
  int id;

  Map<String, dynamic> toMap();

  Future insert() async {
    final db = await DbProvider.instance.database;
    id = await db.insert(table, toMap());
  }

  Future update() async {
    final db = await DbProvider.instance.database;
    await db.update(table, toMap(), where: 'id = ?', whereArgs: [id]);
  }

  Future delete() async {
    final db = await DbProvider.instance.database;
    await db.delete(table, where: 'id = ?', whereArgs: [id]);
  }
}

実際に使うモデルはこれを継承し、tableを指定してtoMapを実装します。あとは下記のような感じでデータを操作できるようになります。

Record record = Record();
record.name = 'hoge';
await record.insert();

record.name = 'hoge2';
await record.update();

await record.delete();

本格的な業務アプリだときついかもしれませんが、これくらいの小さいアプリなら十分汎用的で楽になると思います。

今後の展望

下記のような機能も作っていきたいと思います。どれも大変そうですので時間は掛かりそうですが。

  • 音声を録音して演奏に乗せられるように
  • ストロークを自分でカスタマイズできるように
  • 保存した演奏を編集できるように

まとめ

以上、Flutterで実際にアプリを作ってリリースしてみた話です。ネイティブ連携、CustomPaintとちょっと面白いものも使えたので今後も色々とできることの幅が広がった気がします。

今回はメインの機能がネイティブに依存しているため、そもそも両方ネイティブで開発したほうがいいのでは、とも思いました。しかしUI部分を全部共通にできるのはやはり大きなメリットであり、個人での開発ではやはりかなりの工数削減につながったと思います。

今後もFlutterでアプリを100個目指して作っていきたいと思います。

何か参考になる部分などあればぜひLGTMよろしくおねがいします!

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

Flutterで音楽の知識がなくても演奏できるアプリを作った

音楽の知識がなくても誰でも曲を演奏したり作曲したりできるアプリ(伴奏のみ)をFlutterで作成しました。

技術的に共有できる部分を紹介していきたいと思います。

使い方

基本的な使い方としては並んでいる大きなボタンをタップするだけです。これで選択したコードが勝手に再生されます。

今回のアプリで重視したのは、音楽について何もわからない人でも、子どもでも誰でも曲が演奏&作曲出来てしまうという部分です。実際にうちの7歳児も適当にボタンを押しながら左右にリズムをとって何やらオリジナルの曲を口ずさんでいたので恐らくこの部分は達成できているのではないかと思います。

並んでいるコードは一つの曲を演奏するのに必要な最低限のコードです。これさえあれば基本的な曲は大体演奏できますし、作ることも出来ます。メイン、サブはどんな順番に使ってもだいたいいい感じの流れになるようになっています。右下の上下ボタンでこの曲調は変更もできます。

演奏の保存

いい感じに演奏ができたら下に「直前の演奏を保存する」ボタンがありますのでそれを実行すれば保存し、後でいつでも再生することが出来ます。

image.png

事前に録音ボタンを押してスタートする必要もないため、ふと思いついた時に必ずすぐ保存することが出来ます。

テンポやストロークの変更

テンポはスライダーでいつでもすぐ変更できます。

image.png

ストロークもいくつかの基本的なストロークパターンから選択することが出来ます。

image.png

あとは跳ねるパターンなど、いくつか追加予定です。最終的にはユーザーが自分で作れるようにできると良いかなと思っています。

作り方

作り方を説明していきます。

音の再生

Flutterでは音の再生はできません。iOS、Androidのネイティブ側と連携させて作成する必要があります。これは普通にドキュメントにもあります。

Writing custom platform-specific code - Flutter

あとは普段使われているFlutterのプラグイン等でネイティブの機能を使っているものなども基本的には同様の方法で作られています。誰もがよく使うネイティブの機能はこのようにプラグインとして作成されて、みんながDart側だけの開発でアプリが作れてしまうようになっています。

ネイティブとの連携方法

具体的には、Dart側にはこのように書きます。MethodChannelに識別子を指定し、invokeMethodでネイティブ側に命令するだけです。

class Player {
  static const platform =
      const MethodChannel('com.example.anyone-composer/midi');

  Future setTempo(int tempo) async {
    await platform.invokeMethod('setTempo', {'tempo': tempo});
  }
}

以下はAndroid、iOSでの受け取り方ですが、これもシンプルです。ドキュメントにあるように型も色々使えます。

Android側の処理

Android側で上記の処理を受け取る例です。

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    GeneratedPluginRegistrant.registerWith(flutterEngine);

    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.anyone-composer/midi").setMethodCallHandler {
        call, result ->
        if (call.method == "setTempo") {
            val tempo: Int? = call.argument<Int>("tempo")
            // nanikasuru
            result.success(true)
        }
    }
}

iOS側の処理

iOS側で処理を受け取る例です。

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let midiChannel = FlutterMethodChannel(name: "com.example.anyone-composer/midi",
                                              binaryMessenger: controller.binaryMessenger)
    midiChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "setTempo" {
        if let map = call.arguments as? Dictionary<String, Int>,
          let tempo = map["tempo"] {
          // nanikasuru
        }
        result(true)
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

音声の再生

sf2という拡張子のサウンドフォントファイルを利用してMidiを再生しています。実はFlutterのプラグインには元々Midiを再生するためのプラグインがあったります。

flutter_midi/FlutterMidiPlugin.java at master · rodydavis/flutter_midi

しかしこれだと今回作成したい機能としては不足していました。例えば音を設定したり、音程を指定したり、音を一斉に停止したりする機能が実装されていなかったりしました。そのためこれを参考に独自に作成していくことにしました。ですので基本的な再生の仕組みについては上記のネイティブ側のコードを見るとわかります。使われている機能のドキュメントやサンプルはほとんどなかったので拡張は苦労しましたが……。

注意点

ネイティブ側に入るとスレッドの概念も出てきます。例えば

重い処理を行う場合だったり……

Flutterのネイティブで重い処理をさせる方法(Android) - Crieit

タイマーなどで非同期処理を行う場合だったり……

Swift マルチスレッドでの同期処理(synchronized)

というのを気をつける必要があります。

再生アニメーションの描画

演奏中、再生中はストローク中の今どこを再生しているかをアニメーションで表示しています。赤いラインが右に流れていくだけのアニメーションです。

image.png

これはFlutterにあるCustomPaintという機能を利用しています。CustomPaintはCustomPainterというクラスをオーバーライドした独自の描画を作成したクラスを渡すことで、Canvasに何でも自由に描画することができるウィジェットです。必要なUIを実現するためのウィジェットが見つからない時はこれで何でも作成できます。

ウィジェットの利用は例えばこんな感じでシンプルです。

child: CustomPaint(
  painter: StrokeAndLinePainter(
    context,
    Data.strokeSets[playerStore.strokeIndex],
    playerStore.tempo,
    timeStore.time,
  ),
),

実際のPainterクラスの描画例です。色々処理が入っているのでごちゃごちゃしていますが、要するに canvas.drawなんちゃら のようなものを使って描画するだけです。

  @override
  void paint(Canvas canvas, Size size) {
    strokes.asMap().forEach((index, velocity) {
      final paint = Paint();
      paint.color = Theme.of(context).accentColor;
      final rect = Rect.fromLTWH(
        horizontalMargin +
            (size.width - horizontalMargin * 2) / strokes.length * index,
        margin,
        barWidth,
        size.height - margin * 2,
      );
      canvas.drawRect(rect, paint);
    });
  }

アニメーションの方法

CustomPaintはあくまでも独自の描画を行うための仕組みであり、アニメーションの機能を提供しているわけではありません。Flutter自体がそもそも状態に応じた描画をするためのものですので、単なる描画ウィジェットでは対応ができません。そのため、アニメーションの処理は自分で作る必要はあります。

具体的にはTimerを使ってstateを更新していっています。まずはタイマーを作成します。

      Timer.periodic(Duration(milliseconds: 16), _onTimer);

色々省略していますが、更新しているところです。時間はネイティブ側から受け取り、それをセットしています。

  Future _onTimer(Timer timer) async {
    final timeStore = Provider.of<TimeStore>(context, listen: false);
    final time = await _player.getTime();
    timeStore.setTime(time);
  }

これにより高頻度で画面全体を更新してしまわないように、専用のStoreを作り、アニメーション部分だけを再描画するようにしています。

  Widget _buildStrokePaint() {
    return Consumer2<PlayerStore, TimeStore>(builder: (
      context,
      playerStore,
      timeStore,
      _,
    ) {
      return CustomPaint(
        painter: StrokeAndLinePainter(
          context,
          Data.strokeSets[playerStore.strokeIndex],
          playerStore.tempo,
          timeStore.time,
        ),
      );
    });
  }

電池消費を抑えられたりと、恐らくユーザーに優しいはず……。他にも色々と同様のことを行う方法はあると思います。

演奏データの保存

演奏データの保存は、実際に音声を保存しているわけではなく、単にコードを変更したタイミングとコードを保存しているだけです。そのため非常にデータとしても軽量で、SQLiteに保存しています。わざわざ録音スタートボタンを押さなくてもいい、というのもとりあえず毎回演奏データをstate上に保存しておけるからです。

再生時にはそのタイミングで音を変えたりしているだけです。ですので、再生時に違うテンポや違うストロークで再生することも可能です。

今回はこんな感じでシンプルなベースモデルを作成し、全てのモデルでこれを使うようにしてみました。

abstract class Model {
  final String table = '';
  int id;

  Map<String, dynamic> toMap();

  Future insert() async {
    final db = await DbProvider.instance.database;
    id = await db.insert(table, toMap());
  }

  Future update() async {
    final db = await DbProvider.instance.database;
    await db.update(table, toMap(), where: 'id = ?', whereArgs: [id]);
  }

  Future delete() async {
    final db = await DbProvider.instance.database;
    await db.delete(table, where: 'id = ?', whereArgs: [id]);
  }
}

実際に使うモデルはこれを継承し、tableを指定してtoMapを実装します。あとは下記のような感じでデータを操作できるようになります。

Record record = Record();
record.name = 'hoge';
await record.insert();

record.name = 'hoge2';
await record.update();

await record.delete();

本格的な業務アプリだときついかもしれませんが、これくらいの小さいアプリなら十分汎用的で楽になると思います。

今後の展望

下記のような機能も作っていきたいと思います。どれも大変そうですので時間は掛かりそうですが。

  • 音声を録音して演奏に乗せられるように
  • ストロークを自分でカスタマイズできるように
  • 保存した演奏を編集できるように

まとめ

以上、Flutterで実際にアプリを作ってリリースしてみた話です。ネイティブ連携、CustomPaintとちょっと面白いものも使えたので今後も色々とできることの幅が広がった気がします。

今回はメインの機能がネイティブに依存しているため、そもそも両方ネイティブで開発したほうがいいのでは、とも思いました。しかしUI部分を全部共通にできるのはやはり大きなメリットであり、個人での開発ではやはりかなりの工数削減につながったと思います。

今後もFlutterでアプリを100個目指して作っていきたいと思います。

何か参考になる部分などあればぜひLGTMよろしくおねがいします!

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

こんなソースコードはイヤだ-APIの型=DBの型でいいのかな?

プログラムのソースコードをより良く書く時のポイントをまとめていこうと思います。

sample.swift
  //APIのJSONから取得するユーザ情報
  struct User {
    let id: Int
    let name: String
    let address: String
  }

コードの修正は?

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

「App Extensions」で「Core Data」を使用する

「App Extension」とは?

App extensions は、主要iOSアプリケーションの拡張機能です。例えば、「iMessage Extension」、「Today Extension」、「Siri Extension」などがあります。

Screen Shot 2020-05-12 at 3.38.39 PM.png

「Core Data」とは?

Core Data はAppleのフレームワークであり、ユーザー情報をローカルストレージに格納するのに便利です。また、CloudKitのサポートも組み込まれており、ユーザーたちのすべてのデバイスで変更内容を同期できます。

本日のトピック?

Core Data には、次のコードを使用することで、主要iOSアプリで簡単にアクセスできます:

let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext

ただし、App Extensionからもアクセスできるようにするには、いくつかの追加の手順を行う必要があります。

:bangbang: 警告:この変更は、既存のアプリを更新する時にではなく、未公開のアプリを最初に開発する段階で行ってください。

複数のApp ExtensionにわたるCore data

1. App Group を有効にする

最初に「Signing & Capabilities」タブに進み、「+」アイコンを選択します。

Screen Shot 2020-05-12 at 3.53.18 PM.png

主要iOSアプリとその他のすべてのターゲット向けに「App Group」を追加します。

すべてのターゲットに同じ「App Group」を割り当てます。

Screen Shot 2020-05-12 at 3.55.43 PM.png

2. ヘルパーを追加

まずファイル「*. xcdatamodel」を探します。ファイル名が「Sakura.xcdatamodel」の場合、データベース名が「Sakura」ということです。

import Foundation
import CoreData

class NSCustomPersistentContainer: NSPersistentCloudKitContainer {

    override open class func defaultDirectoryURL() -> URL {
        var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.[App Group Name 名前]")
        storeURL = storeURL?.appendingPathComponent("Sakura.sqlite")
        return storeURL!
    }

}

3. データベースにアクセスする

これで次の方法でデータベースにアクセスできるようになりました:

lazy var persistentContainer: NSPersistentContainer = {
        let container = NSCustomPersistentContainer(name: "Sakura")

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print(error.localizedDescription)
            }
        })
        return container
    }()

let context = persistentContainer.viewContext

警告:再度の警告です。この記事は、複数の拡張機能にわたるデータベースのサポートを新規アプリ向けに追加するためのものです。ユーザーストレージにデータがある場合、アプリのデータを常に復旧できるようにするために、重要な変更を行う前は必ず別途バックアップを取っておくようにしてください。

:sunny:

iOS開発、Swift、プログラミングに関することなら何でも、遠慮なくTwitterで質問してください。
@MaShunzhe

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

[Swift][Eureka]"onCellSelection"でButtonRowは怖くない

UIを実装するためのライブラリEurekaにはrowそのものがbuttonになっているButtonRowというものがあります。
onCellSelectionを使えばButtonRowに好きな処理を持たせることができます。

<<< ButtonRow(){
    $0.onCellSelection{_,_ in
        //ここに任意の処理を書く
    }
}

これでできます。

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

【Swift】RxSwiftでボタンのタップイベントを拾う

RxSwiftはボタンのタップイベントとTableView CollecrionViewの実装が格段に楽になるので大好きです。
今回はボタンのタップイベントの実装をいくつかご紹介します。

subscribe(_ on:)を使う

subscribe(_ on:)の説明

Subscribes an event handler to an observable sequence.
- parameter on: Action to invoke for each event in the observable sequence.
- returns: Subscription object used to unsubscribe from the observable sequence.

つまり監視できるもののイベントを拾えるのです。
これを使ってButtonのタップイベントを拾うとこんな感じになります。

button.rx.tap.subscribe({ [weak self] _ in
    // ボタンタップでキックしたいアクションを記述
    }).disposed(by: disposeBag)

[weak self][unowned self]でももちろんOKですが、私は管理が面倒なので[weak self]を使うことが多いです。
循環参照一度起こすと結構調査が面倒だし、平気でクラッシュするから脳筋weakしちゃうんですよね。。

[weak self]と[unowned self]についてはこちらの記事が面白かったです

bind(to observers:)を使う

私は普段この実装方法を取り入れていることが多いです。

private func setButton() {
    button.rx.tap.bind(to: buttonTapBinder).disposed(by: disposeBag)
}

private var buttonTapBinder: Binder<()> {
    return Binder(self) { base, _  in
        base.button.isSelected = !base.button.isSelected
    }
}

Binderを使用しているので、循環参照は自ずと防がれるので結構使ってます。
subscribeの実装もそうですが、どちらもボタンのタップだけではなく、他のイベントの監視もできるのでぜひ応用してみてください!

RxSwiftに関する記事

【Swift】RxSwiftを使ったTableViewの実装(Delegate/reloadDataは使わない!)

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