20200225のSwiftに関する記事は7件です。

RealmのschemaVersionの管理方法について考える

RealmのschemaVersion

Realmのドキュメントのマイグレーション項目で言及されている通り、Realmを扱う際にはRealm.Configurationの引数であるschemaVersionにモデルにアップデートがあった際に以前のバージョンよりも大きい値を設定する必要があります。なお、この値はマイグレーションなどの処理を行う際に必要となり、Realm内部でoldSchemaVersion(初期値0)よりも最新のschemaVersionが大きいか否かで自動的にマイグレーションされます。(データ移行の際はmigrationBlockで移行処理を記述する)

schemaVersionをどのように管理すべきか

方法1. モデルのアップデートを行う際に手元でschemaVersionをインクリメントする

個人的にオススメしません。複数人で開発していた際に同時にアップデートすることもあるかと思います。その際にコンフリクトしたり、配信されているとschemaVersionが正しくインクリメントされておらずクラッシュも起きうると考えられます。

方法2. ビルド番号を直接入れる

アプリバージョンに関わらず、ビルド番号をGitのコミット数などでインクリメントしている場合、ビルド番号をschemaVersionに入れることも方法の1つかと思います。この場合だと実装者に関わらず常にインクリメントされていくのでschemaVersionも自動で上がってくれます。この方法がお手軽ですが、モデルにアップデートがないのにも関わらずインクリメントされるのは違和感に感じます。また、ビルド番号をどのように管理しているかにも依存するので開発方針に左右されます。

以上の方法を踏まえて実現したい理想

  1. schemaVersionはモデルのアップデートが行われた際のみインクリメントしたい。
  2. 複数人開発においても正しく管理されるようにGitに依存した値を注入したい

モデルのディレクトリ以下のgit logからschemaVersionを設定する

最終的にfastlaneのlaneとして定義できるようにします。

Step1. 特定のディレクトリ以下のマージコミットを取得する

git log --oneline --merges --first-parent master -- DIR_PATH | wc -l

上記のコマンドでDIR_PATH上の変更に対するマージコミットのカウントが取れます。
DIR_PATHはモデルが存在するディレクトリです。最終的にはこの数を最新のschemaVersionとして設定します。

Step2. 現在のブランチの最新マージコミット(コミットハッシュ)を取得する

git log --oneline --merges --pretty=format:"%h" -1

取得したコミットハッシュをHASH_STEP2とします。

Step3. 現在のブランチの最新コミット(コミットハッシュ)を取得する

git log --oneline --pretty=format:"%h" -1

取得したコミットハッシュをHASH_STEP3とします。

Step4. 最新のマージコミットと現在のコミット間で特定ディレクトリ上の変更のコミット数を取得する

git log --oneline HASH_STEP2..HASH_STEP3 -- DIR_PATH | wc -l

これによってDIR_PATHにおいて現在、変更を行ったかが0かどうかで判断することができます。

Step5. Step1~4を用いてlaneを組む

  lane :bump_schema_version do
    latest_merges_commit_hash = sh(%Q[git log --oneline --merges --pretty=format:"%h" -1])
    latest_commit_hash = sh(%Q[git log --oneline --pretty=format:"%h" -1])
    latest_model_commit_count = sh(%Q[git log --oneline #{latest_merges_commit_hash}..#{latest_commit_hash} -- DIR_PATH | wc -l]).strip!.to_i
    latest_schema_version = sh("git log --oneline --merges --first-parent master -- DIR_PATH | wc -l").strip!.to_i

    if latest_model_commit_count > 0 
      latest_schema_version += 1
    end

    set_info_plist_value(path: PLIST_PATH, key: "schemaVersion", value: latest_schema_version)
  end

set_info_plist_valueでInfo.plist内のschemaVersionに書き込み、コードから取得することでRealm.Configurationに渡すことができます。

このlaneを実行することで現在のブランチで変更があるかを取得することができます。

latest_schema_versionをmasterへのマージコミット数としているのはCI上で実行したときにはモデルのディレクトリにおける変更のmasterマージコミット数schemaVersionとするためです。
まだマージしていないブランチ上で実行したときにはStep4で行っている現在変更しているかを加味してlatest_schema_versionがインクリメントされます。

前者と後者の値は一致するので「手元で実行してPR作成によってインクリメントする」or「CI上でフックして実行、PR作成する」などインクリメントする方法を選択することができます。

最後に

schemaVersionの管理方法をどのようにするかはプロジェクトによっても変わりますし、悩ましい部分だと思います。
他の方法もあればぜひ教えてください!

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

XcodeのXCUITestで実行時に引数を渡す

about

こちらの記事でUITest実行時以外の引数の渡し方について解説しているので
基本はこちらを参考にしてください。

Xcodeに引数を渡す LaunchArg & Env Variables

ここではUITest実行時の引数の受け取り方について触れます。

Bitriseの環境変数を渡す方法についても少し触れます。

環境

  • Xcode11.x
  • Swift4.x

設定方法

対象Scheme -> Edit Scheme -> Run -> Arguments

scheme_argv.png

それぞれチェックの入っているものをAppendしてみました。

2種類の入力が可能なので以下でみていきます。

  • Arguments Passed On Launch
  • Environment Variables

Arguments Passed On Launch

型:[String]

取得方法

class SampleTests: XCTestCase {

    override func setUp() {
        if ProcessInfo.processInfo.arguments.contains("isTestMode") {
            print("test mode enable")
        }
    }
}

注意点

似た要素にXCUIApplication().launchArgumentsがありますが、こちらはコード内で設定したい場合に利用すればOKだと思います。

class SampleTests: XCTestCase {

    let app = XCUIApplication()

    override func setUp() {
        app.launchArguments.append("isTestMode")

        if app.launchArguments.contains("isTestMode") {}
    }
}

Environment Variables

型:[String : String]

取得方法

スクショでの引数を受け取る方法は以下

class SampleTests: XCTestCase {

    override func setUp() {
        print(ProcessInfo.processInfo.environment["mode"]) //develop
    }
}

注意点

似た要素にXCUIApplication().launchEnvironmentがありますが、こちらはコード内で設定したい場合に利用すればOKだと思います。

class SampleTests: XCTestCase {

    let app = XCUIApplication()

    override func setUp() {
        app.launchEnvironment = ["mode": "develop"]
        switch app.launchEnvironment["mode"] {
            case "develop":
                print("develop env")
            default:
                print("default env")
        }
    }
}

Bitriseから環境変数を設定してコードから取得したい場合

Schemeファイルは以下のようなファイルになりますが、Bitriseのビルド時に書き換えるのはdiffを見れば分かりますが脆いのでplistを利用するのが良いと思います。
TargetProject.xcodeproj/xcshareddata/xcschemes/TargetUITests.xcscheme

そちらは別記事にて書きたいと思います。

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

Xcodeに引数を渡す LaunchArg & Env Variables

about

ネットには古いXcodeの情報しかなく、使い分けやコツをまとめた記事がなかったので記載します。

Xcodeでは2種類の方法で実行引数を受け取ることができます。

  • Arguments Passed On Launch
  • Environment Variables

Swiftのコード内で引数を使う方法を解説します。

環境

  • Xcode11.x
  • Swift4.x

設定方法

対象Scheme -> Edit Scheme -> Run -> Arguments

scheme_argv.png

それぞれスクショのようにAppendしてみます。

2種類の入力が可能なので以下でみていきます。

  • Arguments Passed On Launch
  • Environment Variables

Arguments Passed On Launch

型:[String]

※ KeyとValueを対で渡すことはできません

取得方法

let argv: [String] = ProcessInfo.processInfo.arguments

argv[0] : 実行ファイルのパスが入る (ex: /Users/user/Library/Developer/CoreSimulator/Devices/3418A817-3034-4DFC-A030-32C90755376A/data/Containers/Bundle/Application/D6D1533B-B307-4C1B-9DE9-1DDD6744B503/test_develop.app/test_develop)
argv[1] : 設定した環境変数
argv[2] : 設定した環境変数2個目
.
.
.

注意点

次で解説するEnvVarのようにkeyを設定できないので、引数が増えたとき番号がずれる可能性があります。

例として、環境変数にisTestModeを渡して以下のような実装をしている場合

if ProcessInfo.processInfo.arguments[1] == "isTestMode" {
    print("test mode running")
}

以下のようにcontainsで含まれているか確認するのが良いでしょう

if arguments.contains("isTestMode") {}

Environment Variables

型:[String : String]

Key(Name)とValueを対で設定することができます。

取得方法

スクショでは以下のようになります。

let value = ProcessInfo.processInfo.environment["mode"]
print(value) // develop

以下のようにswitch文で使うのが良いでしょう

switch ProcessInfo.processInfo.environment["mode"] {
    case "develop":
    case "production":
    defult:
}

おわり

XCTestでの利用については以下に記載しました!

XcodeのXCUITestで実行時に引数を渡す

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

PusherのChatKitを導入する

はじめに

チャットアプリを作ろうとすると、
Firebase Realtime Databaseというサービスを使うと素早く実装できるという記事がたくさんあるんで、
僕はPusherのChatKitというサービスを使ってチャットアプリを作ってみようかなと思います。
いつも通り元のDocはこちら

なぜPusher?

・ある程度なら無料で運用出来る
・有名どころが使用している
・自社サーバーとの連携が可能
主に上記の理由からPusherを選びました。

SDKのインストール

cocoapod

gem install cocoapods
target '<Your Target Name>' do
  pod 'PusherChatkit'
end
pod install

Carthage

$ brew update
$ brew install carthage
github "pusher/chatkit-swift"

公式はこちら

アカウント作成

こちらでアカウント作成
https://dashboard.pusher.com/accounts/sign_up

インスタンスを作成

スクリーンショット 2020-02-25 14.28.36.png

CredentialsでInstace Locatorを取得
スクリーンショット 2020-02-25 14.31.47.png

test用エンドポイントを取得
スクリーンショット 2020-02-25 14.31.56.png

認証用の設定

let provider = PCTokenProvider(url: "test用エンドポイント")
// ChatManagerの生成
let manager = ChatManager(instanceLocator: ChatKitInfo.instaceLocation,
            tokenProvider: provider,
            userID: )

Pusherとのコネクションを作る

self.manager.connect(delegate: ChatManagerDelegateImpl(delegate: self)) { (currentUser, error) in
            if let error = error {
                print("Error sending message: \(error.localizedDescription)")
                onError(error)
                return
            }

            self.currentUser = currentUser
            onSuccess()
        }

チャット部屋の購読

guard let currentUser = self.currentUser else {
    return
}

currentUser.subscribeToRoomMultipart(id: id, roomDelegate: self, messageLimit: 0) { error in
    if let error = error {
        print("Error subscribing to room: \(error.localizedDescription)")
        return
     }
    print("Successfully subscribed to the room! ?")
}

メッセージの送信

currentUser.sendSimpleMessage(roomID: room.id, text: message) { (messageId, error) in
            if let error = error {
                self.delegate?.onError(error: error)
                return
            }

            self.delegate?.onSuccessSendMessage(id: messageId)
        }

メッセージの受信

extension ChatKitManager: PCRoomDelegate {
    func onMultipartMessage(_ message: PCMultipartMessage) {}
}

これで基本的なチャットの機能が使えるようになります。

PCTokenProviderの生成

最後に認証用のプロバイダーですが、この記事ではテスト用のエンドポイントを使用しています。
しかし、実際に使用するときは自分でサーバーを構築して認証用のエンドポイントを作成する必要があります。
その場合の、PCTokenProviderの生成方法以下になります。

PCTokenProvider(url: endPoit,
                requestInjector: { request -> PCTokenProviderRequest in
                            // ヘッダー情報などを設定
                            request.addHeaders(headers)
                            return request
                }, retryStrategy: PCDefaultRetryStrategy(maxNumberOfAttempts: 6, maxTimeIntervalBetweenAttempts: 10))

Pusherではこれ以外にも既読を点けたり、過去のメッセージを取得したりも出来るのでチャットアプリを作ってみたい方は、Pusherというサービスもあるよとだけ覚えておいていただければと思います。

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

【Swift】stride()を使ってループを逆に回したり、増分を任意の値にしたりしよう

stride()についての覚書です。
なんとなく使っていたので、改めてちゃんと調べました。

ループを逆に回したい!!!!!!!

for i in stride(from: 10, to: 0, by: -1) {
    print("countdown:", i)
}

/* 出力
countdown: 10
countdown: 9
countdown: 8
countdown: 7
countdown: 6
countdown: 5
countdown: 4
countdown: 3
countdown: 2
countdown: 1 //0は出力しない
*/

後述しますが、stride(from:to:by:)だと終了条件に指定した数は含まれません。
stride(from:through:by:)を使うと、0まで出せます。

増分を任意の値にしたい

for i in stride(from: 0, to: 30, by: 3) {
    print("3の倍数のときだけ出力します……:", i)
}

/* 出力
3の倍数のときだけ出力します……: 0
3の倍数のときだけ出力します……: 3
3の倍数のときだけ出力します……: 6
3の倍数のときだけ出力します……: 9
3の倍数のときだけ出力します……: 12
3の倍数のときだけ出力します……: 15
3の倍数のときだけ出力します……: 18
3の倍数のときだけ出力します……: 21
3の倍数のときだけ出力します……: 24
3の倍数のときだけ出力します……: 27
*/

toとthroughの違い

stride()を使うと、StrideTo/StrideThroughという見慣れないクラスを返します。
イメージ的にはStrideToがS..<Eで、StrideThroughがS...Eです。
ただRangeオブジェクトと違って、Strideableなオブジェクトは増分情報を持っているので、クラスとして完全に別モノとして言語設計されているみたいです。

StrideTo/StrideThroughはそれぞれイニシャライザがなく、stride()メソッドを呼び出してインスタンス化するように、とのことです。

また一つSwiftでできることが増えました?
よかったですね。

公式

stride(from:to:by:)
stride(from:through:by:)

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

iOSアプリのアイコンをffmpegコマンドを使ってアルファチャンネルなしで各サイズを吐き出す

はじめに

iosアプリのアイコンでは異なるサイズの画像をたくさん作る必要があります。
でこれがめちゃくちゃめんどくさいので、そのためのツールが色々あるのでまずはこれらを試してみることをお勧めします。

一番簡単にWebアプリ上できるのがこちらで
appicon

Macのアプリでできるのがこちらです
Image2icon

遭遇した問題 〜 アルファチャンネルが追加されてしまう 〜

今回自分はillustratorでアイコンを描き画像としてでexportしました。
そして上記のツールでアイコンを作成して、App Store Connectにアップロードしようとすると、以下のようなエラーが出ました。

Error ITMS-90717: "Invalid App Store Icon. The App Store Icon in the asset catalog in 'YourApp.app' can't be transparent nor contain an alpha channel."

image.png

アルファチャンネルが含まれているから使えないよーとのことですね。
なるほど書き出したアイコンにアルファチャンネルが含まれているのねーということで確認してみると、含まれていません。

apole_pngの情報.png

色々試してみたのですが、どのツールを使っても書き出し後の画像にはアルファチャンネルが含まれてしまいました。
Icon-1024_pngの情報_と_ダウンロード.png

解決法 〜 ffmpegでリサイズする 〜

macのプレビューアプリで1つずつ、アルファチャンネルを除去することもできるのですが、そんなことはめんどくさすぎて絶対したくないので、ffmpegを使ったshell scriptを使います。

# Export ios app icons by ffmpeg scale command
# usage: sh export_ios_icons.sh {path_to_your_img}
# example: sh export_ios_icons.sh ./app_icon.png

# sizes of images
# you can get other size images by editing thisarray
size=(20 40 60 29 58 87 80 120 180 76 152 167 1024)
for i in "${size[@]}"
do
   : 
    ffmpeg -i $1 -vf scale=$i:$i output_$ix$i.png
done

gist

作りたい画像サイズを配列で保持しておいて、
ここでリサイズした画像を生成します。

ffmpeg -i $1 -vf scale=$i:$i output_$ix$i.png
# ex) ffmpeg -i $1 -vf scale=1024:1024 output_1024x1024.png

実行すると以下のように各画像サイズのアイコンが出力され、アルファチャンネルがない状態も見事キープされていますね!
ios_icon.png
output_1024_pngの情報.png

めでたしめでたし :confetti_ball:

(あとで気づいたがそもそもjpgでやれば、こんな問題全く起きなかったかも...)

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

【ARKit】配置した3Dモデル(アニメーションつき)を削除する

はじめに

別記事で書いたアニメーションつきの3DモデルをARKitに追加するのに引き続き、ARKItに追加した3Dモデル(アニメーションつき)を削除するときにも躓いたので、そのメモ。

以下のサイトを参考に、長押しで3Dモデルを消そうとしたが削除できませんでした。このサイトではプリミティブなオブジェクト(cube)を配置しているので、今回使う3Dモデル(アニメーションつき)と何が違うのか?

実行環境

  • Xcode 11.2.1

3DモデルはMagicaVoxelで作って、Mixamo でアニメーションをつけたものを使用しました。

.scnファイルの構造

.scnファイル(sitting.scn)を開いてみると、直下に「sitting」ノードがあって、その中に3Dモデルとボーンの設定が含まれています。
cap01.png

アニメーション付き3Dモデルの配置時

配置するスクリプトは、直下の「sitting」のノードを取得して、self.mainSceneView.scene.rootNode に追加するようになっていますが、追加するノードに名前がないため、名前をつける処理(node.name = selectedItem)を追加しました。

※削除する際に、どのノードか判断するためです。

アニメーション付き3Dモデルの配置
    var selectedItem: String? = "sitting"

    //(中略)

    /// アイテム配置メソッド
    func addItem(hitTestResult: ARHitTestResult) {
        if let selectedItem = self.selectedItem {

            // .scnファイルから新しい3Dモデルのノードを作成
            let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn")
            let node = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))!

            // 現実世界の座標を取得
            let transform = hitTestResult.worldTransform
            let thirdColumn = transform.columns.3

            // 3Dモデルの配置
            node.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z)

            // 3Dモデルのサイズを変更
            node.scale = SCNVector3(0.05, 0.05, 0.05)

            // 3Dモデルに名前をつける
            node.name = selectedItem

            // シーンに追加
            self.mainSceneView.scene.rootNode.addChildNode(node)
        }
    }

アニメーション付き3Dモデルの削除

.scnファイルの構造で見たように、配置したモデルは「sitting」というノード名で、その中に3Dモデル(unamed)とボーン(mixamorig_Hips)が含まれています。

長押しでのオブジェクトが存在するかどうかの判定は、ARSCNView.hitTest(_:types:)で行いますが、このときの検出対象は3Dモデル(unamed)になるため、ノード全体を削除するには親である「sitting」ノードを削除する必要があります。

削除の処理は以下のようになります。

アニメーション付き3Dモデルの削除
    // ロングプレスイベントハンドラの登録
     let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressView))
     self.mainSceneView.addGestureRecognizer(longPressGesture)

    //(中略)

    // 長押しでキャラクタを削除する
    @objc func longPressView(sender: UILongPressGestureRecognizer) {
        print("----長押し!")

        if sender.state == .began {
            let location = sender.location(in: self.mainSceneView)
            let hitTest  = self.mainSceneView.hitTest(location)

            if let result = hitTest.first  {

                // 3Dアニメーションモデルは、複数パーツで構成されるため、親ノードの名前で判定・削除する
                if result.node.parent!.name == selectedItem
                {
                    result.node.parent!.removeFromParentNode();
                }
            }
        }
    }

まとめ

ノードを扱う際には、常に階層構造を意識しないとダメってことですね(わかってみれば当然ですが)。

他にもっといい方法があれば、教えてください。

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