20200926のiOSに関する記事は16件です。

日本語表示におけるUILabelのbyCharWrappingとbyWordWrappingの違い

はじめに

Swift の UILabel には折り返しや省略表示を指定するための lineBreakMode というプロパティが用意されています。指定できるモードの中に折り返し位置を単語区切りにする byWordWrapping と、文字区切りにする byCharWrapping があるのですが、byWordWrapping を指定しても日本語の文章は単語区切りで表示されません。それではbyCharWrapping を指定しても byWordWrapping を指定しても違いはないのか、気になったので調べてみました。

環境

Xcode Version 12.0
Swift 5.3

byCharWrapping

lineBreakMode に byCharWrapping を指定して表示したところ画像のようになりました。
わかりやすいようにUILabel の背景を黄色にしています。
image.png

byWordWrapping

次に byWordWrapping を指定して同じ文章を表示してみました。
byWordWrapping を指定すると行頭に句読点や括弧の終わり、小文字、伸ばし棒などがこないように調整されるようです。こちらのほうが読みやすいですね。
image.png

まとめ

byCharWrapping と byWordWrapping のどちらを指定しても良いという場合は禁則処理が考慮されている byWordWrapping を使用するのが良いようです。

■参考サイト
https://developer.apple.com/documentation/uikit/nslinebreakmode

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

Xcodeでショートカットを自作し 煩雑な pod install 作業をなくそう

はじめに

業務で同時に複数のプロジェクトを見ることがあり、平均すると1日1回ぐらいterminalを開いてpod-installのコマンドを叩いています。本記事はiOSアプリ開発者が良く使うCocoaPodsのコマンドpod-installの作業を楽にするtipsです。XcodeにはBehaviorsと言う機能があり、様々な動作やイベントを設定・追加できます。 その機能を利用して、自動でterminalを開き所定のdirectoryに移動してpod-installコマンドを叩いてくれるショートカットキーを作成します。

開発環境

Xcode12 (Xcode11でも動くと思いますが未検証)
Cocoapods

セットアップ

1. Scriptを用意

Script をダウンロード

#!/bin/sh
osascript <<END
tell application "Terminal"
if not (exists window 1) then reopen
activate
do script "cd `pwd`; pod install" in window 1
end tell
END

2. 実行権限の付与

権限の確認

$ ls -l Pod-Install.sh 
-rw-rw-r--@ 1 yasuradodo  staff  158 Sep 19 19:40 Pod-Install.sh

もし実行権限がなければ、実行権限xの付与

$ chmod u+x Pod-Install.sh  

再度権限の確認

$ ls -l Pod-Install.sh 
-rwxrw-r--@ 1 yasuradodo  staff  158 Sep 19 19:40 Pod-Install.sh

3. XcodeのBehaviorsでCustom commandを作成

XcodeのPreferencesからBehaviorsを選択し、左下にある+をタップしてCustom commandを作成します。
ショートカットには好きなキーを登録しましょう。筆者は shift + command + pにしました。
Screen_Shot_2020-09-26_at_18_30_48.png
これで、Xcode上で登録したショートカットでいつでも自動的にpod-installが実行できます。

終わりに

今回はpod-installだけでしたが、Carthage, SourceTree, SwiftLintなど様々な物に対応でき作業効率を向上できます。
他にも便利なBehaviorsの使い方などあれば、教えていただきたいです :pray:

参考文献

https://medium.com/@abhishekbedi/never-type-pod-install-again-ever-eb55386eef59
https://github.com/JeaSungLEE/Awesome-Xcode-Behaviors

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

【Flutter入門】短期集中講座やったので要点を整理してみた

Android/iOS向けアプリ作りたいけどなんかいい開発環境ないかなぁ。
Electronのスマートフォン版あったら便利なんだけどなぁ。
Flutter?聞いたことはあるけどDartが不人気なことで有名だしなぁ。
(念の為改めて調査してみる)
は?Dartめっちゃいいじゃん!なにこのC言語系の正当進化版みたいな可読性!?何が不人気なの?? →完全にただの記憶違いでしたm(_ _)m
Flutterもデスクトップ・モバイル・Webをカバーした理想的な環境っぽいなぁ。
よし、まずは入門だ!
(最終更新:2020.09.27)

作業した環境

  • OS:macOS Catalina バージョン10.15.7

集中講座の元動画様(英語)

Flutter Crash Course
この記事の位置づけとして、基本的に元動画様に沿って進めていって、補助として個人的に記録しておきたいと思った情報を日本語でわかりやすくざっくりまとめた感じです。

作るもの

シンプルな単語ペアジェネレータ。
ランダムに生成された単語ペアをリスト表示する。
気に入った単語ペアはタップで印をつけることができ、別ページで一覧を確認できる。

完成イメージ

Flutterとは

Google製UIツールキット。
一つの共通化されたコードをもとにモバイル・Web・デスクトップ向けのネイティブクロスプラットフォームアプリが開発できる。
パフォーマンスが極めて高い。

Dartとは

Flutterが採用しているオブジェクト指向型プログラミング言語。
UIプログラミングに最適化されている。
すべてのプラットフォームで高速に動作する。
文法的には、例えるならJavaの要素を持つJavaScript。

Widgets

DartではすべてのパーツがWidgetである。
マテリアルデザインを採用している。
Scaffold(土台)、AppBar、Container、Image、Icon、などなど。
StatelessとStatefulがある。
build関数で作成する。

Stateless Widgets

状態を持たない静的なWidgetのこと。
実行中変化せず、自身を再描画する必要がないUIに使われる。

Statefull Widgets

状態を持っている動的なWidgetのこと。
実行中に変化し、自身を再描画する必要があるUIに使われる。

開発環境構築

ざっくりと今回必要な環境をまとめておきます。

  • Flutter本体

    • Flutterから環境にあったFlutterをインストール(圧縮ファイルを解凍して任意の場所に配置)
    • flutter/binにパスを通す
  • Visual Studio Code

    • Visual Studio Codeからインストール
    • Flutterエクステンションを追加
    • Dartエクステンションを追加
  • Xcode

    • AppStoreから最新のXcodeをインストール
    • 下記コマンドを実行してXcodeを設定
    • sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    • sudo xcodebuild -runFirstLaunch
  • Android Studio

    • Android Studioから最新のAndroid Studioをインストール
    • Flutterプラグインを追加
    • 仮想デバイスを作成

開発環境構築チェック

テンプレートアプリを動かして開発環境が構築されたことを確認します。
※ここまでで詳細な手順やトラブルシューティングは省いているので、適宜自分で調べながらになるかと思います。

ターミナルで作業ディレクトリに移動して下記コマンドを実行

flutter create wordpair_generator
cd wordpair_generator
code .

★ターミナルからcodeコマンドが使えない場合は一度VSCodeを開いて、

Cmd+Shift+P
shell と入力
Shell Command: Install 'code' command in PATH を選択

とすると使えるようになる。

wordpair_generatorプロジェクトをVSCodeで開けたら、右下の「No Device」をクリックして「Start iOS Simulator」を選択し、シミュレータを立ち上げます。

上部のメニューから、
Run > Start Debugging > Dart & Flutter
と選択してデバッグ起動します。

テンプレートアプリが無事起動し、右下の+ボタンをクリックしたら画面中央の数字が増えていく、という挙動を確認できればOKです。

テンプレートアプリ

コーディング準備

コードを打ち込んで行く前に、いくつか準備しておきます。

私の場合、Dartのおすすめセッティングとかいうものを適用した結果、保存のたびに強制フォーマットされてめちゃくちゃやりにくかったのでコマンドパレットCmd+Shift+Pからopen settingsと打って設定ファイル(json)を開き、設定を一部OFFにしました。

また、動画とは順番が前後しますが、事前に依存パッケージをインストールしておくとスムーズかもしれません。

pubspec.yaml
# 〜(省略)〜
dependencies:
  flutter:
    sdk: flutter
  english_words: ^3.1.5 # この行を追加
# 〜(省略)〜

コーディング(main.dart)

エントリポイントのある、メインファイルです。
これだけだとまだ動きません。

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primaryColor: Colors.purple[900]),
      home: RandomWords()
    );
  }
}

コーディング(random_words.dart)

画面の構成やランダムな単語ペアの生成・表示といったロジックを担当するファイルです。

random_words.dart
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

class RandomWords extends StatefulWidget {
  @override
  RandomWordsState createState() => RandomWordsState();
}

class RandomWordsState extends State<RandomWords> {
  final _randomWordPairs = <WordPair>[];
  final _savedWordPairs = Set<WordPair>();

  Widget _buildList() {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemBuilder: (context, item) {
        if (item.isOdd) {
          return Divider();
        }

        final index = item ~/ 2;

        if (index >= _randomWordPairs.length) {
          _randomWordPairs.addAll(generateWordPairs().take(10));
        }

        return _buildRow(_randomWordPairs[index]);
      }
    );
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _savedWordPairs.contains(pair);

    return ListTile(
      title: Text(pair.asPascalCase, style: TextStyle(fontSize: 18.0)),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _savedWordPairs.remove(pair);
          }
          else {
            _savedWordPairs.add(pair);
          }
        });
      }
    );
  }

  void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (BuildContext context) {
          final Iterable<ListTile> tiles = _savedWordPairs.map((WordPair pair) {
            return ListTile(
              title: Text(pair.asPascalCase, style: TextStyle(fontSize: 16.0))
            );
          });

          final List<Widget> divided = ListTile.divideTiles(
            context: context,
            tiles: tiles
          ).toList();

          return Scaffold(
            appBar: AppBar(title: Text('Saved WordPairs')),
            body: ListView(children: divided)
          );
        }
      )
    );
  }

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WordPair Generator'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.list),
            onPressed: _pushSaved)
        ]),
      body: _buildList()
    );
  }
}

特に気になった文法

Dartの文法はJavaやJavaScriptの流れを組んだような、素直でわかりやすい文法だと感じていますが、初見で特に気になったりわからなかったものについてまとめます。

  • 「~/」除算、整数(int)の結果を返す
  • 「_」アンダースコア始まりの識別子でプライベートアクセス指定を表す

トラブルシューティング

  • 実行したままコードをいじるとエラーが出て何も表示されなくなる
    • 原因はわかりませんが、実行中のアプリを再起動すると解決しました。

あとがき

Dartについて、よく考えられた直感的な命名の各種機能がそろっていて、文法も素直で堅実な印象で書いていて楽しいと感じました。
ただ、UIに最適化された言語だからなのか、コンストラクタにコンストラクタをどんどん重ねて作っていくやりかたがやりやすいので、気をつけないとついつい"ダーティー"なコードを書いてしまいがちとも思いました。
そこがまたいいのかもしれませんね。

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

複数のXcodeバージョンを共存させている時に切り替えを楽にするための小さな工夫

Mac内に複数バージョンのXcodeを共存させる方法に関してはWeb上にいくつもありまして、こちらの記事などが分かりやすいかと思います。
【iOS開発】Xcodeの旧バージョンをインストールし、最新版と共存させる方法

上の記事にて切り替えのコマンドを紹介していただいていますが、なかなか長いコマンドですし、都度Macのパスワードを入力しなければならないです。
頻繁にバージョンを切り替える必要があると、面倒臭く感じます。

以下の方法で、エイリアスによってコマンドがごく短くなりますし、いちいちパスワードを入力する手間も省けます。

私はzshを使っていますので.zshrcにエイリアスを記述しています。
bashを使っている方は.bashrcと読み替えてください。

環境としては、最新バージョンであるXcode 12と、Xcode 11.3が入っている状態です。

.zshrc
alias xcnew='echo {Macのパスワード} | sudo xcode-select --switch /Applications/Xcode.app'
alias xc113='echo {Macのパスワード} | sudo xcode-select --switch /Applications/Xcode11.3.app' 
alias xcver='xcodebuild -version'
  • xcnewをたたくと最新バージョンに一発で切り替わります。
  • xc113をたたくと11.3に以下同文です。
  • xcverをたたくと現在のバージョンが表示されます。

要は、エイリアスのコマンドにMacのパスワードを設定してしまい、sudo xcode-selectに引き渡す、というだけの単純なアイデアなのですが…:sweat_smile:
皆様の生産性の向上に少しでも寄与できたら幸いです。

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

iOS14でのデフォルトブラウザ/メーラー変更時のcanOpenURL==false問題の整理と横展開調査結果

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3
  • iOS 14.0 / 14.0.1

問題事象

ポイント:

  • 問題の事象はiOS 13 SDKでビルドしてもiOS 14の端末では発生する。
  • 14.0から発生しているが14.0.1で解消していない(すなわちiOSのバグではなく仕様変更?)。

サンプルコード:

let url = URL(string: "https://qiita.com")!
guard UIApplication.shared.canOpenURL(url) else {
    return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
  • iOS 13以下で上のコードを実行すると、Safariが起動する。
  • iOS 14.0 / 14.0.1でデフォルトブラウザをSafari以外(Chromeなど)に設定し上のコードを実行すると、何も起こらない。
  • iOS 14.0 / 14.0.1でデフォルトブラウザがSafariに設定されていれば、iOS 13以下と同様にSafariが起動する。

<デフォルトブラウザをChromeに変更する手順>
1. iOS 14の端末で、Chromeをインストールする。
2. 設定>Chromeを選択する。
3. ブラウザの一覧が表示されるので、Chromeを選択する

原因

iOS 14.0以降でデフォルトブラウザをSafari以外(Chromeなど)に設定すると、UIApplication.shared.canOpenURL()がfalseを返却するため。

回避策

Info.plistのLSApplicationQueriesSchemesに"https"および"http"を設定すると回避できます。

関連事象

iOS 14においてはデフォルトメーラーについても変更できるようになりました。
"mailto"についても同様に、LSApplicationQueriesSchemesに追加しないと、デフォルトメーラーが変更されている場合にはUIApplication.shared.canOpenURL()がfalseを返却します。

横展開調査

以上の事項は、文末の「参考リンク」の記事で得られた有益な情報です。
執筆者の方には深く感謝を申し上げたいです。

一方この項は、私が独自に追加調査をした結果になります。

電話アプリの"tel"および"telprompt"スキーム

"tel"はApple URL Schemesにてドキュメント化されているスキームです。
"telprompt"はドキュメント化されていないスキームです。

いずれも、LSApplicationQueriesSchemesに追加しなくても、iOS 14でも電話アプリが起動します。

ちなみに、「将来、電話アプリのデフォルトが変更できるようになったら?」という疑問があったので、LSApplicationQueriesSchemesに"tel"を追加してみたところ、追加してもちゃんと動作します(当たり前?)。

Walletアプリの"shoebox"スキーム

ドキュメント化されていないスキームです。
LSApplicationQueriesSchemesに追加しなくても、iOS 14でもWalletアプリが起動します。

設定アプリ

こちらのスキームは文字列ではなくUIApplication.openSettingsURLStringとなりますが、(当然ながら)LSApplicationQueriesSchemesに追加しなくても、iOS 14でも設定アプリが起動します。

検証結果の整理

  • iOS 14では、カスタムスキームに加えて、デフォルトを変更できるアプリのスキームもInfo.plistに定義しないとダメです。
  • ドキュメント化されているスキーム、ドキュメント化されていないけど動くスキーム、いずれも、現時点でブラウザとメーラー以外は影響を受けていない模様です。
  • Xcode 12 (iOS 14 SDK)でビルドしたアプリだけではなく、Xcode 11 (iOS 13 SDK)でビルドしたアプリでもiOS 14上での挙動は同じです。

参考リンク

【iOS14】デフォルトブラウザを変更した時にcanOpenURLがfalseになる問題
iOS 14対応で気をつけるべきこと

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

iOS14でデフォルトブラウザ/メーラー変更時にcanOpenURLがfalseになる問題の整理と横展開調査結果

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3
  • iOS 14.0 / 14.0.1

問題事象

サンプルコード:

let url = URL(string: "https://qiita.com")!
guard UIApplication.shared.canOpenURL(url) else {
    return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
  • iOS 14.0 / 14.0.1でデフォルトブラウザをSafari以外(Chromeなど)に設定し上のコードを実行すると、ブラウザが起動しない。
  • Xcode 11 (iOS 13 SDK) でビルドしてもiOS 14の端末では発生する。
  • 14.0から発生しているが14.0.1で解消していないということは、すなわちiOSのバグではなく仕様変更?

<デフォルトブラウザをChromeに変更する手順>
1. iOS 14の端末で、Chromeをインストールする。
2. 設定>Chromeを選択する。
3. ブラウザの一覧が表示されるので、Chromeを選択する

原因

iOS 14.0以降でデフォルトブラウザをSafari以外(Chromeなど)に設定すると、UIApplication.shared.canOpenURL()がfalseを返却するため。

回避策

Info.plistのLSApplicationQueriesSchemesに"https"および"http"を設定すると回避できます。

関連事象

iOS 14においてはデフォルトメーラーについても変更できるようになりました。
"mailto"についても同様に、LSApplicationQueriesSchemesに追加しないと、デフォルトメーラーが変更されている場合にはUIApplication.shared.canOpenURL()がfalseを返却します。

横展開調査

以上の事項は、文末の「参考リンク」の記事で得られた有益な情報です。
執筆者の方には深く感謝を申し上げたいです。

一方以下は、私が独自に追加調査をした結果になります。

電話アプリの"tel"および"telprompt"スキーム

"tel"はApple URL Schemesにてドキュメント化されているスキームです。
"telprompt"はドキュメント化されていないスキームです。

いずれも、LSApplicationQueriesSchemesに追加しなくても、iOS 14でも電話アプリが起動します。

ちなみに、「将来、電話アプリのデフォルトが変更できるようになったら?」という疑問があったので、LSApplicationQueriesSchemesに"tel"を追加してみたところ、追加してもちゃんと動作します(当たり前?)。

Walletアプリの"shoebox"スキーム

ドキュメント化されていないスキームです。
LSApplicationQueriesSchemesに追加しなくても、iOS 14でもWalletアプリが起動します。

設定アプリ

こちらのスキームは文字列ではなくUIApplication.openSettingsURLStringとなりますが、(当然ながら)LSApplicationQueriesSchemesに追加しなくても、iOS 14でも設定アプリが起動します。

検証結果の整理

  • iOS 14では、カスタムスキームに加えて、デフォルトを変更できるアプリのスキームもInfo.plistに定義しないとダメです。
  • ドキュメント化されているスキーム、ドキュメント化されていないけど動くスキーム、いずれも、現時点でブラウザとメーラー以外は影響を受けていない模様です。

参考リンク

【iOS14】デフォルトブラウザを変更した時にcanOpenURLがfalseになる問題
iOS 14対応で気をつけるべきこと

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

SceneKitに平面のラベルを貼る方法

SceneKitに平面のラベルを貼って情報を出す方法のメモ。

仕上がり

  1. GameViewController.swiftに次のコードを追加する。
GameViewController.swift
class LabelScene: SKScene {
    override public init(size: CGSize){
        super.init(size: size)

        self.scaleMode = SKSceneScaleMode.resizeFill

        let label = SKLabelNode(fontNamed: "Chalkduster")
        label.text = "test"
        label.fontSize = 65
        label.fontColor = .blue
        label.position = CGPoint(x:frame.midX, y: label.frame.size.height)

        self.addChild(label)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("Not been implemented")
    }
}

  1. GameViewController.swiftviewDidLoadの最後にコードを追加する。
GameViewController.swift
scnView.overlaySKScene = LabelScene(size:scnView.bounds.size)

これで、画面上にラベルが表示されます。

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

通信処理で重くなり過ぎないようにするには

API呼び出しはコストのかかる処理で、呼び過ぎるとユーザーの回線に余計な負担を与える。また時間がかかるため、反応が遅いと感じられUXに悪影響を与えることがある。そのためどこでどう呼ぶかを考える必要がある。

呼ぶAPIの数の制限

例えばアプリなどで一画面で呼ぶAPIが多すぎると遅くなってしまう。そのため、1画面で呼ぶAPIは2〜3個までなどある程度基準を設け、一度に呼ぶAPI数が多くなり過ぎないように設計する。

回線状況の考慮

できれば、ユーザーがそのアプリ等を使う状況を考慮に入れ、通信回線がどのくらい良いかも念頭に置いておくと良い。例えば、電車の案内のアプリであれば、電車内とりわけ地下鉄など電波状況の悪い場所で使う場合もあることから、そのような場面であまり時間のかかる処理を入れるのは好ましくない。

APIを呼ばなくて済むのではないか検討

同じデータを取ってくるだけなのに何度もAPIを呼ぶのは無駄である。そのため、一度取ってきたデータをデバイスのメモリに一時保存、あるいはストレージに恒久保存して対処する事ができないかを検討する。

また一部のデータが欲しいだけなのに、多くのデータを取ってくるAPIを呼ぶなどもやはり無駄となる。

APIを呼んでいる最中の対応

APIを呼んでいる最中、UIまでも止まってしまう(タップとかクリックとかユーザーの反応を受け付けなくなる)とフリーズしたかのような印象をユーザーに与えてしまう。そのため、通信処理とUI処理のスレッドを分けるなどして対処する。

また、通信処理が終わるまでの間、100ms~200ms程度異常かかるようであればローディングの表示(グルグル回るアイコン、またはゲージが徐々に満タンに近づくバーなど)をするのが望ましい。この場合、通信の開始時にローディング表示を開始する。そして通信が成功しようと失敗しようと、通信が終了すればローディング表示を止めることになる。

参考

MdN Design -[優れたUXを目指して]アプリの性能について:第3回
Android Developers - Keeping your app responsive

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

Xcode 12 「Double-quoted include」エラーの対処法

Xcodeを12にアップデートすると、エラーが大量に。。

スクリーンショット 2020-09-26 13.51.09.jpg

アプリをビルドすると、GoogleDataTransport Groupから大量のエラーが発生しました。

- Double-quoted include "pb.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_common.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_decode.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_encode.h" in framework header, expected angle-bracketed instead
-

ダブルクオートが含まれているのが、エラーの原因か??

解決法

project>Pods>Build Setting>Quoted include in... を以下のように変える
スクリーンショット 2020-09-26 13.51.32.jpg

これでDouble-quotedはエラーとみなされなくなりました!

参考

https://github.com/firebase/firebase-ios-sdk/issues/5987

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

iOS14でのDatePickerの挙動について(SwiftUI)

iOS14でのDatePickerの挙動について(SwiftUI)

前回書かせていただいたiOS14(UIKit)でのDatePickerに関してですが、ついでにSwiftUIでも調べたので書かせていただきました。
今回は従来のものから何が追加さたのかというところと実際の使い方のCodeも載せておきます。

環境

macOS Catalina 10.15.6
Xcode Version 12.0.1

はじめに

まずはじめにSwiftUIでのDatePickerの使い方から

struct ContentView: View {
    @State private var selectionDate = Date()
    var body: some View {
        DatePicker("タイトル",
                   selection: $selectionDate)
    }
}

これだけでDatePicerを使うことができます。
さすがSwiftUI!

デフォルトだとこんな感じです。
これだと流石に使い物にならんのでちょっと修正を加えます

struct ContentView: View {
    @State private var selectionDate = Date()
    var body: some View {
        DatePicker("タイトル",
                   selection: $selectionDate)
            // これを入れることによってラベルを表示させなくします。
            .labelsHidden()
    }
}

これで一旦検証はできそうです。

ちなみに別途Pickerの上にタイトルを入れたい場合

struct ContentView: View {
    @State private var selectionDate = Date()
    var body: some View {
        VStack {
            Text("タイトル")
                .font(.title)
            DatePicker("タイトル",
                       selection: $selectionDate)
                .labelsHidden()

        }
    }
}

このようにすることでタイトルを付けれるようになります。
他にも良い実装はありますが今回は割愛します。

本題

では実際にSwiftUIではどのようにしてStyleやModeを変更するか確認します。

まずUIKitではenumで新しく定義されていました。

public enum UIDatePickerStyle : Int {
    case automatic = 0
    case wheels = 1
    case compact = 2

    @available(iOS 14.0, *)
    case inline = 3
}

swiftUIではまずこのように定義してあります。

extension View {
    /// Sets the style for date pickers within this view.
    @available(tvOS, unavailable)
    @available(watchOS, unavailable)
    public func datePickerStyle<S>(_ style: S) -> some View where S : DatePickerStyle
}

使い方としましては、

DatePicker("タイトル", selection: $selectionDate)
    .datePickerStyle(CompactDatePickerStyle())

このように定義することでStyleの変更ができます。

Style

公式ドキュメントを見るといくつかStyleがあります。

struct DefaultDatePickerStyle
struct CompactDatePickerStyle
struct WheelDatePickerStyle
struct FieldDatePickerStyle
struct StepperFieldDatePickerStyle
struct GraphicalDatePickerStyle

この中の

FieldDatePickerStyle
StepperFieldDatePickerStyle

この二つは macOS 10.15でのみ使えるとのことです。

iOS14で追加されたものは

GraphicalDatePickerStyle

になります。

DefaultDatePickerStyle
CompactDatePickerStyle
WheelDatePickerStyle

こちらの3つはiOS 13.0から追加されたものになります。

では実際の表示を確認してみます。

  • DefaultDatePickerStyle

デフォルトだとこんな感じです。

動きはタップすると、カレンダーがポップアップします。
動き的にはUIKitと同じなので割愛します。

  • CompactDatePickerStyle 

こちらはdatePickerStyleだけ指定してもDefaultDatePickerStyleの挙動となんら変わりありません。

  • WheelDatePickerStyle

こちらは従来通りのweelになります。

  • GraphicalDatePickerStyle

こちらはデフォルトだとUIKitでもあった、圧縮されてしまう現象が起きています。
.frame
でいい感じに調整が必要です。

Mode

次にMode指定をしてみます。

 DatePicker("タイトル", selection: $selectionDate, displayedComponents: .hourAndMinute)

displayedComponents

  • .date
  • .hourAndMinute

上記を指定できます。

DatePicker("タイトル", selection: $selectionDate, displayedComponents: .date)
     .datePickerStyle(DefaultDatePickerStyle())
     .labelsHidden()

DatePicker("タイトル", selection: $selectionDate, displayedComponents: .hourAndMinute)
     .datePickerStyle(DefaultDatePickerStyle())
     .labelsHidden()

上記の挙動としては

  • DefaultDatePickerStyle
  • CompactDatePickerStyle

共に同じ表示でした。

 DatePicker("タイトル", selection: $selectionDate, displayedComponents: .date)
      .datePickerStyle(WheelDatePickerStyle())
      .labelsHidden()

DatePicker("タイトル", selection: $selectionDate, displayedComponents: .hourAndMinute)
     .datePickerStyle(WheelDatePickerStyle())
     .labelsHidden()

こちらに関してはやはり通常で、日にち表示なのか、時間表示七日の違いでした。

DatePicker("タイトル", selection: $selectionDate, displayedComponents: .date)
     .datePickerStyle(GraphicalDatePickerStyle())
     .labelsHidden()

こちらに関してはTimeを消したとしてもやはり圧縮されてしまいます。

.frame(width: 300, height: 500)

フレーム指定してあげると、表示が正常になります。
が、サイズによってバグります(widthを400にしたらバグった)
これは使うのは厳しそうですね。。

DatePicker("タイトル", selection: $selectionDate, displayedComponents: .hourAndMinute)
     .datePickerStyle(GraphicalDatePickerStyle())
     .labelsHidden()

時間のみだと、カレンダーのTimeの部分だけ表示されるようになりました。

まとめ

今回はSwiftUIでの挙動を調べてみました。
使用する場合によってかもしれませんが、
GraphicalDatePickerStyleに関しては、UIKit同様バグというか表示に癖があるので、現段階ではどうしてもということがない限りあまり使わない方実装を考えた方が良さそうです。
正直あまり使わないかな〜とも思ったりもしてます。

まあ、宣言的にCodeをかけるのはやはりSwiftUIの強みだし、SwiftUIは書いてて楽しいので、
今後よくなることを期待して今回の調査を終えたいと思います。

次回はiOS14の位置情報の変更に関しての挙動でも書こうかと思います。

読んでいただきありがとうございました。

UIKit版はこちら
iOS14でのUIDatePickerの挙動について(UIKit)

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

AVFoundationでカメラを使う最小構成

毎回調べている気がするので。

必要なプロパティ。

var captureSession = AVCaptureSession()
var previewView = UIView()
var previewLayer:AVCaptureVideoPreviewLayer?

var videoDataOutput = AVCaptureVideoDataOutput() // VideoDataの場合
var photoOutput = AVCapturePhotoOutput()// PhotoDataの場合

設定。

let device = AVCaptureDevice.default(for: AVMediaType.video)
let deviceInput = try! AVCaptureDeviceInput(device: device!)

captureSession.addInput(deviceInput)
captureSession.addOutput(videoDataOutput)

previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
previewView.layer.addSublayer(previewLayer!)

captureSesion.startRunning()

撮影。

VideoDataの場合、デリゲートメソッド内でフレームを取得。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
}

Photoの場合、capturePhotoしてデリゲートメソッド内で処理。

self.photoOutput?.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let imageData = photo.fileDataRepresentation() {
        let uiImage = UIImage(data: imageData)
    }
}

お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note
相棒
note

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

iOS 14 対応

とあるアプリをiOS14に対応した際に起こった問題を書いていきたいと思います。

DatePickerが配置された画面に遷移するとアプリがクラッシュする...

原因

DatePickerの罫線を表示させたくないため、subviewsを巡り非表示にしているコードがあり、
UIDatePickerのStyleにautomaticが設定されている。

対処

Styleにautomaticが設定されている時の外観が変更されており、wheelsを設定するとクラッシュしなくなった。
根本的には、subviewsを巡り非表示にしているコードを対処する必要がある。いつか...

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

動物を認識して自動撮影するカメラを作る

Sep-26-2020 02-43-16.gif

犬・猫がフレーム内に現れたら自動でシャッターを切る機能を作ります。

手順

Visionに動物認識コンピュータービジョンリクエストがあります。
リクエスト結果でフレームに動物がいるか判別し写真を撮るように設定します。

let animalRequest:VNRecognizeAnimalsRequest = {
    let request = VNRecognizeAnimalsRequest(completionHandler: { (request, error) in
        guard let animalObservation = results.first as? VNRecognizedObjectObservation else { return }
// animalObservationがあれば、動物がいるので、シャッターを切る
        self.avCapturePhotoOutput.capturePhoto(with: settings, delegate: self as! AVCapturePhotoCaptureDelegate)
    })
    request.revision = VNRecognizeAnimalsRequestRevision1 //リビジョン1では、認識できるのは犬・猫のみです。
    return request
}()

captureOutputデリゲートメソッド内で、上記のリクエストを実行し、フレームを解析します。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
    let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: orientation, options: [:])
    do {
        try imageRequestHandler.perform([animalRequest])
       } catch {
        print(error)
       }
}

応用

同じ手法で他のリクエストを使うと、人間や任意の物体を認識して写真を撮れます。
Observationには、犬猫の場所を示すBoundingBoxも含まれていますので、そこにオートフォーカスしたりもできます。


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note
相棒
note

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

Xcodeで Library not found for -l "ライブラリ名" と言われた時の対処法

iOSをゼロから開発していると、おっちょこちょいの自分はたまに上記のエラーに出くわします。

何が原因なのか?

エラーの内容だけだと ライブラリが見つからない のような趣旨しか伝わってきません。
では、何が原因なのでしょうか??

原因

pod init コマンドでpodファイルを生成し pod install すると、.xcodeparoj.xcworkspace が生成されます。そのような場合には、もし.xcodeparoj が開かれている場合は一旦閉じて .xcworkspace を開くべきですが、その工程を怠ってビルドボタンをクリックするとXcodeからエラーが出されます。

解決策

.xcodeparojファイルを開いてビルドするのではなく、.xcworkspaceを開いてビルドする。
スクリーンショット 2020-09-26 2.30.14.png

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

ARKitとAVFoundationは何秒で切り替えられるのか

ARKitとAVFoundation両方の恩恵を受けたい

Gif:ARKitを中断してAVFoundationでキャプチャ、すぐARに戻る
Sep-26-2020 00-42-43.gif

ARKitの世界・顔認識のトラッキングを使いながら、高画質の画像をキャプチャしたいこともあるかもしれません。
ARKitでは1920*1440が最高サイズなので、「AVFoundationでキャプチャすればいいんじゃね?」「ARKitとAVFoundation同時に使えるのかな?」と僕はなりました。

調べたところ、
ARKitとAVFoundationのセッションは、同時には立ち上げられません。
では、素早く切り替えれば、何秒かかるのか。

やってみました。
*iPhone11 iOS14で実験。

実験手順

トラッキング状態を保存して、ARセッションを一時停止し、AVCaptureSessionをスタート。

sceneView.session.getCurrentWorldMap { [self] worldMap, error in
    time = 0.0 // 裏でTimerでtimeを加算しています。0に戻してここからスタート。
    sceneView.session.pause() 
    map = worldMap
    avCaptureSession.startRunning() //AVFoundationスタート
}

実験1、AVCaptureVideoDataOutputで画像を取得する

captureOutputデリゲートメソッド内で画像を取得し、一枚撮ったらすぐにAVCaptureSessionを止めて、ARセッションを再開。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
    let image = UIImage(ciImage: CIImage(cvImageBuffer: pixelBuffer!))
    print(time)
    avCaptureSession.stopRunning()
    let configuration = ARWorldTrackingConfiguration()
    configuration.initialWorldMap = map
    sceneView.session.run(configuration, options: [])
}

0.15秒でキャプチュア。

しかし、AVFoundationでキャプチャした画像は、セッション立ち上げ直後なので暗い。
最初5フレーム落としてキャプチャしたところ、
綺麗に3840*2160で撮れて0.29秒でした。
画面の停止は0.5秒程度(シャッターを切るくらい)の体感でした。

実験2、AVCapturePhotoCaptureで画像を取得する

写真用のアウトプットで撮ってみました。

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let imageData = photo.fileDataRepresentation() {
        let uiImage = UIImage(data: imageData)
        print(time)
        // 0.50
        self.captureSession.stopRunning()
        let configuration = ARWorldTrackingConfiguration()
        configuration.initialWorldMap = map
        sceneView.session.run(configuration, options: [])
}

こちらは綺麗に写真が撮れてキャプチャまで0.5秒でした。
しかし、写真のシャッターを切ってしまうと、ARKitの復帰が遅く、7秒程度画面が止まったままでした。
Sep-26-2020 00-46-53.gif

結果

キャプチャの仕方 キャプチャまでの秒数 画面停止秒数
AVCaptureVideoDataOutput 0.3 0.5
AVCapturePhotoCapture 0.5 7.0

結論

実用には、VideoDataOutputで最初の数コマ落として撮るのがギリギリ、普通のシャッターぐらい中断感覚で使えるかなあ、という意見です。

ちなみに、ARWorldTrackingでデバイスの傾きデータをとったところ、セッション切り替え以前と以後で0.05ラジアン程度ずれていました。デバイス固定したらズレなかったので、キャプチャしている間の0.3秒の僕の手ブレがそれくらいということですね。

追記:ARPositionalTrackingConfigurationというデバイス位置をとるだけの構成があって、ワンチャンAVFoundationと併用できんじゃね?とやってみたら無理でした。


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

【Flutter】Locale data has not been initialized, call initializeDateFormatting(<locale>). の解決

error内容

Locale data has not been initialized, call initializeDateFormatting(<locale>). 

これは言語情報の初期化がされていないとのこと。

DateFormat.MMMM('id')

上記のようにlocale情報を扱う前には、'initializeDateFormatting'を呼び出す必要があるとのことです。

対応

アプリの初期起動時のmain()処理内に以下を追記して対応しました。

initializeDateFormatting('id'); //locale情報は各々変更してください。

参考文献

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