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

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 (全ユーザーの投稿情報が保存されているコレクション)

IMG_0144.jpg

実装方法

コレクションのデータです↓

postsコレクション

posts - id
     - title
     - content
     - reply_count

usersコレクション

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

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

Makefileを利用してiOS開発を賢く便利に運用しよう?

はじめに

VALU Advent Calendar 2019 4日目の記事です!
VALU 社として記事を書きたい! と名乗りを上げて2年目になりました。ことしもよろしくおねがいします!

今回は「Makefile を利用して iOS 開発を賢く運用しよう」ということで,Makefile の補完機能を用いまして,チームメンバーが快適に,かつ本質に集中できる環境を維持することのできる試みのご紹介です。

目次

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 のみで環境構築が完了します:sparkles::sparkles:

補完が効く

zsh-completions などの補完用プラグインを用いることで,どんなコマンドがあるかを実行前に確認することができます。
これが ShellScript だけでなく Makefile を利用する利点です。

screenshot 452.png

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
fi

iOS で実際に利用している Makefile の中身

Swift も LLVM を利用している言語ですが,Xcode によってコンパイル周りのオプションは肩代わりすることができています。
Swift 自体のビルドに必要なすごくながいコマンド とは異なり,とてもシンプルに記述することができます。
あくまで Makefile を使おうというものなので,おまじないや固有表現を避け,ShellScript が読める人なら内容が読めるような形に抑えています。
加えて,具体的なコマンドは環境に依存するパスおよび拡張子以外は別の ShellScript としてファイルを分けており,CI 時に必要なスクリプトは個々に実行できる環境を作り上げています。

こちらの Makefile は「人間がコマンドを覚えず,内容を知らなくとも実現したいことを実現する」という方針で設計しており,
利用者は「とりあえず $ make を打ってみて,補完からデプロイやビルドなどのしたいことを探す」
成長したい人は「Makefile を覗いてみて,必要に応じて質問や修正,追加を行う」
ことで業務の効率化を実現しています。

これにより,QA チームへの環境構築や作業依頼についても「$ make clean をしてみてください」と Slack や口頭で伝達することができるようになっています:tada:

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) を入れています。
発生し得るトラブルには事前に対応しましょう。
「ブランチを変えたら (ライブラリのバージョンが変わったために) ビルドできなくなった:cry:」という苦言に毎回対応するのも,そのためのドキュメントを保守するのもつらいです。

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 およびスクリプトを一人で書いているので......:innocent:

Alfred を利用してあれこれする便利ですが,簡単な操作はターミナルで完結させるのが良いです。ターミナルって結構便利なんですよ,ということが少しでも伝われば幸いです。難しいところは強い人にやってもらって,その恩恵を慣れていない人にも受けられるようにする。みんながハッピーになるといいな。

VALU Advent Calendar 2019,明日は弊社 CTO @mito_memel さんのすてきなサーバーサイドのお話です。

References

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

【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-03 0.04.13.png

参考

https://developer.apple.com/documentation/uikit/nslayoutanchor/1500937-constraintequaltoanchor?language=objc

https://qiita.com/edo_m18/items/5c224c823ca163629f54

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

Xcode11.1のローカライズではめられた!

はじめに

Xcode10系から11系にあげてしばらくしてから下記のような画面を見つけた。

en_bug

??日本語と英語混じってる:scream_cat:

現象

なんか同じ画面なのに一部だけローカライズできていない。

理想は下記のような表示。

日本語版 英語版
ja en

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 の画面だけおかしい:question:

調査

パーツのObjectIDを確認

よくわからないけどなんかのタイミングで ObjectID 変わったのかな?と思い確認してみました。

object_id

とくに問題なし:neutral_face:

ローカライズファイルを入れ直してみる

よくわからないけどとりあえずローカライズのチェックを入れ直してファイルを再生成してみました。

localize

変化なし:neutral_face:

Xcodeのリリースノートを確認

しばらく悩んでXcodeのリリースノートをみてみました。

発見:heart_eyes_cat:

UITableViewCell labels in storyboards and XIB files do not use localized string values from the strings file at runtime. (52839404)

どうやら UITableViewCelllabel がローカライズできないようです。
TableViewContentStatic Cells にして CellStyle が下記いずれかの場合に起こるようです。

  • 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 しました:see_no_evil:

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

HealthKitで水泳の各ラップのストローク数が取りたい

はじめに

Apple Watchで水泳のワークアウトが測定できるの知ってました?
IMG_4193.PNG

ワークアウトアプリにプールスイミングがあるので、これを実行しながらプールで泳げば、測定できます。
測定結果はApple WatchとペアリングしているiPhoneのアクティビティアプリで確認できます。
サンプルデータですが、このように表示されます。
IMG_4195.jpg

ラップ数や泳いでいるときの泳法も測定できていて、すごい! てなるのですが、
このアプリでは各ラップのストローク数は確認できません。
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の公式ドキュメント以外に参考になる事例を見つけられなかったので、
より良いやり方や、同じようなことを試みたことがあれば、コメント下さい。

サンプルコードはこちらにも上げました。

ありがとうございました。

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

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では個人的に一番好きかもしれない

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

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

はじめに

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

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

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

今回は、拡大・縮小ボタンを追加します。

  • ピンチイン・アウトあるから、拡大・縮小ボタンは不要では?
    • Simulatorでピンチアウト(Ctrl+Option+Click)するのは直感的じゃなく忘れがちじゃない?
    • 直感的なボタンがあれば、ピンチアウトの操作を忘れても影響ないから
    • 「やってみました」記事だし、ボタンを配置してみたかったから

ボタンを追加

  1. ボタンを配置
    • [Main storyboard]を表示する
      • 【なぜ?】
        • [MKMapView]の上にボタンを配置する前準備
    • メニューから[View]-[Show Library]を選択、表示されるウインドウで [button] と入力し [Button]を絞り込む
      LibButtonItem.png
      • 【なぜ?】
        • MapViewにボタンを配置したいから
        • 絞り込まなくても、探し出すことができればOK
    • [Button]をドラッグ&ドロップでボタンをMapViewに配置する(「拡大」「縮小」の2箇所)
      • 【なぜ?】
        • 「拡大」「縮小」のふたつでセットだから
        • 邪魔にならず、操作に影響しないような任意の場所に配置する
        • 今回は、画面に左上付近にした
          SetButton.png
    • ボタンの文字[Button]を変更
      • 【なぜ?】
        • 何をするボタンなのかを見ただけでわかるようにするため
        • 今回は「+」「ー」とした
          SetButtonRenamed.png
    • メニューから[View]-[Inspectors]-[Show Attribute Inspector]を選択
      • 【なぜ?】
        • ボタンの文字サイズを変更したいから
          ShowRightSide.png
    • [MapView]に表示されているボタンを選択し、[Attribute Inspector]に表示されている文字サイズを好きなサイズに変更
    • サイズ変更に合わせ、ボタン位置も調整
      • 【なぜ?】
        • デフォルトだと小さく見にくいから、見やすいように大きくする
        • ついでに、Boldにもした(お好きないように、ご自由に)
          ModButtonSize.png
  2. ボタンの関連づけ

    • (以前の記事にも書いたように)[ViewController.swift]と[Main storyboard]を同時に表示する
      • 【なぜ?】
        • [Main storyboard]と[ViewController.swift]が同時に表示されていると[ボタン]の関連づけが容易にできるため
    • Ctrl+クリックで「+」ボタンのOutletを表示
      • 【なぜ?】
        • ボタンのタップとプログラムを関連づけるため(前準備)
          ButtonOutlet.png
    • [Touch Down]の右に表示されている○印を[ViewController.swift]にドラッグ&ドロップ

      • ドロップ場所は、以下の場所
      ViewController.swift
      var locationManager: CLLocationManager!
      
      // ここ付近にドラッグ&ドロップ    
      override func viewDidLoad() {
          super.viewDidLoad()
          // Do any additional setup after loading the view.
      
      • 【なぜ?】
        • ボタンのタップとプログラムを関連づけるため
          EventDD.png
    • 接続情報の入力を促すアイアログが表示されるので[Name]に[clickZoomin]と入力

      • 【なぜ?】
        • ボタンをタップしたときに実行する処理の名称ををプログラムで記述するため
      • ここまでの処理で、「+」ボタンのタップとプログラムの関連づけが完了
        InputConnectionInfo.png
    • 同様に「ー」ボタンにも同じような操作([Name]は[clickZoomout]とする)を実施

      • 【なぜ?】
        • 「ー」ボタンでも同じような操作を行うため
        • ViewController.swiftの関連する部分は、以下のようになっているはず
    ViewController.swift
    class 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.
    
  3. ボタンに対応する処理を記述

    • ViewController.swiftの該当部分を以下のように修正

      • 【なぜ?】
        • 今回は、ボタンをタップしたことが判別できればOKだから
      ViewController.swift
       @IBAction func clickZoomin(_ sender: Any) {
           print("[DBG]clickZoomin")
       }
       @IBAction func clickZoomout(_ sender: Any) {
           print("[DBG]clickZoomout")
       }
      
  4. テスト実行

    • 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
    

今回の到達点

  • 画面に「拡大」「縮小」ボタンを配置することができた
  • ボタンをタップすると、デバッグメッセージが表示された

連載

  1. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1:プロジェクト作成)
  2. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2:地図表示)
  3. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3:位置情報取得)
  4. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その4:位置情報と連携した地図表示)
  5. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その5:アプリアイコン設定)
  6. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その6:拡大・縮小ボタン追加)
  7. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その7:地図を拡大・縮小)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iPadOS13/Xcode11対応】画面が全画面で表示されなくなってしまったのでその対応を行った(Xcodeのキャプチャ付き)

はじめに

iOSアプリ(iPad前提)開発中に、iOS12からiPadOS13(Xcode10から11)に上げたことで、もともと全画面で表示されていた画面が、中央に小さく表示されるようになってしまいました。

本記事ではその時の対応方法(「segueの設定による対応」と「コードによる対応方法」の2種類)を記載します。

前提

  • Xcode Version 11.2
  • iPadOS13

対応前の画面表示

画面の遷移先がこのように中央に小さく表示されてしまいます。

スクリーンショット 2019-12-04 8.51.49.png

対応方法

iOS12(Xcode10)の際にSegueで画面遷移を行っていた箇所は「対応方法1:segueの設定による対応」で、コードで画面遷移を行っていた箇所は「対応方法2:コードによる対応」で対応する必要があります。

対応方法1: segueの設定による対応

  1. 画面遷移を行っているsegueを選択
    スクリーンショット 2019-12-04 8.54.26.png

  2. Storyboard SegueのKindをPresent Modally に変更
    スクリーンショット 2019-12-04 9.07.04.png

  3. Storyboard SegueのPresentationをFull Screenに変更
    スクリーンショット 2019-12-04 9.05.47.png

この設定で期待通り遷移先が全画面表示になる
スクリーンショット 2019-12-04 9.08.03.png

対応方法2: コードによる対応

画面遷移前に、modalPresentationStyleを.fullScreenに変更すれば、期待通り全画面で表示される。

let vc = UIViewController()

// 下記を追加する
vc.modalPresentationStyle = .fullScreen

self.present(vc, animated: true, completion: nil)

参考

私は、下記記事を参考に対応しました。原因なども下記記事に記載あります。

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

【Xcode11対応】画面が全画面で表示されなくなってしまったのでその対応を行った(Xcodeのキャプチャ付き)

はじめに

iOSアプリ開発中に、Xcodeをバージョン10から11に上げたことで、もともと全画面で表示されていた画面が、中央に小さく表示されるようになってしまいました。

本記事ではその時の対応方法(「segueの設定による対応」と「コードによる対応方法」の2種類)を記載します。

前提

  • Xcode Version 11.2

対応前の画面表示

画面の遷移先がこのように中央に小さく表示されてしまいます。

スクリーンショット 2019-12-04 8.51.49.png

対応方法

Xcode10の際にSegueで画面遷移を行っていた箇所は「対応方法1:segueの設定による対応」で、コードで画面遷移を行っていた箇所は「対応方法2:コードによる対応」で対応する必要があります。

対応方法1: segueの設定による対応

  1. 画面遷移を行っているsegueを選択
    スクリーンショット 2019-12-04 8.54.26.png

  2. Storyboard SegueのKindをPresent Modally に変更
    スクリーンショット 2019-12-04 9.07.04.png

  3. Storyboard SegueのPresentationをFull Screenに変更
    スクリーンショット 2019-12-04 9.05.47.png

この設定で期待通り遷移先が全画面表示になる
スクリーンショット 2019-12-04 9.08.03.png

対応方法2: コードによる対応

画面遷移前に、modalPresentationStyleを.fullScreenに変更すれば、期待通り全画面で表示される。

let vc = UIViewController()

// 下記を追加する
vc.modalPresentationStyle = .fullScreen

self.present(vc, animated: true, completion: nil)

参考

私は、下記記事を参考に対応しました。原因なども下記記事に記載あります。

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

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で何ができる?

実際にアプリにCoreMLが取り入れられているものを参考に見ていきましょう

  • BootFinder-App

    これは、写真を撮るだけでブーツの種類を判別し、そのブーツを販売しているオンラインストアを探すことができるアプリですが、ネットワークを介していないため、普通にサーバを介してデータを取得する場合とでは処理の速さは一目瞭然です。一度試しに使って見るとわかるのですが、超高速です。
    ml-2.jpg

  • HomeCourt-App

このアプリはバスケの練習を機械学習を用いることで、より効果的にトレーニングを行うことができるアプリです。

どこで機械学習が使われている?
  • バスケのドリブルを練習したい場合、ボール・人物・人物のバランスを検知し、トレーニングしたいポイントに応じてバーチャルな目標物が画面に出力され。目標物をタッチすることでトレーニングできる仕組み
どう作られてる?

下記の項目が主に使われている機能です。ここではだいたい機能がVisioフレームワークを使うことで実現されています。このVisionフレームワークについては別のセクションでも説明します。

CoreML-Framework

Model Creation

  • MLModel作成用のCreateML-Appで簡単に作ることができる(Xcodeでも可能)

Model生成の流れ

  1. DataCollection
  2. DataPreparation
  3. Training
  4. Testing

MLModelを作る上でポイント

Modelの精度の高まりを認知し、効果的にトレーニングするために、トレーニングの最初からテストを行って行くことが重要です。また、アプリ毎の用途に応じて必要なデータ量などは変わっていきますがImageClassificationの場合1クラス10枚から学習することが可能でしたので、手軽に使えそうです。そして、自分は機械学習についての知識は浅かったため知らなかったのですが、無視するべき関係のないデータ(ネガティブクラス)を含めることもModelを作成する上では重要みたいです。なぜ関係のないデータも必要かというと、例えば、りんご・いちごしか学習クラスがなかった場合、画像認識の際にどちらかのカテゴリーに結果が振り分けられますが、他のデータも学習させておくことで間違いの結果に当てはめてしまう確率を下げるということが狙いみたいですね。また、クラス(カテゴリー)のTrainingデータ量のバランスも大事で、できるだけバランスよく均等にデータ量を整えることが質の高いModelを作る上では重要です。

最後に

今回はあまり触れていませんでしたが、個人的にMLDataTableの活用の幅が広そうだなと感じました。例えば、好みの条件に合わせた地域の家賃相場を算出するアプリなどで、地域間取りキッチンの広さなどをそれぞれテーブルのカラムとし、それぞれの家賃を学習させることで、条件にあった地域の家賃がどのくらいなのかをネットワークを介さず高速に算出することができます。このようにCoreMLには色々な活用方法があり、まだまだ便利な機能があると思うので、何か面白そうな機能を見つけたらぜひ教えてください?

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

【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)
    }
}

おわりに

他にも対応方法はあると思いますが、私のケースではこの方法で期待通りにテストできるようになったので、一旦これで良しとしています。

参考

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

【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)
    }
}

おわりに

他にも対応方法はあると思いますが、私のケースではこの方法で期待通りにテストできるようになったので、一旦これで良しとしています。

参考

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

【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まで)

  1. XcodeでInfo.plistを開く
    スクリーンショット 2019-12-04 4.08.49.png

  2. App Transport Security Settingsを追加する
    スクリーンショット 2019-12-04 4.10.50.png

  3. Allow Arbitrary Loadsを追加し、ValueはNOとする
    ※ 2で追加したApp Transport Security Settingsの子として追加する
    スクリーンショット 2019-12-04 4.32.10.png

  4. Exception Domainを追加する
    ※ 2で追加したApp Transport Security Settingsの子として追加する(手順5も同様)
    スクリーンショット 2019-12-04 4.37.41.png

  5. localhostを追加する
    ※ TypeはDictionaryにする
    スクリーンショット 2019-12-04 4.39.13.png

  6. NSIncludesSubdomainsを追加し、ValueはYESとする
    ※ 5で追加したlocalhostの子として追加する(手順7, 8も同様)
    ※ TypeはBooleanにする(手順7, 8も同様)
    スクリーンショット 2019-12-04 4.42.40.png

  7. NSTemporaryExceptionAllowsInsecureHTTPLoadsを追加し、ValueはYESとする
    スクリーンショット 2019-12-04 4.45.44.png

  8. NSTemporaryExceptionRequiresForwardSecrecyを追加し、ValueはNOとする
    スクリーンショット 2019-12-04 4.49.20.png

ここまでの設定を行えば、HTTP通信でlocalhostにアクセスできる

参考リンク

JSON Serverを立てる時や、ATS無効化手順については下記記事を参考にしました。

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

TabBarControllerをStoryboardReferenceを利用して導入してみた(Storyboardを分ける)

状況

Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
TabBarControllerを使ったのですが
やり方が色々あってこんがらがったのと、いくつかつまづいたのでメモ。

やりたいこと

TabBarControllerStoryboardReferenceを利用して導入する
->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にチェックを入れる
  • TARGETMain interfaceTabBarControllerが存在するstoryboardに設定
  • info.pistStoryboard NameTabBarControllerが存在するstoryboardに設定
  • 以上を行ってもTabが表示されない場合はcleanや再起動諸々を試す(info.plistとかをいじるとcleanとかで解決するパターンが結構あるらしい。僕自身もcleanして解決しました。) スクリーンショット 2019-12-16 0.07.06.png

※一番左のタブから順々に紐づいていく

- 遷移先のVCごとにStoryboardがある場合:
StoryboardReferenceを選択しShow the Attributes inspector->Storyboard reference->StoryboardStoryboardの名前(ファイル名)を入力(Storyboard IDではない)
- 遷移先のVCを1つのStoryboardにまとめている場合:
上記に加えて、VCをStoryboard IDから参照が必要->Show the Identity inspector->Identity->Storyboard IDに入力

  • control+右クリックで各々のStoryboardReferenceに繋ぐ->Relationship Segueview controllersを選択 (※Modalなどではない)
  • 遷移先のVCでIs Initial Viewcontrollerの設定を確認(NCが紐づいているならそちらに設定する)
  • 遷移先のVCでIs Initial Viewcontrollerに設定したものにTabBar Itemを挿入

以上。

参考

[Xcode7] 「Unknown class xxxxx in Interface Builder file.」が出る - Qiita

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

【Swift】Arrayからユニークな順序付きリストへの変換を超絶シンプルに書く

SwiftでArrayから重複なしの順序を保ったままのリストを生成したい!

と思って調べてみると、この辺の記事が参考になりますが、reduce()を使って愚直に全てをなめていく実装が散見されます:eyes:

今回はもっとスマートさを求めてシンプルに書けないか考えていきます:muscle:
(よりシンプルに書くことに特化しているのでパフォーマンスは気にしてません:yum:

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"

毎回結果が変わってしまいました。これでは順序を保つという条件が保証できません:sob:

順序付きの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]
    }
}

こんな感じに書けるかと思います。

どうでしょう、めちゃくちゃシンプルですよね:sparkles:
NSOrderedSetに入れて吐き出すだけです!

NSOrderedSetはObjective-C時代のものなので、インターフェースがNSArray時代のように[Any]となってしまいます。
そのため、入力時も[Any]として扱われ、出力ももちろん[Any]として返ってきてしまいます。

そこで、SwiftのArrayのextensionとしてメソッドを用意し、Elementの型を入出力で縛ってあげることによって、使用する側は型安全でSwiftらしく書けるようにしています。
as!のような汚い実装はこうしてextensionの中に隠蔽してしまいましょう:mask:

では、最後に結果を確認していきます。

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"

いかがでしょうか?
呼び出しもシンプルで今回の目的を達成することができました:clap:
こちらは順序をサポートしているので、何度実行しても結果は変わりません!

P.S.

  • Advent Calendarに登録しようと思っていたのにすっかり忘れてしまったので、普通の投稿です(笑)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 でしておいて大体問題ないと思います。

わかりにくい、説明不足ところや間違えたところあればぜひコメント欄にコメントしてください。

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