20191224のSwiftに関する記事は14件です。

Awesome CoreNFC in iOS 【iPhone でピッと IC カードを読む】

はじめに

CoreNFC は、iPhone でピッと IC カードを読みこめる機能です。

利用できるICカードは幅広く、交通系IC、各種電子マネー、大学生協の電子マネー、運転免許証、パスポート、マイナンバーカードなどあります。

今年9月19日に利用可能になった CoreNFC はまだ日本語の学習記事があまりありません。この記事では、CoreNFC を日本の開発者にもっと活用して頂く目的で、情報をまとめさせて頂きます。将来的に初学者に良いまとめになれば幸いです。

改善コメントや編集リクエストをどんどん募集しております。どうぞよろしくお願い申し上げます?‍♂️

キータ

サンプル・アプリケーション

  • TRETJapanNFCReader/Examples - 交通系ICと運転免許証の読み込み例がのっております。
  • TransitPal - アメリカのクリッパーカードの読み込みですが、とても良いサンプルアプリケーションです。Swift UI でシンプルながら、下記画像のようなかなり綺麗な UI を学べます。

image.png image.png
Screenshots from TransitPal

オープン・ソース・ライブラリ

  • TRETJapanNFCReader - 交通系IC、各種電子マネー、大学生協の電子マネー、運転免許証の読み込みを手軽に扱えます。

記事を良くするコメントや編集リクエスト

この記事は、CoreNFC について学ぶ際に、興味のある各記事に進むのにご利用いただければと思います。皆様の、記事を良くするコメントや編集リクエストをお待ちしております。お願い申し上げます?‍♂️

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

【iOS】算数で横スクロールのCollectionViewの高さを求める

スクローラブルと認識できるデザイン

まず初めに、iOSに限らずですがスクロール可能と認識できるデザインがあります。

Blank Diagram.png

こんな感じです。2つのセルが完全に表示されていて、残り1つは半分ほど出ているデザインです。
これはユーザーが見た時に「あ、横にスクロールできるんだな」と直感的に思わせるためですね。

これをみた時にCollectionViewの高さとセルのサイズってどうするんだろうと思ってました :thinking:
よく考えてみるとある程度条件を絞って算数の力を使えば導き出せることに気づいたので実装してみます(正確には中学生で習う1次方程式ですが、ほぼ算数なので許してください)。

計算してみる

まずは手元で計算してみます。
条件を整理して実際に数式を出すところまでやります。

条件の整理

今回は事前に計算をして計算結果を実装に当てはめる、ということが必要なのである程度の条件が必要となります。
以下が条件です。

  1. 各セルの縦横比が確定している
  2. 各セル同士のスペースが確定している
  3. CollectionViewのwidthが確定している

図にするとこんな感じです。

Blank Diagram (2).png

そこまで条件が多いわけではないので、大体の場合で使えるかなーという感じはしてます。
今回は計算を楽にするためにパディングは上下と一番左を同じ値にしてセル間をその2倍としています。

数式を出す

条件が出たのでそれを数式に落とし込んでみましょう。

セルの比

まずは1つ目の条件 各セルの縦横比が確定している です。
これは比をそのまま計算式にするだけです。

Blank Diagram (1).png

このように定義すると

\begin{aligned}
a:b = w:h => h = \frac{b}{a}w
\end{aligned}

$h$を$w$で表せました。

CollectionViewのwidth

次に2つ目と3つ目の条件を使うとCollectionViewの横幅を表すことができます。
先ほどの図を見るとわかりやすいですが、$W$はセルの横幅の合計とパディングの合計を足した値になるのがわかると思います。

Blank Diagram (2).png

まずセルの横幅の合計は単純にセルの横幅×個数なので$N \times w$ですね。

次にパディングの合計です。図の場合であると$p + 2p + 2p$で$5p$ですね。
これを一般的なセルの個数で表現すると、

最初のパディング(p) + セルの個数の小数点を切り捨てた整数 × $2p$

になります。
数式にするとこのようになります。

\begin{aligned}
(2\lfloor N \rfloor + 1)p
\end{aligned}

ここでの $\lfloor N \rfloor$ はガウス記号です。これは小数点を切り下げするだけなので例えば3.5や3.8であれば3になります。

これで出揃ったので、先ほどのセルの横幅の合計を足してあげると$W$は

\begin{aligned}
W = (2\lfloor N \rfloor + 1)p + Nw
\end{aligned}

と表せます。

CollectionViewのheight

最後にCollectionViewのセルの高さも出してあげます。
これは上下のパディングとセルの高さを足してあげればいいだけなので、

\begin{aligned}
H = 2p + h
\end{aligned}

と表せます。

これで全ての数式が揃いましたので、まとめてみます。

\begin{aligned}
 h & = \frac{b}{a}w \\
 W & = (2\lfloor N \rfloor + 1)p + Nw \\
 H & = 2p + h \\
\end{aligned}

改めて確認すると現時点で判明していない値は$H$, $h$, $w$の3つです。
それでいて式が3つです。つまり3変数の1次方程式なのでそれぞれの値は確定します。

細かい途中式は省きますが、まず簡単なところから求めると2番目の式から$w$が求まります。

\begin{equation}
w = \frac{W - (2\lfloor N \rfloor + 1)p}{N}
\end{equation}

次に一番上の式と$w$を使えば$h$が求まります。

\begin{equation}
h = \frac{b}{a}\frac{W - (2\lfloor N \rfloor + 1)p}{N}
\end{equation}

最後に3番目の式と$h$を使えば$H$が求まります。

\begin{equation}
H = 2p + \frac{b}{a}\frac{W - (2\lfloor N \rfloor + 1)p}{N} 
\end{equation}

これでCollectionViewの高さとセルのサイズが求まりました!
最後に実際に実装してみてどんな感じになるのか見てみます。

実装

準備

まずは実装の前に条件を満たしておきます。
今回はそれぞれの値を以下のようにしておきます(計算が簡単になるように調整してます)。

$a:b = 8:5$
$p = 16$
$W = 画面の横幅$
$N = 2.5$

すると、$H$, $h$, $w$が以下のように出ます。

\begin{aligned}
 H & = \frac{W}{4} + 12 \\
 h & = \frac{W - 5×16}{4} \\
 w & = \frac{W - 5×16}{2.5} \\
\end{aligned}

これで準備は終わりです。

CollectionViewのheight

準備も終わったのでいよいよ実装です。ここではストーリーボードを使って実装していきます。

まずはCollectionViewの高さから設定します。

スクリーンショット 2019-12-23 22.22.30(2).png

CollectionViewをストーリーボード上に配置し、top, right, leftを親に合わせていきます。
これで$W$を画面の横幅と一致させています。

スクリーンショット 2019-12-23 22.22.42(2).png

親のViewのheightとCollectionViewのheightをアスペクト比で合わせます。

スクリーンショット 2019-12-23 22.30.25(2).png

1つ前で親のheightを指定していることに違和感があったかもしれないですが、さっきの段階でheightをwidthに合わせる、みたいなことができないみたいようです。しょうがなく一旦親のViewのheightに合わせていました(もし知っている方がいたら教えて欲しいです :pray: )。
そういう理由もありここでheightからwidthに変更します。

スクリーンショット 2019-12-23 23.06.32(2).png

最後に数式の出番です。
ここでは $H = W/4 + 12$を使います。数式の意味としては$W$を0.25倍して12を足す、です。
なので、Multipler には0.25、 Constant には12を入れます。

これでCollectionViewのheightの設定は完了です。

セルサイズの設定

続いてセルサイズの設定もやります。
色々と省略しますが、viewDidLoad()UICollectionViewFlowLayout を設定してあげます。

ここで使用するのは $h = (W - 5×16)/4$ と $w = (W - 5×16)/2.5$です

override func viewDidLoad() {
    super.viewDidLoad()
    let flowLayout = UICollectionViewFlowLayout()

    flowLayout.scrollDirection = .horizontal

    let padding: CGFloat = 16.0
    let screenWidth = UIScreen.main.bounds.width
    // セルのwidthの計算式: w = (W - 5×16)/2.5
    let cellHeight = (screenWidth - 5.0 * padding) / 4
    // セルのheightの計算式: h = (W - 5×16)/4
    let cellWidth = (screenWidth - 5.0 * padding) / 2.5
    flowLayout.itemSize = CGSize(width: cellWidth, height: cellHeight)
    // セル間のパディング
    flowLayout.minimumLineSpacing = padding * 2
    // 上下左右のパディング
    flowLayout.sectionInset = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)

    collectionView.collectionViewLayout = flowLayout
}

ビルドするとこんな感じになります。

ちゃんとセルが2.5個でそれぞれのパディングが取れていますね。

https://github.com/hiromu21/CalcCollectionView
GitHubにソースは載せてあるので良かったらそちらも見てください。

まとめ

ちゃんと計算したかったので、ガウス記号とかを取り入れました。それっぽい数式になって良かったです笑
ちなみにもっと簡単な方法を知っている方がいたら教えてください :pray:

最後に注意事項としてこのままのやり方だと問題点があります。それは画面回転した時の挙動です。
実際に実装してみて回転させると分かるのですが、セルが2行で表示されたりセルの大きさが変わったりしてしまいます。
そもそも縦固定が想定されてる実装になっているのでしょうがないのですが :rolling_eyes:
もし機会があれば画面回転にも対応した実装をして記事にしてみたいです!

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

スター乞食にスター爆撃をするためのCLTを作った話

Swift/Kotlin愛好会 Advent Calendar 2019の空き枠を埋めに来ました。

作ったもの

https://github.com/417-72KI/SSGH

インストール

Homebrew対応させました

$ brew tap 417-72KI/SSGH
$ brew install ssgh

使い方

GitHubトークンが必要なのではじめに作ります

image.png

image.png

image.png

できたトークンを環境変数にセットします

.bashrc
export SSGH_TOKEN='{コピってきたトークン}'

後はGitHubのアカウントを指定してコマンドを叩くだけ!

ssgh 417-72KI

仕組み

やっていることは非常に単純ですが、GitHub APIの仕様に癖があって微妙に苦戦しました。

リポジトリリストから取得できたエンティティにはスター済みかのフラグが無いため、各リポジトリに対してスター済みか確認するAPIを叩く必要があります。
このスター済み確認API、なんとレスポンスは空っぽで、

  • スター済みならステータス204
  • 未スターならステータス404

という不思議な仕様になっています。
そのため、レスポンスのステータスコードを見て404なら正常系として扱うようにエラーハンドリングをゴニョゴニョする必要がありました。
奮闘の結果はこちらを御覧ください(

使ったもの

TODO

  • ターゲット複数指定爆撃
  • フォロワー全員に爆撃

余談

名前の由来ですが、7年くらい前に流行ったShootingStarというTwitterクライアントをリスペクトしています。
当時のTwitterは「いいね」ではなく「お気に入り(ふぁぼ)」があり、そのアイコンが☆でした。
また、ふぁぼ爆撃という文化もあり爆撃専用に作られたのがShootingStarというクライアントでした。

終わりに

誰かから「スターください」って言われたらこのツールを使って爆撃していってください。

そしてこの記事を見たそこのあなた、後は分かるね?(

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

スター乞食にスター爆撃をするためのCLTを作ったよ

Swift/Kotlin愛好会 Advent Calendar 2019の空き枠を埋めに来ました。

作ったもの

https://github.com/417-72KI/SSGH

インストール

Homebrew対応させました

$ brew tap 417-72KI/SSGH
$ brew install ssgh

使い方

GitHubトークンが必要なのではじめに作ります

image.png

image.png

image.png

できたトークンを環境変数にセットします

.bashrc
export SSGH_TOKEN='{コピってきたトークン}'

後はGitHubのアカウントを指定してコマンドを叩くだけ!

ssgh 417-72KI

仕組み

やっていることは非常に単純ですが、GitHub APIの仕様に癖があって微妙に苦戦しました。

リポジトリリストから取得できたエンティティにはスター済みかのフラグが無いため、各リポジトリに対してスター済みか確認するAPIを叩く必要があります。
このスター済み確認API、なんとレスポンスは空っぽで、

  • スター済みならステータス204
  • 未スターならステータス404

という不思議な仕様になっています。
そのため、レスポンスのステータスコードを見て404なら正常系として扱うようにエラーハンドリングをゴニョゴニョする必要がありました。
奮闘の結果はこちらを御覧ください(

使ったもの

TODO

  • ターゲット複数指定爆撃
  • フォロワー全員に爆撃

余談

名前の由来ですが、7年くらい前に流行ったShootingStarというTwitterクライアントをリスペクトしています。
当時のTwitterは「いいね」ではなく「お気に入り(ふぁぼ)」があり、そのアイコンが☆でした。
また、ふぁぼ爆撃という文化もあり爆撃専用に作られたのがShootingStarというクライアントでした。

終わりに

誰かから「スターください」って言われたらこのツールを使って爆撃していってください。

そしてこの記事を見たそこのあなた、後は分かるね?(

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

8ヶ月かけてTODOアプリを作った話

はじめに

今年の4月頃から開発していたTODOアプリが完成したのでリリースした。
今回はなぜアプリの開発に8ヶ月もかかってしまったのかを自戒も含めて公開したいと思う。

どんなアプリ

リリースしたアプリはこちら
https://apps.apple.com/us/app/tickettask/id1491648458?l=ja&ls=1

使用技術

開発にあたって使用した技術は以下
・Swift
・Xcode
・Firebase(Analytics集計のため)
・Realm

アプリ概要

アプリの概要としては、根幹としてはよくあるTODOアプリ。
それ以外に設定した期間でのリセット機能、定期的なリマインド機能などがある。

ここまで見ていただいて、ある程度のアプリ開発の経験がある人はこう思ったんじゃないでしょうか。

「ん?これくらいの用件のアプリだったら8ヶ月もかからなくないか?下手したら1ヶ月で作れるくらいの規模じゃないか?」

はい。こういったご意見も含めてなぜこんなに期間がかかってしまったのかを書いていく。

開発した経緯

まずなぜこのアプリを開発したかというところだが、当初作りはじめていたときこのアプリは公開などするということを全く考えていなかった。勉強のために様々なデザインのアプリを見ていたときに、すごく好みなUIのアプリがあったため自分でも作ってみたいと思いダラダラと作りはじめていった。

行き当たりばったりの開発

表現したいUIができてきた頃にこんな思いになっていった
「この土台のアプリを使えばストアに公開できるくらいのアプリが作れるんじゃないか?」
こうして本格的に中身を作りはじめた。
もともと開発していたアプリは実験的な感覚で作っていたので画面仕様書だったりを全く作っていなかったし、ましてや公開する気もなかったので中身の部分も仕様なんて全く考えていなかった。
とりあえずTODOアプリを作ってみようと思い、仕様を考えはじめた。そしてざっくりと作りたいアプリのイメージを固めた。

犯した2つの失敗

こうして開発を進める中で自分は2つの失敗を犯してしまった。これらが開発が伸びて行ってしまった原因だと考えている。

・仕様の曖昧さ

一つ目が仕様の曖昧さ。アプリを作り出しがUI経緯なのに対して画面仕様書の一つも作っていなかった。これによってアプリ全体のスケールが想像できずに内部仕様を考えるときに足枷となっていた。
そして開発中にアイデアが浮かんだ時も作り出している構造だと実現できないような物もあったりしたのUIの構造を丸々作り変えたりしていた。これによって開発スケジュールが膨れたりしていた。

・怠惰により開発の先延ばし

 二つ目が怠惰による開発の先延ばし。途中本業が忙しくなる時期があったりして個人プロジェクトを1、2ヶ月放置した期間があったりした。しかしこれも一つ目の仕様の曖昧さが生み出してしまった「スケールが想像できない」ことでモチベーションが保てなかったことが原因でもあると思う。

学びのある開発を

開発を行ってきた中で確かに失敗はあったが次につながる失敗であったと思う。
次からはざっとでいいから画面一覧を作ろうと思ったし、ちゃんと仕様を決めてから開発に取り掛かろうと思った。

エンジニアをはじめたての頃Qiitaである記事を見てすごく衝撃を受けた記事があった。
ジャバ・ザ・ハットリさんのこの記事。
開設後3週間で収益10万円を得た個人開発サイトでやったことの全部を公開する

この記事の中で以下のようなことが書いてある

個人開発で3大やってはいけないこと

・「マネタイズは後で考える」と言ってとりあえず作る
・「コンテンツ(ユーザー投稿数確保)は後で考える」と言ってとりあえず作る
・「ウケるかどうか出してみないと分からない」と言ってとりあえず作る
上記の3つとも推奨されている方もいる。ただ少なくとも私にとってはこの3つが最大の失敗要因だった。
映画「ソーシャル・ネットワーク」のジャスティン・ティンバーレイクのように「広告なんてクールじゃねーよ」と気取ってもいいが、そんなのが上手くいった試しが無い。
だいたいにおいて 考えることを放棄した時点で負けが決定している。じっくり考えるところが個人開発の一番おもしろいところだし「後で考える」ぐらいならその場でとことん知恵を
絞った方が楽しいだろう。

当時この記事を見ながら「自分が個人プロダクトを作るときはとことん思考を張り巡らさせて作りるぞ!!」なんて考えていたがいざアプリを作りおえてみるとアプリを完成させるということだけに拘って他のところか完全にお座なりになっていた。
もちろんこの考えが間違っているというわけではないが、結果アプリのコンセプトも開発途中でふわふわになっていろんな機能を追加したり削除したりして完成が遅くなってしまった。
「コードを書く」ことはすごく楽しいし刺激的だけど、それと同じくらい「準備する期間」という物が大事なのだとわかった。

最後に

今回作ったアプリが受けるか受けないかに関わらずこれからもアプリを作り続けると思います。
やはり作ってきたアプリが公開される瞬間というのはすごく刺激的だしワクワクします。失敗を繰り返しながらより素敵なアプリが自分の中から生み出されていくのを夢見て明日からも開発に勤しんで行きます。
次はAndroid界にも出したいからFlutterで作ろうかなんて考えてます。もちろん技術だけではなく自分なりに「これは受ける!」ってアプリを作っていきます。

宣伝にはなっちゃいますが自分が作ったチケットタスクというアプリ、親バカですがすごく可愛いアプリなので是非インストールしてみてください。

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

コマンドラインだけでiOS開発したい人生だった

こんにちは。最近Pixel 4に乗り換えた一介のiOS開発者です。
時間が余ったので、飛び込みでどこかのアドベントカレンダーに参加しようかと思い筆を取りました。
コマンドラインでiOS開発をするには?というテーマで以前からコツコツ環境を整備していたので、共有してみたいと思います。

課題 / モチベーション

  • Xcodeいろいろ重い。
  • 複数Xcodeで同時ビルドもできるが、どうしても遅いので直列実行したい。
  • XVimもいいけどやっぱりVimがいい。

スコープ

アプリのビルドとテスト実行のみです。
確かSimulatorでの実行とコンソールログまでCLIで完結している人をみた事がありますが、自分は知見がないため触れません。知見募集。

前提条件

このソリューションを使うとハッピーになれるかもしれない人

  • ターミナルが快適すぎてターミナルの外に出ると路頭に迷う人
  • XcodeではないCLIで使えるエディタが好きな人(オブラートに包んだ表現)
  • とはいえBazelとか試すほど冒険する気はないのでとりあえずxcodebuildでいい人
  • (recommended) テスト書いてある人
  • (recommended) コードレイアウトしている人
  • (recommended) XcodeGen使っている人

UI周りやテストがない部分はSimulatorを立ち上げることになり重い腰を上げてターミナルから離脱しないといけないので、なるべくコードで完結する部分を増やしておくのがポイントです。
xib読めるから平気という人間捨てている人は頑張って書いてもいいですが、レビューする方が読めない可能性もあるし通常はオススメしません。

用意する材料

xcodebuildスクリプト

XcodeのGUIを使っていると、何もしていなくてもindexingやインクリメンタルビルドなどのコストがかかってくると思います。
xcodebuild を使うことのメリットとして、そういったコストはかからず、必要な時に必要な処理を実行する事ができるという点がありますね。
xcodebuild を直接使うのにハードルを感じる人は fastlane とか使ってみたらいいと思います。個人的にはMakefileを書く労力とあんまり変わらないと思いますが。

CLIで xcodebuild -project App -scheme ... と書いていくのもまた一興ですが、仕事でビルドする時のパラメータなんて大体いつも一緒だと思うので、Makefileとかスクリプトにしてどこかに置いておきましょう。
パスも含めて短い方がいいので、自分は以下の名前でbashスクリプトを保存しました。

~/bin/my-proj/build   # build-for-testing
~/bin/my-proj/test    # test-without-building
~/bin/my-proj/bt      # build && test

ログフィルターツール

定番の xcprettyxcbeautify でいいという人はそれでも構いません。
カラフルなログがターミナルを流れていくのを見るのは楽しいですしね。
自分の場合はsedgrepを使って必要な情報だけが出るようにしておりまして、誰得かもしれませんが、せっかくなので晒しておきます。
最近のiOSのプロジェクトであれば、大体これで拾えると思います。
SwiftLintの出力も拾っていますね。

tee で元々のログも保存しているので、拾いきれなかった時も生ログを参照できます。

フィルターを適用した各スクリプトはこんな感じです。

~/bin/my-proj/build

#!/bin/bash
make build-for-testing 2>&1 \
    | tee build$(date +%s).log \
    | egrep '(swift:[0-9]+:[0-9]+: error:|cannot be found|BUILD SUCCEEDED|.swift.*warning.*Violation)' \
    | sed 's/:/,/g'

exit ${PIPESTATUS[0]}

~/bin/my-proj/test

#!/bin/bash
set +e

TEST_STDOUT_LOG=test$(date +%s).log
make test-without-building 2>/dev/null \
    | grep -v '(One of the two will be used.|^CoreData:)' \
    | tee $TEST_STDOUT_LOG \
    | egrep -A 2 '(encountered an error|Failing Tests|swift:[0-9]+: error:|Test Suite.*\.xctest.*(passed|failed)|xccovreport|TEST EXECUTE|^Fatal error)'
STATUS=${PIPESTATUS[0]}

exit $STATUS

~/bin/my-proj/bt

#!/bin/bash
rm build[0-9][0-9]*log 2>/dev/null
rm test[0-9][0-9]*log 2>/dev/null
set -e
echo "Building..."
~/bin/my-proj/build
set +e
echo "Testing..."
~/bin/my-proj/test 2> /dev/null

標準エラー出力を捨てているのは、XVimを入れているとpluginのコード署名に関する警告が出るためです.. 消す方法あるのかな。

共有設定 (optional)

これは必須ではないですが、複数のマシンを行き来して開発したり、新しいマシンに交換する時、何も考えなくても共有されていた方が便利です。
Google Driveでもなんでもいいので、ディレクトリ単位でスクリプトを共有できるようにしておくといいと思います。
自分は ~/bin/ のようなよく使うツールがあるところは、iCloud Driveで共有されている~/Documents/配下へのsymlinkにしています。

チームで使うのであれば、cmdshelfでスクリプト共有するというのも手ですね。

waitp

こちらは自作のスクリプトです。
指定されたプロセス(ファイルI/Oを含む処理である事が条件)の完了を待つという汎用的なbashスクリプトです。
関数として ~/.bashrc などに登録しておくと良いでしょう。

function waitp {
    pid=$(ps | grep "${1:?}" | grep -v "grep ${1:?}" | head -1 | awk '{print $1}')
    if [ -z $pid ]; then
        return
    fi
    lsof -p ${pid:?} +r 2 > /dev/null 2>&1
}

notifyme

こちらも雑な自作スクリプトで、AppleScript経由で通知センターに通知を飛ばす関数です。(音量注意)

alias notifyme='echo "display notification with title \"done\" sound name \"Beep\"" | /usr/bin/osascript'

レシピ

以上の材料を組み合わせると、テスト実行のコマンドはこういう感じになります。

waitp my-proj/bt; ~/bin/my-proj/bt ; notifyme

my-proj/btをプロセス名に含むプロセスの完了を待ってからテストを実行し、終わったら成功失敗関係なく通知します。

ログはこんな感じで、必要最低限しか流れません。

$ waitp my-proj/bt; ~/bin/my-proj/bt ; notifyme
Building...
** TEST BUILD SUCCEEDED **
Testing...
Test Suite 'MyProjTests.xctest' passed at 2019-12-24 12:04:18.192.
     Executed 4271 tests, with 0 failures (0 unexpected) in 69.011 (70.223) seconds
Test Suite 'All tests' passed at 2019-12-24 12:04:18.205.
--
** TEST EXECUTE SUCCEEDED **

実演

それでは実際に動かしてみましょう。こんな感じです。

![時間がなくなってきたので後で貼ります!]()

waitp のおかげでビルドが勝手に直列実行されていますね。
ビルド大体5分くらいはかかるので、2つ終わるまでにコーヒー休憩できちゃいます。

まとめと今後の展望

以上、オレオレ開発環境の共有でした。

環境を作るのにちょっと手間をかけてしまっているのですが、一日を通してXcodeを立ち上げないで済んだ日は謎の達成感があります。
前提条件でも書きましたが、テストを充実させておくなど、なるべくコードで完結するように環境を整備していくことをオススメします。テスト書くのは品質の向上にもつながりますしね!この際どうでもいいけど!

ちなみにお気づきの方もいると思いますが、エディタの設定は特にいじっていません。つまりSwiftやUIKitなどのコード補完は一切効きません。
Swiftのlanguage server protocolも少し前に出てきましたが、UIKitなどApple SDKのものがないのが悩みどころですよね。
この辺りは今後の課題です。まあiOS開発に関しては脳内補完がある程度効くので自分は割と事足りちゃってますが..

それでは良いコマンドライン人生を!

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

Swifterでaouth認証してaccesstoken取得するまで

初めに

今回はSwiftでtwitterapi叩こうと思います。
TwitterKitというのが主流だったらしいのですがサポート終了したということでSwifter使っていきます。

流れ

  • cocoapodsでswifter入れる
  • oauth認証のためのメソッドを呼ぶ
  • safariで認証後アプリに戻ってくる。

やってみる

まずpodfileに以下を書き込んでpod installしっます

pod 'Swifter', :git => 'https://github.com/mattdonnelly/Swifter.git'

次にviewControllerに以下を書きます。
buttonを押した時でもViewdidloadの時でもお好きなタイミングで呼び出してください。
SFSafariViewControllerDelegateを準拠させておくとアプリ内でsafariを開けます。

ViewController
//twitterdeveloperで取得したapikeyとapisecretkeyを入れます。
let swifter = Swifter(consumerKey: "APIkey", consumerSecret: "ApiSecretKey")
//safariを開く。callbackURLがTwitterDeveloperのcallBackURLと違っていたりkeyが間違えたりしてるとerrorに飛ぶ。
swifter.authorize(withCallback: URL(string: "各々設定したcallbackURL")!, presentingFrom: self, success: {accessToken, response in
     print(accessToken.key)
},failure: { error in
     print(error)
})

Custom URL Scheme

次にCustomURLSchemeを設定します。
こいつを設定してあげるとそのリンクを開いたときにアプリに戻るかどうかというポップアップが出るようになります。
つまり
1. safariを開く
2. ユーザーがパスワードなどを使ってaouth認証する。
3. 設定したcallBacURLに飛ぶ
4. callBackURLをCustomURLSchemeに設定してあるのでアプリに戻ってくることができる。
という流れができます。
ということで、TARGETS->自分のアプリ->info->URLTypes->URLSchemesに設定したsampleと指定してあげましょう。
例えばcallBackURLをsample://と設定しているのなら、sampleの部分だけ書いてあげます。
これでcallBackURL://と叩かれたときにアプリに戻ってこれます。
info.plistのURLSchemesに設定してあげてもいいです。
最後にappdelegateに戻ってきた時の処理を書きます。

AppDelegate.swift

AppDelegate
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        return Swifter.handleOpenURL(url, callbackURL: URL(string: "設定したcallBackURL")!)
}

これで一通り実装は終わりです。

躓きポイント

  • AppDeveloper側でcallBackURLを設定しているかどうか。
  • callbackURLがsample://だとしたらURLschemeはsampleだけになっているか
  • apikeyが間違っていないか 以上です。

参考にしたサイト

https://qiita.com/noby111/items/532f3d806b19ab46433a
https://github.com/mattdonnelly/Swifter
https://qiita.com/k-boy/items/c83a0505d12cc8e7d4b9

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

SwiftUIを簡易的にUserDefaultsを使う方法

はじめに

SwiftUIでViewModelを使ったUserDefaultsを使う方法はよく見かけるのですが、簡易的にUserDefaultsを使う方法がありましたので紹介します。
あくまでの簡易的な使い方です。

今回のサンプルコード

TextFieldに文字入力が終わるとTextに入力した結果が表示されます。
今回は、文字入力が終わったタイミングでUserDefaultsに保持して、アプリ起動時にUserDefaultsから読み出します。

1.png 2.png 3.png

コード

今回は、文字入力が終わるとonCommit内が実行されるのでそこでUserDefaultsに保存してします。
VStack{}の後に.onAppearが表示前に呼ばれるのでそこでUserDefaultsから値を読み出しています。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var inputText:String = ""
    @State var displayText:String = ""

    var body: some View {
        VStack {
            Text("Hello, \(self.displayText)!")
            TextField("Your name", text: $inputText , onCommit: {
                UserDefaults.standard.set(self.inputText, forKey: "name")
                self.displayText = self.inputText
                self.inputText = ""
                })
                .padding()
        }
        .onAppear {
            self.displayText = "World"
            guard let userdefaultText = UserDefaults.standard.value(forKey: "name") as? String else { return }
            self.displayText = userdefaultText
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

参考サイト

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

[Swift]GitHub検索プログラムをprotocol, associatedTypesで改良する

先日、Swiftの基本的な内容を記した本「改訂新版 Swift実践入門(石川洋資・西山勇世、技術評論社、2018)」に従って、GitHubのリポジトリの検索プログラムを実装しました。

GitHub検索用APIには豊富な検索オプションが用意されているのですが、この本ではユーザーとリポジトリの検索のみ記載されていたのでとりあえずそれに従って実装しました。

検索結果を表すために、ジェネリクスを利用したSearchResponse構造体を実装しています。

SearchResponse.swift
struct SearchResponse<Item: Decodable>: Decodable {
    let totalCount: Int
    let items: [Item]//構造体User, 構造体Repositoryを収められる

    //中略
}

この構造体は、SearchRepositories, SearchUsersという、APIへのリクエストを表す構造体にそれぞれassociatedTypesを用いて関連づけられています。

GitHubAPI.swift
final class GitHubAPI {

    struct SearchRepositories: GitHubRequest {
        //中略

        //リクエストに、対応するレスポンスを関連付けしている
        typealias Response = SearchResponse<Repository>

        // The word you would like to search with.
        let keyword: String
    }

    struct SearchUsers: GitHubRequest {
        //中略

        //リクエストに、対応するレスポンスを関連付けしている
        typealias Response = SearchResponse<User>

        let keyWord: String
    }

}

GitHubRequest.swift
protocol GitHubRequest {
    associatedtype Response: Decodable
    //後略
}

associatedTypesとは、swiftのprotocolに任意の型を関連づけられるというものです。protocol版のジェネリクスのようなものと大雑把に捉えています(認識がおかしかったら教えてください)。

ところが本書の例では、プログラムのエントリポイントとなるmain.swiftで、SearchReponse<Repository>を使ってリポジトリは検索できるようになっているのに、せっかくのSearchReponse<User>は使っておらず、ユーザーでは検索できないようになっていたのです?

main.swift
//中略
//SearchRepository構造体を用いてリクエストを作成
let request = GitHubAPI.SearchRepositories(keyword: "\(keyWord)")

//リクエストをサーバへ送る
client.send(request: request, completion: {(result) in
    switch result {
    case let .success(response):
        response.items.forEach {print($0)}
        exit(0)
    case let .failure(error):
        print(error)
        exit(1)
    }
})
//中略

せっかくユーザーで検索する仕組みを用意しているのに使わないではもったいない。
そこで、ユーザかリポジトリかどちらか選んで検索できるようにしようと実装してみました。

具体的には、SearchReponse<Repository>SearchReponse<User>もどちらも渡して処理できるsendClient<T: GitHubRequest>(request: T)関数を実装しようとしました。

しかしコンパイルエラーが発生しました。

main.swift
func sendClient<T: GitHubRequest>(request: T) {

    let client = GitHubClient()

    client.send(request: request, completion: {(result) in
        switch result {
        case let .success(response):
            response.items.forEach {print($0)} //Error: Value of type 'T.Response' has no member 'items'
            exit(0)
        case let .failure(error):
            print(error)   
            exit(1)
        }
    })   
}

は?

どうやら他の箇所にも手を加えないと実現できないようです。解決策を考えてみました。

エラー内容はValue of type 'T.Response' has no member 'items'

原因は、GitHubRequestプロトコルにて、associatedtype Response: Decodableとのみ規定していた事、つまりコンパイラからはResponseという型に対する制約は「Decodable である」事以外に規定されていなかった、という事でした。

言い換えれば、Responseという型がitemsという変数を持つという制約は今の所どこにもないため、コンパイラからはそのことが分からないのです。だからエラーが発生したのです。

まずは、Responseという型に対して「Decodableである」のみならず「itemsという変数を持つ」という制約も付け加える必要があります。そのため、新しいプロトコルAbstructResponseを作成し、Response型をこれに準拠させることにしました。

SearchResponse.swift
//新たなプロトコルに準拠させる
struct SearchResponse<Item: Decodable>: AbstructResponse {
    typealias Item = Item

    let totalCount: Int
    let items: [Item]

      //中略
    }
}

//新たなプロトコルを定義
protocol AbstructResponse: Decodable {
    associatedtype Item: Decodable

    var totalCount: Int {get}
    var items: [Item] {get} //itemsという変数を持つという制約が加わる
}

はい、このようにするとコンパイラはResponse型を見ただけで、それがitemsという変数を持つことが分かるようになり、エラーが解消します。あとは、変更点に合わせて他の部分も変更するだけです。

全ての変更点をまとめると下記のようになります。

GitHubRequest.swift
螢幕快照 2019-12-12 11.46.04.png

SearchResponse.swift
螢幕快照 2019-12-12 11.46.17.png

main.swift

螢幕快照 2019-12-12 11.46.49.png
螢幕快照 2019-12-12 11.47.05.png
螢幕快照 2019-12-12 11.47.33.png
螢幕快照 2019-12-12 11.47.42.png

コードをコピペなどしたい場合はこちら参照: https://gitlab.com/Satoru_Aikawa/githubsearchproject2/commit/6591bce7dfca238e47afc658a3e3b4e7e6bbd1c0

参考文献・ウェブサイト

www.amazon.co.jp/dp/477419414X

https://github.com/Satoru-PriChan/GitHubSearch

https://www.hackingwithswift.com/example-code/language/what-is-a-protocol-associated-type

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

SwiftUIでTabBarを作る方法

はじめに

SwiftUIでTabBarの作る方法を紹介します。
TIPSとして役立ててください。

今回の作るTabBar画面

TabAViewTabBViewを作りそれをTabBarで画面を切り替えるSwiftUIを作ります。

TabA.png TabB.png

作り方

TabAViewとTabBViewを作る

File>New>File...でSwiftUI ViewでTabAviewTabBViewを新規にファイル追加します。
中のコードは今回はわかりやすいように、TextにThis is Tab A.This is Tab B.と表示するようにします。

TabAview.swift
import SwiftUI

struct TabAView: View {
    var body: some View {
        Text("This is Tab A.")
    }
}

struct TabAView_Previews: PreviewProvider {
    static var previews: some View {
        TabAView()
    }
}
TabBview.swift
import SwiftUI

struct TabBView: View {
    var body: some View {
        Text("This is Tab B.")
    }
}

struct TabAView_Previews: PreviewProvider {
    static var previews: some View {
        TabAView()
    }
}

ContentView.swiftにTabBarを定義する

TabViewと定義しその中に、TabBarの中のデザイン定義をVStack{}内に定義します。
今回は、アイコンと文字にしました。アイコンはどの環境にもあるSF Symbolを使っています。

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        TabView {
            TabAView()
                .tabItem {
                    VStack {
                        Image(systemName: "a")
                        Text("TabA")
                    }
            }.tag(1)
            TabBView()
                .tabItem {
                    VStack {
                        Image(systemName: "bold")
                        Text("TabB")
                    }
            }.tag(2)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

参考サイト

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

RxSwiftをざっくりと理解する(1)基礎用語まとめ

はじめに

andfactory Advent Calendar 23日目の記事です!
RxSwiftの内容を初学者に向けてざっくりとまとめてみました。
第1回は基礎用語です。

Observable

それを購読(≒監視)しているオブジェクトに処理されるデータを流すもの。
* Observableはelementを含むnextイベントを流すことができる。
* terminateイベント(error, completed)が流れるまで続く。
* Observableが停止するとイベントは流れなくなる。

Subscribe

Observableを購読(≒監視)すること。
onNext,onError,onCompleted,onDisposedのタイミングで行いたい処理を登録することができる。

Disposable

メモリリーク防止のために必要であれば購読を解除してくれるオブジェクト

  let observable = Observable.of(1, 2, 3)

  observable
    .subscribe(
        onNext: { element in
            // 値が流れてきたときの処理
        },
        onError: { error in
            // エラー時の処理
        },
        onCompleted: {
            // completeイベントが流れたときの処理
        },
        onDisposed: {
            // disposeされたときの処理
        })
   .disposed(by: disposeBag)

Traits

通常のObservableより機能を限定したもの。
* コードの意図をより明確に伝えるのに役立つ。
* Single,Maybe,Completableの3つがある。
* Singleは success(value) か error しか流さないため、API通信などに便利。

class Reader {
    static func loadData(from data: String) -> Single<String> { // 略 }
}

Reader.loadData(from: "text")
    .subscribe(
        onSuccess: { text in
            print(text)
        },
        onError: { error in
            // エラー処理
    })
    .disposed(by: disposeBag)

Subjects

ObserverとObservableの両方の役割を持てる。
RxSwiftにおいて4つのSubjectとそれをラップした2つのRelayがある。
* PublishSubject -> 初期値は持たない。購読者に対して新しいeventのみを流す。
* BehaviorSubject -> 初期値を持つ。最新の値を新たな購読者に対して流す。
* ReplaySubject -> バッファサイズを持って初期化される。そのサイズ分だけ値を保持して新たな購読者に流す。

let subject = ReplaySubject<String>.create(bufferSize: 2)

subject.onNext("a")
subject.onNext("b")
subject.onNext("c")

subject
    .subscribe {
        print($0)
        // bとcがプリントされる
    }
    .disposed(by: disposeBag)

  • AsyncSubject -> completedを受け取ると、nextイベントの最後の値だけを流す。あまり使われない?
  • PublishRelay / BehaviorRelay -> それぞれPublishSubjectとBehaviorSubjectのラッパー。
  • nextイベントのみを流せる。relayに対して.completedや.errorを流すことはできない。
  • subjectをクラスの外部に公開してしまうと値の書き換えが自由にできてしまう。それを防ぐためにsubjectをprivateにしてObservableを外部に公開する。
class Some {
    private let someSubject = PublishSubject<String>()  //Subjectはprivate

    var observable: Observable<String> { return someSubject } //Observableはpublic
    func sendString(str: String) {
        someSubject.onNext(str) //Subjectでイベントを流す
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift:macOSでSwifterを使ってTwitterの認証をする

いそぎ要点だけまとめます.

Swifter

mattdonnelly/Swifter
TwitterへのOAuth認証をいい感じにやってくれるiOS/macOS向けのライブラリです.

やり方

1. Swifterの導入

  • まずは,GitHubレポジトリに飛んでプロジェクトをダウンロードしてSwifterMacをビルドしてフレームワークを作る.
  • 自分のプロジェクトのTARGETSFrameworks, Libraries, and Embedded Contentにドラッグ&ドロップして追加する.
  • ソースの必要なところでimport SwifterMac

CocoaPodsとかCarthageの方法は調べてください.

2. App Sandboxの設定

Signing & Capabilities > App Sandbox > Network > Outgoing Connections (client)にチェックを入れる

3. Twitter Appsでアプリを登録してKeyを手に入れる

Screen Shot 2019-12-24 at 2.01.01.png

4. Twitter AppsでCallback URLを設定する

swifter-[生成されたConsumer API Key]://
をCallBack URLとして追加

5. URL Schemesの追加

TARGETS > Info > URL TypesにURLSchemesを追加する.
swifter-[生成されたConsumer API Key]
を入力する.
Screen Shot 2019-12-24 at 2.07.43.png

6. AppDelegate.swift実装

AppDelegate.swift
import Cocoa
import SwifterMac

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleEvent),
                                                     forEventClass: AEEventClass(kInternetEventClass),
                                                     andEventID: AEEventID(kAEGetURL))
        LSSetDefaultHandlerForURLScheme("swifter" as CFString, Bundle.main.bundleIdentifier! as CFString)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }

    @objc func handleEvent(_ event: NSAppleEventDescriptor!, withReplyEvent: NSAppleEventDescriptor!) {
        guard let callbackUrl = URL(string: "swifter-[生成されたConsumer API Key]://") else { return }
        guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }
        guard let url = URL(string: urlString) else { return }
        Swifter.handleOpenURL(url, callbackURL: callbackUrl)
    }

}

7. ViewControllerの実装

ViewController.swift
import Cocoa
import Accounts
import SwifterMac

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let swifter = Swifter(consumerKey: "[生成されたConsumer API Key]",
                              consumerSecret: "[生成されたConsumer Secret Key]")
        let callbackUrl = URL(string: "swifter-[生成されたConsumer API Key]://")!
        swifter.authorize(withCallback: callbackUrl, success: { (_, _) in
            Swift.print("Success Authorizing")
            // この後はもう好き放題やってケロ
        }) { (error) in
            Swift.print(error.localizedDescription)
        }
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

}

たぶんこれでいけるはずです.

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

[Cocoa][Swift]Cocoa.swiftのご案内

2019年も、もうすぐ終わります。今年一年ありがとうございました。
勉強会って参加するのもいいですが、運営するのも色々得るものがあるというのも再認識できた一年でした。

以下が、今年開催された勉強会の一覧です。

  • Cocoa.swift 2019-01 (macOS/iOSアプリケーション開発勉強会)
    • 第116回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年1月16日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/111294/
  • Cocoa.swift 2019-02 (macOS/iOSアプリケーション開発勉強会)
    • 第117回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年2月20日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/117577/
  • Cocoa.swift 2019-03 (macOS/iOSアプリケーション開発勉強会)
    • 第118回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年3月13日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/121658/
  • Cocoa.swift 2019-04 (macOS/iOSアプリケーション開発勉強会)
    • 第119回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年4月24日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/124467/
  • Cocoa.swift 2019-06 (macOS/iOSアプリケーション開発勉強会)
    • 第120回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年6月12日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/129470/
  • Cocoa.swift 2019-07 (macOS/iOSアプリケーション開発勉強会)
    • 第121回 Cocoa勉強会 関東
    • 2019年7月17日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/135361/
  • Cocoa.swift 2019-09 (macOS/iOSアプリケーション開発勉強会)
    • 第122回 Cocoa勉強会 関東
    • 2019年9月11日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/145138/
  • Cocoa.swift 2019-10 (macOS/iOSアプリケーション開発勉強会)
    • 第123回 Cocoa勉強会 関東
    • 2019年10月30日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/147201/

昨年度と比較して開催時期がほぼ同じと安定していとうか進歩がないというか。

【関連情報】
Cocoa.swift

Cocoa勉強会 関東

Cocoa練習帳

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

クリスマスまでにSwiftUIでPinterest風のグリッドビューを作りたかったのだけれど...

Qiita Advent Calendar iOS #2 最終日の記事です。

:santa::christmas_tree::tada: Merry Christmas :santa::christmas_tree::tada:

今日はクリスマスですね!大人になってしまったので :santa: にプレゼント :gift: をもらう楽しみはなくなってしまいましたが、逆に子どもたちにプレゼントして喜んだ笑顔 :smile: を見るのが楽しみになりました。

さて、最近そのプレゼント選びを便利に、そして楽しくするためにアプリを個人的に作りはじめました。そのアプリのイメージとしては、(僕の大好きな)Pinterest のアプリのように、並んだ写真を眺めながら選んでいくようなものを考えています。

せっかく新しく作るアプリなので SwiftUI で開発することにしました。まずは一番重要になる Pinterest 風のグリッドビューを SwiftUI で実現する方法をこのアドベンドカレンダーで共有したいと思い、調査を開始しました。

どうやらあの Pinterest 風のグリッドビューは Staggered grid view (ずらしたグリッドビュー) なんて呼ばれているみたいです。

SwiftUI には UIKit の UICollectionView にあたるものがない :cry:

いきなり出鼻をくじかれました...。

Staggered grid view を以前に UIKit で実装した時は UICollectionView を使いました。なので SwiftUI でもコレクションビュー的な View を探したのですが、ありません(キャッチアップ遅すぎ)。UITableView にあたるものは List として存在しているのに〜 :cry:

SwiftUI での実装事例を漁る :mag_right:

調べている間に、SwiftUI で Grid view や Staggered grid view を実装しているような事例をいくつか見つけたので、どんな風に実装されているのか調べてみました。

SwiftUIExtensions/SwiftUIExtensions - Grid based layouts

staggeredGrid.png

GitHub リポジトリ SwiftExtensions/SwiftUIExtensions で Grid based layouts として全て同じサイズの Grid view と Staggered grid view の事例がありました!早速どんな風に実装されているのか見てみます。

重要なレイアウトの部分は主に Sources/SwiftUIExtensions/Layouts/Grid/Grid.swift に書かれていました。そのビュー構造を簡略化すると次の様になっています。

ZStack はその名の通りビューの Z 軸方向のスタックビューです。この下に ForEach でグリッドの数だけビューを並べていき、そのビューは GeometryReader から得た親ビューのサイズ/位置などの情報を元に計算して求められた場所に配置されています。

(GeometryReader については SwiftUIの肝となるGeometryReaderについて理解を深める でわかりやすく解説されています。)

これは親のビューのジオメトリを元に UIView を座標計算しながら配置していた Auto Layout 以前のレイアウトと同じではないか!?ということは表示するグリッドの数が少し増えただけですぐ限界くるやつでは?

試しにこれのデモアプリの Staggered Grid で表示するグリッドの数を 10,000 にしてシミュレータで実行してみたら、ビューが表示されるまでに 20秒 もかかりました(やっぱりそうだよね...)。一度に100くらいのグリッドなら問題なさそうですが、大量のデータでは実用レベルではないことがわかりました。次!

Q-Mobile/QGrid

People3.png

SwiftUI でグリッドビューを実現する Q-Mobile/QGrid GitHub リポジトリを見つけました。このリポジトリの description にはこうあります。

? QGrid: The missing SwiftUI collection view.

これは期待できそう。Staggered が実現できるかどうかはおいておいて、実装の参考にはなることを期待してコードを読んでみます。このリポジトリのソース構成はとってもシンプルで Sources/QGrid/QGrid.swift だけです。そのビュー構造を簡略化すると次の様になっていました。

つまり、垂直方向のスタックビューと水平方向のスタックビューを組み合わせているんですね。なるほど、嫌な予感がします:sweat_drops:いや、でも実は VStack は SwiftUI.List や UIKit.UITableView のように仮想リストビュー的に動いてくれるのかもしれないので念の為検証してみよう、そうしよう。

こちらもリポジトリにデモアプリがあるので、それのグリッドの数を 10,000 にしてシミュレータで実行してみたら、1分経っても何も表示されずに固まってしまいました。やはり VStack や HStack は UIKit の UIStackView 相当のものなんでしょうね(ForEach という繰り返し機構があるのでちょっとだけ期待したのですが)。

自分で考えてみるが...

現状、仮想リストビュー的に動作してくれるものが List しかないということがわかったので、この List を使ってなんとかできないか考えてみます。

  • List を HStack で複数並べて、各 List に流すデータを制御すればいいのでは!?
  • それぞれの List で独立してスクロールすることになってしまうなぁ
  • List のスクロールイベントを拾って、それぞれの List のスクロール位置を同期させれば!?
  • スクロール位置を知る手段も変更する手段もないらしい...

うーん、うーん、うーん ...

:sunrise: :sunrise: :sunrise: :sunrise:

ということで悩んでいるうちにアドベントカレンダーの投稿日の朝となってしまいました。やはり実現するには SwiftUI としてコレクションビューが提供されることを待つしかなさそうな気がします。表示するグリッドの数が少なければ、紹介したライブラリを使うのもありです。何か良いアイデアがあれば教えていただけると嬉しいです。

今作っているアプリは、とりあえずこの Staggerd grid view の画面については UICollectionView で実装することになりそうです。

それではみなさん、良いクリスマスを :wine_glass:

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