20191125のiOSに関する記事は8件です。

TableViewCellをアニメーションで追加/削除する

ViewControllerのボタンをタップしたときにCellを追加したり、削除するのに tableView.reloadData() を毎回呼んでも良いのですが、それだとTableView自体の更新が走ってしまう為、更新したCellがチラついてしまいます。UX的にもあまり良くないですし、そもそも見た目的にもイケてないです。そこでアニメーション付きでTableViewを追加する方法はいくつかある中で、一番ポピュラーな方法で insertRows メソッド呼ぶというのがあります。

insert時

// insert時
// Begins a series of method calls that insert, delete, or select rows and sections of the table view.
tableView.beginUpdates()
// 挿入したいCellのindexPathを配列の形式で渡す
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
tableView.endUpdates()

Delete時

//delete時間
// Begins a series of method calls that insert, delete, or select rows and sections of the table view.
tableView.beginUpdates()
// 削除したいCellのindexPathを配列の形式で渡す
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
tableView.endUpdates()

beginUpdatesendUpdates はinsert時にもdelete時にも必要です。

これらを書いてあげるだけアニメーション付きで追加されたり削除できたりします。

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

[はじめてのiOSアプリ]xcodeで地図アプリを作成(その2)

はじめに

iOSアプリを作ってみたいけど
何から始めて良いのかわからない

とりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思う

という記事の2回目です。

今回は、地図を表示します。

地図を表示

  1. MapKit.framework を追加
    • [MyGpsMap]-[Build Phase]-[Link Binary With Libraries]を選択し、[+]をクリック
      LinkBinLib.png
    • 検索窓に [map] と入力し絞り込み[MapKit.framework]を[Add] で追加
      AddMapkit.png
    • 【なぜ?】
      • Apple が提供してくれる地図を簡単に使うことができるフレームワーク(ライブラリ)
      • 苦労は買ってでもする人じゃない限り使うべき
  2. Mapkit を画面に配置
    • 画面左側のファイルツリーから[Main storyboard]を選択
      MainStoryboard.png
      • 【なぜ?】
        • あとで[Main storyboard]にGUIの地図部品を配置したいから、まずは選択しておく
        • [Main storyboard]を編集することで表示内容を決定できる
    • メニューから[View]-[Show Library]を選択、表示されるウインドウで [map] と入力し [Map Kit View]を絞り込む
      ShowLib-mapkit.png
      • 【なぜ?】
        • [Map Kit View]は地図を表示するためのGUI部品
        • 今回は、機能と名称が一致しているので絞り込みが楽々!
    • 絞り込み表示された[Map Kit View]を、Xcode右画面に表示される iPhone 画面上に、ドラッグ&ドロップする
      MapKitOnStoryboard.png
      • 【なぜ?】
        • [Map Kit View]を[Main storyboard]に配置することで地図が表示できる
    • [Map Kit View]を画面いっぱいに配置
      • 【なぜ?】
        • 地図は画面いっぱいに表示された方が見やすいから
          MapKitOnStoryboard-max.png
  3. テスト実行
    • Xcode 左上の矢印アイコンをクリック
      StartSimulator.png
  • iPhone Simulatorが起動し、日本全体が表示された
    MapJapan.png

今回の到達点

  • シミュレーターを使って地図を表示できた

連載

  1. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1)
  2. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2)
  3. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】VSCode上でIntegration Testを動かす

FlutterのIntegration Testは、公式のcookbookに載っている方法をそのまま利用すると、いくつか不便な点があります。

  • 毎回コマンドラインから実行する必要がある
  • 実行するたびにアプリのビルドが走って時間がかかる
  • ブレイクポイントを打ってデバッグすることができない

ということでなんとかしましょう。

対象IDEはVSCodeです。

参考元

こちらのMedium記事を大いに参考にします。
https://medium.com/flutter-community/hot-reload-for-flutter-integration-tests-e0478b63bd54

内容をザッッッッックリと要約すると

  • 通常flutter driveは次の2つのプロセスを動かして相互通信させる
    • アプリ自体(シミュレーター上)
    • テストコード(単なるDartプログラム)
  • ならば、それぞれを自分でIDE上で動かして通信させれば良い
  • 結果、Hot Reload/Hot Restart、ブレイクポイントなど自由自在

まあ、ザックリこういうことです。

リンク先の記事内ではAndroid Studioでの例が載っているのですが、

これを、VSCodeでやりたい!!というのが今回の目的です。

なお、こちらのQiita記事も参考にさせていただいたのでリンクを貼っておきます。

https://qiita.com/sensuikan1973/items/c8b56dfaf780e61af567

ではやり方を見ていきましょう。

使うアプリ

アプリ自体は何でもいいです。今回はこうしました。

main.dart
import 'package:flutter/material.dart';

void main() =>
    runApp(MaterialApp(home: Scaffold(appBar: AppBar(), body: MyApp())));

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final Hoge _hoge = Hoge();
  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Text(_hoge.text, key: const Key('text')),
      RaisedButton(
        key: const Key('button'),
        child: const Text('button'),
        onPressed: () {
          setState(() {
            _hoge.change();
          });
        },
      )
    ]);
  }
}

class Hoge {
  String text = 'hogehoge';
  void change() {
    if (text == 'hogehoge') {
      text = 'fugafuga';
    } else {
      text = 'hogehoge';
    }
  }
}

ボタンを押すたびに、hogehogeとfugafugaが切り替わります。それだけ。

テストコードから中身にアクセスするため、Keyが設定してあります。

テスト用にアプリを起動するためのコードは次のような感じ。

test_driver/app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:stepbystep/main.dart' as app;

void main() {
  // app_test.dart の方とやりとりしたい場合はこの引数にhandlerを追加
  enableFlutterDriverExtension();

  // runAppに好きなWidgetを渡しても良い
  app.main();
}

アプリ自体(シミュレーター上)を動かす

launch.jsonに、configurationを追加して次のようにします。

追加したconfigurationの名前と、ポート番号は適当です。

launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "App of Integration Test",
      "type": "dart",
      "request": "launch",
      "program": "test_driver/app.dart",
      "args": ["--observatory-port", "8888", "--disable-service-auth-codes"]
    },
    {
      "name": "Flutter",
      "request": "launch",
      "type": "dart"
    }
  ]
}

この状態で、VSCode上から今追加したconfigurationを指定してデバッグを開始すると、test_driver/app.dartに記述したアプリがシミュレーター上で走ります。

いつものようにHotReloadやHotRestartも可能です。ブレイクポイントも機能します。

そして重要な点は、ポート番号8888でこのアプリにアクセス可能になるということです。

試しにブラウザを開いて、アドレスバーに

http://localhost:8888/

と打ち込んでみましょう。

こんな感じの画面が出れば、確かにこのポート番号でFlutterが動いていることがわかります。

Screen Shot 2019-11-24 at 19.49.30.png

細かい注意(DEBUG CONSOLEの出力)

configurationを指定してデバッグ開始した場合、DEBUG CONSOLEの出力がconfigurationごとに別々になるので注意してください。DEBUG CONSOLE内の右の方にプルダウンリストがあるのでそこから選択すればOK。

テストコードの実行

Integration Testの公式cookbookでは、テストコードは/test_driver/ディレクトリに入れることになっていますが、

VSCode上でテストコードをテストとして実行するためには、

_test.dartで終わるファイル名がついたコードを/test/ディレクトリ内に配置する必要があります。

(参考:v3.6 - Dart Code - Dart & Flutter support for Visual Studio Code)

test/integration_test.dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  FlutterDriver driver;

  setUpAll(() async {
    driver =
        await FlutterDriver.connect(dartVmServiceUrl: 'http://localhost:8888/')
            .timeout(Duration(seconds: 10));
  });

  tearDownAll(() async {
    if (driver != null) {
      await driver.close();
    }
  });

  test('the button changes the text from hogehoge to fugafuga', () async {
    expect(await driver.getText(find.byValueKey('text')), equals('hogehoge'));
    driver.tap(find.byValueKey('button'));
    expect(await driver.getText(find.byValueKey('text')), equals('fugafuga'));
  });
}

FlutterDriver.connect()dartVmServiceUrl引数を与えると、そのURLを使ってアプリに接続しようとします。

なので、先程ブラウザで開いたのと同じhttp://localhost:8888/を指定しましょう。

(timeoutについては「問題点」の所で後述します)

あとは、VSCode上でこのファイルを開いた状態で、configurationにいつものFlutterを指定し、デバッグを実行すればOKです。

Integration Testが実行されます。

テストコード内にブレイクポイントを打つと、きちんと機能します。

アプリを起動したまま、テストコードだけ繰り返し実行することもできます。

プロセスを終了させる

これでテストはできましたが、実はテストを終了しても裏でプロセスが残っています。

メモリ・CPUの観点からもよくないし、port 8888も占有されたままです。

そのまま同じアプリの開発を続けるなら恐らく問題ないのですが、

違うアプリの開発に移ってから同様にIntegration Testをすると次のエラーが出ます。

flutter: Could not start Observatory HTTP server:
SocketException: Failed to create server socket (OS Error: Address already in use, errno = 48), address = 127.0.0.1, port = 8888

なので、このプロセスを終了させましょう。

次のコマンドを実行します。

lsof -i :8888

すると、port 8888を使用しているプロセスの一覧が出るので、PIDを指定してkillします。

kill (PIDを指定)

テスト終了時に自動でプロセスも終了したらいいんですけどね。そのような方法をご存知の方は教えていただけると嬉しいです!!!

問題点

実はこの方法ですが、けっこうでかい問題点が含まれております。

何か解決案をお持ちの方は教えていただけると嬉しいです!

アプリ側の状況によってテスト結果が変わる

「アプリを1から起動し、テストを実行して、アプリを閉じる」という一連の流れを手動で断ち切ってしまっていますので、

アプリを操作したり、テストコードを連続で実行したりすると、結果に影響します。

例えばこの記事の例だと、Integration Testを一度実行するとtextがfugafugaに切り替わっている(初期値はhogehoge)ので、そのままもう一度テストを実行すると2回目は失敗します。

テストの独立性を保証する工夫が必要です。

とりあえず毎回アプリをHot Restartするのが一番ラクな気がします。

Dart: Run All TestsでIntegration Testも実行されてしまう

Unit TestとWidget Testだけ全て実行したい場面でも、/test/に配置したIntegration Testも一緒に実行されてしまいます。

その際にテスト用アプリが実行されていなければ、当然エラーになります。

上記コードではとりあえずFlutterDriver.connectにタイムアウトを設定してありますので、アプリが実行されていない場合はここでタイムアウトしてIntegration Testが終了します。

でもそうすると、「Integration Test 以外のテストが全て通れば良しとする」という風に結果を自分で判断しなければいけません。

自動テストや自動デプロイを組んでいると影響が出そうです。

ただ、自動テストの場合はVSCode上で実行するわけではないので、自動で実行するテストをうまく指定できれば問題ないかもしれません。

Unit TestとWidget Testを/test/unit_and_widget_test/のようなディレクトリにまとめて、launch.jsonでこのディレクトリを指定してまとめて実行することも考えましたが、

この方法だと、一つのファイルを実行したらシェルが終了してしまうらしく、次のファイルを実行する際にエラーになりました。残念。

一応解決策

FlutterDriver.connectが失敗した場合にフラグ立てをして、テストケースの中身をif文で回避する手があります。

test/integration_test.dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  FlutterDriver driver;
  bool doTest = true;

  setUpAll(() async {
    driver =
        await FlutterDriver.connect(dartVmServiceUrl: 'http://localhost:8888/')
            .timeout(Duration(seconds: 3), onTimeout: () {
      print('Flutter driver connection failed!!!!!');
      doTest = false;
      return null;
    });
  });

  tearDownAll(() async {
    if (driver != null) {
      await driver.close();
    }
  });

  test('the button changes the text from hogehoge to fugafuga', () async {
    if (doTest) {
      expect(await driver.getText(find.byValueKey('text')), equals('hogehoge'));
      driver.tap(find.byValueKey('button'));
      expect(await driver.getText(find.byValueKey('text')), equals('fugafuga'));
    }
  });
}

無理やり回避した感がすごいですが、一応、Run All Testsの際も、Integration Test以外がすべて通ればOKを出してくれます。

(setUpAllはテストケースの実行が決定してから呼ばれるため、test関数のskip引数はうまく使えない)

おわり

この記事は以上です!

Integration Testでもアプリコードやテストコードにブレイクポイントを打ってデバッグできるのでとっても便利です!

これでストア申請に必要なスクショも撮れますね。

でも問題点には注意が必要です。

最後に。記事に載せようと思ってUnit TestやWidget Testも書きましたが、載せるタイミングがありませんでした。何が誰の参考になるかもわからないので載せておきます。

test/unit_test.dart
import 'package:test/test.dart';
import 'package:stepbystep/main.dart';

void main() {
  test('change method changes text', () {
    final Hoge hoge = Hoge();
    expect(hoge.text, equals('hogehoge'));
    hoge.change();
    expect(hoge.text, equals('fugafuga'));
    hoge.change();
    expect(hoge.text, equals('hogehoge'));
  });
}

test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stepbystep/main.dart';

void main() {
  testWidgets('button changes text',
      (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(home: MyApp()));
    expect(find.text('hogehoge'), findsOneWidget);
    await tester.tap(find.byKey(const Key('button')));
    await tester.pumpAndSettle();
    expect(find.text('fugafuga'), findsOneWidget);
    await tester.tap(find.byKey(const Key('button')));
    await tester.pumpAndSettle();
    expect(find.text('hogehoge'), findsOneWidget);
  });
}

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

【Unity】これ以上セーフエリア対応で消耗しないためのアセットを作った【AutoScreen】

AutoScreen(GitHub)

AutoScreen_01.gif
AutoScreen_02.gif

はじめに

Unityにおけるセーフエリアの対応に関してはScreen.safeAreaというAPIがあるのですが、ビルドして実機で呼ばないと端末ごとの値が取れない=エディタ上ではレイアウトの確認ができないという辛さがあります。

最近だとUnite Tokyo 2019でも紹介されたDevice Simulatorが記憶に新しいところですが、このパッケージはUnity2019.3からでないと使用できず、まだプレビュー版となっています。

個人的な感覚ではUnityはたとえLTS版であってもある程度時間が経つまでは本番で使用するのが怖いので、自分が実際にDevice Simulatorを使えるようになるのは2020年の後半になる気がしています。

そんなに待ってられないよ!ということでUniSafeAreaAdjusterをありがたく使わせてもらってたのですが

  • 解像度に合わせて端末を選択するのが面倒
    • 特に複数のGameObjectSafeAreaAdjusterコンポーネントをつけてるとき
  • デバイスのフレーム表示機能がほしい
  • 手軽に対応端末を追加・拡張できるようにしたい

という気持ちがピークに達して自分でアセットを作ってしまったのでご紹介します。

便宜上「アセット」と書いてますがアセットストアに公開するのはけっこう面倒なのでGitHubでだけ公開しています。

↓↓↓↓↓↓↓
AutoScreen(GitHub)

READMEがまだないのですが、そのうち本記事を元に英語で書くと思います。

機能

  • モバイル実機上でセーフエリアに応じてRectTransformのアンカーを自動調整するコンポーネント
    • SafeArea:セーフエリア用(適用したい方向を上下左右自由に組み合わせ可)
    • UnsafeArea:非セーフエリア用(上下左右どれか選択)
    • RuntimeSafeAreaUpdater:画面の回転を自動検知してSafeArea,UnsafeAreaを更新
  • エディタ上でセーフエリアをリアルタイムプレビュー
    • 既存のGameウィンドウでセーフエリアあり端末の解像度を選択するだけ(追加作業なし)
    • 再生/非再生の状態に関係なく即時反映
  • on/off可能なオプション
    • デバイスのフレームを表示
    • セーフエリアの境界線表示

対応状況

  • Unity2018.3以降対応(Device Simulatorは未対応)
  • iOS:実機・エディタともに対応
    • iPhone X/XS/11 Pro
    • iPhone XS Max/11 Pro Max
    • iPhone XR
    • iPad Pro (第3世代, 11インチ)
    • iPad Pro (第3世代, 12.9インチ)
  • Android:実機のみ対応

Android端末のエディタプレビューは以下の理由でオミットしています。

  • 個人的にAndroid端末にビルドする必要性がない
  • フレーム画像と解像度・セーフエリアデータの収集に手間がかかる
  • 対応が必要な端末数が多そう

端末のマスターデータの追加自体は簡単なので、必要に応じて後述の手順を参考に自分で足してください。

インストール方法

リポジトリ内に*.unitypackageファイルがあるのでこちらを使用してください。

使い方

とりあえず動作を確認してみたい場合はDemoシーンを用意してあるのでいじってみたりビルドしてみてください。

セーフエリアの自動調整機能

Canvas直下のGameObjectSafeArea/UnsafeAreaコンポーネントをAddComponentするとRectTransformAnchorが自動で調整されます。直下じゃなくてもCanvasに至るまでの親GameObjectRectTransformがすべて縦横に完全にストレッチするようにしてあれば正常に動作します。

スクリーンショット 2019-11-24 11.27.57.png
GameObjectが存在する場合のRectTransformの設定

デバイスのフレーム表示/セーフエリアの範囲表示

セーフエリアありの端末解像度を選択するとGameウィンドウの左上に歯車アイコンが表示されるので、そこから表示・非表示を切り替え可能です。

こだわった点

基本的には以下の3点を追求しました。

  • uGUIのオートレイアウトのような簡単で自然な使い心地
  • 使ってて細かい挙動を含めイライラしない
  • シンプルかつ高い拡張性

リアルタイムプレビュー

  • エディタ上で
  • 既存のGameウィンドウの解像度選択を変更するだけで
  • リアルタイムに
  • 再生/非再生の状態を問わず
  • セーフエリアあり端末での見た目が自動で調整

されるので、ビルドしなくてもセーフエリアありの場合のレイアウトを手軽に確認できます。

設定変更がGameウィンドウから可能

通常アセットやエディタの拡張の設定は[MenuItem]を使用してグローバルメニュー(+ショートカットキー)から行えるようにするのですが、Gameウィンドウ上に設定のUIを配置することでon/off切り替えの煩雑さを軽減しました。

ちなみにUIの配置にはUIElementsを使用しています。

高い拡張性

端末解像度とセーフエリアの情報はScriptableObjectを継承したアセットとして保持しているので、追加・拡張が簡単にできます。

SafeArea,UnsafeAreaコンポーネントで満たせない複雑な要件の場合も、SafeAreaBaseクラスを継承することでエディタ上でのリアルタイムプレビューの機能が簡単に実装できます。

Gitフレンドリー

セーフエリアの対応はRectTransformのアンカーを自動調整することで実現していますが、シーンやプレハブの保存前に必ずアンカーをリセットしています。

これにより「Gameウィンドウで異なる解像度を選択して保存 → Gitでdiffが出る」ということが起きません。

実装の詳細

Gameウィンドウの情報を利用するにあたり、Gameウィンドウの実態であるGameViewクラスが公開されていないため、リフレクションを用いてそのデータにアクセスしています。

実際にリフレクションを用いているのはGameViewProxyクラスのみで、値の変更についてはGameViewEventクラスが監視・イベント化しています。

実機のフレーム表示は前もって用意したフレーム画像を描画していて、セーフエリアの大きさは事前にシミュレータビルドで収集した値をScriptableObjectを継承したGameViewScreenアセットに保存・使用しています。

デバイスのフレーム表示とセーフエリアの境界線表示はそれぞれDeviceFrameDrawerコンポーネントとSafeAreaDrawerコンポーネントが行っていて、それらのコンポーネントがアタッチされたAutoScreenManagerプレハブが自動的にHierarchyに配置されるようになっています。このプレハブインスタンスはシーンやビルドには含まれず、Hierarchyにも表示されません。

対応端末追加の手順

エディタ上でのプレビューはマスターデータを追加するだけで簡単に対応機種を増やすことが可能です。

  1. Gameウィンドウから手作業で解像度を追加します。自動化したい場合はGameViewSizeHelperを使うと良いです。
  2. Projectウィンドウで右クリックし、Create -> ScriptableObjects -> GameViewScreenからマスターデータを保持するためのアセットを作成します。
  3. 2.で作成したアセットの各値をInspectorウィンドウから設定します。
    • 解像度やセーフエリアの値はDemoシーンをシミュレータや実機にビルドして確認すると楽です。
    • Base Textは1.で追加した解像度のLabelと同じ文字列にしてください。
    • 必要であればデバイスのフレーム画像を追加し、アセットのFrameにセットしてください。
  4. 解像度を適当に変更すると反映されます。Unityを再起動する必要はありません。

Tips

  • 自動で追加されるRuntimeSafeAreaUpdaterコンポーネントは画面の回転を許可していないアプリの場合は不要
  • 11インチのiPadはGameウィンドウから解像度を登録するとプレビュー可能
    • デフォルトだとUnityに解像度が登録されてない
    • ラベル名は「iPadPro 2388x1668 Landscape」「iPadPro 2388x1668 Portrait」で登録する
  • 12インチのiPadの解像度を選択するとセーフエリアあり(第3世代)としてプレビュー表示される
    • 第1〜2世代のセーフエリアなしレイアウトを確認したい場合は適当なラベル名で別途解像度を追加すればOK
  • セーフエリア境界線の太さ・色はAutoScreenManagerプレハブから調整可能
  • SafeAreaDrawerコンポーネントは単体で実機でも動作可能

ライセンス

MIT

参考

最後に

やりたいことや細かい挙動の調整を全部やろうとしたら結果的に

  • 新しいプレハブAPIの使い方
  • UIElements
  • [ExecuteAlways]
  • HideFlags

等々Unityのよく知らなかった様々な機能について詳しく知る良い機会になりました。個別の知見についてはアドベントカレンダーのときにでもまとめたいと思います。

コードにコメント書かないマンなのですがコード自体はシンプルで読みやすいと思うので、わからないことがあったらとりあえずコードを読んでみてください。

実は実践未投入なので、何か不具合があればPRかTwitterへ → @su10_dev

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

Scoped Model を利用した場合の画面遷移でうまくいったコード例(Flutter)

最近Flutterをいじり始めました。Scoped Modelを利用するとスッキリ書けるので、そのコードを書いたときの画面遷移時に具体的にどうコードを書くとうまくいったかをレポートします。

[Scoped Model 初めて聞いた方へ]Scoped Model とは

公式のパッケージサイトは下記です。

https://pub.dev/packages/scoped_model

基礎原理の解説について下記記事が参考になりました。

https://qiita.com/hayassh/items/690fa0d6528e056617b5

自分の言葉で書くと、数値や文字列の処理部分とデザインテンプレート部分をModel定義をすることで綺麗に分離ができます。あと、このパッケージは、GoogleのFlutter開発チームが提供してくれているVeggie Seasonsというアプリのコードにも採用されていたので、自分もやってみようと思いました。Veggie Seasonsのコードは以下です。

https://github.com/flutter/samples/tree/master/veggieseasons

ページ遷移時の試行錯誤時のエラー解決で得た知見

今回はこちらを共有したいのです。Scoped Modelを使わない場合と比較できるように書いてみました。

#main.dart
//1:route定義をMaterialAppのところで行って、

void main() {

  runApp(MaterialApp(
    title: 'Demo',
    initialRoute: '/',
    routes: {
      '/': (context) => FirstScreen(),
      '/second': (context) => SecondScreen(),
    },
  ));
}

//2:↓最初のページに当たるFirstScreen部分のStatelessWidget記述です。

class FirstScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text('Demo'),
      ),
      body: Center(

      // 中略
      child: RaisedButton(
          padding: EdgeInsets.all(12.0),
          shape: StadiumBorder(),
          child: Text(
            "次のページへ",
            style: TextStyle(fontSize: 20.0, color: Colors.white),
          ),
          color: Colors.green,
          onPressed: () {

            Navigator.pushNamed(context, "/second");
            //3:↑で次のページ"SecondScreen()"へ移動クラス生成です。
          },
        ),

//次の画面(SecondScreen)
class SecondScreen extends StatelessWidget {

   //以下略SecondScreenの表示内容などが書かれています。

}

↑これをScoped Model 活用して記述する際、
下記のように書くとうまくいきました。  

#main.dart
import 'model.dart'; 
/*
↑model(今回はDemoModelという名称です)を別ファイルで定義しました。
このmodelそのものの部分の解説は、
ページ遷移の解説に戻るまでにかなり時間がかかるので割愛します。
※本記事前述の解説などの参考になる記事が複数存在しますので、ご覧ください。
*/

import 'package:scoped_model/scoped_model.dart';
// scoped_modelのパッケージをインポートします。

DemoModel demoModel = DemoModel();
//modelを生成しておきます。

void main() {

  runApp(MaterialApp(
    title: 'Demo',
    home: ScopedModel<DemoModel>(
        model: demoModel,
        child: new FirstScreen()
    )
  ));
}

/*
route指示部分がごっそり無くなっています。書いてみるとエラーになりました。
遷移時に各ボタンなどの実行時のコードに
Scoped Modelの子孫のクラスとなる次のページのクラス生成を指示することになります。
*/

//最初の画面(FirstScreen)
class FirstScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text('Demo'),
      ),
      body: Center(

      // 中略
      child: RaisedButton(
          padding: EdgeInsets.all(12.0),
          shape: StadiumBorder(),
          child: Text(
            "次のページへ",
            style: TextStyle(fontSize: 20.0, color: Colors.white),
          ),
          color: Colors.green,
          onPressed: () {



            Navigator.push(
              context,
              new MaterialPageRoute<Null>(
                settings: const RouteSettings(name: "/second"),
                builder: (BuildContext context) {
                    return MaterialApp(
                      home: ScopedModel<DemoModel>(
                        model: quizBrainModel,
                        child: new SecondScreen(),
                      ),
                    );
                  }
              ),
            );

            /*
            ↑ScopedModelを使わない場合、
            Navigator.pushNamed(context, "/second");となっていた部分です。
            */

          },
        ),

//次の画面(SecondScreen)
class SecondScreen extends StatelessWidget {

  SecondScreen();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text("Second Page"),
      ),
      body: 

      //中略

      new ScopedModelDescendant<DemoModel>(
         builder: (context, child, model) =>
         new Row( children: model.getSomeList())
     )

     /*
     ↑このページは、Scoped Model子孫クラスなので
     new ScopedModelDescendant<Model名称>で
     メソッドや変数が呼び出せます。
     例としてmodelで指定しておいた、
     特定のListを呼び出すメソッドを書いています。
     */

参考になればと思います。他にも書き方があるかもしれません。ご指摘などあればコメントいただければと思います。

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

SwiftUIでtabItemの画像サイズを変更する方法

SwiftUI tips

SwiftUIでタブのアイテムのサイズを変更するのに少し苦戦したので共有します。
なお、大きくしたアイコンがtabの一番上に付いていてpaddingも効かないのでご了承?‍♂️
(解決策あったら米キボンヌ)

成功するパターン

Text("user")
    .tabItem {
        Image(systemName: "person")
            .font(.title)
        }
.tag(4)

IMG_0335.png

ダメなパターン

Text("user")
    .tabItem {
        Image(systemName: "person")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: tabItemSize, height: tabItemSize)
        }
.tag(4)

IMG_0334.png

参考URL

https://developer.apple.com/documentation/swiftui/tabview

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

SwiftUIでtabItemのSF Symbolsのサイズを変更する方法

SwiftUI tips

SwiftUIでタブのアイテムのサイズを変更するのに少し苦戦したので共有します。
なお、大きくしたアイコンがtabの一番上に付いていてpaddingも効かないのでご了承?‍♂️
(解決策あったら米キボンヌ)

成功するパターン

Text("user")
    .tabItem {
        Image(systemName: "person")
            .font(.title)
        }
.tag(4)
.font(.system(size: 10))//とかでも行ける

IMG_0335.png

ダメなパターン

Text("user")
    .tabItem {
        Image(systemName: "person")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: tabItemSize, height: tabItemSize)
        }
.tag(4)

IMG_0334.png

参考URL

https://developer.apple.com/documentation/swiftui/tabview

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

Swift5.1のattribute全解説(全27種)

概要

Swift5.1で利用できるattributeの一覧です。(Public APIのみ)

本記事は以前作成したSwiftのattributeまとめ[Swift4対応]をSwift5.1向けに更新したものです。

attributeは全てを暗記する必要はありません。これなんだっけ、と思ったときにこの記事でさくっと見られる簡易リファレンスになればと思います。

attributeとは

  • コンパイラに対し、宣言や型の補足情報を伝えるもの
  • 属性や修飾子とも呼ばれる
  • Swift5.1では公式リファレンスに記載されているもので全27種類

attributeの記法

  • attributeの記法は以下のようになり、より詳細な情報を補足するために引数も指定することができる
  • @〜という記法はコンパイラディレクティブと呼ばれ、コンパイラに対する指示を記載する際に利用される
// 引数なしの場合
@属性名

// 引数ありの場合
@属性名引数

attribute一覧

  • autoclosure
  • escaping
  • convention
  • available
  • discardableResult
  • objc
  • nonobjc
  • objcMembers
  • GKInspectable
  • UIApplicationMain
  • NSApplicationMain
  • NSCopying
  • NSManaged
  • testable
  • IBAction
  • IBOutlet
  • IBDesignable
  • IBInspectable
  • dynamicCallable
  • dynamicMemberLookup
  • frozen
  • inlinable
  • usableFromInline
  • propertyWrapper
  • requires_stored_property_inits
  • warn_unqualified_access
  • unknown

※ドキュメントに記載のあるPublic APIのみです。

@autoclosure

  • 以下のように、呼び出し部分で引数として渡した値を、メソッド(関数)内ではクロージャーとして扱える
  • クロージャーとして渡しているため、引数が実際に評価されるのは、クロージャーの実行時(遅延評価が可能)
// 定義部分
func someMethod(closure: @autoclosure () -> Int) {
    print(closure())
}

// 呼び出し部分
someMethod(closure: 10)

利用シーンは少ないと想定するが、&&演算子や||演算子の実装などで利用

&&演算子、||演算子の実装
public static func &&(lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool
public static func ||(lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool

@escaping

  • クロージャをスコープ外でも保持する必要があることを示す
  • 以下の例では、completionクロージャーの実行を非同期で実行しているため、スコープ外ではクロージャーが保持されず、コンパイルエラー
// NG: コンパイルエラー
func someAsyncMethod(completion: () -> Void) {
    DispatchQueue.main.async {
       completion()
    }
}

// OK
func someAsyncMethod(completion: @escaping () -> Void) {
    DispatchQueue.main.async {
       completion()
    }
}

@convention

  • 関数のポインタに対し、その呼び出し方式を指定
// Swiftの関数ポインタ(デフォルト)
var swiftFunction: @convention(swift) (Bool) -> Bool

// Objective-Cと互換性をもつブロックポインタ
var block: @convention(block) (Bool) -> Bool

// C言語の関数ポインタ
var cFunction: @convention(c) (Bool) -> Bool

@available

  • 各OSバージョンなどの環境に対し、APIの有効性を表す
  • Swiftのバージョン(swift)
  • プラットフォームの種類(iOS,iOSApplicationExtension, macOS, macOSApplicationExtension, watchOS, watchOSApplicationExtension ,tvOS, tvOSApplicationExtension)
// 全環境(*)で利用不可(unavailable)、SomeProtocolにリネームした(renamed:)
@available(*, unavailable, renamed: "SomeProtocol")
protocol MyProtocol { ... }

// macOS 10.12で廃止(obsoleted)とメッセージを表示(message)
@available(macOS 10.12, obsoleted: 10.12, message: "macOS 10.12で廃止されました")
func someMethod() { ... }

// iOS2で導入(introduced)、iOS9で非推奨(deprecated)
@available(iOS, introduced: 2.0, deprecated: 9.0)
var some: String

例えば、iOS9で非推奨となった、UIAlertViewは以下のようにattribute指定することで、UIAlertViewを利用した際にコンパイラ警告を出し、UIAlertControllerの利用を推奨する。

スクリーンショット 2017-06-30 4.50.06.png

UIAlertView.h
@available(iOS, introduced: 2.0, deprecated: 9.0, 
message: "UIAlertView is deprecated. Use UIAlertController with a 
preferredStyle of UIAlertControllerStyleAlert instead")
open class UIAlertView : UIView { ... }

@discardableResult

  • 返り値を持つ関数やメソッドの返り値を利用しなかった場合のコンパイラ警告を無視する
  • 返り値を必ずしも利用しなくて良いメソッドを定義するのに適する
// メソッドの定義部分
func someMethod1() -> Bool { ... }

@discardableResult
func someMethod2() -> Bool { ... }

// 呼び出し部分
someMethod1() // NG:メソッドの返り値を利用していないためコンパイラ警告がでる
someMethod2() // OK

@discardableResultを指定しない場合は「Result of call to 'someMethod()' is unused」とwarningが表示される。
スクリーンショット 2017-06-30 4.57.07.png

@objc

  • Objective-Cから使用できることを明示的に宣言
  • extensionに指定すると全てのメンバに一括で指定できる
  • @objc(引数)でObjective-Cで使いたい名前を指定できる
  • Swift4以前ではNSObjectを継承したクラスやdynamicでは暗黙的に@objcが付け加えられていましたが、Swift4では付与されなくなりました(Proposal: SE-0160
// someMethodとしてObjective-Cから利用可能
@objc(someMethod)
func someMethodForObjc() { ... }

@nonobjc

  • Objective-Cから使用できないことを明示的に宣言
  • extensionに指定すると全てのメンバに一括で指定できる
@nonobjc
func SomeMethodForSwift() { ... }

@objcMembers

  • クラス全体に対し一括でObjective-Cから使用できることを明示的に宣言
  • @objcMembersを付与したクラスのサブクラスやエクステンションにも影響
  • Swift4で導入(Proposal: SE-0160
@objcMembers
class MyClass : NSObject {
    // @objcになる
    func foo() { }             

    // @objcにならない(タプルを返しているため)
    func bar() -> (Int, Int) { return (1, 1) }   
}

extension MyClass {
    // @objcになる
    func baz() { }   
}

class MySubClass : MyClass {
    // @objcになる
    func wibble() { } 
}

extension MySubClass {
    // @objcになる
    func wobble() { }   
}

@GKInspectable

  • カスタムのGameplayKitコンポーネントプロパティをSpriteKitエディタのUIに公開する
  • 暗黙的に@objcが付け加えられる
class MyComponent: GKComponent {
    @GKInspectable var speed: Float = 1.0
    @GKInspectable var friction: Float = 2.0
}

@UIApplicationMain

  • アプリケーションデリゲートであることを示す(iOSアプリ用)
  • この属性がない場合はmain.swiftを用意し、UIApplicationMain(::_:)関数を利用してデリゲート設定を行う
例(AppDelegate.swift)
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions
        launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }
    func applicationWillResignActive(_ application: UIApplication) { }
    func applicationDidEnterBackground(_ application: UIApplication) { }
    func applicationWillEnterForeground(_ application: UIApplication) { }
    func applicationDidBecomeActive(_ application: UIApplication) { }
    func applicationWillTerminate(_ application: UIApplication) { }
}

@NSApplicationMain

  • アプリケーションデリゲートであることを示す(macアプリ用)
  • この属性がない場合はmain.swiftを用意し、UIApplicationMain(::_:)関数を利用してデリゲート設定を行う
例(AppDelegate.swift)
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) { }
    func applicationWillTerminate(_ aNotification: Notification) { }
}

@NSCopying

  • ストアドプロパティのセッターでコピーした値をセットする
  • Objective-Cのcopy属性と同様
  • Swift4でイニシャライザでの挙動が改善されました(Proposal: SE-0153
@NSCopying var foo: Foo

@NSManaged

  • クラスのインスタンスメソッドやストアドプロパティに対し、CoreDataで実行時に動的に実装が生成されることを宣言する
@NSManaged var name: String

@testable

  • モジュールのimport宣言に対し、internal以上のアクセスレベルで公開されているメソッドやプロパティに対しテストクラスがアクセス可能なことを宣言する
@testable import SomeModule

@IBAction

  • メソッドがInterfaceBuilder(StoryBoard)に配置したパーツのアクションに紐付けられることを示す
  • 暗黙的に@objcが付け加えられる
@IBAction func buttonTapped() { ... }

@IBOutlet

  • プロパティがInterfaceBuilder(StoryBoard)に配置したパーツに紐づけられることを示す
  • 暗黙的に@objcが付け加えられる
@IBOutlet weak var detailDescriptionLabel: UILabel!

@IBDesignable

  • UIViewまたはNSViewを継承したカスタムクラスに指定するとデザインやサブビューがInterfaceBuilder(StoryBoard)上でライブレンダリングされる
  • 暗黙的に@objcが付け加えられる
@IBDesignable class CustomView: UIView { ... }

@IBInspectable

  • プロパティに指定すると、InterfaceBuilder(StoryBoard)のAttribute Inspectorで設定でき、ライブレンダリングでデザインを確認できる
  • 対応する型(Int, CGFloat, Double, String, Bool, CGPoint, CGSize, CGRect, UIColor, UIImage)
  • 暗黙的に@objcが付け加えられる
@IBInspectable 
public var cornerRadius: CGFloat = 2.0 { 
    didSet { 
        self.layer.cornerRadius = self.cornerRadius 
    } 
}

@IBSegueAction

  • StoryboardでSegueを設定し、画面遷移する際の処理を指定できる
  • 引数のNSCoderインスタンスを利用し、遷移先のViewControllerの任意のイニシャライザを指定することが可能
@IBSegueAction
private func openDetail(coder: NSCoder, sender: Any?, segueIdentifier: String?)
    -> DetailViewController? {
    return DetailViewController(coder: coder, item: item)
}

@dynamicCallable

  • 型に@dynamicCallableを付与し、dynamicallyCall(withArguments:)メソッドもしくはdynamicallyCall(withKeywordArgument:)メソッドを実装することで、その型を関数のように直接呼び出しができるようになる
@dynamicCallable struct Sum {
    // 引数ラベルなし
    func dynamicallyCall(withArguments args: [Int]) -> Int {
        return args.reduce(0, +)
    }

    // 引数ラベルあり
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
        return args.map { $0.value }.reduce(0, +)
    }
}

let sum = Sum()
print(sum(1, 2, 3)) // 6
print(sum(first: 1, second: 2, third: 3)) // 6

@dynamicMemberLookup

  • 型に@dynamicMemberLookupを付与し、subscript(dynamicMember:)を実装することで、コンパイル時に存在しないプロパティに.でアクセスが可能となる
@dynamicMemberLookup struct Dog {
    subscript(dynamicMember key: String) -> String {
        return key
    }

    subscript(dynamicMember key: Int) -> Int {
        return key
    }
}

let dog = Dog()
print(dog.name) // "name"
print(dog.1) // 1

@frozen

  • library evolution mode(-enable-library-evolutionオプション)でコンパイル時のみ有効
  • structもしくはenumに付与し、将来のバージョンでの型の変更を制限する
  • structではストアドプロパティ、enumではcaseの追加、削除、並び替えを制限する
  • library evolution modeでない場合には全てのstruct、enumが暗黙的に@frozenとなる
@frozen enum IceCream {
    case vanilla
    case chocolate
    case greenTea
}

@inlinable

  • 関数、メソッド、コンピューテッドプロパティ、subscript、コンビニエンスイニシャライザ、デイニシャライザに付与することで、実装をmodule interfaceの一部として公開する(インライン展開)
  • publicもしくはinternalで宣言されたものにのみ付与可能(実際にはinternalで定義された@inlinable@usableFromInlineを意味する)
@inlinable func hoge() {
    print("hoge")
}

@usableFromInline

  • @inlinableの実装でアクセスする宣言に対して必要なattribute
  • アクセス制御修飾子をサポートするすべての宣言に適用可能
  • internalで宣言されたものにのみ付与可能
@inlinable func hoge() {
    print("hoge")
    fuga()
}

@usableFromInline func fuga() {
    print("fuga")
}

@propertyWrapper

  • プロパティの値にアクセスする方法を独自のattributeとして定義できる
  • attributeを定義するにはこのattributeをclass,enum,structのいずれかに付与
  • アクセス方法はwrappedValueプロパティのget、setを定義することで記述
  • projectedValueプロパティを定義することで、$プロパティ名でアクセス可能な値を定義できる
  • イニシャライザでattributeに初期値を与えることも可能
@propertyWrapper struct SmallNumber {
    private var max: Int
    private var number: Int
    var projectedValue = false

    var wrappedValue: Int {
        get { return number }
        set {
            number = min(newValue, max)
            projectedValue = newValue > max
        }
    }

    init(wrappedValue: Int, max: Int) {
        self.number = min(wrappedValue, max)
        self.max = max
    }
}

struct SmallPoint {
    @SmallNumber(wrappedValue: 5, max: 10) var x: Int
    @SmallNumber(wrappedValue: 5, max: 10) var y: Int
}

var smallPoint = SmallPoint()
print(smallPoint.x) // 5
print(smallPoint.$x) // false

smallPoint.x = 200
print(smallPoint.$x) // true
print(smallPoint.x) // 10

@requires_stored_property_inits

  • クラス定義に付与することで、すべてのストアドプロパティのデフォルト値を要求する
  • NSManagedObjectを継承するクラスでは暗黙的に付与される
@requires_stored_property_inits
class Hoge {
    // error: Stored property 'number' requires an initial value
    let number: Int

    init(number: Int) {
        self.number = number
    }
}

@warn_unqualified_access

  • トップレベルの関数、インスタンスメソッド、classメソッド、staticメソッドに付与することで、モジュール名や型名、インスタンス変数やインスタンス定数など、メソッドを特定するものがない場合にwarningを発生させる
  • 同一スコープ内で同名の関数にアクセス可能な場合に曖昧さをなくすのに役立つ
  • Sequenceのmin()で利用
@warn_unqualified_access func min() -> Self.Element? { 
   /// 
}

@unknown

  • 将来的に新しいcaseが追加された場合に、そのケースをhandlingできるようにする
  • コンパイル時にはenumが網羅されていない旨のwarningが発生する
enum Signal {
    case red
    case yellow
    case green
    case purple // 新しいcase
}

let signal = Signal.yellow

switch signal { // warning: Switch must be exhaustive
case .red:
    print("red")
case .yellow:
    print("yellow")
case .green:
    print("green")
@unknown default:
    print("未知のケース")
}

参考

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