20201207のiOSに関する記事は10件です。

VTCompressionSessionEncodeFrame deadlock現象

現象

動画のsample bufferを独自作成されたqueueでエンコードしてますが、たまにencoderリセットしたあとエンコード走るとVTCompressionSessionEncodeFrame
でdeadlockになって、statusも返ってこないまあまあ死んでしまう

code

-(void)append:(CMSampleBufferRef)sb
{
    CFRetain(sb);
    dispatch_async(queue, ^{
         .....

         // ここでdead lock,statusも返ってこない
         auto s = VTCompressionSessionEncodeFrame(mImpl.mSession, cr, CMSampleBufferGetPresentationTimeStamp(sb), CMSampleBufferGetDuration(sb),    nullptr, nullptr, &flags);
      if (s) {
         NSLog(@"encode status %d", (int)s);
      }
        // samplebuffer解放
        CFRelease(sb);
    });
}

原因

encoder session解放処理をエンコード処理と同じqueueで処理してなかったから。

修正

-(void)invalidateSession
{
    dispatch_async(queue, ^{
     if (mSession) {
            VTCompressionSessionInvalidate(mSession);
        }
   });
}


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

SwiftLintのAnalyzeを使って高度な解析をする方法

はじめに

本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の8日目の記事です。
SwiftLintのAnalyze機能を紹介します。

環境

  • OS:macOS Big Sur 11.0.1
  • Swift:5.3.1
  • Xcode:12.2 (12B45b)
  • SwiftLint:0.41.0

本記事で説明しないこと

SwiftLintの「Analyze」とは?

かんたんにいうと「ビルドログのASTを使って解析する」機能です。

Analyzeは実験的な機能であり、いつでも変更される可能性があるとのことです。

Analyzeの使い方

Analyzeの使い方を紹介します。

設定ファイルの作成

Analyze用のルールを設定ファイルの analyzer_rules に記述します。

.swiftlint.yml
analyzer_rules:
  #- explicit_self # 関数は `self.` を付けずに呼び出したいため
  - unused_declaration
  - unused_import

公式ドキュメントで「Analyzer rule: Yes」となっているルールが対象で、私が見た限りでは以下の3つのみでした。

ルール 説明 参考リンク
Explicit Self self. を明示的に書くべき https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed#explicit-self
Unused Declaration 宣言した変数やクラスなどは使われるべき https://qiita.com/uhooi/items/8e9767c2e746f4171ded#unused-declaration
Unused Import インポートされたモジュールは使われるべき https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed#unused-import

私は同一クラス内の関数を self. なしで呼び出したいため、下の2つのみ有効にしています。

Analyzeの実行

READMEに記載されている通り xcodebuild コマンドでログをファイルに出力し、それを --compiler-log-path オプションで渡します。

コマンドが長くなるので、私は Makefile に定義して make analyze で実行できるようにしました。
make build-debug はCIでビルドが通るかの確認にも使っているため、 swiftlint analyze に対して必要最小限にはなっていません。

Makefile
PRODUCT_NAME := UhooiPicBook # 製品名を適宜変更する
PROJECT_NAME := ${PRODUCT_NAME}.xcodeproj
SCHEME_NAME := ${PRODUCT_NAME}

TEST_SDK := iphonesimulator
TEST_CONFIGURATION := Debug
TEST_PLATFORM := iOS Simulator
TEST_DEVICE ?= iPhone 12 Pro Max
TEST_OS ?= 14.2
TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}'

XCODEBUILD_BUILD_LOG_NAME := xcodebuild_build.log

.PHONY: analyze
analyze: # Analyze with SwiftLint
    $(MAKE) build-debug
    mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./${XCODEBUILD_BUILD_LOG_NAME}

.PHONY: build-debug
build-debug: # Xcode build for debug
    set -o pipefail \
&& xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-project ${PROJECT_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
build \
| tee ./${XCODEBUILD_BUILD_LOG_NAME} \
| bundle exec xcpretty --color

--autocorrect オプションを付けると自動で修正されるのでオススメです。

make analyze を実行します。

$ make analyze
# ...
# `make build-debug` のログは省略
# ...
mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./xcodebuild_build.log
Loading configuration from '.swiftlint.yml'
Correcting Swift files at paths 
Collecting 'Debug.swift' (1/23)
Collecting 'ActivityRouter.swift' (2/23)
# ...
Correcting 'MonsterListInteractor.swift' (15/23)
/Users/uhooi/Documents/Repos/GitHub/uhooi/UhooiPicBook/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift:10:1 Corrected Unused Import
Correcting 'MonsterListViewController.swift' (16/23)
# ...
Correcting 'MonsterDetailViewController.swift' (22/23)
Correcting 'SceneDelegate.swift' (23/23)
Done correcting 23 files!

.../SpotlightRepository.swift:10:1 Corrected Unused Import のログからわかる通り、 SpotlightRepository.swift の10行1列目に未使用のモジュールがインポートされているので自動で削除されました。

git diff で確認します。

$ git diff
diff --git a/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift b/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift
index e435a9b..ba45ffb 100644
--- a/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift
+++ b/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift
@@ -7,7 +7,6 @@

 import CoreGraphics.CGGeometry
 import CoreSpotlight
-import MobileCoreServices

 /// @mockable
 protocol SpotlightRepository: AnyObject { // swiftlint:disable:this file_types_order

確かにインポート文が削除されています。

今回は一度実行済みなので1箇所のみ削除されましたが、初回は大量の import Foundationimport UIKit が削除されて気持ちいいです。

おまけ①: 使用しているインポートが削除される場合がある

先ほど削除された import MobileCoreServices ですが、実はないとビルドエラーになります。

理由としては、iOS 13以前で kUTTypeData の呼び出しに使っているためです。

SpotlightRepository.swift
    private func createAttributeSet(title: String, contentDescription: String, thumbnailData: Data?) -> CSSearchableItemAttributeSet {
        let attributeSet: CSSearchableItemAttributeSet
        if #available(iOS 14.0, *) {
            attributeSet = .init(contentType: .data)
        } else {
            attributeSet = .init(itemContentType: kUTTypeData as String) // !!!: ここでビルドエラーになる
        }
        // ...
        // 中略
        // ...
        return attributeSet
    }

xcodebuild-destination オプションでiOS 14.2を指定しているためだと思われます。
仕方ないので手動でインポート文を戻しました。

このようなケースはまれですが、自動修正する場合はコミット前にビルドが通るか確認しましょう。

おまけ②: Analyzeの実行タイミング

READMEに記載されている通り、Analyzeには時間がかかります。
そのため、Xcodeのビルドフェイズで毎回実行するのを避けています。

現在は手動実行しているのですが、それだと漏れる可能性があるため、いい方法を考え中です。

おわりに

これで未使用のインポート文を大量に削除できます!

以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の8日目の記事でした。
明日はまだ埋まっていません。ぜひ こちら から参加しましょう!
@Sho-heikun さんが参加してくださりました!
ありがとうございます :relaxed:

参考リンク

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

macOS/iOS Keychain と Security framework のCLIで証明書を管理しよう

はじめに

macOS/iOS アプリを開発していると、定期的に証明書の更新が必要になりますね。

証明書を作った後に、手元の開発マシンに証明書を登録したり、CIとして運用されているビルドマシンに証明書を登録したり、といったように証明書を登録するところはいくつかあり、それぞれに対して手作業のメンテナンスが発生することがあります。

運用形態に合わせてできるところから自動化して管理していこうと、まずは macOS で証明書の操作やキーチェーンの管理などのセキュリティフレームワークをコマンドラインから実行して試してみました。

具体的には /usr/bin/security コマンドについて試しています。

macOS Catalina Version 10.15.7 の環境で試しています。

コマンド操作してみよう

まずは man コマンドを使ってできることを見てみます。

$ man security

security(1)               BSD General Commands Manual              security(1)

NAME
     security -- Command line interface to keychains and Security framework

SYNOPSIS
     security [-hilqv] [-p prompt] [command] [command_options] [command_args]

DESCRIPTION
     A simple command line interface which lets you administer keychains, manipulate keys and certificates, and do just about anything the Security framework is
     capable of from the command line.

     By default security will execute the command supplied and report if anything went wrong.

     If the -i or -p options are provided, security will enter interactive mode and allow the user to enter multiple commands on stdin.  When EOF is read from
     stdin security will exit.

     Here is a complete list of the options available:

     -h       If no arguments are specified, show a list of all commands.  If arguments are provided, show usage for each the specified commands.  This option is
              essentially the same as the help command.

     -i       Run security in interactive mode.  A prompt (security> by default) will be displayed and the user will be able to type commands on stdin until an
              EOF is encountered.

     -l       Before security exits, run
                    /usr/bin/leaks -nocontext
              on itself to see if the command(s) you executed had any leaks.

     -p prompt
              This option implies the -i option but changes the default prompt to the argument specified instead.

     -q       Will make security less verbose.

     -v       Will make security more verbose.

SECURITY COMMAND SUMMARY
     security provides a rich variety of commands (command in the SYNOPSIS), each of which often has a wealth of options, to allow access to the broad functional-
     ity provided by the Security framework.  However, you don't have to master every detail for security to be useful to you.

     Here are brief descriptions of all the security commands:

     help                        Show all commands, or show usage for a command.
     list-keychains              Display or manipulate the keychain search list.
     default-keychain            Display or set the default keychain.
     login-keychain              Display or set the login keychain.
     create-keychain             Create keychains.
     delete-keychain             Delete keychains and remove them from the search list.
     lock-keychain               Lock the specified keychain.
     unlock-keychain             Unlock the specified keychain.
     set-keychain-settings       Set settings for a keychain.
     set-keychain-password       Set password for a keychain.
     show-keychain-info          Show the settings for keychain.
     dump-keychain               Dump the contents of one or more keychains.
     create-keypair              Create an asymmetric key pair.
     add-generic-password        Add a generic password item.
     add-internet-password       Add an internet password item.
     add-certificates            Add certificates to a keychain.
     find-generic-password       Find a generic password item.
     delete-generic-password     Delete a generic password item.
     set-generic-password-partition-list
                                 Set the partition list of a generic password item.
     find-internet-password      Find an internet password item.
     delete-internet-password    Delete an internet password item.
     set-internet-password-partition-list
                                 Set the partition list of a internet password item.
     find-key                    Find keys in the keychain
     set-key-partition-list      Set the partition list of a key.
     find-certificate            Find a certificate item.
     find-identity               Find an identity (certificate + private key).
     delete-certificate          Delete a certificate from a keychain.
     delete-identity             Delete a certificate and its private key from a keychain.
     set-identity-preference     Set the preferred identity to use for a service.
     get-identity-preference     Get the preferred identity to use for a service.
     create-db                   Create a db using the DL.
     export                      Export items from a keychain.
     import                      Import items into a keychain.
     cms                         Encode or decode CMS messages.
     install-mds                 Install (or re-install) the MDS database.
     add-trusted-cert            Add trusted certificate(s).
     remove-trusted-cert         Remove trusted certificate(s).
     dump-trust-settings         Display contents of trust settings.
     user-trust-settings-enable  Display or manipulate user-level trust settings.
     trust-settings-export       Export trust settings.
     trust-settings-import       Import trust settings.
     verify-cert                 Verify certificate(s).
     authorize                   Perform authorization operations.
     authorizationdb             Make changes to the authorization policy database.
     execute-with-privileges     Execute tool with privileges.
     leaks                       Run /usr/bin/leaks on this process.
     smartcards                  Enable, disable or list disabled smartcard tokens.
     list-smartcards             Display available smartcards.
     export-smartcard            Export/display items from a smartcard.
     error                       Display a descriptive message for the given error code(s).

上記は一部ですが、できることがたくさんあります。今回はその中の一部ですが、試したことを順を追って書いてみます。

キーチェーンのサーチリストを表示する

$ security list-keychain
"/Users/user/Library/Keychains/login.keychain-db"
"/Library/Keychains/System.keychain"

デフォルトキーチェーンを表示する

$ security default-keychain
"/Users/user/Library/Keychains/login.keychain-db"

ログインキーチェーンを表示する

$ security login-keychain
"/Users/user/Library/Keychains/login.keychain-db"

キーチェーンを作成する

今回は test.keychain キーチェーンを作成してみることにします。

$ security create-keychain -p password test.keychain

上記の作成コマンドを実行しただけだとサーチリストに表示されません。

$ security list-keychain
"/Users/user/Library/Keychains/login.keychain-db"
"/Library/Keychains/System.keychain"

lsコマンドでファイルが作成されていることが見えます。

$ ls -l /Users/user/Library/Keychains/

キーチェーンのサーチリストに追加します。
これを実行するとGUIのKeychain access にも表示されるようになりました。

$ security list-keychains -d user -s login.keychain-db test.keychain-db

キーチェーンのサーチリストを見ると、上記の追加コマンドで引数に指定した順で表示されていました。

$ security list-keychain
"/Users/user/Library/Keychains/login.keychain-db"
"/Users/user/Library/Keychains/test.keychain-db"
"/Library/Keychains/System.keychain"

キーチェーンの中身を表示する

$ security dump-keychain test.keychain-db

キーチェーンに証明書をインポートする

アプリのビルドに使うためにコードサインを指定して証明書をインポートします。

$ security unlock -p password "/Users/user/Library/Keychains/test.keychain-db"
$ security import Certificates.p12 -k "/Users/user/Library/Keychains/test.keychain-db" -P "" -T /usr/bin/codesign
1 identity imported.
$ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password "/Users/user/Library/Keychains/test.keychain-db"

キーチェーンから証明書をエクスポートする

初回の一回だけ許可するポップアップダイアログで許可する必要がありました。キーチェーン単位で1回ポップアップダイアログが表示されるようでした。2回目以降はコマンドだけで実行できました。

$ security unlock-keychain -p password "/Users/user/Library/Keychains/test.keychain-db"
$ security export -k "/Users/user/Library/Keychains/test.keychain-db" -f pkcs12 -P "" -o ./mycerts.p12

キーチェーンから証明書を削除する

common name を指定して削除する場合は、

$ security unlock -p password "/Users/user/Library/Keychains/test.keychain-db"
$ security delete-certificate -c name "/Users/user/Library/Keychains/test.keychain-db"

または、SHA-256 または SHA-1 ハッシュ値を指定して削除する場合は、

$ security unlock -p password "/Users/user/Library/Keychains/test.keychain-db"
$ security delete-certificate -Z SHA-1 "/Users/user/Library/Keychains/test.keychain-db"

このハッシュ値は、コマンド操作で取得する方法は見つけることができず、GUI の Keychain Access から証明書の詳細を見ると Fingerprints に表示されている値で実行できました。

キーチェーンを削除する

$ security delete-keychain "/Users/user/Library/Keychains/test.keychain-db"

おわりに

OSのバージョンが上がったりで見直しは定期的に必要になりますが、煩雑な作業をコマンドで処理することで運用形態に合わせた運用フローの自動化が進められると思います。

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

条件分岐 ? :  の使い方

はじめに

この書き方を見た時に、こんな書き方もあるんや!って思ったが、
この書き方を調べてもわからなかった(?:と調べてもわからなかった)ので、自分なりに一旦まとめていこうと思います。
色々わかり次第、情報を追加していこうと思います。

使われ方

条件式 ? 式か値① : 式か値②
説明すると、条件式がtrueなら?の後ろの①、falseなら:の後の②を実行すると言うもの。

試してみた

switchを用意してisOnがtrueの時とfalseの時を比較して見た。

let switch1 = UISwitch()
let switch2 = UISwitch()

switch1.isOn = true
let number1 = 1 * (switch1.isOn ? 1 : -1)   //1 * 1が実行され、number1 = 1

switch2.isOn = false
let number2 = 1 * (switch2.isOn ? 1 : -1)   //1 * -1が実行され、number2 = -1

このように、trueの時に?の後が実行され、falseの時には:の後の処理が行われる。

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

react nativeでのIDFA対応(AdjustSDK利用)

※まだリリースまでこじつけてないので、問題はあるかも・・・

背景

年明けにAppleさんが広告ID利用の許諾確認必須にするらしいので、その対応を実施したので、そのときAdjustSDKのバージョンアップ対応時のメモ(微妙にgithubの説明がわかりづらかったので

前提

react native: 0.61.4
yarn : 1.21.1
react-native-adjustを既に導入済みでアップグレード対応

対応概要

  1. package.jsonの更新とライブラリのnpmのパッケージの更新
  2. podsの該当ライブラリの更新
  3. Xcodeプロジェクトに必要ライブラリの追加
  4. 許諾ポップアップを表示するコードの追加
  5. Info.plistに許諾用文言の追加

1.package.jsonの更新とライブラリのnpmのパッケージの更新

プロジェクトのpackage.jsonの該当ライブラリのバージョンをIDFAの対応が入っている4.23.0以上にする

package.json
“react-native-adjust”: “^4.23.1",

package.json更新後以下コマンドを実行

yarn upgrade react-native-adjust

2.podsの該当ライブラリの更新

iOSプロジェクトのディレクトリで以下コマンドを実行

pod update react-native-adjust

3.Xcodeプロジェクトに必要ライブラリの追加

  1. Xcodeで該当プロジェクトを開く
  2. サイドメニューのshow the Project navigatorを開いて、プロジェクトファイルを選択する
  3. メイン部分でTARGETSを選択して、Build Phasesを選択する
  4. Link Binary With Librariesを開いて、ボタンを押下して以下のライブラリを追加する。追加後追加したすべてのライブラリのStatusRequiredからOptionalに変更する
    • StoreKit.framework
    • AppTrackingTransparency.framework
    • CoreTelephony.framework
    • iAd.framework
    • AdSupport.framework

4.許諾ポップアップを表示するコードの追加

この辺は公式の記述通りにすんなり。以下コードを自分はトップの画面のcomponentDidMountに記述しました。

Adjust.requestTrackingAuthorizationWithCompletionHandler(function(status) {
    switch (status) {
        case 0:
            // ATTrackingManagerAuthorizationStatusNotDetermined case
            break;
        case 1:
            // ATTrackingManagerAuthorizationStatusRestricted case
            break;
        case 2:
            // ATTrackingManagerAuthorizationStatusDenied case
            break;
        case 3:
            // ATTrackingManagerAuthorizationStatusAuthorized case
            break;
    }
});

5.Info.plistに許諾用文言の追加

ルート直下でNSUserTrackingUsageDescriptionの項目を追加して、Valueには許諾のポップアップに表示したい、文言を記述する。以上でbuildすればiOS14以降で広告ID取得許諾のポップアップが表示されるかと思います。

参考

https://github.com/adjust/react_native_sdk#att-framework

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

SwiftのKeyPathと戯れる

KeyPathとは?


KeyPath とは?

Swift4で追加された、プロパティに動的にアクセスするための記法

Use key-path expressions to access properties dynamically.1


Swift3.xまでの #keyPath

class Hoge: NSObject { // ?
    @objc var foo: String // ?
    let bar: String

    init(foo: String, bar: String) {
        self.foo = foo
        self.bar = bar
    }
}

let foo = hoge.value(forKeyPath: #keyPath(Hoge.foo))
print(type(of: foo)) // Optional<Any>

print(#keyPath(hoge.bar)) // error: argument of '#keyPath' refers to non-'@objc' property 'bar'

Obj-C時代からあるKVOでのtypoを防ぐ程度の役割しかない

  • NSObject にしか使えない
  • @objc 宣言されたプロパティのみ
  • 返却値はAny! (型情報が消える?)
  • 解析が遅い、Darwinでしか使えない2といった問題点も

Swift4からの KeyPath

  • Swiftの強力な型システムに基づいた機構
    • structにもenumにも使える
    • 型情報が維持される
    • Swiftのstdlibに含まれているのでプラットフォームに依存しない

NSObject@objc宣言もいらない

class Hoge {
    var foo: String
    let bar: String
}
let hogeFoo: ReferenceWritableKeyPath<Hoge, String> = \.foo
let hogeBar = \Hoge.bar // KeyPath<Hoge, String>

let hoge = Hoge(foo: "foo", bar: "bar")
print(hoge.foo) // foo
hoge[keyPath: hogeFoo] = "hoge-foo"
print(hoge.foo) // hoge-foo
hoge[keyPath: hogeBar] = "bar" // error: cannot assign through subscript: 'hogeBar' is a read-only key path

structにも使える

struct Fuga {
    var foo: String
    let bar: String
}
let fugaFoo: WritableKeyPath<Fuga, String> = \.foo
let fugaBar = \Fuga.bar // KeyPath<Fuga, String>

var fuga = Fuga(foo: "foo", bar: "bar")
print(fuga.foo) // foo
fuga[keyPath: fugaFoo] = "fuga-foo"
print(fuga.foo) // fuga-foo
fuga[keyPath: fugaBar] = "bar" // error: cannot assign through subscript: 'fugaBar' is a read-only key path

KeyPath関連クラス

_AppendKeyPath
└ AnyKeyPath
  └ PartialKeyPath<Root>
    └ KeyPath<Root, Value>
      └ WritableKeyPath<Root, Value> // Value is variable
        └ ReferenceWritableKeyPath<Root, Value> // Root is class

小ネタ


TupleもRootになれる

let hogeFuga = (hoge, fuga)

let hogeKeyPath = \(Hoge, Fuga).0 // no label
let fugaKeyPath = \(hoge: Hoge, fuga: Fuga).fuga // with labels

hogeFuga[keyPath: hogeKeyPath] // hoge
hogeFuga[keyPath: fugaKeyPath] // fuga

プロパティのネストも可能

struct FooBar {
    var hoge: Hoge
    var fuga: Fuga
}

let fooBarHogeFoo = (\FooBar.hoge).appending(path: \Hoge.foo)
\FooBar.hoge.foo == fooBarHogeFoo // true

print(fooBar.hoge.foo) // hoge-foo
fooBar[keyPath: fooBarHogeFoo] = "foo-bar-hoge-foo"
print(fooBar.hoge.foo) // foo-bar-hoge-foo
fooBar[keyPath: \.hoge.foo] = "foo"
print(fooBar.hoge.foo) // foo

let fooBarFugaFoo = (\FooBar.fuga).appending(path: \Fuga.foo)
fooBar[keyPath: fooBarFugaFoo] = "foo-bar-fuga-foo" // error: cannot assign through subscript: 'fooBar' is a 'let' constant

KeyPathの実用例


プロパティでソートする3

public extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }

    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T?>) -> [Element] {
        sorted {
            guard let l = $0[keyPath: keyPath],
                let r = $1[keyPath: keyPath] else { return false }
            return l < r
        }
    }

    func min<T: Comparable>(by keyPath: KeyPath<Element, T>) -> Element? {
        self.min { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }
}

struct Person {
    var id: Int
    var name: String
    var age: Int
}

extension Person: CustomStringConvertible {
    var description: String { name }
}

let people: [Person] = [
    .init(id: 3, name: "Bob", age: 28),
    .init(id: 1, name: "Emma", age: 40),
    .init(id: 4, name: "Amelia", age: 18),
    .init(id: 2, name: "George", age: 22),
]

// Before
people.sorted(by: { $0.id < $1.id })      // ["Emma", "George", "Bob", "Amelia"]
people.sorted(by: { $0.name < $1.name })  // ["Amelia", "Bob", "Emma", "George"]

// After
people.sorted(by: \.id)   // ["Emma", "George", "Bob", "Amelia"]
people.sorted(by: \.name) // ["Amelia", "Bob", "Emma", "George"]

同じAnchor同士へのConstraintを貼る

public extension UIView {
    func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>,
                                                   to target: UIView) -> NSLayoutConstraint {
        self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor])
    }

    func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>,
                                                   to target: UIView,
                                                   constant: CGFloat) -> NSLayoutConstraint {
        self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor], constant: constant)
    }
}

// Before
NSLayoutConstraint.activate([
    view.topAnchor.constraint(equalTo: target.topAnchor),
    view.leadingAnchor.constraint(equalTo: target.leadingAnchor),
    view.trailingAnchor.constraint(equalTo: target.trailingAnchor),
    view.bottomAnchor.constraint(equalTo: target.bottomAnchor)
])

// After
NSLayoutConstraint.activate([
    view.equal(\.topAnchor, to: target),
    view.equal(\.bottomAnchor, to: target),
    view.equal(\.leadingAnchor, to: target),
    view.equal(\.trailingAnchor, to: target)
])


更に関数チックに書いた例
https://www.objc.io/blog/2018/10/30/auto-layout-with-key-paths/


Key Path Expressions as Functions


Key Path Expressions as Functions

  • Swift5.2で追加(SE-0249)
  • KeyPath 表現をクロージャを引数とする関数に渡すことができる
    • コレクション操作などが簡潔に書けるように
  • Xcode12.xならコード補完が効く

struct Department {
    var name: String
    var member: [Person]
}

extension Department {
    var boss: Person {
        member.min(by: \.id)!
    }
}

let departments: [Department] = [
    .init(name: "General Affairs Department",
          member: [
            .init(id: 3, name: "Bob", age: 28),
            .init(id: 1, name: "Emma", age: 40)
    ]),
    .init(name: "Development Department",
          member: [
            .init(id: 4, name: "Amelia", age: 18),
            .init(id: 2, name: "George", age: 22)
    ])
]

Before

departments.flatMap { $0.member }
    .map { $0.name } // ["Bob", "Emma", "Amelia", "George"]
departments.map { $0.boss }
    .map { $0.name } // ["Emma", "George"]
departments.map { $0.boss.name }

After

departments.flatMap(\.member)
    .map(\.name) // ["Bob", "Emma", "Amelia", "George"]
departments.flatMap(\.member.name) // Value of type '[Person]' has no member 'name'
departments.map(\.boss)
    .map(\.name) // ["Emma", "George"]
departments.map(\.boss.name) // ["Emma", "George"]

Performance4

image.png

extension Person {
    var canDrink: Bool { age >= 20 }
}

注意

変数/定数化したKeyPathを渡すことはできない(2020/12/9現在)

let bossKeyPath = \Department.boss
let nameKeyPath = \Person.name
let bossNameKeyPath = bossKeyPath.appending(path: nameKeyPath)

departments.map(bossKeyPath) // Cannot convert value of type 'KeyPath<Department, Person>' to expected argument type '(Department) throws -> T'
    .map(nameKeyPath) // Cannot convert value of type 'WritableKeyPath<Person, String>' to expected argument type '(T) throws -> T'

departments.map(bossNameKeyPath) // Cannot convert value of type 'KeyPath<Department, String>' to expected argument type '(Department) throws -> T'

最後に

  • KeyPathによってSwiftコードがより簡潔に書きやすくなった
  • RxSwift / Combine のストリーム変換にも使えるのでより宣言的に書ける
  • Genericsと併用することで処理の汎用化も大幅に進みそう
  • Sample in Xcode Playground
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] Delegateが分からない初心者のための最後の受け皿

今回作るもの

黄色のボタンを押すと画面が遷移され黄色くなる実装をdelegateを使って実装&説明します。

image.png image.png

きっかけ

Swiftのdelegateの説明をQiitaでたくさん読みましたがいまいち理解できず、なんとなく実装していました。delegateで詰まることが多かったので、アウトプットの機会にしようと思います。

対象

私のように他の記事を読んでも理解に苦しんだ方々に読んで頂きたいです。

Delegateとは

Delegateは委譲という意味ですが、そのクラスではできないことを他のクラスでやってもらうため委譲が必要になるのです。
詳しくコードと共に見ていきましょう。

実際のコード

3ファイル作成しました。
1つめははじめの画面のViewController
2つめは黄色画面遷移先のYellowViewController
3つめはViewControllerのCellのクラス(ButtonTableViewCell)

まず1つ目のViewControllerです。

スクリーンショット 0002-12-07 12.38.21.png
ここでは一枚目の黄色ボタンのViewの部分を作っています

次に黄色画面遷移先のYellowViewControllerです

スクリーンショット 0002-12-07 13.06.19.png
黄色の画面にするViewの部分を作っています

最後に先ほど作った黄色ボタンのCellのクラスです

スクリーンショット 0002-12-07 13.04.26.png
ここではcellの中のボタンタップ時の処理を行えます。
「ここに処理を書きたい」の部分に

let yellowViewController = YellowViewController()
navigationController?.pushViewController(yellowViewController, animated: true)

を入れて黄色画面に飛ばしてやりたいですが、CellではNavigationControllerが使用できないので、他の画面で処理をする必要があるのです。そこでdelegateという考え方が必要になります。

Delegateの使い方

1, protocolを宣言
2, delegateを宣言
3, 発火させたい場所にdelegate設置
4, 受け渡し先のクラスで継承
5, 受け渡し先のViewにdelegateを呼び出す
6, 処理を実装する

具体的な実装方法

スクリーンショット 0002-12-07 13.44.04.png
1, まずprotocolを発火させたい(ここでは@IBActionのあるButtonTableViewCell)クラスで宣言します。

protocol [delegate名]: class {
   func [メソッド名]
}

2, delegateを発火させたい(ここでは@IBActionのあるButtonTableViewCell)クラスで宣言します
3, 実際に発火させたい箇所(ここでは@IBActionの中)でdelegateを設置
4, ViewControllerに戻って、delegate名を継承する
5, ViewControllerでcellを使える場所で、delegateを呼び出す
6, そのままでは実際にしたい処理が書かれていないので、protocolで宣言したmethodをViewController内で書きます

おまけ (元々あるDelegate)

ViewControllerにも元々

buttonTableView.delegate = self
buttonTableView.dataSource = self

と書かれていますが、これも同様に

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,

UITableViewDelegate, UITableViewDataSourceを継承しているからです。
その証拠に、
スクリーンショット 0002-12-07 13.57.30.png
UITableViewDelegateを右クリックして見てみましょう。
スクリーンショット 0002-12-07 13.59.19.png
protocolが宣言されている訳です。
ViewControllerでデフォルトでtableViewが使えていたのも理由があったのですね。

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    }

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    }

最後に

最後まで読んでいただきありがとうございます。理解していただけたしょうか?
delegateはswiftを学習し始めで必ずと言っていいほど、詰まる箇所だと思います。
とても複雑に見えていましたが、根本を理解してみると簡単なものでした。

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

iosショートカットの凄技でこんなこともできます

みなさんショートカット使ってますか?

https://support.apple.com/ja-jp/guide/shortcuts/welcome/ios
https://qiita.com/tags/shortcuts

もともとは Workflow というサードパーティアプリだったのですが、 Apple に買収され、iOS に組み込まれてさらにパワフルなツールになりました。

最近だと、ホーム画面のアプリ起動アイコンをカスタマイズする方法としてニュースに取り上げられたりしています。

さて、そんなショートカット機能、ぱっと見はブロックプログラミングみたいな、最近流行りのノーコードみたいな感じで、「コードを書ける俺には必要ないぜ」と見くびって深堀りしていない諸氏も多いことでしょう。

さて本日は、プログラミング大好きな皆さんを刺激する技をご紹介します。

「WebページでJavaScriptを実行」アクション

4A014759-3DB2-4430-A8D4-E092FBD405D0.png

まずはこちら。御覧の通り、JavaScriptのコードを実行できそうな気配満々のアクションです。アプリ不要で、標準でついてます。
Safariで表示中のページの Content Scope に JavaScript を埋め込んで実行できる、というもので、一昔前に流行した bookmarklet のように使うことができます。
戻り値を返すことができ、それを次のアクションへつなげるので、サイト上から要素を拾ってリマインダーに登録、といったようなことも簡単にできます。戻り値は completion() の呼び出しで返すことになっているので、非同期処理の結果を取得することもできます。

ただし、スクリプトの実行には「Safariウェブページ」というオブジェクトが必要です。
ショートカット起動からあらかじめ指定したURLにアクセスして云々みたいな使い方は出来ず、iOS の safari ブラウザのシェアシートからショートカットを起動することになります。 (筆者は発見できませんでしたが、何か裏技をご存知の方はぜひコメントください!)

便利だけど、期待していたのとちょっと違う、と思った方は次節へどうぞ。

「Scriptable」アプリ

https://scriptable.app/
72112C9C-9C98-4426-88B3-4AC17EBBFE8B.png
無料アプリです。インストールすると「ショートカット」アプリから Scriptable アクションを呼び出して任意のJavaScriptを実行でき、戻り値をショートカットアプリに返すこともできます。
上のスクショは、 Google Cloud のインスタンスを起動、停止するために私が書いたものです。こんな感じで Web API を自在に呼び出すことができます。 (gist に置いておきました)

なぜわざわざこんなことを?という方に念のため説明しますと、ショートカットに「マシン起動」のような名前をつけて保存しておくと、 Apple Watch の竜頭を押しながら「マシン起動」というだけで GCP インスタンスを立ち上げることができるのです。戦隊モノ気分です。便利です。

独自のAPIを持っており、 iOS の標準カレンダーやリマインダーへのアクセスもできるので、ショートカットアプリでぽちぽちするのが大変なループ処理や if 分岐をコードで書けます。
iCloud ドライブにそのままテキストファイルで保存されているので、 Mac や PC で iCloud 接続すれば、使いなれたエディタとキーボードでコード編集することができます。

iOS14 の Widget を出力する機能もあり、任意の WebAPI の実行結果をウィジェットとしてホーム画面に表示しておくこともできてしまいます。

「Pushcut」アプリ

https://www.pushcut.io/
D4E771B2-7C21-4112-9860-2DA519DB6B37.png

こちらも無料アプリです。自作の Web アプリや Slack bot などから手元の iPhone へ通知を送ることができ、それをトリガーにして任意のショートカットを iOS 上で実行できます。
簡単な使い方としては、 Mac/PC で開いているページのURLを iPhone へ送ってすぐに開く、だとか、画像のURLを送るとそのURLの画像をiPhone のクリップボードに入れる、のようなことが実現できます。

まとめ

ショートカットアプリの持つ強力な連携パワーをさらに引き出す技をいくつかご紹介しました。関連する連携アプリやテクをご存じの方、ぜひコメントください。

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

[Swift] SkyWayを使ったWebRTCの映像をFirebase MLに接続して顔認識させる

前書き

とあるハッカソンで使った技術で、実装するにあたり壁にぶつかりました。
おそらく前例がないので、備忘録として残します。

技術に関して

1. SkyWayとは

スクリーンショット 2020-12-07 3.46.42.png

ほとんど画像に記載がありますが、Webでリアルタイムコミュニケーションを実現する標準技術「WebRTC(Web Real Time Communication)」を簡単にアプリに導入できるSDK&APIです。(公式引用

2. Firebase MLとは

maxresdefault.jpg

ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。(公式引用)

今回はこの部分の中身についての実装は話しません。
あくまでもSkyWayとFirebaseを繋ぐ部分のみになります。
(中身の参考資料「Firebase MLKitを使って動画の顔認識をさせる」)

3. 構成図

アプリ間で動画のリアルタイム通信を行いながら、その映像をFirebase MLで判定させるということ行います。

スクリーンショット 2020-12-07 3.44.07.png

SkyWayが2つ導入されていますが、

- 上部はSkyWayサーバー
- 右下アプリ内はSkyWayのSDK

になります。

実装

1.前提

Firebase ML側でデータを分析するためには、動画をCMSampleBufferという型で受け取る必要があります

幸いなことにiOSでは映像からCMSampleBuffer型を受け取るために、
AVCaptureVideoDataOutputSampleBufferDelegate
というものが用意されています。

Example

final class ExampleVideoManager {

    private var videoOutput: AVCaptureVideoDataOutput!
    private let queue = DispatchQueue(label: "videoOutput", attributes: .concurrent)

    init() {
        self.videoOutput = AVCaptureVideoDataOutput()
        self.videoOutput.setSampleBufferDelegate(self, queue: queue) // デリゲードのセット
    }
}

extension CaptureVideoManager: AVCaptureVideoDataOutputSampleBufferDelegate {

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // CMSampleBuffer型の値が受け取れる場所。Firebase MLへ接続。
    }
}

2.問題点

通常、カメラや動画を実装するためにはAVKitをimportとして色々と実装していく必要があります。
(実装説明は割愛するので参考として「Swiftでカメラアプリを作成する」)

前提のように自前でAVCaptureVideoDataOutputを生成してFirebase ML接続できれば良いですが、SkyWayの場合は指定された実装方法で行う必要があるため、わからないようになっています。

具体的には、SKWVideoという指定されたUI型に、勝手にレンダリングされるようになっていることが問題になります。

Example

@IBOutlet weak var streamView: SKWVideo! // レンダリングする画面

func setuo() {
     /* ~略~ */

    let constraints = SKWMediaConstraints()
    localStream = SKWNavigator.getUserMedia(constraints)
    localStream?.addVideoRenderer(self.remoteStreamView, track: 0) // レンダリングの指定

     /* ~略~ */
}       

3.解決法

中で隠蔽化されていますが、必ずAVCaptureVideoDataOutputは実装されているので、それをSKWVideoの中から自力で探し当てました。

具体的には、レンダリングされる画面のヒエラルキーをちまちまとデバッグしてAVCaptureVideoDataOutputを持つレイヤー層を探り当てました。

イメージ図
0v2Bh.png

探り当てた部分をコードで取り出す場合は、以下のようになります。

// レンダリング画面
@IBOutlet weak var streamView: SKWVideo!

// レンダリングしている画面の階層を深掘りし、中から`layer`層を取り出す
var videoLayer: AVCaptureVideoPreviewLayer? {
    return streamView.subviews.first?.layer as? AVCaptureVideoPreviewLayer
}

// 上記の`layer`層から変換に必要な`AVCaptureVideoDataOutput`を取り出す
var output: AVCaptureVideoDataOutput? {
    return videoLayer?.session?.outputs.first as? AVCaptureVideoDataOutput
}

上記からAVCaptureVideoDataOutputにアクセスできるので、outputsetSampleBufferDelegateセットしてあげることで、CMSampleBuffer型の値を手に入れることができるようになります。

これでFirebase MLへの接続も可能となりました。

あとがき

複数のストリームで受け取って加工する、など色々試した結果、この方法が最適なかつ実現可能な方法でした。

少しマニアックなネタだったかもしれませんが、誰かの参考になれば嬉しい所存です。

文献

SkyWayの実装周りは解説しなかったので、以下を参考にすると良いでしょう。
- SkyWay - アプリやWebサービスに、ビデオ・音声通話をかんたん導入
- iOSのCallKitフレームワークとSkyWay
- SKYWAY SDKを使ったIOS版通話アプリ開発

Firebase周りはこちらをご覧ください、
- Firebase MLKitを使って動画の顔認識をさせる

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

[Swift] SkyWayを使ったWebRTCの映像をFirebase MLに接続する

前書き

とあるハッカソンで使った技術で、実装するにあたり壁にぶつかりました。
おそらく前例がないので、備忘録として残します。

技術に関して

1. SkyWayとは

スクリーンショット 2020-12-07 3.46.42.png

ほとんど画像に記載がありますが、Webでリアルタイムコミュニケーションを実現する標準技術「WebRTC(Web Real Time Communication)」を簡単にアプリに導入できるSDK&APIです。(公式引用

2. Firebase MLとは

maxresdefault.jpg

ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。(公式引用)

今回は映像部分の顔判定をになっていますが、この部分の中身についての実装は話しません。
あくまでもSkyWayとFirebaseを繋ぐ部分のみになります。
(中身の参考資料「Firebase MLKitを使って動画の顔認識をさせる」)

3. 構成図

アプリ間で動画のリアルタイム通信を行いながら、その映像をFirebase MLで顔認識させるということ行います。

スクリーンショット 2020-12-07 3.44.07.png

SkyWayが2つ導入されていますが、

- 上部はSkyWayサーバー
- 右下アプリ内はSkyWayのSDK

になります。

実装

1.前提

Firebase ML側でデータを分析するためには、動画をCMSampleBufferという型で受け取る必要があります。

幸いなことにiOSでは映像からCMSampleBuffer型を受け取るために、
AVCaptureVideoDataOutputSampleBufferDelegate
というものが用意されています。

Example

final class ExampleVideoManager {

    private var videoOutput: AVCaptureVideoDataOutput!
    private let queue = DispatchQueue(label: "videoOutput", attributes: .concurrent)

    init() {
        self.videoOutput = AVCaptureVideoDataOutput()
        self.videoOutput.setSampleBufferDelegate(self, queue: queue) // デリゲードのセット
    }
}

extension CaptureVideoManager: AVCaptureVideoDataOutputSampleBufferDelegate {

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // CMSampleBuffer型の値が受け取れる場所。Firebase MLへ接続。
    }
}

2.問題点

通常、カメラや動画を実装するためにはAVKitをimportとして色々と実装していく必要があります。
(実装説明は割愛するので参考として「Swiftでカメラアプリを作成する」)

前提のように自前でAVCaptureVideoDataOutputを生成してFirebase ML接続できれば良いですが、SkyWayの場合は指定された実装方法で行う必要があるため、わからないようになっています。

具体的には、SKWVideoという指定されたUI型に、勝手にレンダリングされるようになっていることが問題になります。

Example

var localStream: SKWMediaStream?
@IBOutlet weak var streamView: SKWVideo! // レンダリングする画面

func setup() {
     /* ~略~ */

    let constraints = SKWMediaConstraints()
    localStream = SKWNavigator.getUserMedia(constraints)
    localStream?.addVideoRenderer(self.streamView, track: 0) // レンダリングの指定

     /* ~略~ */
}       

3.解決法

中で隠蔽化されていますが、必ずAVCaptureVideoDataOutputは実装されているので、それをSKWVideoの中から自力で探し当てました。

具体的には、レンダリングされる画面のヒエラルキーをちまちまとデバッグしてAVCaptureVideoDataOutputを持つレイヤー層を探り当てました。

イメージ図
0v2Bh.png

探り当てた部分をコードで取り出す場合は、以下のようになります。

// レンダリング画面
@IBOutlet weak var streamView: SKWVideo!

// レンダリングしている画面の階層を深掘りし、中から`layer`層を取り出す
var videoLayer: AVCaptureVideoPreviewLayer? {
    return streamView.subviews.first?.layer as? AVCaptureVideoPreviewLayer
}

// 上記の`layer`層から変換に必要な`AVCaptureVideoDataOutput`を取り出す
var output: AVCaptureVideoDataOutput? {
    return videoLayer?.session?.outputs.first as? AVCaptureVideoDataOutput
}

上記からAVCaptureVideoDataOutputにアクセスできるので、outputsetSampleBufferDelegateセットしてあげることで、CMSampleBuffer型の値を手に入れることができるようになります。

output?.setSampleBufferDelegate(self, queue: /* Queue */)

これでFirebase MLへの接続も可能となりました。

あとがき

複数のストリームで受け取って加工する、など色々試した結果、この方法が最適なかつ実現可能な方法でした。

少しマニアックなネタだったかもしれませんが、誰かの参考になれば嬉しい所存です。

文献

SkyWayの実装周りは解説しなかったので、以下を参考にすると良いでしょう。
- SkyWay - アプリやWebサービスに、ビデオ・音声通話をかんたん導入
- iOSのCallKitフレームワークとSkyWay
- SKYWAY SDKを使ったIOS版通話アプリ開発

Firebase周りはこちらをご覧ください
- Firebase MLKitを使って動画の顔認識をさせる

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