- 投稿日:2019-12-04T22:20:55+09:00
FirestoreのArrayContainAnyクエリでタイムラインを実装してみた
はじめに
初アドベントカレンダー!!
普段はiOS開発のインターンをしながら、個人でコツコツアプリを作ったりしております。
今回はFirestoreのアップデートでArrayContainAnyというQueryが追加され、どうやらタイムライン実装で活躍してくれそうで、自分なりにやってみたので記事にしてみました。
※ 間違えている点や、改善点などございましたらコメントかTwitter(https://twitter.com/yuto_nakano44)
にご連絡いただければ幸いですArrayContainAnyクエリとは
こちらの記事がとても分かりやすかったので詳しくはこちらをみていただけると!↓
https://medium.com/anti-pattern-engineering/待ちに待ったfirestoreにarray-contains-anyとin検索がキタ-1771cbc14724使い方としては、newsコレクションのsports配列にbasketBallが要素として含まれているドキュメントが欲しい!
となれば下記のようになります↓collection("news").whereField("sports", arrayContainsAny: ["basketball"])basketballだけでなく、baseballが要素として含まれているドキュメントが欲しい!
となれば下記のようになります↓collection("news").whereField("sports", arrayContainsAny: ["basketBall", "baseball"])タイムライン機能実装
現在、個人でアプリを作っており、その過程で以前シンプルなタイムライン機能を実装しました。
タイムラインを構成するデータとしては下記の通りです。
・users (ユーザーのプロフィール情報が保存されているコレクション)
・posts (全ユーザーの投稿情報が保存されているコレクション)実装方法
コレクションのデータです↓
postsコレクション
posts - id - title - content - reply_countusersコレクション
users - id - name - user_icon - posts <- userが投稿したpostを格納した配列手順としては
1, postsを取得
2, posts一つ一つのidを配列に格納
3, arrayContainsAnyクエリでpostのidにヒットしたusersのドキュメントを取得コード
func fetchUser(posts: [Post], completion: @escaping ((Result<[User], Error>) -> Void)) { // postのidを配列に格納 let postIds = posts.map { (post) -> String in return post.id } // usersコレクションのposts配列にpostIds内のidが含まれていたら Firestore.firestore().collection("users").whereField("posts", arrayContainsAny: postIds) .addSnapshotListener({ (snapshot, error) in guard let snapshot = snapshot else { return } let users = snapshot.documents.map { (user) -> User in return try! Firestore.Decoder().decode(User.self, from: user.data()) } completion(.success(users)) }) }ユーザー取得できた?
※クエリの配列(今回で言うpostIds)には10個の要素までしか配列に格納できず、
10個より多いと以下のエラーが出てクラッシュします。↓"Invalid Query. 'arrayContainsAny' filters support a maximum of 10 elements in the value array."配列の要素が空の場合も↓
Invalid Query. A non-empty array is required for 'arrayContainsAny' filters.ログでしっかりエラーの原因を出してくれるとありがたいですね。
今回の方法でタイムラインを実装するなら10件までしか一度に表示できないですね。
必要なタイミングで都度データを取得する必要がありそうです。終わりに
自分なりにタイムラインの実装をやってみたので記事にしてみました。
Firestoreのデータ構造について勉強しなければ。
フィードバックいただけると嬉しいです!
ありがとうございました。参考: https://medium.com/anti-pattern-engineering/待ちに待ったfirestoreにarray-contains-anyとin検索がキタ-1771cbc14724
- 投稿日:2019-12-04T21:03:55+09:00
Makefileを利用してiOS開発を賢く便利に運用しよう?
はじめに
VALU Advent Calendar 2019 4日目の記事です!
VALU 社として記事を書きたい! と名乗りを上げて2年目になりました。ことしもよろしくおねがいします!今回は「Makefile を利用して iOS 開発を賢く運用しよう」ということで,
Makefile
の補完機能を用いまして,チームメンバーが快適に,かつ本質に集中できる環境を維持することのできる試みのご紹介です。目次
- Makefile との出会い
- Makefile によるスクリプト管理は何が嬉しいのか
- iOS で実際に利用している Makefile の中身
- Makefile に置くべき便利なワンライナー
- おわりに
- References
Makefile との出会い
初めて Makefile と出会ったのは,そうですね。もともとは組み込み寄りであった自身の弱点を克服するため,gohome という Go 製の API サーバーを作成していた最中の出来事でした。
Go 言語自体も初めてであったことで色々調べているうちに Golang を使うなら Makefile を恐れるな という記事に出会いました。
本来の Makefile の使い方とは異なりますが,
Makefile
を作成し,よく使うコマンドを:
区切りの Key とスクリプトで構成する。ターミナルから$ make build
などと入力してスクリプトを実行させると便利だよ,という記事です。Makefile の例。画像は YutoMizutani/gohome/Makefile から一部を抜粋
わたしはこれを読み大変感動しまして,ぜひ iOS にも導入しようと決意しました。
Makefile によるスクリプト管理は何が嬉しいのか
環境構築を一括で管理できる
Makefile を利用することで,個々のコマンド操作を統制することが可能です。
最も恩恵を得られるのは新しい人が参入した際の環境構築だと思います。あなたの
README.md
はどうでしょうか?「CocoaPods のインストール方法は...」等の長い文章で書かれていませんか? 利用されているツールしか書かれていない場合もあるかと思います。
新しい開発者の目的は,「そのリポジトリがどう動いているかを知ること」より「目的の機能または問題に着手すること」です。フルスタックエンジニアが助けに来たとしても,1日を環境構築で終えるような組織ではとてももったいないですよね。
VALU では
$ make all
のみで環境構築が完了します補完が効く
zsh-completions などの補完用プラグインを用いることで,どんなコマンドがあるかを実行前に確認することができます。
これが ShellScript だけでなく Makefile を利用する利点です。Branch 間の環境変化にも常に同じコマンドで対応できる
「今回新しく CocoaPods を Homebrew から Gem を利用するように変えました! 以降開発者はこのコマンドを利用してください! 古いコマンドは使わないでください!」
という初心者が泣いてしまう呪文を
Makefile
は解決します。
Makefile
内にスクリプトを書いてコマンドとスクリプトを分離させることで変更に強くなります。iOS 風に言うとprotocol
経由でアクセスする,と言えばイメージがしやすいですかね。呼ぶ側は常に
make
コマンドから。$ make install
と呼ぶことで環境が変わってもインストール作業が継続できます。自動環境構築の肝,自動インストール
Homebrew がない? インストールします。 CocoaPods ももちろん。Ruby のバージョンが違う? rbenv を利用するように変更します,というように存在しない場合にも自動でインストールさせるようにしています。
最近「おま環」という言葉を知りました。OSS だとそうもいきませんが,会社PCの社内プロジェクトなので,必要なものは自動で構築させる方が効率的です。
以下は Xcode のバージョンを指定するスクリプトです。CocoaPods 等の Ruby 製ツールは,VALU では Gemfile を用いて管理しています。
各スクリプトは,そのコマンドの実行前にtype
gem contents
mint list | grep -s
等を用いて目的のコマンドが存在するかを判定し,存在しない場合にインストールスクリプトが走るようにしています。
$(dirname $0)/../gems/bundle-install.sh
の先では,bundle
コマンドが存在するかを判定しており,必要に応じて Ruby 自体がインストールされているかまで遡り,対応が必要なPCにのみ適切にインストールが走るようになっています。このような綺麗なスクリプトも
$ sh select-xcode-version.sh
と長ければ全メンバーに実行してもらえない可能性があります。sh
って打てば補完されるだろう,というのは「おま環」でした。
make
にコマンドを集めておき,短いコマンドで呼び出せるようにしておくことでスクリプトとの距離が縮まります! すてきです!select-xcode-version.sh#!/bin/sh # Install Gems if !(gem contents xcode-install > /dev/null 2>&1); then sh $(dirname $0)/../gems/bundle-install.sh fi # Check argv if [ $# -ne 1 ]; then echo "select-xcode-version.sh: error: Required the version" 1>&2 exit 1 fi # xcode-install says `Xcode VER.SION.NUM.BER` XCVERSION_OUTPUT_PREFIX="Xcode " EXPECT_XCODE_VERSION=$1 CURRENT_XCODE_VERSION=`xcversion selected | grep -i $XCVERSION_OUTPUT_PREFIX | tr -d $XCVERSION_OUTPUT_PREFIX` # Select Xcode version if these are different if test $EXPECT_XCODE_VERSION != $CURRENT_XCODE_VERSION; then bundle exec xcversion install $EXPECT_XCODE_VERSION ; : bundle exec xcversion select $EXPECT_XCODE_VERSION fiiOS で実際に利用している Makefile の中身
Swift も LLVM を利用している言語ですが,Xcode によってコンパイル周りのオプションは肩代わりすることができています。
Swift 自体のビルドに必要なすごくながいコマンド とは異なり,とてもシンプルに記述することができます。
あくまでMakefile
を使おうというものなので,おまじないや固有表現を避け,ShellScript が読める人なら内容が読めるような形に抑えています。
加えて,具体的なコマンドは環境に依存するパスおよび拡張子以外は別の ShellScript としてファイルを分けており,CI 時に必要なスクリプトは個々に実行できる環境を作り上げています。こちらの Makefile は「人間がコマンドを覚えず,内容を知らなくとも実現したいことを実現する」という方針で設計しており,
利用者は「とりあえず$ make
を打ってみて,補完からデプロイやビルドなどのしたいことを探す」
成長したい人は「Makefile を覗いてみて,必要に応じて質問や修正,追加を行う」
ことで業務の効率化を実現しています。これにより,QA チームへの環境構築や作業依頼についても「
$ make clean
をしてみてください」と Slack や口頭で伝達することができるようになっていますMakefile# Paths PROJECT_PATH=./ # File extensions PROJECT_EXTENSION=.xcodeproj WORKSPACE_EXTENSION=.xcworkspace # Definition PROJECT_NAME=`find $(PROJECT_PATH) -maxdepth 1 -mindepth 1 -iname "*$(PROJECT_EXTENSION)" | xargs -L 1 -I {} basename "{}" $(PROJECT_EXTENSION)` PROJECT=$(PROJECT_PATH)$(PROJECT_NAME)$(PROJECT_EXTENSION) WORKSPACE=$(PROJECT_PATH)$(PROJECT_NAME)$(WORKSPACE_EXTENSION) XCODE_VERSION=`cat $(PROJECT_PATH).xcode-version` # Xcode を開く open: sh scripts/general/xcode/open-xcode.sh $(WORKSPACE) # Xcode を強制終了させる kill: sh scripts/general/xcode/kill-xcode.sh # 一括で環境構築を行う all: make config make select make generate make install make sort make clean # ~~~ 省略 ~~~ # ビルドに必要なファイルを生成する generate: make generate-xcodeproj generate-xcodeproj: sh scripts/general/xcodegen/generate-xcodeproj.sh # 依存するツールやライブラリをインストールする install: make install-mint make install-gems make install-pods install-mint: sh scripts/general/mint/mint-install.sh install-gems: sh scripts/general/gems/bundle-install.sh install-pods: sh scripts/general/cocoapods/pod-install.sh余談ですが,この
$ make all
内では複雑なスクリプトを隠蔽しています。
上記のmake config
内では,post-checkout
の Git hook に紐づけてインストール (bootstrap) を入れています。
発生し得るトラブルには事前に対応しましょう。
「ブランチを変えたら (ライブラリのバージョンが変わったために) ビルドできなくなった」という苦言に毎回対応するのも,そのためのドキュメントを保守するのもつらいです。
Makefile に置くべき便利なワンライナー
この程度,
.bashrc
に書いておけという意見も分かります。
一方で,リポジトリで共有するということは「あの人の便利な技」チーム全体がその commit から便利になるということです。
何を書いたら良いか分からないという方へ! まずこれを入れて幸せになりましょう!ディレクトリ配下の Xcode workspace を検索して起動する
open
コマンドは macOS で default で指定されたアプリを用いて起動させるコマンドです。実際にはプロジェクトディレクトリやxcworkspace
の拡張子を変数に切り出していますが,xcworkspace
を利用していることが分かっている場合は,より短いコマンドでターミナルから Xcode を開くことが可能です。ターミナル$ open `find . -maxdepth 1 -mindepth 1 -iname "*.xcworkspace"`
-a
オプションによって指定したアプリを用いて開くことも可能です。複数の Xcode をインストールしている場合,xcode-select
で指定されている PATH を利用して起動させることもできます。PROJECT_PATH=`find . -maxdepth 1 -mindepth 1 -iname "*.xcworkspace"` XCODE=`xcode-select --print-path | awk 'match($0, /^.*.app/){ print substr($0, RSTART, RLENGTH) }'` open -a $XCODE $PROJECT_PATHキャッシュと Derived Data の削除
よく使うキャッシュと Derived Data の削除を行うワンライナーです。
; :
と最後に付けることでrm
時に Fail しないようにしています。
われわれは記憶のテストをしているわけではないので DerivedData の場所 (~/Library/Developer/Xcode/DerivedData
) は暗記するよりどこかに書いておきましょう。ターミナル$ xcodebuild -alltargets clean ; rm -rf ~/Library/Developer/Xcode/DerivedData/ ; :Xcode project をソートする
有名な Xcode のソートスクリプト をありがたく利用させていただくワンライナーです。Perl ファイルなので
curl
でお借りして使い終わったら破棄するだけです。
Makefile
内に書いておくことで,常に綺麗な状態で開発を始めることができて便利です。ターミナル$ curl -sS https://raw.githubusercontent.com/WebKit/webkit/master/Tools/Scripts/sort-Xcode-project-file \ > ./script.pl \ && perl ./script.pl `find . -maxdepth 1 -mindepth 1 -iname "*.xcodeproj"` \ && rm -f ./script.plおわりに
これらは別のリポジトリとして GitHub に公開した方が良いのでは?という声もありました (失念しましたが,実際別リポジトリからスクリプトを叩けるものを最近見かけましたね)。
一方で,ブランチを分けることによって考慮しなければならない (e.g. Xcode のバージョン更新時に一部スクリプトは利用できるが,これは巻き戻さないといけない) 等の問題が発生するためにやめました。Makefile およびスクリプトを一人で書いているので......Alfred を利用してあれこれする便利ですが,簡単な操作はターミナルで完結させるのが良いです。ターミナルって結構便利なんですよ,ということが少しでも伝われば幸いです。難しいところは強い人にやってもらって,その恩恵を慣れていない人にも受けられるようにする。みんながハッピーになるといいな。
VALU Advent Calendar 2019,明日は弊社 CTO @mito_memel さんのすてきなサーバーサイドのお話です。
References
- YutoMizutani/gohome - GitHub
- Golang を使うなら Makefile を恐れるな
- macOS用パッケージマネージャー — Homebrew
- CocoaPods
- rbenv/rbenv - GitHub
- おま環」とは?意味や使い方を元ネタを含めてご紹介 - コトバの意味辞典
- Carthage/Carthage - GitHub
- Alfred - Productivity App for macOS
- zsh-users/zsh-completions
- apple/swift/CMakeLists.txt - GitHub
- WebKit/webkit/Tools/Scripts/sort-Xcode-project-file
- 投稿日:2019-12-04T21:02:44+09:00
【Objective-c】AutoLayoutで位置を決める
UIButton、UILabel等はstorybord上で、位置を指定して配置できますが
コード上でもAutoLayoutを使用して位置を指定することができます。-(void) button_autolayout:(UIButton *)button top:(int)top left:(int)left right:(int)right bottom:(int)bottom { //AutoLayoutを使用することを指定 [button setTranslatesAutoresizingMaskIntoConstraints:NO]; // 上 位置指定 NSLayoutConstraint* topAnchor = [button.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:top]; // 左 位置指定 NSLayoutConstraint* leftAnchor = [button.leftAnchor constraintEqualToAnchor:self.view.leftAnchor constant:left]; // 右 位置指定 NSLayoutConstraint* rightAnchor = [button.rightAnchor constraintEqualToAnchor:self.view.rightAnchor constant:right]; // 下 位置指定 NSLayoutConstraint* bottomAnchor = [button.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-bottom]; //viewに追加 [self.view addConstraint:button_topAnchor]; [self.view addConstraint:button_leftAnchor]; [self.view addConstraint:button_rightAnchor]; [self.view addConstraint:button_bottomAnchor]; }例えば上記の関数をボタンごとに指定することで、以下のように配置できます。
//上 緑色 [self button_autolayout:top_button top:0 left:0 right:0 bottom:596]; //右 赤色 [self button_autolayout:right_button top:298 left:207 right:0 bottom:298]; //左 黄色 [self button_autolayout:left_button top:298 left:0 right:207 bottom:298]; //下 青色 [self button_autolayout:bottom_button top:596 left:0 right:0 bottom:0];参考
- 投稿日:2019-12-04T20:50:59+09:00
Xcode11.1のローカライズではめられた!
はじめに
Xcode10系から11系にあげてしばらくしてから下記のような画面を見つけた。
??日本語と英語混じってる
現象
なんか同じ画面なのに一部だけローカライズできていない。
理想は下記のような表示。
日本語版 英語版 Storyboardのローカライズがおかしい?ローカライズファイルは下記のようなもの
Main.strings(Japanese)/* Class = "UILabel"; text = "かすたむ"; ObjectID = "3Iv-wk-3sA"; */ "3Iv-wk-3sA.text" = "かすたむ"; /* Class = "UINavigationItem"; title = "テスト"; ObjectID = "7pt-6w-M5O"; */ "7pt-6w-M5O.title" = "テスト"; /* Class = "UILabel"; text = "でぃてぃーる"; ObjectID = "DV3-DC-Qdc"; */ "DV3-DC-Qdc.text" = "でぃてぃーる"; . . . /* Class = "UILabel"; text = "フッター"; ObjectID = "vyK-5m-pLY"; */ "vyK-5m-pLY.text" = "フッター";Main.strings(English)/* Class = "UILabel"; text = "かすたむ"; ObjectID = "3Iv-wk-3sA"; */ "3Iv-wk-3sA.text" = "Custom"; /* Class = "UINavigationItem"; title = "テスト"; ObjectID = "7pt-6w-M5O"; */ "7pt-6w-M5O.title" = "Test"; /* Class = "UILabel"; text = "でぃてぃーる"; ObjectID = "DV3-DC-Qdc"; */ "DV3-DC-Qdc.text" = "Detail"; . . . /* Class = "UILabel"; text = "フッター"; ObjectID = "vyK-5m-pLY"; */ "vyK-5m-pLY.text" = "Footer";なんか
static cell
の画面だけおかしい調査
パーツのObjectIDを確認
よくわからないけどなんかのタイミングで
ObjectID
変わったのかな?と思い確認してみました。とくに問題なし
ローカライズファイルを入れ直してみる
よくわからないけどとりあえずローカライズのチェックを入れ直してファイルを再生成してみました。
変化なし
Xcodeのリリースノートを確認
しばらく悩んでXcodeのリリースノートをみてみました。
発見
UITableViewCell labels in storyboards and XIB files do not use localized string values from the strings file at runtime. (52839404)
どうやら
UITableViewCell
のlabel
がローカライズできないようです。
TableView
のContent
をStatic Cells
にしてCell
のStyle
が下記いずれかの場合に起こるようです。
- Basic
- Right Detail
- Left Detail
- Subtitle
(最初のスクショは上から Basic, Right Detail, Left Detail, Subtitle, Custom のセルです)
対応
Xcodeのバグなのでリリースを待てばいいのですが、アプリの提出期限もあり下記のような暫定対応をしました。
1 TMPLocalizable.strings ファイルを作る
TMPLocalizable.strings(Japanese)"かすたむ" = "かすたむ"; "テスト" = "テスト"; "でぃてぃーる" = "でぃてぃーる"; . . . "フッター" = "フッター";TMPLocalizable.strings(English)"かすたむ" = "Custom"; "テスト" = "Test"; "でぃてぃーる" = "Detail"; . . . "フッター" = "Footer";2
ViewController
にローカライズ処理を書く// FIXME: Xcodeのバグがなおったら消したい override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = super.tableView(tableView, cellForRowAt: indexPath) if let text = cell.textLabel?.text { cell.textLabel?.text = tmpLocalizedString(text) } if let text = cell.detailTextLabel?.text { cell.detailTextLabel?.text = tmpLocalizedString(text) } return cell } private func tmpLocalizedString(_ key: String) -> String { return NSLocalizedString(key, tableName: "TMPLocalizable", comment: "") }これで表示は想定通りになります。
さいごに
上記のような対応で無事ローカライズ対応することができました!!!が、対応後すぐにバグが修正されたXcode11.2がリリースされました...
私はそっと
revert
しました
- 投稿日:2019-12-04T18:44:11+09:00
HealthKitで水泳の各ラップのストローク数が取りたい
はじめに
Apple Watchで水泳のワークアウトが測定できるの知ってました?
ワークアウトアプリにプールスイミングがあるので、これを実行しながらプールで泳げば、測定できます。
測定結果はApple WatchとペアリングしているiPhoneのアクティビティアプリで確認できます。
サンプルデータですが、このように表示されます。
ラップ数や泳いでいるときの泳法も測定できていて、すごい! てなるのですが、
このアプリでは各ラップのストローク数は確認できません。
HealthKitで取れるだろう、と思ってやってみました。アプリでHealthKitが使えるようにする
公式ドキュメントを参照して、手順通りに行います。
プロジェクトファイルとInfo.plistを編集します。水泳のワークアウトデータを取ってみる
サンプルコード
import HealthKit let healthStore = HKHealthStore() let readDataTypes: Set<HKObjectType> = [HKWorkoutType.workoutType()] healthStore.requestAuthorization( toShare: nil, read: readDataTypes, completion: {(success, error) in if success { let predicate = HKQuery.predicateForWorkouts(with: HKWorkoutActivityType.swimming) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) let sampleQuery = HKSampleQuery( sampleType: HKObjectType.workoutType(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor], resultsHandler: {(_, results, error) in if error == nil { let workouts = results as! [HKWorkout] if let workout = workouts.first { print(workout.workoutEvents) } } }) healthStore.execute(sampleQuery) } })このコードをざっと説明すると、
1. ヘルスケアデータからワークアウトを取る許可を取得する
2. swimmingのワークアウトを最新から順に取得する
3. 最新のワークアウトのworkoutEventsを出力する
になります。
print文で出力したworkoutEventsから、サンプルとして一つを抜き出してみるとHKWorkoutEventTypeLap, <_NSConcreteDateInterval: 0x28300eb40> (Start Date) 2019-10-24 10:56:05 +0000 + (Duration) 6.520985 seconds = (End Date) 2019-10-24 10:56:11 +0000 { HKSwimmingStrokeStyle = 5;水泳のワークアウトの1ラップのデータと読み取れますが、ストローク数と思われるデータはありません。
公式のドキュメントを参照すると分かるのですが、HKWorkoutEventクラスにはストローク数と思われるものはありません。
また、HKWorkoutクラスにtotalSwimmingStrokeCountというものはあるのですが、これはワークアウト単位のものであって、各ラップのストローク数ではありません。
別のアプローチが必要です。そこで、ヘルスケアデータには水泳のストローク数というタイプがあるので、そこから取得しました。
水泳のストローク数から取得する
let readDataTypes: Set<HKObjectType> = [HKWorkoutType.workoutType(), HKObjectType.quantityType(forIdentifier: .swimmingStrokeCount)!]に書き換えて、先ほど取得したworkoutEventsを使って
let type = HKObjectType.quantityType(forIdentifier: .swimmingStrokeCount)! for (index, workoutEvent) in workoutEvents.enumerated() { // workoutEventのtypeにはlap, segment, pauseなどがあります if workoutEvent.type == .lap { let predicate = HKQuery.predicateForSamples(withStart: workoutEvent.dateInterval.start, end: workoutEvent.dateInterval.end, options: .strictStartDate) let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: [.cumulativeSum]) { (_, statistic, error) in guard let statistic = statistic, error == nil else { return } let sum = statistic.sumQuantity()?.doubleValue(for: HKUnit.count()) ?? 0 print("stroke:\(sum) index:\(index)") } healthStore.execute(query) } }print文の箇所は非同期に実行されるため、インデックスも追加して、workoutEventsの何番目か分かるようにしました。
stroke:8.0 index:1 stroke:3.0 index:2 stroke:15.0 index:3 stroke:11.0 index:4 stroke:11.0 index:5 stroke:6.0 index:6 stroke:11.0 index:7 stroke:10.0 index:9 stroke:7.0 index:10 stroke:13.0 index:12 stroke:7.0 index:15 stroke:10.0 index:13 stroke:4.0 index:16 stroke:11.0 index:14出力はworkoutEventsの順番通りではありませんが、これでストローク数を取得できました。
HKStatisticsQueryの公式ドキュメント以外に参考になる事例を見つけられなかったので、
より良いやり方や、同じようなことを試みたことがあれば、コメント下さい。サンプルコードはこちらにも上げました。
ありがとうございました。
- 投稿日:2019-12-04T11:09:15+09:00
apple好きやiOSエンジニアが活用したいよく見る情報リソース
Androidも大切だが、iOSアプリなどは特にレギュレーションが変わったり仕様の変更が激しい。
そんな中で随時情報を追いかけるのは大切なことなので個人的に定期的に参考にしている情報リソースのメモです。
そしてapple好き・ガジェット好きとしての趣味も含まれる。そんな方にオススメ。iPhone Mania
その名の通りiPhoneマニア
apple関連のニュースや、レギュレーションの変更、リーク情報、iPhoneの便利機能など満載カミアプ
iPhoneの小技やニュースなど
gori.me
iPhone/iPad/Macbookの使い方や最新ニュースなど
Akiba PC Watch
秋葉原のショップでapple商品のオトクな入荷情報など
ガジェット好きにはおすすめITmedia News
appleに関わらずIT関連の時事ニュース満載
GIZMODE
ITmedia同様、appleに関わらずガジェットからITニュースまで幅広く
Appleが大好きなんだよ
youtubeでは個人的に一番好きかもしれない
- 投稿日:2019-12-04T10:38:58+09:00
[はじめてのiOSアプリ]xcodeで地図アプリを作成(その6)
はじめに
iOSアプリを作ってみたいけど
何から始めて良いのかわからないとりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思うという記事の6回目です。
今回は、拡大・縮小ボタンを追加します。
- ピンチイン・アウトあるから、拡大・縮小ボタンは不要では?
- Simulatorでピンチアウト(Ctrl+Option+Click)するのは直感的じゃなく忘れがちじゃない?
- 直感的なボタンがあれば、ピンチアウトの操作を忘れても影響ないから
- 「やってみました」記事だし、ボタンを配置してみたかったから
ボタンを追加
- ボタンを配置
- [Main storyboard]を表示する
- 【なぜ?】
- [MKMapView]の上にボタンを配置する前準備
- メニューから[View]-[Show Library]を選択、表示されるウインドウで [button] と入力し [Button]を絞り込む
![]()
- 【なぜ?】
- MapViewにボタンを配置したいから
- 絞り込まなくても、探し出すことができればOK
- [Button]をドラッグ&ドロップでボタンをMapViewに配置する(「拡大」「縮小」の2箇所)
- ボタンの文字[Button]を変更
- メニューから[View]-[Inspectors]-[Show Attribute Inspector]を選択
- [MapView]に表示されているボタンを選択し、[Attribute Inspector]に表示されている文字サイズを好きなサイズに変更
- サイズ変更に合わせ、ボタン位置も調整
ボタンの関連づけ
- (以前の記事にも書いたように)[ViewController.swift]と[Main storyboard]を同時に表示する
- 【なぜ?】
- [Main storyboard]と[ViewController.swift]が同時に表示されていると[ボタン]の関連づけが容易にできるため
- Ctrl+クリックで「+」ボタンのOutletを表示
[Touch Down]の右に表示されている○印を[ViewController.swift]にドラッグ&ドロップ
- ドロップ場所は、以下の場所
ViewController.swiftvar locationManager: CLLocationManager! // ここ付近にドラッグ&ドロップ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view.接続情報の入力を促すアイアログが表示されるので[Name]に[clickZoomin]と入力
同様に「ー」ボタンにも同じような操作([Name]は[clickZoomout]とする)を実施
- 【なぜ?】
- 「ー」ボタンでも同じような操作を行うため
- ViewController.swiftの関連する部分は、以下のようになっているはず
ViewController.swiftclass ViewController: UIViewController, CLLocationManagerDelegate { @IBOutlet var mapView: MKMapView! var locationManager: CLLocationManager! @IBAction func clickZoomin(_ sender: Any) { } @IBAction func clickZoomout(_ sender: Any) { } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view.ボタンに対応する処理を記述
ViewController.swiftの該当部分を以下のように修正
- 【なぜ?】
- 今回は、ボタンをタップしたことが判別できればOKだから
ViewController.swift@IBAction func clickZoomin(_ sender: Any) { print("[DBG]clickZoomin") } @IBAction func clickZoomout(_ sender: Any) { print("[DBG]clickZoomout") }テスト実行
- Simulatorを起動し、「+」ボタンや「ー」ボタンを押してみる
- ボタンを押すたびに、Xcodeのログに位置情報に混ざり[clickZoomin][clickZoomout]出力された
log[DBG]latitude : 37.33124551 [DBG]clickZoomout [DBG]longitude : -122.03073097 [DBG]latitude : 37.33121136 [DBG]clickZoomin [DBG]clickZoomout [DBG]longitude : -122.03072292 [DBG]latitude : 37.33117775 [DBG]clickZoomin [DBG]longitude : -122.03071071 [DBG]latitude : 37.33114614 [DBG]clickZoomout [DBG]longitude : -122.03069859今回の到達点
- 画面に「拡大」「縮小」ボタンを配置することができた
- ボタンをタップすると、デバッグメッセージが表示された
連載
- 投稿日:2019-12-04T09:22:58+09:00
【iPadOS13/Xcode11対応】画面が全画面で表示されなくなってしまったのでその対応を行った(Xcodeのキャプチャ付き)
はじめに
iOSアプリ(iPad前提)開発中に、iOS12からiPadOS13(Xcode10から11)に上げたことで、もともと全画面で表示されていた画面が、中央に小さく表示されるようになってしまいました。
本記事ではその時の対応方法(「segueの設定による対応」と「コードによる対応方法」の2種類)を記載します。
前提
- Xcode Version 11.2
- iPadOS13
対応前の画面表示
画面の遷移先がこのように中央に小さく表示されてしまいます。
対応方法
iOS12(Xcode10)の際にSegueで画面遷移を行っていた箇所は「対応方法1:segueの設定による対応」で、コードで画面遷移を行っていた箇所は「対応方法2:コードによる対応」で対応する必要があります。
対応方法1: segueの設定による対応
対応方法2: コードによる対応
画面遷移前に、modalPresentationStyleを.fullScreenに変更すれば、期待通り全画面で表示される。
let vc = UIViewController() // 下記を追加する vc.modalPresentationStyle = .fullScreen self.present(vc, animated: true, completion: nil)参考
私は、下記記事を参考に対応しました。原因なども下記記事に記載あります。
- 投稿日:2019-12-04T09:22:58+09:00
【Xcode11対応】画面が全画面で表示されなくなってしまったのでその対応を行った(Xcodeのキャプチャ付き)
はじめに
iOSアプリ開発中に、Xcodeをバージョン10から11に上げたことで、もともと全画面で表示されていた画面が、中央に小さく表示されるようになってしまいました。
本記事ではその時の対応方法(「segueの設定による対応」と「コードによる対応方法」の2種類)を記載します。
前提
- Xcode Version 11.2
対応前の画面表示
画面の遷移先がこのように中央に小さく表示されてしまいます。
対応方法
Xcode10の際にSegueで画面遷移を行っていた箇所は「対応方法1:segueの設定による対応」で、コードで画面遷移を行っていた箇所は「対応方法2:コードによる対応」で対応する必要があります。
対応方法1: segueの設定による対応
対応方法2: コードによる対応
画面遷移前に、modalPresentationStyleを.fullScreenに変更すれば、期待通り全画面で表示される。
let vc = UIViewController() // 下記を追加する vc.modalPresentationStyle = .fullScreen self.present(vc, animated: true, completion: nil)参考
私は、下記記事を参考に対応しました。原因なども下記記事に記載あります。
- 投稿日:2019-12-04T08:22:19+09:00
Appleの機械学習がヤバい
近年、機械学習はさまざまな分野で利用され注目を高めてきました。また、Firebaseをはじめてとした様々なクラウドサービスで手軽に利用できるほど便利なものになりました。そんな中、今年発表されたAppleの機械学習フレームワークCoreML3がすごかったため、今回CoreML周辺の情報を簡単にですがまとめてみました。
CoreMLとは?
Appleが2017年に発表した機械学習を扱うためのフレームワークであり、今年新たにCoreML3という名前で強力な機能の追加がなされた。機械学習モデルの仕様はオンデバイスで完結させることができ、さらに
CreateML
というFrameworkを使い機械学習モデルを超簡単に作成することが可能。モデルを作成するためのCreateMLアプリはこちら
下記の画像でCoreMLの上位レイヤー部分にあたるフレームワークがCoreML3にのFeatuesです。参照: https://developer.apple.com/documentation/coreml より
CoreMLを使用するメリットは?
CoreMLを使用するメリットは下記の通り大きく分けて4つあり、どれもオンデバイスで完結することによる効力を受けています。
Privacy
- ネットワークを介してデータのやりとりする必要がないため、個人情報を守ることができる
Speed
- デバイス上ですでにある学習モデルを使用するため、データ取得にかかる時間が超高速
No Server
- デバイス上で完結するためサーバが必要なくなります
Available
- トレーニング済みモデルをCoreMLに変換することも可能なため(keras -> CoreML)、自作Modelを有効的に使うことができる(https://developer.apple.com/jp/documentation/coreml/converting_trained_models_to_core_ml/)
CoreMLで何ができる?
実際にアプリにCoreMLが取り入れられているものを参考に見ていきましょう
これは、写真を撮るだけでブーツの種類を判別し、そのブーツを販売しているオンラインストアを探すことができるアプリですが、ネットワークを介していないため、普通にサーバを介してデータを取得する場合とでは処理の速さは一目瞭然です。一度試しに使って見るとわかるのですが、超高速です。
このアプリはバスケの練習を機械学習を用いることで、より効果的にトレーニングを行うことができるアプリです。
どこで機械学習が使われている?
- バスケのドリブルを練習したい場合、ボール・人物・人物のバランスを検知し、トレーニングしたいポイントに応じてバーチャルな目標物が画面に出力され。目標物をタッチすることでトレーニングできる仕組み
どう作られてる?
下記の項目が主に使われている機能です。ここではだいたい機能が
Visio
フレームワークを使うことで実現されています。このVisionフレームワークについては別のセクションでも説明します。
- ObjectDetection(物体検知) -> Vision-framework(https://developer.apple.com/documentation/vision/recognizing_objects_in_live_capture)
- ObjectTracking(物体追跡) -> Vision-framework(https://developer.apple.com/documentation/vision/tracking_multiple_objects_or_rectangles_in_video)
- PoseEstimation(姿勢検知) -> https://github.com/tucan9389/PoseEstimation-CoreML <- CoreMLを使った姿勢検知機能。Appleから提供されているModelは見つかりませんでした、、
- HitDetection(衝突検知) -> Vision-framework
CoreML-Framework
- Vision(https://developer.apple.com/documentation/vision)
- FaceDetection 顔をリアルタイムで認知し、トラッキングすることができる(https://developer.apple.com/documentation/vision/tracking_the_user_s_face_in_real_time)
- FaceLandmarks 顔の76pointの位置を検知することで、細かな表情の変化や瞳の向いている方向などを取得することができる(https://developer.apple.com/documentation/vision/vnfacelandmarks2d)
- ObjectTracking ビデオ内で複数のオブジェクトをトラッキングすることができる -> トラック競技などで選手などをそれぞれトラッキングすることなどが可能(https://developer.apple.com/documentation/vision/tracking_multiple_objects_or_rectangles_in_video)
- NaturalLanguage(https://developer.apple.com/documentation/naturallanguage)
- 言語判定
- MLTextClassifierを用いてカスタムモデルを作ることも可能
- textがポジティブな内容かネガティブな内容かなどの判断をすることができる(カスタムModelを作ればさらに細分化して感情を判断することができる)
- Word Embeddings ワードの類似語を取得するsuggestions機能を簡単に実装することができる(https://www.hackingwithswift.com/example-code/naturallanguage/how-to-find-similar-words-for-a-search-term)(https://developer.apple.com/documentation/naturallanguage/nlembedding)
- SoundAnalysis(https://developer.apple.com/documentation/soundanalysis)
- MLSoundClassifierを用いてカスタムモデルを作ることも可能
- 笑っている声や泣いている声を判断することができる
- 水の音、車の音、鳥の鳴き声など普段耳にしているような音も判別することができる
- 活用の例 ?
- カメラアプリなどで人物が笑っている時などに自動でシャッターを切ることができるなど
- CreateML(https://developer.apple.com/documentation/createml)
- Image, Sound, Textなど様々なデータを学習させModelを作成することのできるフレームワーク
- TableDataも学習させることができる※番外編で活用例紹介(https://developer.apple.com/documentation/createml/mldatatable)
- これ以外にも様々な機械学習用のFrameworkは数多くありますが、全て紹介することはできないため今回はこのぐらいで?♂️
Model Creation
- MLModel作成用のCreateML-Appで簡単に作ることができる(Xcodeでも可能)
Model生成の流れ
- DataCollection
- DataPreparation
- Training
- Testing
MLModelを作る上でポイント
Modelの精度の高まりを認知し、効果的にトレーニングするために、トレーニングの最初からテストを行って行くことが重要です。また、アプリ毎の用途に応じて必要なデータ量などは変わっていきますが
ImageClassification
の場合1クラス10枚から学習することが可能でしたので、手軽に使えそうです。そして、自分は機械学習についての知識は浅かったため知らなかったのですが、無視するべき関係のないデータ(ネガティブクラス)を含めることもModelを作成する上では重要みたいです。なぜ関係のないデータも必要かというと、例えば、りんご・いちごしか学習クラスがなかった場合、画像認識の際にどちらかのカテゴリーに結果が振り分けられますが、他のデータも学習させておくことで間違いの結果に当てはめてしまう確率を下げるということが狙いみたいですね。また、クラス(カテゴリー)のTrainingデータ量のバランスも大事で、できるだけバランスよく均等にデータ量を整えることが質の高いModelを作る上では重要です。最後に
今回はあまり触れていませんでしたが、個人的に
MLDataTable
の活用の幅が広そうだなと感じました。例えば、好みの条件に合わせた地域の家賃相場を算出するアプリなどで、地域
・間取り
・キッチンの広さ
などをそれぞれテーブルのカラムとし、それぞれの家賃を学習させることで、条件にあった地域の家賃がどのくらいなのかをネットワークを介さず高速に算出することができます。このようにCoreMLには色々な活用方法があり、まだまだ便利な機能があると思うので、何か面白そうな機能を見つけたらぜひ教えてください?
- 投稿日:2019-12-04T07:13:41+09:00
【XCUITest】UITextFieldの値の削除と入力(iPadOS13/Xcode11対応)
はじめに
iOSアプリ開発でXCUITestを実装しており、その際に、「UITextFieldの値を削除し、そこに新しい値を入力する」という処理が必要になりました。
stack overflowのこちらの記事( UI Test deleting text in text field )を参考に、XCUIElementのExtensionでclearAndEnterTextを作成することでiOS12(Xcode10)では期待通り動いていたのですが、iPadOS13(Xcode11)に上げてからUITextFieldの値が削除されなくなってしまったので、その対応を行いました。その対応方法を記載します。前提
- Xcode Version 11.2
- iPadOS13
iOS12(Xcode10)では期待通りに動いていたclearAndEnterText
UI Test deleting text in text field からclearAndEnterTextのコードを抜粋します。
UITextFieldをタップすることで、カーソル位置が入力されている値の最後尾に来るので、そこからdeleteStringを文字数分入力することで値を削除しています。extension XCUIElement { /** Removes any current text in the field before typing in the new value - Parameter text: the text to enter into the field */ func clearAndEnterText(text: String) { guard let stringValue = self.value as? String else { XCTFail("Tried to clear and enter text into a non string value") return } self.tap() let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) self.typeText(deleteString) self.typeText(text) } }iPadOS13(Xcode11)でUITextFieldの値が削除されなくなってしまった原因
Xcode11から、UITextFieldをタップするとカーソル位置が入力されている値の先頭に移動するようになり、deleteStringを入力しても1文字も消えなくなっていました。
iPadOS13(Xcode11)対応のために修正したclearAndEnterText
UITextFieldをダブルタップすることで、テキストを全選択し、deleteStringを1度入力することで値を削除するようにしてみました。
extension XCUIElement { /** Removes any current text in the field before typing in the new value - Parameter text: the text to enter into the field */ func clearAndEnterText(text: String) { // ダブルタップでテキストを全選択 self.doubleTap() // deleteは1回のみ let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: 1) self.typeText(deleteString) self.typeText(text) } }おわりに
他にも対応方法はあると思いますが、私のケースではこの方法で期待通りにテストできるようになったので、一旦これで良しとしています。
参考
- 投稿日:2019-12-04T07:13:41+09:00
【XCUITest】UITextFieldの値の削除と入力(Xcode11対応)
はじめに
iOSアプリ開発でXCUITestを実装しており、その際に、「UITextFieldの値を削除し、そこに新しい値を入力する」という処理が必要になりました。
stack overflowのこちらの記事( UI Test deleting text in text field )を参考に、XCUIElementのExtensionでclearAndEnterTextを作成することでXcode10では期待通り動いていたのですが、Xcode11に上げてからUITextFieldの値が削除されなくなってしまったので、その対応を行いました。その対応方法を記載します。前提
- Xcode Version 11.2
Xcode10では期待通りに動いていたclearAndEnterText
UI Test deleting text in text field からclearAndEnterTextのコードを抜粋します。
UITextFieldをタップすることで、カーソル位置が入力されている値の最後尾に来るので、そこからdeleteStringを文字数分入力することで値を削除しています。extension XCUIElement { /** Removes any current text in the field before typing in the new value - Parameter text: the text to enter into the field */ func clearAndEnterText(text: String) { guard let stringValue = self.value as? String else { XCTFail("Tried to clear and enter text into a non string value") return } self.tap() let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) self.typeText(deleteString) self.typeText(text) } }Xcode11でUITextFieldの値が削除されなくなってしまった原因
Xcode11から、UITextFieldをタップするとカーソル位置が入力されている値の先頭に移動するようになり、deleteStringを入力しても1文字も消えなくなっていました。
Xcode11対応のために修正したclearAndEnterText
UITextFieldをダブルタップすることで、テキストを全選択し、deleteStringを1度入力することで値を削除するようにしてみました。
extension XCUIElement { /** Removes any current text in the field before typing in the new value - Parameter text: the text to enter into the field */ func clearAndEnterText(text: String) { // ダブルタップでテキストを全選択 self.doubleTap() // deleteは1回のみ let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: 1) self.typeText(deleteString) self.typeText(text) } }おわりに
他にも対応方法はあると思いますが、私のケースではこの方法で期待通りにテストできるようになったので、一旦これで良しとしています。
参考
- 投稿日:2019-12-04T05:02:13+09:00
【iOSアプリ開発メモ】JSON Serverを使ってiOSアプリ開発する時のATS無効化手順(Xcodeのキャプチャ付き)
はじめに
まだ完成していないWebAPIを利用してiOSアプリ開発を行う必要があり、その際、ローカルPCにAPIモックサーバー(Node.jsのJSON Server)を立てて開発を進めました。
通常だとiOSアプリはATS(App Transport Security)によりHTTP通信が利用できないため、上記作業を行う際に「ATS無効化の設定」を行いました。本記事では、その時の手順をXcodeのキャプチャ付きでメモしておきます。前提
- Xcode Version 11.2
- iOSアプリからは「localhost」でAPIサーバーへアクセスする
- (下記のXcodeのキャプチャはSingle View Appでprojectを作成した直後の状態)
ATS無効化手順(1から8まで)
Allow Arbitrary Loadsを追加し、ValueはNOとする
※ 2で追加したApp Transport Security Settingsの子として追加する
Exception Domainを追加する
※ 2で追加したApp Transport Security Settingsの子として追加する(手順5も同様)
NSIncludesSubdomainsを追加し、ValueはYESとする
※ 5で追加したlocalhostの子として追加する(手順7, 8も同様)
※ TypeはBooleanにする(手順7, 8も同様)
NSTemporaryExceptionAllowsInsecureHTTPLoadsを追加し、ValueはYESとする
ここまでの設定を行えば、HTTP通信でlocalhostにアクセスできる
参考リンク
JSON Serverを立てる時や、ATS無効化手順については下記記事を参考にしました。
- 投稿日:2019-12-04T02:26:33+09:00
TabBarControllerをStoryboardReferenceを利用して導入してみた(Storyboardを分ける)
状況
Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
TabBarController
を使ったのですが
やり方が色々あってこんがらがったのと、いくつかつまづいたのでメモ。やりたいこと
TabBarController
をStoryboardReference
を利用して導入する
->StoryboardをTabBarController
,遷移先のVC
(Tabの数ぶんStoryboardを用意 or 遷移先を1つのStoryboardにまとめる)でそれぞれ分けたいのでStoryboardReference
を利用
->遷移先のVCにNavigationController
を継承させたい対策
TabBarController.storyboard
を新しく作る->TabBarController
を新しく作って、つながっている2つのVCを消しておくStoryboardReference
をTabの数だけ作る- 各々の
StoryboardReference
をTabと紐付けたい遷移先のVCと紐付ける- TabBarControllerの
is initial viewcontroller
にチェックを入れるTARGET
のMain interface
をTabBarControllerが存在するstoryboardに設定info.pist
のStoryboard Name
をTabBarControllerが存在するstoryboardに設定- 以上を行ってもTabが表示されない場合はcleanや再起動諸々を試す(info.plistとかをいじるとcleanとかで解決するパターンが結構あるらしい。僕自身もcleanして解決しました。)
![]()
※一番左のタブから順々に紐づいていく
- 遷移先のVCごとに
Storyboard
がある場合:
StoryboardReference
を選択しShow the Attributes inspector
->Storyboard reference
->Storyboard
にStoryboardの名前(ファイル名)を入力(Storyboard ID
ではない)
- 遷移先のVCを1つのStoryboard
にまとめている場合:
上記に加えて、VCをStoryboard IDから参照が必要->Show the Identity inspector
->Identity
->Storyboard ID
に入力
- control+右クリックで各々の
StoryboardReference
に繋ぐ->Relationship Segue
のview controllers
を選択 (※Modal
などではない)- 遷移先のVCで
Is Initial Viewcontroller
の設定を確認(NCが紐づいているならそちらに設定する)- 遷移先のVCで
Is Initial Viewcontroller
に設定したものにTabBar Item
を挿入以上。
参考
・[Xcode7] 「Unknown class xxxxx in Interface Builder file.」が出る - Qiita
- 投稿日:2019-12-04T01:48:06+09:00
【Swift】Arrayからユニークな順序付きリストへの変換を超絶シンプルに書く
SwiftでArrayから重複なしの順序を保ったままのリストを生成したい!
と思って調べてみると、この辺の記事が参考になりますが、reduce()を使って愚直に全てをなめていく実装が散見されます
今回はもっとスマートさを求めてシンプルに書けないか考えていきます
(よりシンプルに書くことに特化しているのでパフォーマンスは気にしてません)
Setは使える?
SwiftにはSetがあります。が、答えはノーです。
なぜなら、Setは重複しない順序を持たないリストだからです。
やってみます。let array: [String] = ["hoge", "fuga", "hoge", "piyo", "piyo"] // 最初に出てきた順でユニークになってほしい -> ["hoge", "fuga", "piyo"] print(Set(array)) // "["fuga", "piyo", "hoge"]\n" print(Set(array)) // "["piyo", "hoge", "fuga"]\n" print(Set(array)) // "["piyo", "hoge", "fuga"]\n"毎回結果が変わってしまいました。これでは順序を保つという条件が保証できません
順序付きのSetってないの?
Swift用はSet一択のようで、存在しないようです。
ですが、Foundation FrameworkにはNSOrderedSetという昔ながらのImmutableな順序付きSetのクラスが現在もサポートされています。NSOrderedSetを使って順序付きのユニークリストを作る
さっそく書いていきましょう。
import Foundation extension Array { func unique() -> [Self.Element] { return NSOrderedSet(array: self).array as! [Self.Element] } }こんな感じに書けるかと思います。
どうでしょう、めちゃくちゃシンプルですよね
NSOrderedSetに入れて吐き出すだけです!NSOrderedSetはObjective-C時代のものなので、インターフェースがNSArray時代のように
[Any]
となってしまいます。
そのため、入力時も[Any]
として扱われ、出力ももちろん[Any]
として返ってきてしまいます。そこで、SwiftのArrayのextensionとしてメソッドを用意し、Elementの型を入出力で縛ってあげることによって、使用する側は型安全でSwiftらしく書けるようにしています。
(as!
のような汚い実装はこうしてextensionの中に隠蔽してしまいましょう)
では、最後に結果を確認していきます。
let array: [String] = ["hoge", "fuga", "hoge", "piyo", "piyo"] print(array.unique()) // "["hoge", "fuga", "piyo"]\n" print(array.unique()) // "["hoge", "fuga", "piyo"]\n" print(array.unique()) // "["hoge", "fuga", "piyo"]\n"いかがでしょうか?
呼び出しもシンプルで今回の目的を達成することができました
こちらは順序をサポートしているので、何度実行しても結果は変わりません!P.S.
- Advent Calendarに登録しようと思っていたのにすっかり忘れてしまったので、普通の投稿です(笑)
- 投稿日:2019-12-04T01:02:32+09:00
RxSwift MVVM における ViewModel 設計
自分は RxSwift で ViewModel はどうやって設計していくか下記三つ分けて書いていこうと思います。
- Immutable
- テストのしやすさ
- プロトコルの読み取りやすさ
Immutable
- 可変を避けるため struct を使う。
- Observable, PublishSubject など値が流れ流けど不可変にする。
- Protocol には var で定義されるが、struct では let で宣告します。
テストのしやすさ
テストしやすく、 mock しやすくするため、 外部リソースはできれば全て DI できるようにします。
例えばデータ処理、API 叩きなどが書いてある struct や class らはコンストラクタの引数で渡します。
そして、 ViewModel 自身の中身に、全域的なオブジェクトやクラスを直接使用すること、できればしないようにします。プロトコルの読み取りやすさ
- インプットとアウトプットを分けて定義する
RxSwift コンポーネントの使い分け
- インプットは PublishSubject を使う
- アウトプットは Driver を使う
- 出口は UI なので Driver の方が良い
- Driver はコンストラクタで初期化する
実装例
import RxSwift import RxCocoa protocol MyViewModelInput { var titleChanged: PublishSubject<String?> { get } var contentChanged: PublishSubject<String?> { get } } protocol MyViewModelOutput { var titleLength: Driver<Int> { get } var contentLength: Driver<Int> { get } } typealias MyViewModelType = MyViewModelInput & MyViewModelOutput struct MyViewModel: MyViewModel { let titleChanged = PublishSubject<String?>() let contentChanged = PublishSubject<String?>() let titleLength: Driver<Int> let contentLength: Driver<Int> init() { titleLength = titleChanged .map { title in guard let title = title else { return 0 } return title.count } .asDriver { _ in .empty() } contentLength = contentChanged .map { content in guard let content = content else { return 0 } return content.count } .asDriver { _ in .empty() } } }ViewModel 以外は?
アウトプットがある場合、相手は UI 出なければ Observable や Single でしておいて大体問題ないと思います。
結
わかりにくい、説明不足ところや間違えたところあればぜひコメント欄にコメントしてください。