20201130のiOSに関する記事は10件です。

UINavigationControllerの戻るイベントを取得したい!

Xcode-12.0 iOS-14.0

はじめに

UINavigationController の左上に表示される「戻る」ボタンのイベントを取得したいことがたまにあります!

「戻る」ボタンのイベントを取得するために試行錯誤した結果です。

結論から言うとちゃんとイベントを取得したいなら navigationItem.leftBarButtonItem をカスタムしましょう:frowning2:

サンプル

サンプルとして FirstiViewController(赤) -> SecondViewController(青) -> ThirdViewController(緑) と push で遷移する画面構成で SecondViewController の「戻る」について考えます。

screens

SecondViewController からは Full Screen の modal 遷移(黄)もつけています。

deinit を使う

SecondViewController から戻る際は deinit が呼ばれるのでここで試してみます。

deinit {
    print("戻る!!!")
}

SecondViewController から FirstViewController に戻る際に呼ばれますがここだと ThirdViewController から popToRootViewController などで FirstViewController に戻った場合と区別することが困難です:no_good:

UINavigationControllerDelegate を使う

UINavigationController には UINavigationControllerDelegate がありこのデリゲートを使ってイベント取得を試みます。

SecondViewController から戻った場合は FirstiViewController に戻るのでデリゲートで表示されるのが FirstiViewController かどうかで下記のように判定します。

extension SecondViewController: UINavigationControllerDelegate {

    // ナビゲーション遷移の直前に呼ばれる
    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        if viewController is FirstViewController {
            print("戻る!!!")
        }
    }

    // ナビゲーションの遷移直後に呼ばれる
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        if viewController is FirstViewController {
            print("戻る!!!")
        }
    }
}

willShow の方は ThirdViewController から popToRootViewController などで FirstViewController に戻った際も呼ばれてしまうので使うなら didShow の方になるでしょう。

「戻る」押下時にログ表示とかならできますがアラートを表示したいなど遷移を止めたいときなどは残念ながらこれではダメです:no_good:

(あと個人的に UINavigationControllerDelegate を各画面で使いたくない:no_good:

viewDidDisappear を使う

viewWillDisappearviewDidDisappear を使って試してみます。

こちらは willShow, didShow と同じようなタイミングで呼ばれます(SecondViewController が非表示になるタイミングです)。

気をつけないといけないのは modal 遷移の Full Screen などでも呼ばれることです。

SecondViewController から ThirdViewController に遷移する場合と modal 遷移の Full Screen などで他の画面に遷移する場合を判定しないといけないので下記のようにします。

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if navigationController?.viewControllers.contains(self) == false {
        print("戻る")
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    if navigationController == nil {
        print("戻る")
    }
}

didShow と同じで「戻る」押下時にログ表示とかならできますがアラートを表示したいなど遷移を止めたいときなどは残念ながらこれではダメです:no_good:

leftBarButtonItem をカスタムする

上に書いたやつはすべて厳密に言うと「戻る」ボタンを押下したかどうかは取れていません:no_good:
スワイプで戻るでも呼ばれるし、別にボタンを置いて popViewController しても呼ばれてしまいます。それに遷移途中でイベントを取っているので遷移自体を止めることはできません(アラート出してキャンセルとかできない)。

結局のところ leftBarButtonItem をカスタムしてボタンの押下イベントを取るしかありません!!

override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton(type: .system)
    button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
    button.setTitle("Back", for: .normal)
    button.setImage(UIImage(named: "back"), for: .normal)
    button.titleLabel?.font = UIFont.systemFont(ofSize: 16)
    button.imageEdgeInsets = .init(top: 0, left: -8, bottom: 0, right: 0)
    navigationItem.leftBarButtonItem = .init(customView: button)
}

@objc private func back(_ sender: Any) {
    print("戻る!!!")
    navigationController?.popViewController(animated: true)
}

見た目も限りなく戻るボタン:sunglasses:

back_img

使った画像はこれです(てきとーに作りました)。

back

これで「戻る」ボタン押下イベントを完璧に取得できました:tada:

がしかし、 leftBarButtonItem をカスタムするとスワイプバックできないし iOS14 からできたロングタップのメニュー表示もできなくなります:expressionless:

結論

厳密に「戻る」ボタン押下イベントを取得したいなら leftBarButtonItem のカスタムかなという感じです。

(わたしの場合は戻るときにログとりたいという要件だったので viewDidDisappear を使いました。)

おわりに

「戻る」ボタン押下イベントの取得方法としては結局、 leftBarButtonItem のカスタムに落ち着きましたがそもそも戻るときにアラート出したいとかの場合は本当に push でいいのか遷移方法を見直すべきな気もします:no_good:

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

UITextDocumentProxyのselectedTextは3行以上かつ改行含めて65文字以上選択していると正しい選択部分を返さない

想定外でした。

挙動の概要

動作確認は第1・2世代のiPhoneSE (iOS14.2)において行っています。

タイトルの通り、UITextDocumentProxyのselectedTextは3行以上かつ65文字以上選択していると正しい選択部分を返しません。代わりに最初と最後の2行を結合したものを返してきます。そんなことある?

論より証拠なのでまずは実行結果の例です。
IMG_3811.jpg
IMG_3812.jpg
IMG_3813.jpg
IMG_3814.jpg

なぜか最後だけ文字数が32と判定されています。
それぞれの場面でtextDocumentProxy.selectedText?.debugDescriptionを出力すると以下のようになります。

Optional("aaaaaaaaaaaaaaaa")
Optional("aaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbb")
Optional("aaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbb\ncccccccccccccccc")
Optional("aaaaaaaaaaaaaaaa\ndddddddddddddddd")

挙動をさらに理解するために次の二例を見ましょう。

IMG_3815.jpg
IMG_3816.jpg

非常にわかりにくくて申し訳ないのですが、文字数は改行を含まない設定で算出しているため、改行を含める場合一枚目は64文字、二枚目が65文字です。ここを境に挙動が変わっていることがわかります。

同様の事象は3行でも起こります。

IMG_3817.jpg
IMG_3818.jpg
同様に改行を含めて64文字、65文字を境に挙動が変わります。

ところが、2行の場合はこの制限はありません。
IMG_3819.jpg

ハラスメントではないでしょうか。

挙動の問題点

さて、この挙動は実用上どう問題になるでしょうか。
まず、こちら側では得られた二行分のテキストが内部を省略されてしまったものなのか、それともそうでないのか、という判断ができません。従って次のように判断する必要があります。

  • 三行以上のテキストがselectedTextに含まれていた場合→問題ない
  • 二行のテキストが含まれていた場合→内部の省略かどうか判断ができない
  • 一行のテキストが含まれていた場合→問題ない

ちょうど二行のテキストが得られた場合のみ挙動を変えなければならないということです。
しかしユーザ側から見れば、複数行選択した場合に望む挙動を得られる場合と得られない場合が生じることになります。この奇天烈な動作は不具合にしか見えませんし、「3行以上かつ改行含めて65文字以上選択しないで下さい」とはとても頼めません。

従って実際は次のようにせざるを得ません。

❌ 三行以上のテキストがselectedTextに含まれていた場合→諦める
❌ 二行のテキストが含まれていた場合→諦める
⭕️ 一行のテキストが含まれていた場合→用いる

提供できる機能がぐっと制限されますが、やむを得ないでしょう。
恣意的にやらなければ起こらなそうな動作なのに、ドキュメントに書いていないのが本当に意味不明でした。

※こちらの環境によるものの可能性があるので、ぜひ検証をお願いします。

参照

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

【Flutter】はじめてのFlutterでカウントアプリ作ってみた

はじめに

この記事は「HTC Advent Calendar 2020」の1日目の記事です。

執筆はHTCチームの開発担当のこーたです。
普段は、SI企業に勤め、インフラエンジニアとして働いています。
今回はチームメンバの技術向上のためにアドベントカレンダーにチームで参加してみることにしました。

いやHTCってなに?!って感じだと思うので、HTCについて簡単に触れておきますね。
ONE JAPAN HACKATHON 2020」というハッカソンに参加した際に結成されたチームです。
ハッカソンで作成した作品についてはチームメンバーが記事を書いてくれているので、興味がある方はこちらをご覧ください。

本記事はタイトルにも記載している通り、はじめてFlutterをやってみたという内容です。
似たような記事が多くあり、新規性は皆無ですが、個人的なアウトプットと後学のために書かせて頂きます。

対象読者

  • はじめてFlutterの開発を行う人
  • Flutterの環境構築が完了している

実行環境

  • macOS Catalina バージョン10.15.7
  • Flutter バージョン1.20.4
  • Dart バージョン2.9.2

Flutterとは

Googleが提供しているUIツールキットです。モバイル・Webおよびデスクトップようにネイティブコンパイルされた、美しいアプリケーションを単一のコードベースから構築できると言われています。

今回作るもの

ボタンが2つあり、足し算と引き算ができるカウントアプリを作ります。
qiita_demo.gif

アプリの実装

まずは、新規Flutterプロジェクトの作成を行い、以下のような画面が表示されることを確認します。
first_view.png

ボタンの追加

次にボタンの追加を行います。今回はRaisedButtonを使用します。
.icon()を使用することでアイコン付きのボタンになります。
Icons.add_circleでアイコンを選択することができます。こちらからアイコン一覧を確認することができます。
onPressed: _incrementCounter:ボタンをタッチした際の処理を指定できます。

main.dart
// ... 略
class _MyHomePageState extends State<MyHomePage> {
// ... 略
  @override
  Widget build(BuildContext context) {
// ... 略
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton.icon(
                  icon: const Icon(
                    Icons.add_circle,
                    color: Colors.white,
                  ),
                  label: const Text('Plus'),
                  onPressed: _incrementCounter,
                  color: Colors.red,
                  textColor: Colors.white,
                ),
                SizedBox(width: 30),
                RaisedButton.icon(
                  icon: const Icon(
                    Icons.remove_circle,
                    color: Colors.white,
                  ),
                  label: const Text('minus'),
                  onPressed: _decrement,
                  color: Colors.blue,
                  textColor: Colors.white,
                ),
              ],
            ),
// ... 略
  }
}

ボタン追加後はこのような画面になっていると思います。
全てのパーツが中央に寄っていますが、とりあえずボタンの配置は完了です。
add_button.png

マイナス処理の追加

ボタンを押すたびに加算される処理は最初から実装されているので、ここでは減算処理を追加します。

main.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrement() {
    setState(() {
      _counter--;
    });
  }
// ... 略
}

レイアウトの変更

最後に簡単にレイアウトを整えます。方法はいくつかあるようですが、今回はSpacerとSizedBoxを使用しました。
Spacerは文字通りスペースをとるためのウィジェットです。RowColumnウィジェットでmainAxisAlignmentプロパティだけでは制御できないレイアウトを作るときによく利用されるようです。
SizedBoxウィジェットは、指定されたサイズの箱を作れます。プロパティはwidthheightchildだけとなっています。

main.dart
class _MyHomePageState extends State<MyHomePage> {

// ... 略

  @override
  Widget build(BuildContext context) {

// ... 略

      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Spacer(
              flex: 2,
            ),
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            Spacer(),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,

// ... 略

            ),
            Spacer(),
          ],
        ),
      ),
    );
  }
}

レイアウトを変更した結果が以下になります。簡単にボタンやテキスト間にスペースをとることができました。
(Flutter初心者過ぎて、このようなアプリ設計が正しいのかわからないので、どなたか詳しい方がいたら教えていただきたいです。)
add_space.png

参考文献

Flutter Doc JP
https://flutter.ctrnost.com/basic/

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

GNSS(GPS)と「高さ」の話

はじめに

GNSS(GPS)を使った位置取得は、人類の大いなる進歩です!
我々はもう道に迷わないのです。(NEVER LOST AGAIN グーグルマップ誕生 なかなか面白い本でした。)
しかしながら、地図で自分の場所がわかって便利という以上のことを考えるためには、様々なこの分野特有の背景知識を必要とします。
本稿では、「高さ」というものに注目して、なんとなく常識だと思っていた概念の裏を見ていきましょう。

「高さ」という概念について

皆さんは、高さという概念について、どのようにとらえていますか?
富士山は3776mだし、東京タワーは333mでスカイツリーは634mという有名な数字は出てくる人が多いでしょう。
富士山の数字は標高の数字で、東京タワーやスカイツリーは所在地の標高からの差分なので、若干富士山の数字とは基準が違うことになります。

この基準というところが高さを考えるうえで重要になってきます。

ちなみに太陽系で最も高い山は火星のオリンポス山で、地表からの高さは実に約27000mだそうです。将来火星観光の目玉の登山になるかもしれませんね。

標高とは?

さて、改めて高さの表し方の一つ、「標高」の定義についてみていきましょう。

標高の定義

日本での標高の定義は、「東京湾の平均海水面からの高さ」です。海抜という言葉もあるように、海からの高さというのは、高さの概念の基本となります。
日本でのと書いたのは、実は国によって違う場合が多いからです。
東京湾という日本国内の基準をベースとするわけで、これが国際的に通用するものではないことは容易に想像がつきますが、
他の国も「近代的な」測地方法では、平均海水面を基準とすることが多いようです。

東京湾の平均海水面の定義をさらに詳しく見ると、

平均海水面定義(Wikipediaより https://ja.wikipedia.org/wiki/日本水準原点)
霊岸島量水標(現在の東京都中央区新川、当時の隅田川河口にあたる。)における1873年6月から1879年12月までの毎日(一時期欠測あり)の満干潮位を測定して平均値を算出し、量水標の読み(荒川工事基準面、Arakawa Peil、A.P.)で1.1344メートルを東京湾平均(中等)海面 「T.P.」(Tokyo Peil)とし、この位置をゼロメートルとして全国の標高の基準と定めた

となります。この高さを基準にして、全国の標高を測っています。

標高の測り方

では、任意の地点の標高はどのように測るのでしょうか。
標高の測量、水準測量と言っていますが、近代的な測量手法では、絶対的なその地点の標高をその地点だけで測ることができません。そのため、基準点からの相対的な高さとして求めていきます。
三角点や水準点という形で日本全国に既知でかつ定期的に測量しなおされている基準点があり、そこからの相対的な高さとして求めます。
もちろん、それぞれの基準点も基準原点・水準原点からの相対的な位置・高さになります。

水準測量は、以下の図のように水平をとれる水準器を中に置き、大きな定規のような標尺の目盛りを測ることで2点間の高さの相対値を測ります。
image2.png
これを基準点から連続して行うことで任意地点の基準点からの相対的な高さを測るのです。

そのため、標高の基準としてどこかに必ず0mの基準が必要なのと、どう頑張ってもそこからの相対値になります。
また、長い距離があると誤差や見通せないといった問題で測量ができません。
そのため、各国ごとに標高の基準は違いました。

After GPSの世界

GPSの登場で、この状況に終止符が打たれます。これまで海水面という場所によって違う基準で測っていたものが、地球という基準をもとにして、世界中で統一された座標系で測ることができるようになりました。これ以降が今我々が享受している「現代的な」測量の世界です。
今、我々がGNSS(GPS)によって得ている座標は、世界のどこに行っても同じ基準で測られたものなのです。
素晴らしいですね。

ちなみに、このGPSによる地球基準の座標系(測地系)としてWGS84というものが決められています。このWGS84の定義の中に、後述する楕円体の定義やジオイドモデルの定義も含まれています。この測地系という概念は、地理情報系の事を扱うときにとても重要なものなので、是非覚えて帰ってください。

(とはいえ、GNSS(GPS)は深宇宙では使えません。太陽系の中での探査機の位置決定には、VLBIという方法や、光学カメラから星の位置を調べて自己位置を決定するなどのやり方があるそうです。人類が太陽系を縦横無尽に旅するような時代になったら宇宙版GPSみたいなものができるのかもしれませんね。)

楕円体高

しかし、これまでの標高という概念とGNSS(GPS)が計測する高さには大きな違いがあります。
GNSS(GPS)が測る高さは、地球を回転楕円体としてみたときのその回転楕円体表面からの高さになります。
これを楕円体高と言っていますが、この高さは標高とは違うものです。

ジオイド

そこで、標高と楕円体高を相互に関連付けるためにジオイドという概念が導入されます。
(ジオイド自体は地質学などで概念としてありましたが、衛星測位のためにより精密なモデルが作成されたそうです)
ジオイドの表面は地球の平均海水面に一致する等ジオポテンシャル面という定義なのですが、標高の基準となる平均海水面を仮想的に定義したようなものです。
このジオイド面の楕円体高からの高さがジオイド高で、地球全体でジオイド高を定義したものをジオイドモデルといいます。
そして、高さの計算は、以下の計算式で行えます。

標高 = 楕円体高 - ジオイド高

なお、ジオイドは重力と密接に関連した概念です。地球上では場所によって実は重力が微妙に違い、その違いを表しているのがジオイドという解釈もできます。水準測量のときは、水平器という液体に気泡が入った器具で水平を出すのですが、器具の仕組みとして重力的な水平を出していることになります。そのため、標高という数値は重力と紐ついているものなのでジオイドにより楕円体高から計算できている、というように自分では理解しています。

ジオイドモデル

よく地球の形を、洋ナシのような形ということがありますが、大体このジオイドモデルの形状を言っています。
単純な計算では表せない、非常に複雑な形をしています。
以下の図は、実際にEGM96のジオイドモデルをUnityで3D表示したものです。(高さ方向に引き伸ばして強調しています)

image3.png

このジオイドモデルですが、重力の観測や、水準測量の結果から作成されます。日本周辺の高精度なジオイドモデルを国土地理院が整備しています。GNSS(GPS)による座標測位を補強する重要な情報であり、様々な観測の基盤情報となります。

スマートフォンにおける高さの定義

さて、ここまでは事前知識の説明です。ここから本題。
ほぼすべてのスマートフォン端末にGNSS(GPS)が組み込まれるようになり、皆さん便利に使っていますが、実はAPIで取得できる情報には大きな違いがあります。

iOSのCoreLocationでは、CLLocationで返ってくる高さはAltitudeつまり標高となります。
https://developer.apple.com/documentation/corelocation/cllocation

Androidでは、LocationのAltitudeの説明としてWGS84楕円体からの高さと定義が書いてあります。つまり楕円体高になります。
https://developer.android.com/reference/android/location/Location.html

問題点

おそらくiOSの内部では、GNSS(GPS)で取得した楕円体高から内部に持ったジオイドモデルにより標高を計算しているのでしょう。
この違いを知らずに、両方で動くアプリケーションを作ると、場合によっては高さが数十mずれたりすることにもなります。
また、iOSの方は、生のセンサー値としての楕円体高を正確には取得できないことになります。
どちらにしろ、より正確な位置情報のマルチプラットフォーム運用をしたい場合には、ジオイド高の計算を自前で持つ必要がありそうです。

ジオイド高の計算

ということで、ジオイド高を計算してみましょう。
今回は、アメリカ合衆国のNGA(国家地理空間情報局)とNASAが公開しているEGM96という少し古いジオイドモデルのデータと補間計算のプログラムを、Unityで使うことを前提にC#に移植してみました。もっとも、Unityの標準のGNSS(GPS)座標を取得する仕組みだと緯度経度がfloatで渡されて非常に残念なので、プラットフォーム側で位置情報取得してUnityに精度を維持して渡すのがよいでしょう。

元にしたデータとプログラムはこちらです。
https://earth-info.nga.mil/GandG/wgs84/gravitymod/egm96/egm96.html

元になるプログラム

上記ページのINTPT.FというFORTRANで書かれたプログラムを元にします。
FORTRANとか久々ですね。大学の時にやはり同じような分野のプログラムを解読して以来です。

移植で苦労したのは、配列の添字の開始が1からだったところですね。FORTRANの常識として知ってはいたので注意深く書いたつもりだったんですが、いくつかバグを仕込みました。
また、数値の丸めをちゃんと指定しないと結果が同一にならないなどもありました。

コメントを見ると、元のプログラムは1996年に作成されたもので、(EGM"96”なので1996年に制定されたモデルなのでプログラムも同時期に作成されたのでしょう)ベンチマークに載っていたCDCの3文字が時代を感じさせますね。今はなきControl Data Corporationのマシンでベンチマークされています。

こういう、過去のプログラムに関わるとコンピュータ考古学みたいで楽しいですね。

なお、近い時代のコンピュータ開発のドキュメンタリとして、「超マシン誕生」という本がありますね。Windows NTの開発を描いた「闘うプログラマー」と合わせて、コンピュータ業界を描いたドキュメンタリとして有名な1冊です。

C#での移植実装

以下が補間計算のメインプログラムです。コンストラクタでArrayHのインスタンスを指定して、interpメソッドに緯度経度を与えるとジオイド高が取得できます。

GeoidInterpolateEGM96.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.CompilerServices;
using System.IO.MemoryMappedFiles;

namespace GeoidInterpolateLib
{
    public class GeoidInterpolateEGM96
    {
        private const double south = -90.000000, north = 90.000000, west = 0.000000, east= 360.000000, dphi= 0.250000, dlam = 0.250000;
        private const int IWINDO = 4;
        public const int NBDR = 2 * IWINDO;
        public const int NLAT = 721, NLON = 1441 + NBDR;
        private const double DLAT = 0.25, DLON = 0.25;

        private const double PHIS = south, DLAW = west - NBDR / 2 * dlam;
        private const double RHO = 57.29577951;
        private const double REARTH = 6371000.0;

        private ArrayH H;

        private const int IPA1 = 20;
        private double[] A = new double[IPA1];
        private double[] R = new double[IPA1];
        private double[] Q = new double[IPA1];
        private double[] HC = new double[IPA1];

        public GeoidInterpolateEGM96(ArrayH AH)
        {
            H = AH;
        }

        public float interp(double lat,double lon)
        {
            double gh = interp(12.0, lat, lon);
            return (float)Math.Round(gh,3,MidpointRounding.AwayFromZero);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public double interp(double DMIN, double PHI, double DLA)
        {
            double ILIM = DMIN * 1000.0 * RHO / (REARTH * DLAT);
            double JLIM = DMIN * 1000.0 * RHO / (REARTH * DLON * Math.Cos((PHIS + DLAT * NLAT / 2.0) / RHO));
            double RI = (PHI - PHIS) / DLAT;
            double RJ = (DLA - DLAW) / DLON;
            int I0, J0;

            I0 = (int)RI;
            J0 = (int)RJ;

            I0 = I0 - IWINDO / 2 + 1;
            J0 = J0 - IWINDO / 2 + 1;
            int II = I0 + IWINDO - 1;
            int JJ = J0 + IWINDO - 1;

            if (I0 < 0 || II >= NLAT || J0 < 0 || JJ >= NLON)
            {
                System.Console.Error.WriteLine("ERROR : PHI=" + PHI + " DLA=" + DLA + " STATION TOO NEAR GRID BOUNDARY  - NO INT. POSSIBLE");
                throw new Exception();
            } else if (I0 < ILIM || II > NLAT - ILIM || J0 < JLIM || JJ > NLON - JLIM)
            {
                System.Console.Error.WriteLine("ERROR : PHI=" + PHI + " DLA=" + DLA + " STATION OUTSIDE ACCEPTABLE AREA - NO INT. PERFORMED");
                throw new Exception();
            }

            for(int i = 0;i < IWINDO; i++)
            {
                for(int j = 0;j < IWINDO; j++)
                {
                    A[j] = H.GetH(I0 + i,J0 + j);
                }
                initspA();
                HC[i] = splineA(RJ - J0 + 1.0);
            }
            initspHC();
            return splineHC(RI - I0 + 1.0);
        }

        private static void printArray(double[] array)
        {
            for(int i = 0;i < array.Length; i++)
            {
                System.Console.WriteLine("" + i + "\t" + array[i]);
            }
        }

        private void initspA()
        {
            Q[0] = 0.0;
            R[0] = 0.0;

            for(int k = 1; k < IWINDO - 1; k++)
            {
                double P = Q[k - 1] / 2.0 + 2.0;
                Q[k] = -0.5 / P;
                R[k] = (3.0 * (A[k + 1] - 2.0 * A[k] + A[k - 1]) - R[k - 1] / 2.0) / P;
            }
            R[IWINDO - 1] = 0.0;
            for (int k = IWINDO - 2; k > 0; k--)
            {
                R[k] = Q[k] * R[k + 1] + R[k];
            }
        }
        private void initspHC()
        {
            Q[0] = 0.0;
            R[0] = 0.0;

            for (int k = 1; k < IWINDO - 1; k++)
            {
                double P = Q[k - 1] / 2.0 + 2.0;
                Q[k] = -0.5 / P;
                R[k] = (3.0 * (HC[k + 1] - 2.0 * HC[k] + HC[k - 1]) - R[k - 1] / 2.0) / P;
            }
            R[IWINDO - 1] = 0.0;
            for (int k = IWINDO - 2; k > 0; k--)
            {
                R[k] = Q[k] * R[k + 1] + R[k];
            }
        }

        private double splineA(double X)
        {
            int J = ifrac(X);
            double XX = X - J;
            return A[J - 1] +
                XX * ((A[J] - A[J - 1] - R[J - 1] / 3.0 - R[J] / 6.0) +
                XX * (R[J - 1] / 2.0 +
                XX * (R[J] - R[J - 1]) / 6.0));
        }

        private double splineHC(double X)
        {
            int J = ifrac(X);
            double XX = X - J;
            return HC[J - 1] +
                XX * ((HC[J] - HC[J - 1] - R[J - 1] / 3.0 - R[J] / 6.0) +
                XX * (R[J - 1] / 2.0 +
                XX * (R[J] - R[J - 1]) / 6.0));
        }

        public static int ifrac(double R)
        {
            return (int)Math.Floor(R);
        }
    }
}

補間用のジオイドモデルのデータとしてArrayHというインターフェースを実装した2種類のクラスを用意しています。

ArrayH.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace GeoidInterpolateLib
{
    public interface ArrayH
    {
        public void LoadData(string path);
        public void Dispose();
        public float GetH(int i, int j);

    }
}
ArrayImpl.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO.MemoryMappedFiles;

namespace GeoidInterpolateLib
{
    public class ArrayImpl : ArrayH
    {
        private float[] H = new float[GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON];
        public void Dispose()
        {
            H = null;
        }

        public float GetH(int i, int j)
        {
            return H[j + i * GeoidInterpolateEGM96.NLON];
        }

        public void LoadData(string path)
        {
            using (var mmf = MemoryMappedFile.CreateFromFile(path))
            {
                using (var accessor = mmf.CreateViewAccessor(0, H.Length * 4))
                {
                    for (int i = 0; i < H.Length; i++)
                    {
                        H[i] = accessor.ReadSingle(i * 4);
                    }
                }
            }
        }
    }
}
DictImpl.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO.MemoryMappedFiles;

namespace GeoidInterpolateLib
{
    public class DictImpl : ArrayH
    {
        private MemoryMappedFile mmf;
        private MemoryMappedViewAccessor accessor;
        private Dictionary<int, float> hDic = new Dictionary<int, float>(100);

        public void Dispose()
        {
            mmf.Dispose();
            accessor.Dispose();
        }

        public float GetH(int i, int j)
        {
            int index = j + i * GeoidInterpolateEGM96.NLON;
            float f;
            if (!hDic.TryGetValue(index, out f))
            {
                f = accessor.ReadSingle(index * 4);
                hDic.Add(index, f);
            }
            return f;
        }

        public void LoadData(string path)
        {
            mmf = MemoryMappedFile.CreateFromFile(path);
            accessor = mmf.CreateViewAccessor(0, GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON * 4);
        }
    }
}

内部のデータの持ち方として、ArrayとDictionaryで作り分けています。
これは、自分のいる場所のジオイド高を取得するなどの用途だと、地球全体のデータは必要ないので必要な部分だけ読み込むDictで、地球全域の座標データを変換する用途などの時は、あらかじめ全データを読み込んでおいた方が効率が良いのではということで、Arrayの方を使うとよいのではないかということで作り分けています。

また、ジオイドモデルのデータは、あらかじめ以下のプログラムで変換しておいて使います。

DataConvert.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using GeoidInterpolateLib;

namespace DataConvert
{
    class DataConvert
    {
        private float[] H = new float[GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON];
        private void LoadData()
        {
            StreamReader sr01 = new StreamReader(@"WW15MGH.GRD");
            string headerLine = sr01.ReadLine();

            string line;
            double[] data = new double[GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON];
            int ii = 0;
            while ((line = sr01.ReadLine()) != null)
            {
                string[] tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
                foreach (string token in tokens)
                {
                    data[ii++] = double.Parse(token.Trim());
                }
            }

            for (int i = 0; i < GeoidInterpolateEGM96.NLAT; i++)
            {
                for (int j = 0; j < GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR; j++)
                {
                    H[j + GeoidInterpolateEGM96.NBDR / 2 + (GeoidInterpolateEGM96.NLAT - i - 1) * GeoidInterpolateEGM96.NLON] = (float)data[j + i * (GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR)];
                }
                for (int k = 0; k < GeoidInterpolateEGM96.NBDR / 2; k++)
                {
                    H[k + (GeoidInterpolateEGM96.NLAT - i - 1) * GeoidInterpolateEGM96.NLON] = (float)data[GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR - GeoidInterpolateEGM96.NBDR / 2 + k + i * (GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR)];
                    H[k + GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR / 2 + (GeoidInterpolateEGM96.NLAT - i - 1) * GeoidInterpolateEGM96.NLON] = (float)data[k + i * (GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR)];
                }
            }
        }

        public void WriteData()
        {
            BinaryWriter bw = new BinaryWriter(new FileStream(@"ww15mgh.bin", FileMode.OpenOrCreate, FileAccess.Write));
            foreach (float f in H)
            {
                bw.Write(f);
            }
            bw.Close();
        }

        static void Main(string[] args)
        {
            var dc = new DataConvert();
            dc.LoadData();
            dc.WriteData();
        }
    }
}

これで、iOSでは、生のセンサ値に近い楕円体高を計算できますし、Androidで標高を計算することもできるようになります。

おわりに

なぜこんなことをやっているかなのですが、ARを街中で使うときに、この辺りの計算ができると色々便利じゃないかなというところが、最初の動機になります。
さまざまな情報をより正確に扱えることは悪いことではないと思いますので、皆さんも機会があれば参考にしてみてください。

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

Xcodeのビルド時にアーキテクチャが異なるframeworkファイルを差し替える

qnote Advent Calendar 2020 の2日目です。

とあるプロジェクトで使用している外部ライブラリが実機用とシミュレーター用の2つのframeworkファイルがありプロジェクトファイルには実機用がリンクされています。
実機用のファイルには実機のアーキテクチャが含まれているバイナリファイルがあり、シミュレータ用のファイルにはシミュレータのアーキテクチャがあり、実機のアーキテクチャにはシミュレータが含まれていないためシミュレーターで実行ができない状態になっているのでシミュレーターで実行ができるようにしたいと思います。
シミュレータで実行する際にビルド時にエラーになってしまうのでビルド前差し替えてビルド後に戻せば良いのではと思いましたのでこの方法で進んでみます。

frameworkのバイナリに含まれているアーキテクチャについて

詳しくは以下のサイトをご確認ください。
https://qiita.com/fuwamaki/items/ce1cd438ec5dfd4ccb9a#simulator%E3%81%A7%E3%81%AF%E3%83%93%E3%83%AB%E3%83%89%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%AE%E3%81%AB%E5%AE%9F%E6%A9%9F%E3%81%A0%E3%81%A8%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84
https://blog.ymyzk.com/2015/12/ios-tvos-watchos-architectures/

どのように差し替えるか

・Build PhaseのRun Script
・スキームのPre-actionsとPost-actions
のどちらかで差し替えをします。
調べると大体シェルスクリプトが出てきますがせっかくなのでSwiftでやっていこうと思います。
Run Scriptはドラッグ&ドロップで移動できるのでビルド前の方はCompile Sourcesより前に配置します。

注意点

どちらもビルドが失敗するとビルド後の処理が実行されないので万が一のことを考えてGit等で元に戻せるようにしておいた方が良いです。

プロジェクトフォルダの状態

プロジェクトを新規作成してデフォルトでできる(プロジェクト名).xcodeprojファイルがあるディレクトリに
(プロジェクト名)/SDK/hoge/lib/iphoneos/hoge.framework
(プロジェクト名)/SDK/hoge/lib/iphonesimulator/hoge.framework
に各frameworkファイル(iphoneosが実機、iphonesimulatorがシミュレータ)と
script/script.swift
にスクリプト用Swiftファイルがある状態です。

Swiftのファイルの実行

ビルド前

#ログを書き出して確認する場合は下の行を実行する
#exec > ${PROJECT_DIR}/script/prebuild.log 2>&1

/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/script/script.swift "pre" ${PROJECT_DIR} ${PLATFORM_NAME}

ビルド後

#ログを書き出して確認する場合は下の行を実行する
#exec > ${PROJECT_DIR}/script/postbuild.log 2>&1

/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/script/script.swift "post" ${PROJECT_DIR} ${PLATFORM_NAME}

preの部分がpostになっています。

script.swiftの中身は以下の内容になっています。

script.swift
#!/usr/bin/swift

import Cocoa
import Foundation

let actionType = CommandLine.arguments[1]       // 引数1 "pre" または "post"
let projectDirectory = CommandLine.arguments[2] // 引数2 ${PROJECT_DIR}
let platform = CommandLine.arguments[3]         // 引数3 ${PLATFORM_NAME}

let frameworkFileName = "hoge.framework"
let tempFrameworkFileName = "temp_\(frameworkFileName)"
let libraryDirectory = "\(projectDirectory)/(プロジェクト名)/SDK/hoge/lib/"
let iphoneFrameworkDirectory = "\(libraryDirectory)/iphoneos/"
let iphoneOriginalPath = "\(iphoneFrameworkDirectory)\(frameworkFileName)"
let iphoneTempPath = "\(iphoneFrameworkDirectory)\(tempFrameworkFileName)"
let simulatorFrameworkDirectory = "\(libraryDirectory)/iphonesimulator/"
let simulatorOriginalPath = "\(simulatorFrameworkDirectory)\(frameworkFileName)"

class Script {

    var isSimulator: Bool {
        return platform == "iphonesimulator"
    }

    func preAction() {
        reverseActionIfNeeded()
        guard isSimulator else {
            return
        }
        let _ = rename(iphoneFrameworkDirectory, oldName: frameworkFileName, newName: tempFrameworkFileName)
        let _ = copy(simulatorOriginalPath, toPathName: iphoneOriginalPath)
    }

    func postAction() {
        guard isSimulator else {
            return
        }
        let _ = remove(iphoneOriginalPath)
        let _ = rename(iphoneFrameworkDirectory, oldName: tempFrameworkFileName, newName: frameworkFileName)
    }

    func reverseActionIfNeeded() {
        guard FileManager.default.fileExists(atPath: iphoneTempPath) else {
            return
        }
        let _ = remove(iphoneOriginalPath)
        let _ = rename(iphoneFrameworkDirectory, oldName: tempFrameworkFileName, newName: frameworkFileName)
    }

    func copy(_ atPathName: String, toPathName: String) -> Bool {
        do {
            try FileManager.default.copyItem(atPath: atPathName, toPath: toPathName)
        } catch {
            print(error)
            return false
        }
        return true
    }

    func rename(_ pathName: String, oldName: String, newName: String) -> Bool {
        let atPathName = "\(pathName)/\(oldName)"
        let toPathName = "\(pathName)/\(newName)"
        do {
            try FileManager.default.moveItem(atPath: atPathName, toPath: toPathName)
        } catch {
            return false
        }
        return true
    }

    func remove(_ pathName: String) -> Bool {
        do {
            try FileManager.default.removeItem(atPath: pathName)
        } catch {
            return false
        }
        return true
    }
}

if actionType == "pre" {
    Script().preAction()
} else if actionType == "post" {
    Script().postAction()
}

大まかに動作内容を解説します。
ビルド前のpreAction()でシミュレータの場合に
iphoneos/hoge.frameworkiphoneos/temp_hoge.frameworkにリネームして
iphonesimulator/hoge.frameworkiphoneos/hoge.frameworkにコピーします。
これでプロジェクトファイルにリンクされている実機用のhoge.frameworkをシミュレータ用のファイルと入れ替えることができ、ビルドが成功するようになります。
ビルド後のpostAction()にて
iphoneos/hoge.frameworkを削除して
iphoneos/temp_hoge.frameworkiphoneos/hoge.frameworkにリネームしています。
これで元の状態に戻ります。

注意点でも記載しましたがビルドに失敗した場合にpostAction()が実行されないので
preAction()のreverseActionIfNeeded()にてiphoneos/temp_hoge.frameworkが存在していた場合iphoneos/hoge.frameworkがシミュレータ用のままなので最初に元の状態に戻してから処理を行うようにしています。

参考
https://qiita.com/kuluna/items/2225298e8a9a7b3b3ef6

中々こういう状況にはならないかと思いますが、もし誰かが同じことで困った時の助けになればと思います。

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

iOSでの半モーダル/ハーフモーダルの実装についてのまとめ

はじめに

  • iOSのUIでハーフモーダルを見かけることが増えました。Apple純正のアプリでも実装されることが多く、今後実装することが多くなると思います。 今後の実装の参考になるように情報をまとめたいと思いました。

半モーダル/ハーフモーダルとは

・モードの完全遷移が起こらない
・モードを多重化しながらパラレルに対話可能
・モードを終了させずにモードからの一時退避が可能
・擬似的なマルチウインドウ・インターフェイスに応用可能
・スワイプなどインタラクションコストの低い操作方法によってモードの切り替えが可能
  • Pull dissmissや画面遷移せずにタスクを切り替える機能などによく使われます
  • 代表的なものはAppleのMapアプリです

Kapture 2020-11-30 at 16.59.16.gif

 実装方法

UIModalPresentationStyleを使う

Kapture 2020-11-30 at 16.57.26.gif

     let vc = UIViewController()
     vc.modalPresentationStyle = .pageSheet
     present(vc, animated: true, completion: nil)
  • この実装だと下階層の画面に対してタップできないなどの仕様の制限があります。

ライブラリを使う

  • すでに完成度の高いライブラリが存在するので実装コストを下げることができます。

SCENEE/FloatingPanel

  • Apple純正のアプリのような動きを再現できます

Kapture 2020-11-30 at 18.10.27.gif

Kapture 2020-11-30 at 18.12.03.gif

slackhq/PanModal

Kapture 2020-11-30 at 18.16.30.gif

自前で実装する

  • より完成度の高い完成度を目指す場合は自前で実装することも選択肢に入ります。実装する上でのポイントをあげておきます。

徹底調査

  • 完成度の高いライブラリが多いので中身を覗いてみると理解が進みます。とくに上記で挙げたライブラリは参考になります。
  • cocoacontrolsなどを使うとたくさんのライブラリを見つけることができます。

UIViewControllerTransitioningDelegate

  • この実装をすることで表示のアニメーションのカスタマイズと表示後の画面もカスタマイズできます
let vc = UIViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
present(vc, animated: true, completion: nil)
extension UIViewController: UIViewControllerTransitioningDelegate {
    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return UIPresentationController(presentedViewController: presented, presenting: presenting)
    }

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PresentationAnimator()
    }

    public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissionAnimator()
    }
}

PSPDFTouchForwardingView

final class PSPDFTouchForwardingView: UIView {

    final var passthroughViews: [UIView] = []

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let hitView = super.hitTest(point, with: event) else { return nil }
        guard hitView == self else { return hitView }

        for passthroughView in passthroughViews {
            let point = convert(point, to: passthroughView)
            if let passthroughHitView = passthroughView.hitTest(point, with: event) {
                return passthroughHitView
            }
        }

        return self
    }
}
private var touchForwardingView: PSPDFTouchForwardingView?
override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()
    touchForwardingView = PSPDFTouchForwardingView(frame: containerView!.bounds)
    containerView?.insertSubview(touchForwardingView, at: 0)
}

まとめ

  • 近年ハーフモーダルのUIを採用するアプリが増えてきている
  • 完成度の高いライブラリも多く実装コストを下げることができる
  • 自前実装もポイントを押さてしまえば簡単です?‍♂️

参考リンク

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

iOSでの半モーダル/ハーフモーダルの実装についてまとめ

はじめに

  • iOSのUIでハーフモーダルを見かけることが増えました。Apple純正のアプリでも実装されることが多く、今後実装することが多くなると思います。 今後の実装の参考になるように情報をまとめたいと思いました。

半モーダル/ハーフモーダルとは

・モードの完全遷移が起こらない
・モードを多重化しながらパラレルに対話可能
・モードを終了させずにモードからの一時退避が可能
・擬似的なマルチウインドウ・インターフェイスに応用可能
・スワイプなどインタラクションコストの低い操作方法によってモードの切り替えが可能
  • Pull dissmissや画面遷移せずにタスクを切り替える機能などによく使われます
  • 代表的なものはAppleのMapアプリです

Kapture 2020-11-30 at 16.59.16.gif

 実装方法

UIModalPresentationStyleを使う

Kapture 2020-11-30 at 16.57.26.gif

     let vc = UIViewController()
     vc.modalPresentationStyle = .pageSheet
     present(vc, animated: true, completion: nil)
  • この実装だと下階層の画面に対してタップできないなどの仕様の制限があります。

ライブラリを使う

  • すでに完成度の高いライブラリが存在するので実装コストを下げることができます。

SCENEE/FloatingPanel

  • Apple純正のアプリのような動きを再現できます

Kapture 2020-11-30 at 18.10.27.gif

Kapture 2020-11-30 at 18.12.03.gif

slackhq/PanModal

Kapture 2020-11-30 at 18.16.30.gif

徹底調査

  • 完成度の高いライブラリが多いので中身を覗いてみると理解が進みます。とくに上記で挙げたライブラリは参考になります。
  • cocoacontrolsなどを使うとたくさんのライブラリを見つけることができます。

自前で実装する

  • より完成度の高い完成度を目指す場合は自前で実装することも選択肢に入ります。実装する上でのポイントをあげておきます。

UIViewControllerTransitioningDelegate

  • この実装をすることで表示のアニメーションのカスタマイズと表示後の画面もカスタマイズできます
let vc = UIViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
present(vc, animated: true, completion: nil)
extension UIViewController: UIViewControllerTransitioningDelegate {
    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return UIPresentationController(presentedViewController: presented, presenting: presenting)
    }

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PresentationAnimator()
    }

    public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissionAnimator()
    }
}

PSPDFTouchForwardingView

final class PSPDFTouchForwardingView: UIView {

    final var passthroughViews: [UIView] = []

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let hitView = super.hitTest(point, with: event) else { return nil }
        guard hitView == self else { return hitView }

        for passthroughView in passthroughViews {
            let point = convert(point, to: passthroughView)
            if let passthroughHitView = passthroughView.hitTest(point, with: event) {
                return passthroughHitView
            }
        }

        return self
    }
}
private var touchForwardingView: PSPDFTouchForwardingView?
override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()
    touchForwardingView = PSPDFTouchForwardingView(frame: containerView!.bounds)
    containerView?.insertSubview(touchForwardingView, at: 0)
}

まとめ

  • 近年ハーフモーダルのUIを採用するアプリが増えてきている
  • 完成度の高いライブラリも多く実装コストを下げることができる
  • 自前実装もポイントを押さてしまえば簡単です?‍♂️

参考リンク

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

Macのターミナルでファイル名の部分文字列を一括で置換する

たまにしかやらなくて忘れて毎回探すので、自分用にメモ。

方法

以下のコマンドで置換できる。

rename -s [置換元] [置換先]

first → secondへの置換
該当ディレクトリに移動し、
rename -s first second

command not foundが出る方へ

※renameはHomebrewでインストールできます。
Homebrewをインストール済みであれば、以下のコマンドでインストールできます。
brew install rename

環境

OS:Mac OS Catalina 10.15.6
Homebrew 2.5.12

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

iPhoneのショートカットAppを使い倒す

プロローグ

iOS12より新機能として追加されたSiriショートカット。一連の操作をあたかもプログラミングのように構築することで、デバイスにおけるあらゆる操作を自動かつ簡単にこなすことができる。まさに生活のショートカットである。現在はSiriによる音声認識だけではなく、ショートカットをウィジェットやデスクトップに配置することでボタンひとつで簡単に起動させることや、スマートデバイスと組み合わせて使うことでスマートホームをより発展させることが出来るなど、ショートカットアプリには無限の可能性が秘められている
ここでは、日常に転がる"不満や不便さ"をショートカットアプリ使ってどのように解決しているのか、私の1日の生活をを追いながら説明する。

Apple公式 ショートカット ユーザガイド

そもそもiPhoneショートカットAppでなにができるのか?

例を交えて説明した方がわかりやすいだろう。もっとも有名なiPhoneショートカットは"Wifiオフ"であると思われる。iOS以降、コントロールセンターからWifi機能をオンオフを切り替える際、オンは普通に使えるのだがオフは設定アプリから変更しなければ完全に設定することができない。時間が経つと自動的にWifiがオンになってしまう。
そこで便利なのがショートカットである。ショートカットにはWifiのオンオフ切り替えのアクションが設定できるため、これを作成すればWifiを完全にオフにすることができる。
以下に共有リンクを貼っておくので、持っていない人がいればぜひ利用して欲しい。
Wifiオフショートカット ダウンロードリンク

よりスマートな自動化を行えるオートメーション

ショートカットは設定したショートカットを押したりSiriに頼んだりしなければ使えないわけではない。ある特定の条件が揃った場合でのみ、自動的にショートカットを起動させることも可能である。
例えば、先ほど紹介した"Wifi完全オフ"ショートカットだが、Wifi環境のない自宅を出る際にわざわざショートカットアプリを起動するのはとても面倒である。そこで役立つのがこの"オートメーション機能"である。オートメーションには"ある時刻になった時"、"ある場所に着いた時or離れた時"、"〇〇からメールが届いた時"など、各条件をとりがーとしてショートカットを起動させることが可能である。ここで、"ある場所を離れた時(出発)"のオートメーションで自宅を設定し、それをトリガーとしてWifiオフのショートカットを連動させれば、自宅から離れた際に自動でWifiをオフにすることができる。
Screenshot at Oct 27 18-00-47.png
Screenshot at Oct 27 18-00-47.png

ショートカットの共有

ショートカットアプリにはリンクを介した共有機能が存在する。今回の記事に掲載しているほとんどのショートカットはリンクで共有されているのでぜひ利用してもらいたい。リンクで共有されているショートカットをインストールするにはiPhoneの設定 -> ショートカット -> 信頼されていないショートカットを許可をオンにする必要があるので各自設定の変更をお願いしたい。

朝の身支度

朝。起床時刻となる。iPhoneから黒電話のアラーム音が鳴る。
眠い目を擦りながらアラームを止める。
すると早速、"アラームを止めた"をトリガーにショートカットが起動する。

まず、自室に備え付けてある赤外線リモコン"Nature Remo mini"がシーリングライトをオンにする。

実際はこの動作の前に現在地情報を取得し、if関数で自宅だった場合のみシーリングライトをつけるように設定してあります。そうしないと、旅行や出張先でアラームを停止した際に部屋の明かりがついてしまいます。

その後、iPhoneから今日の天気予報が流れる。
iPhone 「現在8時30分。〇〇県〇〇市の天気は晴れ。平均気温は〇〇度です。最高気温は〇〇度となります。また、降水確率はパーセント、降水量はミリです。」
この後、降水量が50%を超える場合は傘を持って行くよう伝えてくれたり、最高気温が28度だととても暑いと注意を促してくれます。
天気予報 ダウンロードリンク

本ショートカットは下記ブログより拝借しました。また、現在地の天気を取得する場合、全ての情報が1つのテキストに入っていると誤作動を起こす場合があるため、別のテキストとして分割したものを独自で作成しましたので、今回はそれを共有リンクとして載せておきます。
転載元: 未来生活5:今日の天気をSiriに読み上げてもらう方法(iPhone)

出勤

出勤のための身支度を整える。そしていざ家を離れる。
玄関を出たらインターホンに設置しているNFCタグをiPhoneでタッチする。すると、"NFCタグ"をトリガーとしたオートメーションが起動する。

NFCタグとは、近距離無線通信・NFCを搭載している小さなアンテナのようなもので、SuicaやPASMOのように端末と通信が行えるタグのことです。

ここで、スマートロックである"SESAMI mini"によって、玄関の鍵が閉まる。そして、Nature Remo miniによってシーリングライトがオフになる。さらに、iPhoneのWifi機能がオフになる。

通勤中、私はワイヤレスイヤホンで音楽を聴く。イヤホンをオンにすると、自動でiPhoneとBluetooth接続される。この"イヤホンとのBluetooth接続"をトリガーとしてショートカットが起動する。
iPhoneにはストリーミング再生用のAmazon Musicと、PCから転送した音楽データを再生するEver Playが入っているので、"イヤホンとのBluetooth接続"が行われると、Amazon MusicとEver Playどちらを使うか尋ねられるので使用する方を選択すると、そのアプリが自動で立ち上がる。

また、音源の音量調整はiPhone本体横のスイッチでは増減の幅が大きいため、シュートカットを使って微調整を行うことができる。
【改良版】iPhoneショートカットでシステム音量を微調整する

職場に到着

自分のデスクについたら"出勤"のNFCタグにタッチする。
自分が習慣でつけているタイムカード用カレンダー(Googleカレンダー)に現在時刻が記録される。
iPhoneショートカットで出退勤のタイムカードを作成する

また、Wifi機能がオンになり、職場のWifiに自動で接続することができる。
そして、おやすみモード(マナーモード)がオンになる。

あと、何かの間違いで玄関が解錠されている状態にならないように、解錠されている場合は施錠するようなショートカットを組んでいる。
Screenshot at Oct 27 18-00-47.png
Screenshot at Oct 27 18-00-47.png

お昼のスマホチェック

お昼の時間。買い物依存症の私は近日ネットでポチった商品の配達状況を確認する。運送業者と発送番号さえわかれば、わざわざ発送業者の荷物追跡ページを開かなくても手軽に現在の状態を確認することができる。
iPhoneショートカットで荷物の追跡を行う

会議

会議前に装着しているApple Watchをシアターモードに設定して消音にする。ここもショートカットでワンタップで切り替えができる。
Watchシアターモードオン ダウンロードリンク

仕事を終えて帰宅しよう

夜。帰宅の時間。
"退勤"のNFCタグにタッチすると、タイムカード用カレンダーに出勤〜退勤時刻が記入される。また、出勤時とは逆におやすみモードオフ、Wifiオフの状態になる。
そして、Nature Remo miniが現在の室温を検知して寒すぎたら暖房を、暑すぎたら冷房をつけてくれる。
帰宅時の快適な空間を作る (Nature Remo × iPhoneショートカット)

ちょっとお買い物

帰り道にスーパーへ寄り道してちょっとお買い物をする。支払いはお店に合わせたQRコード決済で行う。レジで素早く目的のアプリが起動できるように、決済アプリをリストとしてショートカットにまとめている。
Payアプリ起動 ダウンロードリンク

また、ショートカットを使えば商品の割引計算や税込価格計算が簡単にできる。
商品の割引額から税率計算までやってくれるiPhoneショートカット

自宅に到着

自宅玄関に着く少し前のタイミングで帰宅のショートカットを起動させる。すると、玄関の鍵が開き、部屋の照明がつく。
そしてWifiがオンになる。
部屋はあらかじめ空調が効いているため最高の環境が私を迎えてくれる。

おやすみなさい

寝る前ももちろんショートカットを使用する。布団に潜り、手元のiPhoneからシーリングライトおよびデスクの照明を一括でオフにする。

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

React Nativeのメリット・デメリット

はじめに

先日、React Nativeを使った個人開発アプリをリリースしました。
そこで、この記事ではReact Nativeでアプリを開発してみて良かった点、良くなかった点を紹介したいと思います。

「React Nativeを始めようかな」と考えている人の役に立つことができれば嬉しいです。

メリット❶ 学習コストが少ない

1つ目のメリットは学習コストが少なかったことです。
私は今回のアプリを開発する前は、JavaScriptのライブラリであるReactでWebアプリケーションの開発を行っていました。
React Nativeはコンポーネントの概念などReactと共通している部分が多いので、非常に開発に取り掛かりやすかったです。
Reactを使用してWebアプリケーションの開発を行っている方であれば、React Nativeの学習は参入障壁が非常に低く、取り組みやすいと感じます。

メリット❷ コードの修正が簡単

2つ目のメリットはコードの修正が簡単なことです。React Nativeは、ホットリロード機能があるため、自分が書いたコードを保存すれば、自動でリロードしてくれます。
例えば、UIを修正する際は変更したコードが瞬時に画面に反映されるため、スムーズに開発を進めることが出来ます。

メリット❸ 開発が効率的

3つ目のメリットは開発が効率的になることです。React Nativeは、クロスプラットフォームフレームワークであるため、iOSとAndroidのアプリを同時に開発することができます。この点はReact Nativeの大きな魅力であると感じました。

デメリット❶ エラーの解決や機能の実装に時間がかかる

続いてデメリットの紹介です。
1つ目のデメリットはエラーの解決や機能の実装に時間がかかることです。
個人的にReact Nativeは、エラーの解決や新しい機能を実装したいときに参考にするネット記事や資料が、他言語に比べて少ないと感じました。
そのため、アプリ開発中は英語の記事や資料を参考にする必要がありました。

デメリット❷ネイティブエンジニアには学習コストが高い

普段からネイティブ言語で開発しているエンジニアにとっては、1からJavaScriptを学習しなければいけないため、学習コストが高くなると考えられます。

まとめ

以上のように今回の記事では、私がReact Nativeでアプリを開発して感じたメリット・デメリットについて紹介しました。Reactの知識を使って、モバイルアプリケーションを開発できるという点が非常に素晴らしいと感じています。

ちなみに、今回私がリリースしたアプリはこちら

次回の記事では、実際に開発したアプリについて紹介してみようと思います!

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