[iOS][Xcode] error: Building for iOS Simulator, but the linked framework ‘Hoge.framework’ was built for iOS.というエラーが出たときの対処法

はじめに

Xcode でビルドしようとした際にタイトルのエラーに遭遇したので対処法を書いていきます。

テストターゲットにCarthage で framework を追加しており、アプリをビルドしようとした際に遭遇しました。

前提条件

Mac: macOS Catalina 10.15.6
Xcode: Version 12.4 (12D4e)
Carthage: 0.36.0 (後半で0.37.0 以降にアップデートします)

現象

ビルド時に以下のようなエラーが出ました。

テストビルドのエラー

テキストに起こすと以下。

error: Building for iOS Simulator, but the linked framework 'Quick.framework' was built for iOS. (in target 'SampleApp02Tests' from project 'SampleApp02')

SampleApp02 というアプリなんですが、それのTestターゲットに
Quick.framework を追加した状態でのエラーです。
(このエラーメッセージはCarthage のバージョンで少し異なってくるかもしれません)

iOSシミュレータをビルドしているが、
リンクされているのはiOS用(=シミュレータ用ではない)のframeworkです、
と怒られています。

簡単な対処法(ただし根本的ではない)

Stackoverflowに回答があります

Build SettingsValidate Workspace の設定を一度 Yes にし、その後 No に変更すればOKです。
デフォルト表示は No ですが、実際の挙動はどうやら Yes (Error) となっているようです。

Yes のままでもエラーは回避できますが、ビルド時にワーニングが出ます。

ただ、この対処法は少なくとも「フレームワークの検証をしない」という状態になるため、
将来的にはビルドできなくなる可能性も出てくるのかなと思います🤔

より根本的な対処法

そもそもの原因

そもそも Validate Workspace が Yes (Error) でエラーになるのは、
フレームワークの検証で引っかかっているからのようです。

どういうことかと言うと。

Carthage 0.36.0 で作成された framework は、
その中に実機用やシミュレータ用の複数のバイナリが含まれています。
それらが1つのframework として生成されます。

これはuniversal framework もしくは、より直感的に fat framework と呼ばれています。

fat framework のイメージは以下が非常に分かりやすいと思います。
Qiita: XCFrameworksに対応したCarthageを使ってみた CarthageがIntel Mac上でどのように動いていたか

そして、Xcode12.3 からはデフォルト(つまりValidate Workspaceが Yes(Error)の状態)で
fat framework を弾くようになりました。

そのため、ビルドができなくなったようです。

ちなみにアプリのビルド時、Carthage の copy-frameworks スクリプトを実行していると思いますが、
そこでは fat framework を元に
– 実機ビルドなら実機用のみの framework を出力する
– シミュレータビルドならシミュレータ用のみの framework を出力する
という処理をしているようです。

そこで XCFramework

fat framework が弾かれるようになった。
ではどうするか。

fat framework をやめ、XCFramework を使います。

XCFramework とは何か。
イメージを掴むには、中身を見るのが手っ取り早そうです。

XCFramework の中身

このようにXCFramework の中には複数の framework が含まれています。
ios-arm64-armv7 がiOS用、ios-arm64_i386_x86_64-simulator がシミュレータ用のframework かな🤔

このXCFramework を、既存の framework の代わりにXcode に組み込むことで、
ビルド時にXcode が適切なアーキテクチャのframework を選択してリンクするようになります。

ということは先に述べた copy-frameworks スクリプトも不要になります🙌

XCFramework を使う

主流になりそうだし使った方がよさそう、ということでCarthage でXCFramework を作ります!

そのためには Carthage 0.37.0 以降が必要なので準備しましょう。

準備できたらあとは --use-xcframeworks オプションを付けて
carthage コマンドを打つだけです。

carthage update --use-xcframeworks --platform iOS

これ以降の導入手順は難しくないです。

DevelopersIO: [iOS] CarthageでもXCFrameworkが使いたい
Qiita: XCFrameworksに対応したCarthageを使ってみた CarthageでXCFrameworksをビルドする
などが参考になりそうです。

最後に

以上です。

可能ならワークアラウンド的な方法ではなく根本的な対応をすると、
今後困る可能性が減りそうですね🙌

お疲れ様でした!

[Mac][Homebrew] brew update で unshallow しろと言われたときの対処法

はじめに

Carthage 0.38.0 をインストール しようとしたら、
既に0.37.0 が入っているしそれが最新だよと言われました。

$ brew upgrade carthage
Warning: carthage 0.37.0 already installed

Homebrew 自体のアップデートから Carthage をアップデートするまで
諸々対処したのでまとめます。

前提条件

Mac: macOS Catalina 10.15.6

Homebrew をアップデートする

まずは Homebrew 本体をアップデート。
brew update コマンドを打ちます。

$ brew update
Error: 
  homebrew-core is a shallow clone.
  homebrew-cask is a shallow clone.
To `brew update`, first run:
  git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow
  git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow
These commands may take a few minutes to run due to the large size of the repositories.
This restriction has been made on GitHub's request because updating shallow
clones is an extremely expensive operation due to the tree layout and traffic of
Homebrew/homebrew-core and Homebrew/homebrew-cask. We don't do this for you
automatically to avoid repeatedly performing an expensive unshallow operation in
CI systems (which should instead be fixed to not use shallow clones). Sorry for
the inconvenience!

いきなり怒られました!🤯

brew update を実行するためにはまず以下のコマンドを打て、と言われています。

  git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow
  git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow

その下に書かれているのは、意訳すると以下のような感じです。

GitHub の要請で shallow clone にしていました。
unshallow は時間などのコストがかかるため、CIで毎回自動実行されるとやばいことになるので、
手動で unshallow してもらってる状況です。
(というかそのような場合はshallow clone しないように修正すべきですね)

そもそも shallow clone とは🤔

shallow clone とは

古いコミットを切り捨ててgit clone する機能のようです。
公式ブログに説明がありました。

シャロークローンは、 git clone の --depth=<N> パラメータを使って
コミット履歴を切り捨てます。

https://github.blog/jp/2021-01-13-get-up-to-speed-with-partial-clone-and-shallow-clone/

clone するのが早くなりそうなので一見良い機能に見えますが、そうでもなさそうです。
非常にピーキーな機能のようなので、日々の業務では積極的に採用しない方がよさそうですね。

これらの理由から、
その後すぐにリポジトリを削除するようなビルドのケースを除いて、
シャロークローンはお勧めしません
シャロークローンからのフェッチは、
メリットよりもデメリットの方が大きくなることがあります。

https://github.blog/jp/2021-01-13-get-up-to-speed-with-partial-clone-and-shallow-clone/

unshallow コマンドを打つ

Homebrew が指定した2つのコマンドを打ちます。

$ git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow
略
From https://github.com/Homebrew/homebrew-core
   fe112d0e1b..259931b1d2  master     -> origin/master
$ git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow
略
From https://github.com/Homebrew/homebrew-cask
   05081b9fd0..2e49b12876  master     -> origin/master

数分待てば完了します。

改めて brew update を打つ

$ brew update
略
You have 12 outdated formulae and 2 outdated casks installed.
You can upgrade them with brew upgrade
or list them with brew outdated.

正常に完了したようです。

続いて Carthage をアップデートします!

パッケージをアップデートする

brew upgrade と打ちます。

$ brew upgrade
略
==> Upgrading carthage 0.37.0 -> 0.38.0 
略
==> Pouring carthage--0.38.0.catalina.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
==> Summary
🍺  /usr/local/Cellar/carthage/0.38.0: 8 files, 7.7MB
略

0.37.0 しか入れられなかったのが、0.38.0 を入れることに成功しました!

$ carthage version
0.38.0

最後に

以上です。

Carthage を 0.36.0 から 0.38.0 に上げるのにそこそこ苦労しました。

  1. [Mac][Homebrew] brew reinstall XXX で /usr/local/lib/node_modules/npm/node_modules/.bin/ のパーミッションエラーが出る場合の対処法
  2. [Mac] UNIX のユーザとグループの世界観

本ポストも本当の目的の途中経過です。
環境構築は難しいですね 😇

お疲れ様でした!

[Mac][Homebrew] brew reinstall XXX で /usr/local/lib/node_modules/npm/node_modules/.bin/ のパーミッションエラーが出る場合の対処法

はじめに

以下のコマンドを実行した際、エラーが出て完了できませんでした。

$ brew reinstall carthage
中略
==> `brew cleanup` has not been run in 30 days, running now...
Error: Permission denied @ apply2files - /usr/local/lib/node_modules/npm/node_modules/.bin/JSONStream

これの対処法をまとめます

前提条件

Mac: macOS Catalina 10.15.6

何が起こっているのか

ターミナルを見ると、brew cleanup をしようとして失敗しているようです。

`brew cleanup` has not been run in 30 days, running now...

brew reinstall, brew install, brew upgrade を実行すると自動で brew cleanup が走るようです。

brew cleanup is run periodically (every 30 days) and triggers for individual formula cleanup on reinstall, install or upgrade. 

https://brew.sh/2019/02/02/homebrew-2.0.0/

で、そこでパーミッションの問題で弾かれているようですね。

対処法

Stack Overflow に対処法がありました

以下のコマンドを実行します。

sudo chown -R ${LOGNAME}:staff /usr/local/lib/node_modules

/usr/local/lib/node_modules とその配下のファイルやディレクトリの所有権を
再帰的に<アカウント名>:staff に変更するコマンドです。

補足1. staff グループとは

「staff グループ」を初めて知ったんですが、
PCの管理者以外の触る人、という位置付けのグループのようです。

そもそもグループとはどういうものかあまりイメージできなかったので、
調べて本ブログの [Mac] UNIX のユーザとグループの世界観 にまとめました。

補足2. staff でよかったのか

Stack Overflow の一つ下の回答 には少し別のコマンドが書かれていました。

sudo chown -R $(whoami):admin /usr/local/* \
&& sudo chmod -R g+rwx /usr/local/*

/usr/local/ 配下の所有権や権限を全て変更するコマンドで、
個人的にちょっと怖かったのでこちらのコマンドは採用しませんでした。

が、こちらの回答の方が支持されているようですし、
Qiita などで様子を見る限りこちらでも問題はなさそうです。

むしろサーバなど共有PCで作業する場合は staff より admin の方がよいかもしれませんね🤔

試していませんが staff を admin に読み替えたコマンドを試してみるのもいいかもしれません。

$ sudo chown -R ${LOGNAME}:admin /usr/local/lib/node_modules

brew cleanup 成功

さて、前述のコマンドを打って改めて brew cleanup すると、成功しました!🙌

$ brew cleanup
略
Pruned 19 symbolic links and 318 directories from /usr/local

更に carthage を reinstall してみます。

$ brew reinstall carthage
略
==> Summary
🍺  /usr/local/Cellar/carthage/0.37.0: 8 files, 7.7MB

成功しました🍺

最後に

以上です。

Carthage は本当は0.37.0 ではなく 0.38.0 を入れたかったんですが、Homebrew の何かが古いのかな?
次回はupdate, upgrade まわりについてまとめようと思います。

お疲れ様でした!

[Mac] UNIX のユーザとグループの世界観

はじめに

普段あまり意識しないMacのユーザやグループですが、
調べる機会があったのでコラム的な話も交えつつ概要を書いていきます!

世界観・外観を知ることで、
何が分からないか分からない状態から、何が分からないか分かる
くらいになる助けになればと思います。

前提条件

Mac: macOS Catalina 10.15.6

ことの発端

調べ物をしていたとき、adminstaff グループというものが出てきて、
admin とは何だ!となったところから始まります。

管理者なのでroot 的な権限を持てるグループか??などとも思いましたが、
後述しますが実際はけっこう違いました。

ユーザとグループについての僕の当初のイメージ

ユーザにはどんな種類があるでしょうか。
思いつくのは自分のアカウント、root、くらいかな……
サーバで開発していると他の人のアカウントもありますね。

グループは意識したことないのでよく分かりません。
というかなぜグループがあるのかもよく分かりません🤯

とっかかりとしてまず root ユーザについて調べます。

root ユーザとは

Linuxでは、あらゆる権限が与えられているユーザーを「root」あるいは「rootユーザー」「スーパーユーザー」と呼びます。rootというユーザー名でログインすると、あらゆるコマンドの実行やファイル操作ができるようになります。

https://www.atmarkit.co.jp/ait/articles/1706/02/news014_2.html

出典はLinux についての記事ですが、Mac と同じUnix系システムということでご容赦を。

root ユーザは何でもできます。
sudo コマンドを打つとパーミッションを突破して色々操作できるのは
その間だけroot ユーザになっているからなんですね。


ちなみに、sudo は super user do の略と思いきや substitute user do (代理人として実行)の略という説もあるようです。


少し調べると、初期状態では wheel グループに所属するのは root ユーザのみ、という情報がありました。
https://itectec.com/superuser/the-difference-between-the-default-groups-on-mac-os-x/

つまり、所謂 root 的な権限を持つ特権的なグループが wheel ということになりそうです。

微妙に見覚えのある wheel グループですが、本当に root ユーザだけなのか調べます。

wheel グループについて

以下のコマンドを打ちます。
参考) https://superuser.com/a/444218

$ dscacheutil -q group -a name wheel
name: wheel
password: *
gid: 0
users: root

wheel グループについての情報が表示されました。

password はそのままですがパスワードで、 * でマスクされています。
gid はグループIDで0。0なのでいかにも特権的な感じですね。
users がグループに所属しているユーザ。

確かにroot のみ所属しているようです。
やはり強力なグループのようです。

admin や staff グループは?

同じようにコマンドをうちます。

$ dscacheutil -q group -a name admin
name: admin
password: *
gid: 80
users: root <自分のアカウント> ... 

$  dscacheutil -q group -a name staff
name: staff
password: *
gid: 20
users: root <自分のアカウント> _serialnumberd ...

admin グループと staff グループには自分のアカウントが所属していました。

持っているMac が自分専用の場合 admin と staff の違いが実感しづらいかも知れません。

が、やはり admin グループの方が権限が強く
例えばアプリのインストールなどは admin グループの方のユーザしかできないようです。
参考) https://superuser.com/a/20430

自分のMac にはどんなグループやユーザが定義されているのか?

グループ一覧を見てみる

以下のコマンドで見てみます。

 $ dscl . list /GROUPS
_amavisd
_analyticsd
略
utmp
wheel

思っていたよりめっちゃ多かったです。
dscl . list /GROUPS | wc -l でカウントすると131個もありました。

_hogehoge のようなアンダースコアで始まるグループが多かったのですが、
触れるなよ的な圧を感じます。

mail や network や sys などもあり、用途によって色々グループ分けされているようです。

ユーザ一覧を見てみる

以下のコマンドで見てみます。

$ dscl . list /USERS
_amavisd
_analyticsd
略
<自分のアカウント>
root

やはりこちらも思ったより多く、dscl . list /USERS | wc -l でカウントすると103個ありました。

こちらもアンダースコアで始まるユーザが多いです。

daemon や nobody といったユーザがいましたが、
軽く調べたところ古いお作法だったりと個人的にはあまり有効な用途がなさそうに見えます。

ですので僕の場合、ユーザに関しては
root と自分や他人のアカウントのみ意識していればよさそうかなと思いました。

ユーザが所属しているグループ一覧を見てみる

ついでにこちらもコマンドで確認してみます。

$ groups root
wheel daemon kmem sys tty 略

root ユーザについて見てみると、所属グループは20個ほどでした。
131個の全グループに所属していると思ってましたが、そんなことはないんですね。

groups <自分のアカウント> を実行してみるのも面白いかもしれません。

ユーザとグループについての僕の今のイメージ

ユーザは root と自分や他人のアカウント。
あとは nobody とか daemon とか。

グループは権限の持ち方を束ねるもので、そこにユーザを振り分けることで
各ユーザに対する権限を、適切に効率的に管理することができる。

そもそもなぜwheel が特権グループか

けっこう気になるポイントで、
ここが意味不明なので覚えづらいという面があると思います。

wheel はホイール、車輪です。
なぜ車輪が特権グループなのか。

実は英語にはBig Wheel というスラングがあり、
それは大きな車輪の馬車に乗る人、つまり大きな権力を持つ者・組織の重要人物のことを指していたそうです。
参考) https://unix.stackexchange.com/a/1271

Unix はこのスラングを取り入れたと。

なるほどね!
諸説あるようですが、確からしさはありそう。

ということで wheel の意図ができたので、だいぶ覚えやすくなりました。

最後に

以上です。

ユーザとグループについて雲を掴むようなふわふわしたイメージしか持てませんでしたが、
今回調べてみてなんとなく輪郭は分かったと思います。

お疲れ様でした!

[Mac][Android] Android Studio のキーボードショートカットが効かなくなったときの復元手順

はじめに

Android Studio をアップデートしたらキーボードショートカットが全然効かなくなりました。
設定が勝手に変更されてそうなった場合があるので、復元手順を載せます。

秒で復元できます。

前提条件

MacOS Catalina 10.15.6
Android Studio 4.2

復元手順

まず「Android Studio」の「Preference」を開きます。

「Android Studio」の「Preference」を開く

Preference で Keymap を選択します。
詳細のところにプルダウンがあるので、そこで「macOS」を選択し、「apply」を押します。
以上です!

「macOS」を選択

ちなみに僕の場合はなぜか「IntelliJ IDEA Classic」が勝手に設定されていました。

最後に

以上です。
お疲れ様でした!

[Mac][iOS][Android] 個人的に利用頻度の高いXcode と Android Studio のキーボードショートカットまとめ

はじめに

個人的に利用頻度の高いMac版のキーボードショートカットをまとめてみました。

前提条件

Mac: 10.15.6
Xcode: 12.5
Android Studio: 4.1.1

公式ドキュメント

キーボードショートカットの公式ドキュメントはこちらです!

※ Android Studio は IntelliJ IDEA をベースにしているため、そちらのショートカットも使えます

キーボードショートカット

コード探索

説明XcodeAndroid Studio
クラス名、メソッド名、プロパティ名などを検索Command Shift oCommand Option o もしくは Shift 2回
クラス名を検索Command o
ファイル名を検索Command Shift o
プロジェクト内を全文検索Command Shift fCommand Shift f
クラスやメソッドやプロパティの定義にジャンプCommand Control jCommand ↓
クラスやメソッドのプロパティの定義にジャンプ
(画面分割で開く)
Command Control Option j
ファイル内の構造を一覧するControl 6Command F12
指定の行へCommand LCommand L
メソッドやプロパティの呼び元を一覧するCommand Click から Callers を選択Command Click
現在のファイルをファイルツリーでフォーカスCommand Shift jOption F1 から Project View を選択
コード検索

コード編集

説明XcodeAndroid Studio
アンドゥCommand zCommand z
リドゥCommand Shift zCommand Shift z
行末へCommand →Command →
行頭へCommand ←Command ←
オートインデントControl iControl Option i
コメントアウト / コメントアウトを外すCommand /Command /
カーソル位置のコードを折りたたむCommand Option ←Command .
ファイル内のコードをすべて折りたたむCommand Option Shift ←Command Shift –
コード編集

ナビゲーション

説明XcodeAndroid Studio
戻るCommand Control ←Command Option ←
進むCommand Control →Command Option →
ナッビゲーション

実行など

説明XcodeAndroid Studio
実行Command rControl r
コミットCommand Option cCommand k
実行など

最後に

以上です。
お疲れ様でした!

[iOS] Decodable でJSONをデコードする時に要素の型を変換する その3 KeyedDecodingContainer のエクステンションと Property Wrapper で一部の要素にのみ独自のデコード処理を適用しつつ狙った型に変換する

はじめに(承前)

本ブログの 以前のポスト で以下のJSONをデコードしようとしました。

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉",
    "栄養": ["タンパク質", "ビタミンA", "ビタミンB"]
}

食材 (String) と 栄養 (String の配列) を、どちらも文字列の配列でデコードしたいという話でした。
そのためのコードとして以下を書きました。

struct Food: Decodable {
    let 食品名: String
    let 食材: [Foodstuff]  // 独自の型に変更
    let 栄養: [String]
}

struct Foodstuff: Decodable {
    let value: String
}

extension KeyedDecodingContainer {
    func decode(_: [Foodstuff].Type, forKey key: Key) throws -> [Foodstuff] {
        let valueString = try decodeIfPresent(String.self, forKey: key)
        let valueArray = valueString?.components(separatedBy: ",")
        return valueArray?.map { Foodstuff(value: $0) } ?? []  // 独自の型に変更
    }
}

[Foodstuff] という型に変えてしまえば、KeyedDecodingContainer のエクステンションで
食材 要素のみに独自のデコード処理を適応できます。
で、 Foodstuff.first?.value とかで文字列を取得できます。

いやー[String] で扱いたいですよね!

KeyedDecodingContainer のエクステンションではなくinit(from decoder: Decoder) throws {
自前でデコード処理を書けば[String] でデコードできます。
が、それはそれで手間です。

ということで、「一部の要素にのみ独自のデコード処理を適用しつつ、狙った型に変換する手法」
を書いていきます。

タイトルが長い!

前提条件

Xcode: 12.4
Swift: 5.3

まずは最終的なコード

struct Food: Decodable {
    let 食品名: String
    @StringComponents var 食材: [String]
    let 栄養: [String]
}

extension KeyedDecodingContainer {
    func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
        if let value = try decodeIfPresent(StringComponents.self, forKey: key) {
            return value
        } else {
            return StringComponents(wrappedValue: [])
        }
    }
}

@propertyWrapper
struct StringComponents: Decodable {
    var wrappedValue: [String] = []

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let valueString = try container.decode(String.self)
        wrappedValue = valueString.components(separatedBy: ",")
    }
    
    init(wrappedValue: [String]) {
        self.wrappedValue = wrappedValue
    }
}

これで狙い通り、食材[String] として扱えます。

説明

食材 に、定義した StringComponentsPropertyWrapper を適応します。
(PropertyWrapper の基本的な挙動については、
以前のポスト[iOS] Property Wrapper の概要について にてまとめています)

KeyedDecodingContainer のエクステンションに
func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
を定義します。

そうすると食材 をデコードする時のみ、その処理を通ります。

その処理の中で if let value = try decodeIfPresent(StringComponents.self, forKey: key) { でデコードを試みます。

実際のデコード処理は StringComponentsinit(from decoder: Decoder) throws { にて行います。
ここで String[String] に変換しています。

あとは StringComponents を返却して、食材 のデコードは完了です。

結果をデバッガを見ると以下のようになります。

デコード結果

左のペインでは _食材 というプロパティ名で表示されますが、
.食材 でアクセスすると [String] で取得できています。


また、KeyedDecodingContainer のエクステンションを変更して、
以下のように返り値などを [String] にしても同様のデコード結果になります。

extension KeyedDecodingContainer {
    func decode(_: StringComponents.Type, forKey key: Key) throws -> [String] {
        if let valueString = try decodeIfPresent(String.self, forKey: key) {
            return valueString.components(separatedBy: ",")
        } else {
            return []
        }
    }
}

プロパティラッパと欲しい型、どちらを返却すると筋がいいかまでは理解が及んでいません🙏

説明は以上となります!

最後に

デコード周りは一区切りとします。
また何か知見が出てきたら書くかもしれません。

以上です。
お疲れ様でした!

[iOS] Property Wrapper の概要について

はじめに

SwiftUI でもよく使われているProperty Wrapper についてさらっとまとめます!

Property Wrapper とは

ある「プロパティ」を「何らかの処理を施したラップ後のプロパティ」に変更する仕組みです。

@HogePropertyWrapper var a = 1

と書いた場合、a というプロパティは
HogePropertyWrapper にて何らかの処理を施された後(= 何らかの処理をラップした後)のプロパティ、
というものに変質します。

プロパティを get / set するときの何らかの処理をメソッド化して分離し、
それを適応したプロパティに対し共通処理として使いまわせる、
というのが Property Wrapper の思想になります。

Property Wrapper の書き方

とっかかりとして、 2速で歩くヒト:【Swift 5.1】Property Wrappersとは?
というブログ記事がとても参考になると思います。

struct, enum, class で定義可能で、wrappedValue というプロパティを定義する必要があります。

以下、Swift.org のコード そのままですが例示します。

@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

これは Int の上限を12に制限する Property Wrapper となります。

プロパティにこの TwelveOrLess の処理を適応するには、以下のように @ をつけて宣言します。

class HogeClass {
    @TwelveOrLess var number: Int
    func hogeMethod() {
        number = 100
        print("🐱 mumber: \(number)") // "🐱 mumber: 12" と表示される
    }

Property Wrapper の仕組み

この @TwelveOrLess var number: Int の部分は、コンパイル時に以下のように展開されます。

private var _number = TwelveOrLess()
var number: Int {
    get { return _number.wrappedValue }
    set { _number.wrappedValue = newValue }
}

_number というプロパティができ、 number プロパティへのアクセスは
実態として _number.wrappedValue へアクセスする、という処理に変更されます。

number_number 、つまり TwelveOrLess のインスタンスをラップした値、ということですね。

これが PropertyWrapper の仕組みです!

Property Wrapper の初期化

Property Wrapper を適用したプロパティの初期化は以下のように書きます。

@TwelveOrLess var number: Int = 1

これは number プロパティに初期値1を設定しようとしています。

Swift はこれを以下のコードに変換します。

@TwelveOrLess(wrappedValue: 1) var number: Int

TwelveOrLess 側には以下のコードが必要になります。

@propertyWrapper
struct TwelveOrLess {
    // 略
    init(wrappedValue: Int) { self.number = wrappedValue } // メソッドの中身はお好きに
    // 略
}

このwrappedValue の初期化メソッドを書かないと
Argument passed to call that takes no arguments というエラーが出るかと思います。

複数の引数で初期化する場合

以下のように書きます。

@TwelveOrLess(wrappedValue: 1, hoge: "hoge") var number: Int
@propertyWrapper
struct TwelveOrLess {
    // 略
    private var hoge: String
    init(wrappedValue: Int, hoge: String) {
        self.number = wrappedValue
        self.hoge = hoge
    }
    // 略
}

Property Wrapper の概要については以上となります。

Projected Value という概念もありますが、それはまた別の機会に…。

最後に

以上です。
お疲れ様でした!

[iOS] Decodable でJSONをデコードする時に要素の型を変換する その2 KeyedDecodingContainer のエクステンションで一部の要素にのみ独自のデコード処理を適用する

はじめに(承前)

以前のポスト [iOS] Decodable でJSONをデコードする時に要素の型を変換する
以下のようなデコード処理を書きました。

struct Food: Decodable {
    var 食品名: String
    var 食材: [String]
    
    enum コーディングキー: String, CodingKey {
        case 食品名
        case 食材
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: コーディングキー.self)
        
        食品名 = try container.decode(String.self, forKey: .食品名)
        let 食材String = try container.decode(String.self, forKey: .食材)
        食材 = 食材String.components(separatedBy: ",")
    }
}

この方法のデメリットとして、init(from decoder: Decoder) throws で処理しているので、
全てのデコード処理を自前で書く必要があります。

要素の数が増えれば増えるほど記述するコストが増えてしまいます。

ある程度複雑な構造の場合、自前で全てデコード処理を書くのはつらいです。

そこで本ポストでは、要素の一部のみ独自のデコード処理を適用する方法を書いていきます!

前提条件

Xcode: 12.1
Swift 5.3

やりたいこと

以前のポスト [iOS] Decodable でJSONをデコードする時に要素の型を変換する と同様、
以下のJSONデータをパースします!

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉"
}

また、前提としてデコード処理は以下のように呼び出しています。

let food = try! JSONDecoder.init().decode(
    Food.self,
    from: data
)

まずは最終的なコード

struct Food: Decodable {
    var 食品名: String
    var 食材: [String]
}

extension KeyedDecodingContainer {
    func decode(_: [String].Type, forKey key: Key) throws -> [String] {
        let value = try decodeIfPresent(String.self, forKey: key)
        return value?.components(separatedBy: ",") ?? []
    }
}

説明

KeyedDecodingContainer のエクステンションを定義し、
そこに var 食材: [String] のためのデコード処理を書けばOKです。

引数の型の [String].Type ですが、[String] 型でデコードしたい要素は
全てこのエクステンションを通るという処理になります。
(微妙に臭うな…? 🤔 (このあたりは後述))

引数の key には 「食材」 という CodingKeys enum が入ってきます。

let value = try decodeIfPresent(String.self, forKey: key) の部分で、
String 型を持ち且つ「食材」の CodingKey の rawValue (= 今回の場合は「食材」となる。デフォルトのCodingKey なので、rawValue もそのようになっている)
を持つJSON要素のデコードを試みています。

デコードできない可能性を考慮して、オプショナルのStringを返却する
decodeIfPresent(_:forKey:) を利用しています。

これで "鶏肉, 豚肉, 牛肉" という String が取得できるはずなので、
あとは , で文字列を分割して配列に加工し、その配列を返却しています!


var 食品名: String のデコードではこのメソッドに入ってこず、特に記述しなくてもデコードされます。
こちらはおそらく内部的には decode(_:forKey:) のような処理が走っているのではないかと思います。

CodingKeys について

前回のポストでは以下のように CodingKey たちを独自に定義していましたが、
今回は省略して記述できています。
デコード処理で明示的には利用しない(且つ JSON と デコード後 の要素名が同じ)ためです。

enum コーディングキー: String, CodingKey {
    case 食品名
    case 食材
}

もしJSONデータと異なる要素名でデコードしたい場合、
且つ今回のように一部の要素のみ独自のデコード処理を適用したい場合は以下のように
CodingKeys というenum で定義し、rawValue でJSONでの要素名を定義します。

    enum CodingKeys: String, CodingKey {
        case 食品名
        case 食材 = "JSONでの食材要素名"
    }

iOS がデコードする際、要素名のリストとして
CodingKeys という名前の enum を参照しているようです。
参考: Apple の公式ドキュメント: Encoding and Decoding Custom Types


これで必要な箇所のみ独自のデコード処理を施すことができました!🙌

しかしこの方法にもデメリットがあります!

[String] 型にデコードしたい要素がすべてこの処理を通ってしまう

そうです、例えばJSONデータが以下のような構造だとします。

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉",
    "栄養": ["タンパク質", "ビタミンA", "ビタミンB"]
}

で、以下のようなモデルデータにデコードしたいとします。

struct Food: Decodable {
    var 食品名: String
    var 食材: [String]
    var 栄養: [String]
}

食材 はJSON要素の String を [String] に変換したいが、栄養 は [String] そのままでデコードしたい。

この場合 栄養 のデコードで失敗します。

なぜなら let value = try decodeIfPresent(String.self, forKey: key) で、
栄養String 型としてデコードしようと試みますが、
栄養[String] 型だからです。

以下のようなエラーが出ると思います。

typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "栄養", intValue: nil)], debugDescription: "Expected to decode String but found an array instead.", underlyingError: nil))

ではどのようにするのがよいのか。

一つは、引数のKey によって処理を出し分ける方法が考えられます。

Key によってデコード処理を出し分ける

以下のようなコードに変えれば問題なくデコードできます。

extension KeyedDecodingContainer {
    func decode(_: [String].Type, forKey key: Key) throws -> [String] {
        if key.stringValue == "食材" {
            let value = try decodeIfPresent(String.self, forKey: key)
            return value?.components(separatedBy: ",") ?? []
        } else if key.stringValue == "栄養" {
            let value = try decodeIfPresent([String].self, forKey: key)
            return value ?? []
        } else {
            return []
        }
    }
}

これはやばい香りがしますね… 🤔
今後 [String] 型の要素が増えるたびにここを考慮する必要が出てきます。

独自の型としてデコードする

影響範囲を限定できるので、まだこちらの方がよさそうかなと思います。

struct Food: Decodable {
    let 食品名: String
    let 食材: [Foodstuff]  // 独自の型に変更
    let 栄養: [String]
}

struct Foodstuff: Decodable {
    let value: String
}

extension KeyedDecodingContainer {
    func decode(_: [Foodstuff].Type, forKey key: Key) throws -> [Foodstuff] {
        let valueString = try decodeIfPresent(String.self, forKey: key)
        let valueArray = valueString?.components(separatedBy: ",")
        return valueArray?.map { Foodstuff(value: $0) } ?? []  // 独自の型に変更
    }
}

Foodstuff 構造体を RawRepresentable に準拠させると、より Swifty になるかな? 🤔
場合によってはそうしてもいいかもしれません。

struct Foodstuff: Decodable, RawRepresentable {
    let rawValue: String
}

独自の型を定義する場合のデメリットは、そのまんまですが [String] 型として扱えなくなることですね!

let 食材: [Foodstuff] の部分を [String] で扱いつつ、
KeyedDecodingContainer のエクステンションでキーによる処理の出し分けも避けたい?

PropertyWrapper と組み合わせれば実現できそうです🤔

長くなったのでそちらのパターンについては後ほど書きます!

最後に

Codable は色々できてるので、複雑なことをやろうとすると混乱しがちです😂

以上です。
お疲れ様でした!

[Firebase] 開発環境構築時のTips

はじめに

Cloud Functions for Firebase を試そうとしたらちょいちょいすんなりいかなかったので、メモを残します。

環境

macOS Catalina バージョン10.15.6

firebase init で失敗する

Firebaseプロジェクトが存在する状態で

$ firebase init firestore

とコマンドを打ったら以下のようなエラーが出ました

Error: Failed to get Firebase project YOUR_PROJECT_NAME. Please make sure the project exists and your account has permission to access it.

どうも前回ログインから時間が経っていると発生する場合があるらしいです。

以下のコマンドでログインしなおしたら正常に動作しました。

$ firebase logout
$ firebase login

firebase emulators:start で失敗する

以下のようなエラー文が出ました。

$ firebase emulators:start
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub
✔  functions: Using node@14 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  firestore: Fatal error occurred: 
   Firestore Emulator has exited with code: 1, 
   stopping all running emulators
i  hub: Stopping emulator hub
i  functions: Stopping Functions Emulator
i  firestore: Stopping Firestore Emulator
⚠  firestore: Error stopping Firestore Emulator

Having trouble? Try firebase [command] --help

JDKが必要だとというプロンプトが出るし、firestore-debug.log にもそのように書かれています。

Oracle のJDKダウンロードページ から落としてきてインストールしました。

Firebase のバージョンがインストールしたものと異なる

以下のように9.5.0を入れたと思ったのに。

$ npm i -g firebase-tools
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
/usr/local/opt/nvm/versions/node/v12.18.3/bin/firebase -> /usr/local/opt/nvm/versions/node/v12.18.3/lib/node_modules/firebase-tools/lib/bin/firebase.js
+ firebase-tools@9.5.0
updated 1 package in 30.88s

バージョン確認をすると8.7.0 が入っている。

$ firebase --version
8.7.0

以下のように解決しました。


参照しているfirebase を確認。

$ which firebase
/usr/local/bin/firebase

npm のグローバルディレクトリの確認。

$ npm bin -g 
/usr/local/opt/nvm/versions/node/v12.18.3/bin
(not in PATH env variable)

想定とは異なるfirebase を参照していたようです。
ので、ローカルのfirebase を削除。

$ rm -rf /usr/local/bin/firebase

その上で以下のようにエイリアスをはる。

$  alias firebase="`npm config get prefix`/bin/firebase"

すると、想定通りのバージョンになりました。

$  firebase --version
9.5.0
$ which firebase
/usr/local/opt/nvm/versions/node/v12.18.3/bin/firebase

参考: stackoverflow

alias コマンドはマシンを再起動すると設定が消えると思うので、
より丁寧にするなら ~/.bash_profile などで以下のようにパスを通します。

export PATH=$PATH:$(npm get prefix)/bin

で、source ~/.bash_profile で変更を反映すればOK。

firebase deploy –only functions で失敗する

以下のようなエラーが出ました。

Error: HTTP Error: 403, Unknown Error

この場合も冒頭と同様、ログアウト・ログインで解消しました

$ firebase logout
$ firebase login

最後に

以上です。

お疲れ様でした!