- 投稿日:2019-12-18T23:37:26+09:00
RxSwiftのオペレーターの勉強 その2(結合編)
Atrae Advent Calendar 2019 の18日目を担当するアガツマです。
普段は、ビジネス版マッチングアプリ yenta のiOS版を開発をしています。前回に続いて、RxSwiftのオペレーターについて書きます。
今回は、オペレーターの中でも複数のobservableを合成(結合)するようなものについてまとめました。
RxSwiftを勉強する際の参考になったら嬉しいです。※RxSwiftのオペレーターとは、Observableに用意されているイベントを加工する用のメソッドのことを指します
複数のobservableを合成するオペレーター
combineLatest
処理
2つのObsrvableのを監視してどちらか1つのObsrvableが送信される時、それぞれのObservableから送信される最新の値をまとめて送信する。用途
複数の変数を監視する必要がある場合(複数入力のバリデーションなど)
複数の値を組み合わせる処理コード
let i = PublishSubject<Int>() let s = PublishSubject<String>() _ = Observable.combineLatest(i, s) { "\($0) + \($1)" } .subscribe { print("onNext: ", $0) } i.onNext(1) s.onNext("A") i.onNext(2) s.onNext("B") s.onNext("C") i.onNext(3)出力結果
onNext: next(1 + A) onNext: next(2 + A) onNext: next(2 + B) onNext: next(2 + C) onNext: next(3 + C)withLatestFrom
処理
あるObservableがイベントを送信した際に、もう一方の Observabe の最新のイベントを合成する。用途
1つのアクションが起きた際に、他の値に対して何かしら処理をする場合。
(ボタンが押された時などに、他の要素がどのような値になっているか知りたいときなど)コード
let i = PublishSubject<Int>() let s = PublishSubject<String>() i.withLatestFrom(s) .subscribe({ string in print("onNext: ", string) }) .disposed(by: disposeBag) i.onNext(1) s.onNext("A") i.onNext(2) s.onNext("B") s.onNext("C") i.onNext(3)出力結果
onNext: next(A) onNext: next(C)zip
処理
複数の Observable のイベントを1つづつ順番に合成する。用途
並列処理をしたい場合(API通信で複数のレスポンスを待ってから処理を進めたいときなど)コード
let i = Observable.of(1,2,3,4,5) let s = Observable.of("A","B","C") _ = Observable.zip(i, s) { "\($0) + \($1)" } .subscribe { print("onNext: ", $0) }出力結果
onNext: next(1 + A) onNext: next(2 + B) onNext: next(3 + C) onNext: completedおわりに
今回は、RxSwiftでのObservableの合成についてまとめてみました。
このような、オペレーターを組み合わせることで、様々な処理を実現することができるのだなと思いました。
今後は、RxSwiftを用いたアーキテクチャーなどの理解を深めていきたいと思います!参考
- 投稿日:2019-12-18T20:13:56+09:00
Firestoreリアルタイムリスナーで通知ドットを実装してみたよ
リアルタイムリスナーってすごい機能なのにあまり使い道を見出せず単一リクエストばかりしていたのですが、ちょっと使えそうな余地のある部分を発見したので試してみました。
通知ドット
SNSやお知らせなどのタブ上にあるアレです。不定期更新な機能でも見て欲しい時に見てもらうための導線になるのでよいものです。
未読がないか毎回フェッチするよりも、リアルタイムアップデートしたほうが面白いのでリアルタイムにデータを同期して通知ドットを表示させてみようと思います?仕様
- お知らせ一覧へ遷移するタブをもつアプリ
- お知らせに未読があればお知らせタブ上に通知ドットを表示してあげる
- お知らせのソートは常に最新順
実装方法
未読判定
未読かどうかの判定は取得された最新1件のドキュメントIDと既読とみなされたローカルに保存されているドキュメントIDで判断するシンプルなものです。
public struct UnreadState { private(set) var latestLocalId: String? private(set) var latestRemoteId: String? public var hasUnread: Bool { guard let local = latestLocalId, let remote = latestRemoteId else { return false } return local != remote } // ... }お知らせのコレクションをつくる
notifications/{notificationId}
Firestoreリアルタイムリスナーでデータを監視
お知らせコレクションの最新1件をリッスンする
Firestore.firestore() .collection("notifications") .order(by: "createdAt", descending: true) .limit(to: 1) .addSnapshotListener({ (snapshot, _) in // 最新1件目のドキュメントID let latestRemoteId = snapshot?.documents.first?.documentID }) }ローカルの未読は とりあえず UserDefaults
お知らせ一覧へ遷移した時に最新の1件のドキュメントIDを既読とみなして保存する。
// お知らせ一覧を取得してきた想定 let notificationId = notifications.first?.id userDefaults.set(notificationId, forKey: .lastReadNotificationId)お知らせ一覧へ遷移時に単一リクエストで取得した最新1件を保存する。
このようにフラットに保存してしまうと複数アカウントの未読を管理できないですがここでは知らなかったことにする。
Viewの更新
// 通知ドットView let notificationDot: DotView = .init() // state is Observable<UnreadState> state .map { $0.hasUnread } .distinctUntilChanged() .bind(to: Binder(self) { me, hasUnread in me.notificationDot.isHidden = !hasUnread }) .disposed(by: disposeBag)RxSwiftを利用していますがなんらかの方法でViewに変更を伝えます。
通知ドットView
仕上げに通知ドットViewにはいい感じの出現アニメーションを実装してあげます。
class DotView: UIView { init() { super.init(frame: .init(x: 0, y: 0, width: 5, height: 5)) backgroundColor = SPColor.badge layer.cornerRadius = bounds.midX isHidden = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var isHidden: Bool { didSet { guard oldValue != self.isHidden else { return } if !self.isHidden { appearAnimation() } } } private func appearAnimation() { self.transform = CGAffineTransform(scaleX: 0, y: 0) UIView.animate( withDuration: 0.6, delay: 0, options: .curveEaseInOut, animations: { self.transform = CGAffineTransform.identity } ) } }
isHidden
プロパティをオーバーライドして出現時に拡大アニメーションを追加しました。ドーパミンが分泌されます?
おわりに
このまま実運用だと並び順の変更や未読数カウントなどによって破綻する未来が見えますが、わりと最小限実装で事足りるケースも少なくないかと思います。この機能を応用して更新フラグに利用したりもできそうです。
- 投稿日:2019-12-18T18:48:38+09:00
【iOS】サポートバージョンを上げた時にやったこと
概要
アプリを長い間開発していると、必ず生じるサポートバージョン問題。
そんなサポートバージョンを上げた時にどこを変更するのか忘れそうなので
備忘録的な意味合いも兼ねて更新したいと思います。iOS Deployment Target
まぁ、これは言わずもがなですね。
ここで内部的には判定されているものと思われます。@available属性
旧バージョンに対応していた場合、新しいOSでしか対応していないAPIを使う際には
@available属性などで条件分けして実装していたと思います。
これが必要なくなります。@available(iOS, [version], *)
[version]の値が今回のサポートバージョンに含まれている場合は
消してしまって問題ありませんので消してしまいましょう!また、この表記がある場合、この[version]に満たないバージョンで
代わりに呼ばれるメソッドがあるはずなので、そちらも消してしまいましょう!Podfile
忘れがちなのが、これではないでしょうか。
(今回この記事を書いたのは、これを忘れてたからです。)platform :ios, "9.0"みたいな表記があると思うので、これを今回サポートするバージョンへと変更して
pod install
を叩きましょう。
多くの場合は、ほぼほぼ変更点はない(checksumのみ)と思います。他に皆様でやったことがあれば教えていただけると嬉しいです!
誰かのお役に立てば。
- 投稿日:2019-12-18T18:09:30+09:00
プロパティをクロージャで初期化する(Initialization Closure)
前回の投稿から長い時を経て、就職してiOSエンジニアとして日々勉強をしています。よろしくお願いします。
今回はタイトルの通り、Initialization Closureについてまとめようと思います。(この記事は就職先の技術ブログの内容を転機したものです)
TL; DR
- Initialization ClosureはStored Propertyの初期化に使われる書き方。
- Computed Propertyではないので、
{}
の中は1度しか呼ばれない。未知との遭遇
現在開発中のアプリでUIViewControllerのライフサイクルとAuto Layoutの反映タイミングに悩まされ、
viewDidLayoutSubview()
で最初の一回だけ処理をしたくなり、この記事にたどり着きました。実際に以下のようなコードを書けば問題は解決したのですが、何がどうなっているのか・なぜ上手くいってるのかがわかりませんでした。
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() _ = initCollectionViewFlowLayout } private lazy var initCollectionViewFlowLayout : Void = { // ここで色々設定する // frame、boundsとか使う }()何がわからんのか
当時わからなかったことをまとめるとこんな感じだったと思います。
- 見た目Computed Propertyと似てるけど何が違うのか
- 何故これで
initCollectionViewFlowLayout
の中身が一度しか呼ばれないのか{}
の後の()
は何者なのか最近ようやくこの謎が解けた(っぽい)ので、これらの点について話をします。
Computed Propertyとの違い
変数名の後に
{ return }
みたいなのがあるのでComputed Propertyと混同してしまったのですが、このinitCollectionViewFlowLayout
はInitialization Closureという書き方でStored Propertyとして定義されているみたいです(=
があるのでそれもそうか...という感じ)。Initialization Closureとは
Stored Propertyの初期化の際に、初期値としてクロージャの実行結果を渡す書き方のことです。
例1 ) 要素数50のフィボナッチ数列 (Stored Property)
let fibonacci: [Int] = { var temporaryArray = [1, 1] let numberOfElement = 50 for _ in 0..<numberOfElement - temporaryArray.count { let nextElement = temporaryArray[temporaryArray.count - 2] + temporaryArray[temporaryArray.count - 1] temporaryArray.append(nextElement) } return temporaryArray }()上の例では、フィボナッチ数列の初期化を行うクロージャの返り値を、定数
fibonacci
に渡しています(return temporaryArray
)。
{}
の中で初期化の処理をクロージャとして定義して、最後に()
をつけることでそのクロージャを即実行して[Int]
の値を取得しているという感じです。
最後の()
を付けないと、fibonacci
にクロージャ(この場合() -> [Int]
のクロージャ)を代入する形になってしまい、コンパイルエラーが起きます。何故処理が一度しか呼ばれないのか
答えはシンプルで、
initCollectionViewFlowLayout
がStored Propertyだからです。
Computed Propertyは値を保持しないので参照するたびに{}
の中の計算が走ります(例2)が、
Stored Propertyの場合は値を保持するので、参照しても{}
内の計算は行われず、初期化の時に{}
内でreturn
された値を使うだけになります(例3)。例2 ) 要素数50のフィボナッチ数列 (Computed Property)
var fibonacci: [Int] { var temporaryArray = [1, 1] let numberOfElement = 50 for _ in 0..<numberOfElement - temporaryArray.count { let nextElement = temporaryArray[temporaryArray.count - 2] + temporaryArray[temporaryArray.count - 1] temporaryArray.append(nextElement) } print("おるで") return temporaryArray } for i in 0..<fibonacci.count { print(fibonacci[i]) }おるで // fibonacci.countでの参照で呼ばれている おるで // 以降、print(fibonacci[i])での参照で呼ばれている 1 おるで 1 おるで 2 (中略) おるで 4807526976 おるで 7778742049 おるで 12586269025例3 ) Initialization Closureが1度しか呼ばれていないことの確認
let fibonacci: [Int] = { var temporaryArray = [1, 1] let numberOfElement = 50 for _ in 0..<numberOfElement - temporaryArray.count { let nextElement = temporaryArray[temporaryArray.count - 2] + temporaryArray[temporaryArray.count - 1] temporaryArray.append(nextElement) } print("おるで") return temporaryArray }() for i in 0..<fibonacci.count { print(fibonacci[i]) }おるで // 初期化のタイミングで呼ばれている 1 1 2 (中略) 4807526976 7778742049 12586269025まとめ
ここまでの内容をまとめると、今回の
initCollectionViewFlowLayout
は、Initialization Closureの正規の使い方というよりかは、その仕組みを応用した手法なのかな?と感じました。これ思いついた人賢いな〜〜と感心しました。ここまで理解するのにかなり時間がかかってしまいましたが、また便利そうな手法を1つ知ることができたのでこれから上手く使っていこうと思います。何か間違い等あればご指摘お願いします。
参考
- 投稿日:2019-12-18T15:00:47+09:00
【Swift】Partial application of 'mutating' method is not allowedってなんだ
structを使っていたら、
Partial application of 'mutating' method is not allowed
というエラーに遭遇しました。
僕の場合、原因は別件だったんですが、日本語情報があまりないエラーメッセージだったので、記事にしてみます。僕のエラーの原因
僕の場合、
protocol aDelegate { var aView: ViewClass } struct AIRadarMapBorderRenderer { private weak var delegate: aDelegate? private mutating func aFuncation() { switch aView.aVariable { //この行で"Partial application of 'mutating' method is not allowed" case .any //any default: //any } } }という書き方をしたらこのエラーが出ました。
誤switch aView.aVariableここを
正switch delegate?.aView.aVariableとすればOKでした。
それで終わりなんですが、ついでなんでエラーメッセージの意味も調べてみました。
Partial application of 'mutating' method is not allowedってなんだ
Partial application
Partial applicationは、日本語にすると部分適用と訳される技術用語です。
関数型プログラミングっぽい用語で、日本語記事見ると、だいたいカリー化と一緒に紹介されている用語です。
複数の引数を持った関数に対して、普通は全変数に値を渡して、戻り値を得ます。
たとえば、f(a,b,c) = a + b + c という関数があったら、f(1,1,1) = 1 + 1 + 1 = 3 ですね?
これを部分適用だと、f(a,b,c) = f(a, 1, 1) = f(a) + 2 = f(1) + 2 = 3
ただしf(a) = a, f(b, c) = b + cみたいな計算ができます。
命令形言語使ってると、「何が嬉しいんだ……?」という気になりますが、
関数型言語だと関数が汎用的なので、利便性のために汎用性を減らしたいときがあるみたいです。Partial application of 'mutating' method is not allowedの例
Partial application of 'mutating' method is not allowed
エラーメッセージでググったら一番上に出てくるのがこちらの記事。
例struct MyStruct { var count = 0 mutating func add(amount: Int) { count += amount } } var myStruct = MyStruct() [1, 2, 3, 4].forEach(myStruct.add) // Partial application of 'mutating' method is not allowed想定している結果としては、1+2+3+4でcount=10となるのを想定していると思われます。
そもそもforEachの中にカッコ省略してメソッド書いて動くのか……?とかはあるんですが、とりあえず"Partial application of 'mutating' method is not allowed"でエラー。
記事の中でも動くコードの例は出てますが、下記でも動きます。動く例var count = 0 [1, 2, 3, 4].forEach({ amount in count += amount }) print(count) //10なぜこれがエラーになるかというと、内部的には
myStruct.add(1)
↓
myStruct.add(2)
↓
myStruct.add(3)
↓
myStruct.add(4)
↓
myStruct.add(1,2,3,4) ※この関数の場合ここでは何もせずという順番で処理する必要があり、これが部分適用になる訳ですね。
Swiftで部分適用?
Swiftで部分適用(カリー化)にある、
func addTwoNumbers(a: Int)(b: Int) -> Int { return a + b } let add1 = addTwoNumbers(1) add1(b: 2) //< 3という書き方がその昔Swiftオフィシャルでできたらしいのですが、Swiftいくつからなのかは不明ですが、廃止されました。
- 投稿日:2019-12-18T14:54:56+09:00
Sketchと1対1を目指すAtomic designなSwiftUIの作り方
はじめに
VALU Advent Calendar 2019 11日目 (!) の記事です!
VALU ではデザインに Atomic design (アトミックデザイン) を採用しています。
本記事では,VALU のデザイナーが作成した素敵な Sketch シンボルを差分なく反映し,かつ変更に耐えうる SwiftUI を運用するための方法をご紹介しようと思います。昨年の VALU Advent Calendar にて,Sketchと1対1を目指すAtomic designなStoryboardの作り方 を投稿しました。本記事はそれを SwiftUI で行ったものです。まだご覧になっていない方は先にご覧ください。
Highlights
- SwiftUI を利用することで,「ファイル数増加」「子View更新時の伝播」「親ViewのConstraints破棄」など,UIKit (昨年の実装) での懸念点が全て解決された
- Atomic design x SwiftUI の親和性はとても高く,ほとんど1対1で再現することができた
- SwiftUI が迫る今,我々 iOS エンジニアはどう闘っていくのだろうか
昨年の実装方法を振り返る
昨年作成した Storyboard および Xib を以下に改めて掲載します。
Sketch の Level と同様に,細かい単位で Xib ファイルを生成した後,StackView を利用することにより,増減に強い UI の作成が可能となりました。
一見完全に再現できているように見える一方で,View の変更が追従しない問題 など,開発する上での限界も存在していました。
これらの問題は,SwiftUI によって解決されたのでしょうか?Atomic にファイルを分割することによって発生した問題
Atom レベルから Xib と,それに対応する Swift ファイルを生成したことにより,ファイル数が多くなる問題 が生じます。
この問題は,最終的にコンパイルさえ通らなくなるという予期せぬ事態を生む結果となりました。本件につきましては,別の記事で詳細とその解決策を記しています。Atomic design 自体が悪かった,ということではないエラーでしたが,情報共有のため以下に掲載しておきます。
【Xcode】細分化する iOS Architecture に向き合う上で気をつけなければらないただひとつのエラーについて
Atomic design (アトミックデザイン) とは
Atomic Design で有名なこのイラスト。上のイラストはそれを端的に説明しています。
Atomic Design とは,ひとつのページを個々の細かなコンポーネントを組み合わせて作成するデザインの方針を指します。
Atom (原子),Molecules (分子),Organisms (生体),Templates,Pages
その Atomic (原子-の) の名と矢印の方向が指す通り,個々の小さな汎用部品を元に,より大きな View が構成されています。
Sketch に合わせた View ファイルの作成
Sketch では,Atom,Molecules,といった表現が「Lv」によって示されています。
低レベルのシンボルを元に,より大きなシンボルを作成していく形となっています。以下は,弊社デザイナーさんから受け取った,素敵な Sketch ファイルです。
これを,シンボル毎に SwiftUI ファイルにしていきます。
今回は前回同様,複数の View から構成され,いいね等のアクションを行なう画面 (Lv2/Post/Action/Icon Button) に焦点を当てていきます。
Atom の作成
まずは Atom の作成です。
ActionCountableView
という View を作成します。最初だけ振り返りつつ進みます。昨年の実装は上の通り,Xib ファイルと Swift ファイルを用意し,IB 上またはコード上で値を指定していました。
以下が SwiftUI にて作成した内容です。SwiftUI は,左側のコードと右側のプレビューが対応しており,プレビュー画面での変更が即時にコードとして反映されます。ですので,編集するファイルは
.swift
ファイルのみとなります。
#if DEBUG
で囲まれた部分はデバッグ用のプレビューに当たる部分であり,View の構成に必要な struct 部分はたった 20 行で表現できます (Xib を使った昨年の実装は Xwift ファイル 12 行 + Xib ファイル 50 行 → 2 ファイル 62 行)。驚くほど簡単に View を生成することができたのが分かるでしょう。さらに,プレビュー用の記述を追加することで,画像の差し替えや値のランダム化,ダークモード時の様子まで確認することができるようになっています。
これだけでもいくつか SwiftUI としてのテクニックがあります。
1 .
Button
のハイライト領域SwiftUI では,ボタンは
Button
構造体を利用しています。いくつか初期化方法がありますが,ボタンとして認識させ,かつタップ領域としてハイライトさせるためには,Button<Label> : View where Label : View
のLabel
部分がボタン UI として認識されます。Label
は protocolView
に適合した Generics となっています (SwiftUI では UIKit で言うUILabel
はText
に相当します)。
init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
を利用して ボタン内部に View を描く ことによって,ハイライト領域を指定しています。2 .
Image
の色変更およびサイズ変更
.renderingMode(_:)
を.template
指定することにより,色を変更することができます。
また,.frame(width:height:)
でサイズを変更する前には,.resizable()
を入れサイズが可変であることを明示的に指定しなければなりません。3 .
static func
によるプレビュー内再利用ダークモード対応など,同じ Atom の内容を複数の環境で一覧したい場合があるかと思います。プレビューに用いられる
static var previews: some View
は static ですので,再利用したい View を別の static な変数または関数に切り出しておくと,スタブを注入する手間も省けるためお得です。Molecule の作成
次は,先ほど作成した Atom を複数組み合わせた Molecule に当たる View を作成していきます。
Sketch では,この View に対して Override を活用しつつそれぞれの画像を挿入していました。Atomic Design に従った場合,こちらも同様に変更することが可能です。
昨年と異なる点は,値を保ちつつ複数の画面にコードを共有させることができる※ 点です。
Xib のコピペから開放されることで,子 View での変更が 親 View への変更に常に追従できるようになります。これは UI がコードのみから生成できるようになった利点でもあります。UIKit の
StackView
のようにHStack
に積んでいるだけですが,Spacer()
を利用することで,長さ指定のないUIView
同様伸縮するようになります。※
@IBDesignable
を利用した生成方法等によってこれまでも Atoms または Molecules レベルのソースコードを共有することができました。一方でその方法は,Xcode のコンパイル時間が長くなってしまったり,ソースコードの量によって最終的には IB 上で描画が実行されなくなってしまうなどの問題があり,昨年度は採用しなかった経緯があります。Organism の作成
Organisms に入ります。ここで一旦内容を以下のような struct にまとめておきます。後にデータをリスト状に表示させるため,
Hashable
にも適合させることが必要です (ここでは処理を簡略にするためImage
を直接プロパティとして持たせていますが,Domain 層にある API 通信の結果だと想像してください)。投稿内容を反映する
PostView
を作成します。個々の View は Molecules において作成済みですので,VStack を利用して詰み上げるだけで完成します。投稿画像など,存在しない可能性がある場合は
AnyView
とEmptyView
の組み合わせによって動的に実現が可能です。三項演算子を利用していますが,どちらの View もAnyView
でラップしてあげることで結果を返し,異なる View を切り替えることができます。Page の作成
最後に,Page の作成です。
ScrollView
内部にForEach
を作成し,Hashable
に適合したPost
構造体をもとに投稿内容を反映しています。ダークモードにも対応すると,プレビューも映えますね。画像を見れば分かる通り,高さは内容によって自動で調整が入るようになっています。ここまで来れば,もう描画についての責任は Pages から離れることとなり,スクロール処理や遷移等の動作に関わる状態に集中できるようになるでしょう。
実際の開発では,ここから
@State
として記述している部分を,@ObservedObject var viewModel
に置き替え,通信部分や画面遷移の処理を書いていくことになります。まとめ
今回は,過去に使ったものと同じデザインを利用し,SwiftUI を用いて改めて Atomic design に向き合いました。
その結果,SwiftUI を利用することで,「ファイル数増加」「子 View 更新時の伝播」「親 View の Constraints 破棄」など,昨年の UIKit での懸念点が全て解決され,View をデザイン通りに記述することができるようになりました。さらに,UIKit を用いるよりも格段に Sketch に近づいた印象です。これがコードのみで表現されるのは当時は想像もつかなかったのではないでしょうか。これ,HTML で言う CSS を使ってないんですよ!?
Atomic design x SwiftUI の親和性はとても高く,React の知見と上手く融合していくのだと感じました。
これからコード生成プラグインが React のように SwiftUI にも対応するような流れになれば,UI 部分はもっと進化していくのではないでしょうか。一方で,危惧されることが 1 点あります。
(SwiftUI で 1 アプリを作った程度の人間でも)数時間でデザイナーの意図する画面を 1 対 1 として容易に実現可能な点です。これはひとつに,デザイナーが UI 層に進出することを容易に想像できる1つの証拠です。現在の SwiftUI では,スクロール時のページング処理など,ちゃんとした機能を利用しようとすると View が大変複雑になります。Atomic design との親和性は高まったものの,どこまで似せるのか,SwiftUI の得意不得意にどう対応していくのか。
簡単な UI であれば誰でも作れるようになった世界において,領域を奪われた iOS エンジニアの勝負どころはどこになるでしょうか。証明書周りやリジェクト対応と答えた人はセンスがあると思います。実際には ViewModel 以後ですよね。そうなった際にエンジニアとしての価値が再度問われるわけです。
あなたはどうですか?何ができますか?どこまでできますか? と。
References
Sketchと1対1を目指すAtomic designなStoryboardの作り方
Atomic design
Apple Design Resources
Human Interface Guidelines
センスのある某での呟き
- 投稿日:2019-12-18T13:33:03+09:00
Flutterで作ったアプリをfastlane+App Distributionで配信する(iOS編)
開発以外のメンバーに実機テストをしてもらう場合にTestFlightだけで配信してたけど、流石に面倒なのと、ステージング環境用にビルドしたアプリを本番用とは別アプリとして配信したいので、App Distributionで配信することにした。
ひとまずローカル環境でfastlaneを使ってApp Distributionに配信できるようにしたので、備忘録。1.環境を分けてビルド
基本的には@mono0926さんの記事で大体できるはず。とても丁寧に書かれていて、ありがとうございます。
Flutterで環境ごとにビルド設定を切り替える — iOS編1個注意点として、ステージング用に別アプリにする場合、Identifiersをそれ用に用意しないといけない。
例えば、com.test.app
が本番用だとして、com.test.app.stg
みたいなIdentifiersをAppleDeveloperで作成しておく必要がある。普段iOS開発してると当たり前かもだけど、そうじゃないと忘れがち。2.fastlaneのインストール
fastlane周りの設定を行う。
$ sudo gem install fastlane -NVgemでインストールしようとしたところ、
Failed to build gem native extension.
のエラーが出てインストールできなかった。色々調べて、行ったことは以下。
$ xcode-select --install
- AppleDevelopersからCommand Line Toolsをダウンロード(バージョンはmacOS_10.14_for_Xcode_10.3)してインストール
$ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
- rbenvでRubyのバージョンを更新(2.3.0 -> 2.6.5)
こんな感じでなんとかfastlaneのインストールと実行ができるようになった。
3.Fastfileを作る
Flutterの公式ドキュメントを参考にしつつ、fastlaneの設定をする。
$ flutter build ios --release --flavor staging --target lib/main_staging.dart $ cd ios $ fastlane init
fastlane/Fastfile
とfastlane/Appfile
ができるので、Appfileのapp_identifier
やteam_id
、apple_id
を確認する。
TestFlightにアップしたい場合はFASTLANE_PASSWORD
の環境変数にiTunesConnectのパスワードを設定しておく。iOS側のFastfileはこんな感じ。今回はステージング環境用のビルドなのでschemeはStagingを指定している。
fastlane_version "2.28.3" default_platform :ios platform :ios do desc "Submit a new Staging Build to Firebase AppDistribution" lane :staging do gym( scheme: "Staging", export_options: { method: "ad-hoc" } ) end endこれで下記のコマンドでipaファイルを出力できるようになる。
$ fastlane staging4.Firebase App Distributionにアップする
ipaは作れるようになったので、次はApp Distributionにアップできるようにしていく。
まずはプラグインの追加。Gemfileを作るか聞かれるので、yと答えておく。$ fastlane add_plugin firebase_app_distribution [✔] ? [17:42:46]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile [17:42:47]: It looks like fastlane plugins are not yet set up for this project. [17:42:47]: fastlane will create a new Gemfile at path 'Gemfile' [17:42:47]: This change is necessary for fastlane plugins to work [17:42:47]: Should fastlane modify the Gemfile at path 'Gemfile' for you? (y/n) y [17:43:00]: Successfully modified 'Gemfile' [17:43:00]: Make sure to commit your Gemfile, Gemfile.lock and Pluginfile to version control Installing plugin dependencies... Successfully installed pluginsこれで、
Gemfile
とGemfile.lock
、fastlane/Pluginfile
が作成される。あとはFastfileにfirebase_app_distributionのアクションを追加する。fastlane_version "2.28.3" default_platform :ios platform :ios do desc "Submit a new Staging Build to Firebase App Distribution" lane :staging do gym( scheme: "Staging", export_options: { method: "ad-hoc" } ) + firebase_app_distribution( + app: "(Firebase上のアプリ ID)", + groups: "dev", + release_notes: "from fastlane", + firebase_cli_path: `which firebase`.strip() + ) end end
app
にはFirebaseのアプリIDを指定する。Settingsから確認することができる。
groups
はApp Distributionでのテスターのグループを指定する。
あとは、firebase_app_distributionの実行にfirebase-toolsのバージョン7.4.0以上が必要なので、インストールする。今回はローカルからのアップなので、Firebaseへのログインもしておく。$ npm install -g firebase-tools $ firebase login
firebase login
するとブラウザでログイン画面が開かれるので、アカウントを選んでログインする。ちなみにCI等で実行する場合はfirebase login:ci
で取得したトークンをFIREBASE_TOKEN
の環境変数に設定すると良いらしい。以上で準備は整ったので、fastlaneを実行する。Gemfileを作ったので、
bundle exec
から起動する。$ bundle exec fastlane stagingおわり
以上でApp Distributionでアプリが配信されるようになる。
今回はローカルでやったので、署名周りをflutter側で行ったけど、この辺りもfastlaneに任せてGithub ActionsでCDできるようにしたいところ。参考記事
- https://medium.com/flutter-jp/flavor-b952f2d05b5d
- https://qiita.com/skycat_me/items/0a0f5e3982fbfca14444
- https://qiita.com/fuwamaki/items/a9745e1242e24430eb4c#fastlane%E3%82%92%E7%94%A8%E3%81%84%E3%81%9Ffirebase-app-distribution%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E6%96%B9%E6%B3%95
- https://note.com/watura/n/nf49c04e54d09
- https://kouki.hatenadiary.com/entry/2019/10/02/094358
- 投稿日:2019-12-18T12:35:22+09:00
iOS でマルチモジュールを試したときにハマったこと
Web ではマイクロサービスの話をよく聞くようになり、また Android でもたまにマルチモジュールの話を聞くようになりました。
iOS でも企業で採用している話をチラチラ聞くようになり、また今回のアドベントカレンダーにもマルチモジュールについての話が上がっています。
iOSでもマルチモジュール化したい!
https://qiita.com/hironytic/items/3fcd825cc1ef135f5b0fということで私もプライベートで作成している iOS アプリをモジュール分割してみたらハマりまくった話。
環境
- Swift: 5.1
- Xcode: 11.3
- Deployment Target: iOS 13.1
今回はサンプルとして、
- インターフェースなどを提供する
Common
モジュールCommon
モジュールを実装するCore
モジュール- 画面などの機能を提供する
ModuleA
・ModuleB
モジュール- アプリのエントリポイントとなる
MultiModuleSample
モジュールのモジュールに分割してみたいと思います。
依存関係は以下。この構成では
MultiModuleSample
が全てのモジュールを参照していて、逆にCommon
モジュールは全てのモジュールから参照されています。また、
Core
モジュールではみんな大好き Firebase を CocoaPods を使って参照してみました。さらに、
ModuleA
とModuleB
について、
- これから機能が増えてモジュール分割していったとき、このまま Dynamic Library のままだとアプリの起動時間が増えそう
ModuleA
・ModuleB
などの機能のモジュールはCommon
モジュールのように複数から参照される想定ではない- じゃあ全部 Static Library にしようと思ったけど今度はビルドのキャッシュが効きにくくなったりしてビルド時間が増えそう(多分)
- じゃあ Release ビルド時は Static Library、 Debug ビルド時は Dynamic Libary ならちょうどいいのでは?(錯乱)
ということで、
ModuleA
とModuleB
は
- Mach-O Type
- Debug: Dynamic Library
- Release: Static Library
でいきます。
実際のプロジェクトは以下に置きました。
https://github.com/k-ymmt/MultiModuleSample
ただ、あくまでサンプルでいろいろ適当です
Firebase が見つからない
さっそくプロジェクトをマルチモジュール化したのですが、エラーが出ました。
import Core
で Firebase が見つからないと言われてますね。この解決策はいろいろあると思うのですが、Firebase を CocoaPods で導入したことにより毎回のビルド時間がかなり肥大化してしまったこともあり、
以下の記事を参考に CocoaPods の自動統合をやめて手動リンクする道を選びました。CocoaPodsをWorkspaceに自動統合せずに利用する - 24/7 twenty-four seven
最初にプロジェクトの CocoaPods 参照を全て消します。
次に CocoaPods をあらかじめビルドしておくためのスクリプトを用意しました。
そして Core モジュールで参照するための .xcconfig を用意します。
Firebase はリンカー設定がいろいろ増えて管理が大変なので CocoaPods が生成してくれるファイルを参照し、必要箇所だけ上書きする方針にしました。
#include "../Pods/Target Support Files/Pods-Core/Pods-Core.release.xcconfig" PODS_ROOT = $(SRCROOT)/../Pods PODS_BUILD_DIR = $(PODS_ROOT)/Build PODS_CONFIGURATION_BUILD_DIR = $(PODS_BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)最後に MultiModuleSample に参照を追加すれば終わりです。
今回も
Core
モジュールの .xcconfig を参照し、OTHER_LDFLAGS
(Xcode の Build Settings > Other Linker Flags) がいらないので何も指定しないようにしました。#include "../Core/Core/build.xcconfig" OTHER_LDFLAGS = ""これでとりあえずビルドは通るようになりました!
Framework が見つからない
上記の対応をすればビルドは通るようになるのですが、シミュレータで実行すると以下のようなエラーを吐いて死にます。
dyld: Library not loaded: @rpath/GTMSessionFetcher.framework/GTMSessionFetcher Referenced from: ~/Library/Developer/CoreSimulator/Devices/C9744A7D-E41D-4759-9741-8BDF4AFA516A/data/Containers/Bundle/Application/46366FAE-BA12-4645-858A-A93477C7BDF1/MultiModuleSample.app/MultiModuleSample Reason: image not foundCarthage を使ったことがある人は1度は見たことあるエラーですね(過言)
原因は、上で CocoaPods をやめたときに リンカーの設定をしたものの、 .framework をアプリに含めるようにしてないからです。
従来は CocoaPods がよしなに追加してくれていたのですが、 CocoaPods の自動統合をやめた結果、自分でアプリに含めるようにする必要があります。なので MultiModuleSample プロジェクトに以下の Build Phase Script を用意しました。
# ほぼ https://blog.kishikawakatsumi.com/entry/2019/06/17/090724 の「フレームワークのコピー」と同じです ?♂️ code_sign() { # Use the current code_sign_identitiy echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements $1" /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements "$1" } if [ "$ACTION" = "install" ]; then echo "Copy .bcsymbolmap files to .xcarchive" find . -name '*.bcsymbolmap' -type f -exec mv {} "${CONFIGURATION_BUILD_DIR}" \; fi echo 'Copying frameworks' # ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/ がないときにエラーが起きるので作るようにする ls "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" || mkdir "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" if [ $SCRIPT_INPUT_FILE_LIST_COUNT -ne 0 ]; then for i in $(seq 0 $(expr $SCRIPT_INPUT_FILE_LIST_COUNT - 1)); do inputFileListVar="SCRIPT_INPUT_FILE_LIST_${i}" inputFileList="${!inputFileListVar}" cat "${inputFileList}" | while read inputFile; do cp -rf "$inputFile" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" for file in $(find ${inputFile} -type f -perm +111); do # Skip non-dynamic libraries if ! [[ "$(file "$file")" == *"dynamically linked shared library"* ]]; then continue fi if [ "${CODE_SIGNING_REQUIRED}" = "YES" ]; then code_sign "${file}" fi done done done fiあとは Carthage と同じように
Input File Lists
の欄にコピーしたい Framework のパスを書いた .xcfilelist を用意すれば OK です。$(PODS_CONFIGURATION_BUILD_DIR)/GTMSessionFetcher/GTMSessionFetcher.framework $(PODS_CONFIGURATION_BUILD_DIR)/GoogleUtilities/GoogleUtilities.framework $(PODS_CONFIGURATION_BUILD_DIR)/gRPC-Core/grpc.framework $(PODS_CONFIGURATION_BUILD_DIR)/gRPC-C++/grpcpp.framework $(PODS_CONFIGURATION_BUILD_DIR)/leveldb-library/leveldb.framework $(PODS_CONFIGURATION_BUILD_DIR)/nanopb/nanopb.framework $(PODS_CONFIGURATION_BUILD_DIR)/BoringSSL-GRPC/openssl_grpc.framework $(PODS_CONFIGURATION_BUILD_DIR)/abseil/absl.frameworkこれで Debug できるようになりました!
ちなみに
本当は モジュールを使う
MultiModuleSample
ではなく、各 Framework に依存しているCore
モジュールにコピーする処理を書きたかったのですが、dyld: Library not loaded: @rpath/GTMSessionFetcher.framework/GTMSessionFetcher Referenced from: /private/var/containers/Bundle/Application/7B9A24BA-DFC7-4EBB-A5ED-D098BBB3D7F5/MultiModuleSample.app/Frameworks/Core.framework/Core Reason: no suitable image found. Did find: /private/var/containers/Bundle/Application/7B9A24BA-DFC7-4EBB-A5ED-D098BBB3D7F5/MultiModuleSample.app/Frameworks/Core.framework/Frameworks/GTMSessionFetcher.framework/GTMSessionFetcher: code signature in (/private/var/containers/Bundle/Application/7B9A24BA-DFC7-4EBB-A5ED-D098BBB3D7F5/MultiModuleSample.app/Frameworks/Core.framework/Frameworks/GTMSessionFetcher.framework/GTMSessionFetcher) not valid for use in process using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.のようなエラーが発生して死にました。
code signature in (フレームワーク名) not valid for use in process using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.
とあるので、おそらく署名関連で落ちてると思うのですがどう解決したものか悩ましかったので諦めて
MultiModuleSample
でコピーする処理を書きました。
Archive できない
これで実行できるようになったので、試しに Archive をしてみると以下のようなエラーが発生します。
エラー画面に一切の情報がない上にログの中にある
IDEDistribution.critical.log
も空っぽです。ただ、
IDEDistributionPipeline.log
の中をよく見てみると、Assertion failed: Duplicate symbols for 17B1BDAC-7347-380D-B067-67115D777DF5
という記述があり、
17B1BDAC-7347-380D-B067-67115D777DF5
を検索するとbitcode-build-tool built /var/folders/sb/{省略}/MultiModuleSample.app/Frameworks/Core.framework/Frameworks/Common.framework/Common arm64:17B1BDAC-7347-380D-B067-67115D777DF5
というログが見つかります。
どうやら
Common.framework
が .app に複数含まれているせいで bitcode コンパイルに失敗しているみたいです。この原因ですが、各モジュールでの
Common.framework
の参照方法は以下のようになっています。はい、 Embed になっていますね。
この場合、各モジュールはそれぞれモジュール名.framework/Frameworks/
内にCommon.framework
をコピーします。
なのでCommon.framework
が .app に複数含まれてエラーになっていたっぽいです。この解消法は単純に、 エントリポイントである MultiModuleSample プロジェクト以外の全ての Common モジュールを参照するプロジェクトの Embed を 「Do Not Embed」 にするだけです。
結果・・・
Archive に成功しました!
ちなみに
Xcode 10 までは Embed と Link Framework の欄が別々で、見るところだけ気をつければいいだけでした。
Xcode 11 で Embed と Link Framework が合体してデフォルトが Embed になったので、ついポンポン Link させると罠にハマります(ハマりました)。
余計なモジュールが含まれている
上の Archive を見ると必要そうなモジュールのみ含まれているように見えますが、実際にできた .ipa をバラしてみると
ModuleA.framework
とModuleB.framework
が含まれています。上記2つは最初に書いた通り、 Release ビルド時は Static Library としてビルドしているはずなので .framework が含まれる必要はありません。
なので ModuleA ・ ModuleB についても上記のように Embed をやめます。
ただ、このままでは Debug ビルド時に逆に .framework が含まれなくなってしまい、また image not found が発生してしまいます。
なので、今回も前回のように手動コピーさせます。https://blog.kishikawakatsumi.com/entry/2019/06/17/090724 の「フレームワークのコピー」と同じです ?♂️ if [ ${CONFIGURATION} != 'Debug' ];then exit 0 fi # あとは上と同じなので略$(BUILT_PRODUCTS_DIR)/ModuleA.framework $(BUILT_PRODUCTS_DIR)/ModuleB.frameworkただ、このまま実機でデバッグを実行した際、以下のようなメッセージが出てインストールに失敗します。
またもや優しさのかけらもないメッセージですね。
ちなみに Details をクリックすると以下のように詳細を教えてもらえました。なるほど、わからん。
ただ、 Console.app でインストールしたときのログを見たところ、以下のようなものがありました。
0x16ef17000 +[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:]: 183: Failed to verify code signature of /private/var/installd/Library/Caches/com.apple.mobile.installd.staging/temp.5JSbAi/extracted/MultiModuleSample.app/Frameworks/ModuleB.framework : 0xe800801c (No code signature found.)まあなんのことやらさっぱりわかりませんが、なんとなく ModuleB で問題が発生してそうです。
また、 code signature で問題が起きているので署名周りで文句を言われてるのかなと思いました。とりあえず、 ModuleA ・ ModuleB の Signing が 「Automatic」 になっていたので 「Manual」 に変更してみました。
うまくいきました。
なるほど、わからん。
リソースをコピーしたい
ここまでで実行できるようになりましたが、 Release ビルドを Static Library にしたせいでもし
ModuleA
で .xib などリソースファイルを使った場合、 .app に含まれません。
なのでこれも手動でコピーしてやる必要があります。これも上に書いた .framework をコピーするのと同じ要領でいけます。
if [ $CONFIGURATION != "Release" ];then exit 0 fi echo 'Copying Bundle Resources' if [ $SCRIPT_INPUT_FILE_LIST_COUNT -ne 0 ]; then for i in $(seq 0 $(expr $SCRIPT_INPUT_FILE_LIST_COUNT - 1)); do inputFileListVar="SCRIPT_INPUT_FILE_LIST_${i}" inputFileList="${!inputFileListVar}" cat "${inputFileList}" | while read inputFile; do echo "copy $inputFile" cp -rf "$inputFile" "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/" done done fiあとは Input File List に
$(BUILT_PRODUCTS_DIR)/ModuleA.framework/ModuleAXib.nibのようにコピーしたいファイルを書いた .xcfilelist を指定すれば OK です。
ここまでで
これでようやく開発をしていけるくらいには体裁が整ったのかなと思います。
ただ、露見してないだけでまだ問題が潜んでいるかもしれませんし、上に書いたようにこの手の問題は非常にわかりにくいです。
しかし、あくまで個人の見解ですが、大きなプロジェクト、特に大人数で長期間メンテするようなプロジェクトはマルチモジュールの恩恵が大きいと思います。
個人的にもマルチモジュールな構成は好きなのでいろいろ試していきたいです!
学んだこと
- ログをちゃんと見る
- 大抵の問題はログで吐き出されている。 Console.app だったり Xcode の Report Navigator(⌘9)のビルドログに結構ヒントが転がっている
- Diff 大事
- git commit をこまめにしていこう(反省)
- 最後に頼りになるのはやっぱり勘
- なんやかんや頼りになる
- 投稿日:2019-12-18T11:41:09+09:00
[iOS] IPAファイルのプロビジョニングプロファイルを差し替え
[iOS] IPAファイルのプロビジョニングプロファイルを差し替え
iPAファイルを展開する。
unzip *.ipa再署名に必要になるentitlementsファイルを作成
codesign -d --entitlements :- Payload/*.app > entitlements.plist再署名に必要になるentitlementsファイルを作成
「○○○○○○.mobileprovision」は、Apple Developerから更新したプロビジョニングプロファイルcp -p ○○○○○○.mobileprovision ./Payload/*.app/embedded.mobileprovision「replacing existing signature」で成功
再署名を実施
「'iPhone Distribution: ○○○○○○'」は、キーチェーンアクセス.app から確認できる証明書の名称codesign --force --sign 'iPhone Distribution: ○○○○○○' --entitlements entitlements.plist 'Payload/*.app'バージョン番号を更新
Payload/*.appファイル内のInfo.plistを編集iPAファイルを作成
「○○○.ipa」は、任意のIPAファイル名mkdir new-ipa zip -ry new-ipa/○○○.ipa Payloadnew-ipaフォルダにプロビジョニングプロファイルを差し替えたIPAファイルが作成されています。
- 投稿日:2019-12-18T11:40:00+09:00
アプリ開発初心者が Flutter に入門して 1 か月でアプリをリリースした話
はじめに
最近 Flutter に入門しました。無事に一つアプリをリリースすることができたのですが、ネイティブアプリの開発が全くの素人で苦労した点があったので、同じく入門する初心者の方に記録として残していこうと思います。
また、事情があり家の PC でなかなか開発が進められないため社内の PC で業務終了後に開発を進める形になりました。社内環境では Proxy にネットワーク通信が阻まれてしまうため、よくある入門の記事通りに行かないことがとにかく多くあり・・・(というかむしろここが一番苦労したと思います。)
同じく Proxy 環境での開発を余儀なくされている方の助けになれば幸いでございます。なお、Flutter やその周辺等知識が甘い部分が多いと自負しているため誤りやアドバイス等あればコメントしていただけると幸いです。ただちに確認致します。
リリースしたアプリの紹介
BundleApps
こちらのアプリです ⇒ BundleApps
BundleApps とは
インストールしてあるアプリを一覧に表示して使いたいアプリだけを選んで表示することができるアプリです。Web アプリだとStationやStackなど高機能なものがありますが、ネイティブアプリを管理するツールは現在ないのかなと思いました。(調査が足らないだけでもしかしたらあるかもしれない)比較するのもおこがましいほど簡素な作りですがまずは一つアプリをリリースしてみようと思った次第です。
本当は Firebase とバリバリ連携するアプリを作りたかったのですが後述する Proxy の弊害のせいでうまくいかなかったため、ローカルでこじんまりとデータを管理するつくりになっています・・・宜しければインストールして使ってみてください。
開発の際に参考にした書籍・サイトなど
書籍
下記二冊を購入しました。
⇒ 全くの初心者がガンガン読み進めていくのは難しいかなあという印象を受けました。アプリ開発の知見があって Flutter だとどのように書いていくかを知りたい人向けみたいな。まだ読み終えてません。
⇒ 情報が古いからか、Proxy のせいかもわかりませんが Firebase 連携の部分から動かなくて躓きました・・・説明はわりと丁寧かなと思うのですが Flutter は新しめで更新も早いので環境によっては動かないという人も多いと思います。
サイト
色々なところで言われていますが、入門より少し込み入ったことをやろうとすると英語のリソースが多めだと感じました。これ以外は直接パッケージの GitHub リポジトリを見に行ったり、やりたいことでキーワード検索をしたりして開発を進めていきました。
★ Flutter 公式
⇒ 公式が素晴らしいです。英語ですが確かに動きますし、チュートリアルが充実しています。環境構築からサンプルアプリの作成まで解説されているのでこれをみておけば入門としては間違いないと思います。⇒ Flutter で自動生成される初期サンプルを非常に丁寧に説明されています。初心者への配慮があり、理解の助けになりました。これを読んでから圧倒的に開発のスピードが上がった気がします。
⇒ まだ見始めた段階ですが、字幕付きの動画で電車などで学習を進める際に活用しています。
開発期間内訳
【1~2 週目】 Proxy 設定との戦い
問題発生
冒頭で申し上げた通り Proxy に阻まれ苦労しました・・・
Android Studio に抵抗があり、Visual Studio Code で開発を進めようと思い環境構築を行ったのですがシミュレータとの接続がうまくいかない。初期サンプルアプリが立ち上がらない。
Android Studio やコマンドラインからの flutter run だと無事に起動するのでこれは Visual Studio Code の問題だと判断したのですが検索しても有用な情報がなかなか見つからず悩みに悩んでいました。
光明差す?
Proxy 関連の設定だとあたりを付けてよくよく考えてみるとエミュレータと Visual Studio Code 間の通信はローカル通信ではないか?つまり逆に Proxy 設定ありで通信しようとしているからダメなのではないか?と考えて Visual Studio Code の no proxy 設定を検索しだします。
しかし、探せど探せどそれっぽい情報は見つからず・・・
付けるのは Setting.json の編集でいけるのにそこに no proxy の設定が見つかりませんでした。解決?
結論から言うと Visual Studio Code から proxy の設定を外すことで解決しました。
Visual Studio Code では setting.json に Proxy 設定を記入するとそちらを優先してしまうようなのですが、消した場合はシステム環境変数のほうを参照するような仕組みになっているようです。元々環境変数に
変数 値 http_proxy http://proxy-hogehoge.co.jp https_proxy http://proxy-hogehoge.co.jp no_proxy localhost,127.0.0.1 といった記載があったため、そちらを参照してくれるようになり無事にサンプルアプリが動作するようになりました!
さらば Firebase
サンプルアプリは立ち上がったものの書籍通りに進めても Build に成功はするものの Firestore から一切データが取得できないという事態に陥りました。
いくつもサンプルを写経して試したのにダメだったのでこれもやはり Proxy の弊害なのかなと。
アプリ側に Proxy を超えて通信する設定をする必要があるのかと考え調べたのですが良い情報が見つからず、結果一旦 Firebase との連携は諦めてできる範囲でアプリ開発をすることとしました。
※良い情報をお持ちの方がいらっしゃいましたら教えていただけるとありがたいです・・・
アプリ制作開始【3 週目】
躓いた点 ①
チュートリアルに毛が生えた程度のアプリなので、基本的には書籍や解説サイトを参考にしあげることができました。
ただそれでもいくつか躓いた点がありまして・・・まず Dart の非同期処理である async / await に関して理解が甘く、想定通りの動作にならず苦労しました。
具体的には
. . . //アプリ起動時に一度だけ実行される @override void initState() { super.initState(); . . . } . . .というウィジェット作成のタイミングで処理を行うことができる部分で、アプリ一覧を取得 ⇒ 表示という処理を行おうと思ったのですが
. . . // ローカルからアプリケーションのリストを取得する処理 _getLocalData() { SharedPreferences pref = await SharedPreferences.getInstance(); List<String> apk = pref.getStringList("apk") ?? new List<String>(); } // アプリケーション一覧を取得する処理 _createAppList() async { . . //ローカルの情報を元にアプリ情報を取得、リストに追加 apk.forEach((pn) async { ApplicationWithIcon app = await DeviceApps.getApp(pn, true); setState(() { _iconApps.add(app); }); }); } //アプリ起動時に一度だけ実行される @override void initState() { super.initState(); _getLocalData(); _createAppList(); . . } . . .と書いたところ一向にアプリケーションが格納されているはずのリストが空で半日程度悩んでいました。
結論として、
. . . // アプリケーション一覧を取得する処理 createAppList() async { // ここを中にいれる! // ローカルからアプリケーションのリストを取得する処理 SharedPreferences pref = await SharedPreferences.getInstance(); List<String> apk = pref.getStringList("apk") ?? new List<String>(); //ローカルの情報を元にアプリ情報を取得、リストに追加 apk.forEach((pn) async { ApplicationWithIcon app = await DeviceApps.getApp(pn, true); setState(() { _iconApps.add(app); }); }); } //アプリ起動時に一度だけ実行される @override void initState() { super.initState(); _createAppList(); . . } . . .このようにしたら無事にアプリのリストが取得できるようなりました。
認識に誤りがあったら訂正していただきたいのですが、async の内側で処理の完了を待つひとくくりということでしょうか? そもそも非同期処理の理解を深める必要があるなあと痛感した事例です・・・躓いた点 ②
package についてです。
device_appsという package を利用させていただいているのですが、README.md には
Image.memory(app.icon);でアプリのアイコンが取得できると書いてあるのにそのように書くと icon などという情報はないとエラーが出てしまい悩みました。
これも半日程度悩み、アイコン出せないんじゃ・・・と諦めかけたのですが GitHub ソースを読み issue を参照した結果何とか解決することができました。
issue には
' Cannot call "icon" method #20 '
とまさに問題の issue があり、その回答が
'you need to cast to ApplicationWithIcon to use.'
となっていて、なるほど!とソースを読みだした次第です。
. . static Future<Application> getApp(String packageName, [bool includeAppIcon = false]) async { if (packageName.isEmpty) { throw Exception('The package name can not be empty'); } return _channel.invokeMethod('getApp', { 'package_name': packageName, 'include_app_icon': includeAppIcon }).then((app) { if (app != null && app is Map) { return Application(app); } else { return null; } }).catchError((err) { print(err); return null; }); } . . . class ApplicationWithIcon extends Application { final String _icon; ApplicationWithIcon._fromMap(Map map) : assert(map['app_icon'] != null), _icon = map['app_icon'], super._fromMap(map); get icon => base64.decode(_icon); }となっていて、includeAppIcon のフラグを立てたうえでキャストしなきゃいけないのかな?と気づくことができました。
開発経験も浅く、Package のソースを見るということをしてこなかったのですが良い解決策を得ることができると学べたので今後は積極的にソースを参照する癖を付けたいと思います。
結論と今後の展望
- Flutter(Dart)の仕様理解が甘い部分が多いので一旦書籍や動画で理解を深めることから始めたいと思います。
- コードを読むと得られるモノが多かったので Package を利用したい場合はその内側まで踏み込んで利用したいと思います。
今後は付き合いのある店舗のポイントカードアプリを作成してみようかと考えているのですが、流石に高機能になるのかなと・・・
Firebase との連携も必要でしょうし、おすすめのリソースがあればコメントいただけるとありがたいです。特に切実に Proxy 関連( ^ ω ^)・・・
最後に
ここまで読んでいただきありがとうございました!
よろしければアプリを使ってみてもらえたら嬉しいです。
BundleApps
- 投稿日:2019-12-18T11:21:56+09:00
Xcode11でiPadの画面回転設定を行う
iOSアプリを新たに横画面に対応させようとした時にハマったのでメモ。
問題点
通常、画面回転を行いたい場合はTargetのGeneralタブの「Device Orientation」部分のチェックボックスで設定を行うのだが、ここでの設定変更はiPadでは反映されない。(特にエラーメッセージも出ないので、ユーザーのレビューで指摘されるまで気付けなかった…)
解決策
Info.plist を直接編集する。
<key>UISupportedInterfaceOrientations</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeLeft</string> </array> <key>UISupportedInterfaceOrientations~ipad</key> <array> <string>UIInterfaceOrientationPortrait</string> //ここに追記 </array>終わりに
Info.plistが自動で追記されないのは仕様なのか不具合なのか…?
自分の見落としがあるかもしれないので、もっと簡単な解決方法があればコメントで教えていただけると助かります。
- 投稿日:2019-12-18T06:53:24+09:00
SwiftUI UITextField キャレット動作
こんにちはフリーランスの永田です。最近は法人化の手続きを開始しました。
SwiftUI案件を1月から実施予定で、現在技術を調査中です。
今回はキャレット動作 returnButtonを押下しましたら、水平移動する対応です。
環境
Xcode 11.3
SwiftUISwiftUIではない場合(オリジナルです。)
https://gist.github.com/daisukenagata/253ae79692234dbf89d042f5010f2387
キャレット動作しない場合
https://gist.github.com/daisukenagata/5002c49061f18d72e0a40dfda1290b1a
キャレット動作
https://gist.github.com/daisukenagata/253ae79692234dbf89d042f5010f2387
import UIKit import SwiftUI struct ContentView: View { @State var text: String = "" @State var text2: String = "" @State var spacing: CGFloat = 0 @State var didTap = false var body: some View { VStack { HStack(alignment: .bottom, spacing: spacing) { SATextField(tag: 0, placeholder: "placeholder", changeHandler: { (newString) in self.text = newString }, onCommitHandler: { // write something }) self.text.isEmpty == false ? HorizontalLine(color: self.didTap ? Color.red : Color.black) : HorizontalLine(color: self.didTap ? Color.black : Color.red) SATextField(tag: 1, placeholder: "placeholder2", changeHandler: { (newString) in self.text2 = newString }, onCommitHandler: { // write something }) text2.isEmpty == false ? HorizontalLine(color: didTap ? Color.red : Color.black) : HorizontalLine(color: didTap ? Color.black : Color.red) } }.position(.init(x: UIScreen.main.bounds.width/2+50, y: UIScreen.main.bounds.height/2)) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct HorizontalLine: View { private var color: Color? = nil private var height: CGFloat = 1.0 private var shape: HorizontalLineShape? init(color: Color, height: CGFloat = 1.0) { self.color = color self.height = height } var body: some View { HorizontalLineShape().fill(self.color!).frame(minWidth: 0, maxWidth: .infinity, minHeight: height, maxHeight: height) } } struct HorizontalLineShape: Shape { func path(in rect: CGRect) -> Path { let fill = CGRect(x: -rect.size.width, y: 0, width: rect.size.width, height: rect.size.height) var path = Path() path.addRoundedRect(in: fill, cornerSize: CGSize(width: 2, height: 2)) return path } } class Model: ObservableObject { @Published var text = "" var placeholder = "Placeholder" } // check is this // https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2 struct SATextField: UIViewRepresentable { private let tmpView = WrappableTextField() //var exposed to SwiftUI object init var tag:Int = 0 var placeholder:String? var changeHandler:((String)->Void)? var onCommitHandler:(()->Void)? func makeUIView(context: UIViewRepresentableContext<SATextField>) -> WrappableTextField { tmpView.tag = tag tmpView.delegate = tmpView tmpView.placeholder = placeholder tmpView.onCommitHandler = onCommitHandler tmpView.textFieldChangedHandler = changeHandler return tmpView } func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<SATextField>) { uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) uiView.setContentHuggingPriority(.defaultLow, for: .horizontal) } } class WrappableTextField: UITextField, UITextFieldDelegate { var textFieldChangedHandler: ((String)->Void)? var onCommitHandler: (()->Void)? func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField { nextField.becomeFirstResponder() } else { textField.resignFirstResponder() } return false } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let currentValue = textField.text as NSString? { let proposedValue = currentValue.replacingCharacters(in: range, with: string) textFieldChangedHandler?(proposedValue as String) } return true } func textFieldDidEndEditing(_ textField: UITextField) { onCommitHandler?() } }
参考サイト
こちらのロジックを拝借させていただきました。ロジックの部分は、今まで通りのプログラムになります。
https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2
プログラムがわかる場合は
changeHandler
を追えば、すぐにわかると思います起動時に
makeUIView
メソッドでtmpView.textFieldChangedHandler = changeHandler
を代入します。
既存のUITextFieldメソッドで実装しています。onCommitHandler
もbind処理をしていますが、
Flowは同じです。このメソッドは文字が1文字づつ変化する事に処理が行われます。
textFieldChangedHandler?(proposedValue as String)
でbindしてchangeHandler
が呼ばれています。func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let currentValue = textField.text as NSString? { let proposedValue = currentValue.replacingCharacters(in: range, with: string) textFieldChangedHandler?(proposedValue as String) } return true }return Buttonを押下時 これでキャレット
nextField.becomeFirstResponder()
UITextFieldのキーボードを開くメソッドです。func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField { nextField.becomeFirstResponder() } else { textField.resignFirstResponder() } return false }以上、とても簡単に解説しました
貴重なお時間お読みくださいまして、誠にありがとうございます。
- 投稿日:2019-12-18T00:33:06+09:00
レガシーアプリのiOS13対応(UISegmentedControl編
はじめに
UISegmentedControl編とタイトルには記載してますが、
続編等は今の所は全くありませんwwwさて、私が普段担当しているアプリは、業務で利用するInHouseの古いアプリなんですが
今年もiOSのバージョンアップ対応を毎度のごとく行っておりました。UISegmentControlに関しても、今年は大幅に見た目が変わると知っていたので
TintColorの設定をして、アプリに合う見た目にして対応する事で十分だろうと思っていました。先方にも、見た目がiOS13から変更された旨を伝え、一度はOKをもらっていましたが
やっぱり、前の見た目に戻してくれないか?
…
オォォーーー!! w(゚ロ゚;w(゚ロ゚)w;゚ロ゚)w オォォーーー!!
AppleがiOS13で変更した見た目に対して
iOS12以前の状態に戻していくことは
Appleに対しての反逆行為やぞーーーー(´;ω;`)(勝手に思っているだけ?)できれば今のiOS13の見た目のままでカスタマイズしたかったのですが、、、
NGが出てしまったのでは仕方ない。iOS12以前の見た目にするしかない。。できるのか。。。結果的に言えば 可能 でした!!
比較すれば元々のiOS12以前のモノと若干違うのは分かりますが、全然許容範囲!実際のイメージをどうぞ!!
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() if #available(iOS 13.0, *) { let segSizeSingle = CGSize.init(width: self.segment.frame.size.width, height: self.segment.frame.size.height) self.segment.setBackgroundImage(self.makeImage(UIColor.clear, size: segSizeSingle), for: .normal, barMetrics: .default) self.segment.setBackgroundImage(self.makeImage(self.segment.tintColor, size: segSizeSingle), for: .selected, barMetrics: .default) self.segment.setTitleTextAttributes([.foregroundColor : UIColor.white], for: .selected) self.segment.setTitleTextAttributes([.foregroundColor : self.segment.tintColor!], for: .normal) self.segment.layer.borderColor = self.segment.tintColor.cgColor self.segment.layer.borderWidth = 1 } } func makeImage(_ color : UIColor, size : CGSize) -> UIImage { UIGraphicsBeginImageContext(size) let rect = CGRect(origin: CGPoint.zero, size: size) let context = UIGraphicsGetCurrentContext()! context.setFillColor(color.cgColor) context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! }セグメントのBGImageや、レイヤー等の見た目を色々いじって実現しました!
今回のサンプルでは
ViewDidLoad
に記載しましたが
実際に実装する場合はExtention
やObjective-CだとCategory
で実装すると
アプリ全体で見た目を合わす事も簡単にできると思いますので是非、ご活用ください!!!!
(そんな古い見た目に変える要望ねーよ!とかは言わないで…)
最後までご覧いただき、ありがとうございました!!
- 投稿日:2019-12-18T00:31:28+09:00
R.swiftとSwiftGenの導入方法とどちらを採用した方がいいのか
CA Tech Dojo/Challenge/JOB Advent Calendar 2019の18日目は@ostk0069が書かせていただきます。
次の日、19日目は@hmarfさんです!楽しみにしてます!
自分は、2019年8月にCATechDojo(Kotlin編)に参加させていただいた後、11月にCATechJOBでマッチングエージェントさんでiOSエンジニアとしてインターンをさせていただきました。大変お世話になりました。はじめに
私は現在進行形で個人アプリの開発をしています。その際に初めはR.swiftを導入していたのですが、途中からSwiftGenへ移行したのでそこでわかった、互いの良い面、悪い面について触れていければと思います。
R.swift、SwiftGenの話はこの記事を見たら理解が十分な状態に仕上げられていると思うのでよかったら最後まで読んでいただけるとありがたいです。
R.swift、SwiftGenとは
これらは主に文字列管理のしやすさのために使用するライブラリです。
文字列管理で一番最初に思い浮かべるのは多言語化対応かと思いますが、それだけではありません。
多言語化対応しないアプリでも導入する価値は十分にあります。UIImage
やUIColor
の指定もコードでtypeSafeで利用することができるので利便性があります。image literal
やcolor literal
で指定したものは次に開いたときには何を指定していたかわからなくなるのでそれが気に入らない方はコードで指定している分、管理しやすいとも言えます。結論
結論から言うと、R.swiftとSwiftGen、どちらを導入するか迷っている方で少なくともSwiftGenを導入することを大きなコストと捉えていない方はSwiftGenを導入する方がいいのではという気持ちです。
その理由も含めて、まずは導入方法から色々と説明していこうと思います。
このようなフローで説明していきます。
(飛ばしたい方はこちらから選択して見て行って下さい)本来であれば比較とどっちを選ぶべきであるかのみでもいいかなと思ったのですが、
導入方法について、紹介したいTipsがあったので導入方法も追加することにしました!R.swift
導入方法
GitHubに記載されているドキュメントを参照するとCocoaPodsでの導入が推奨されているのでそちらでの方法で説明していきたいと思います。
ここではCocoaPods自体の導入方法は説明しないのでわからない方はこちらからどうぞ。
Podfileへ以下の一行を追加します。
Podfilepod 'R.swift'その後、Terminal上で以下のコマンドを叩きます。
pod installこの後、ドキュメントでは
New Run Script Phase
を選択したのち、"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"
を追加するように書いてあります。しかし、この記述方法はあまりオススメしません。自動生成されるファイルの
R.generated.swift
が自分のプロジェクトファイルのレポジトリ直下に生成されてしまうからです。GitHubでソースコードを管理している人は特にレポジトリ直下に.swift
のファイルが存在するのは結構違和感があるのではないかと思います。なのでNew Run Script Phase
を選択したのち、以下を追加しましょう。"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/"プロジェクトファイル"/"hoge"/"fuga"/R.generated.swift" # プロジェクトファイル以下は自分の好きな場所に追加すればいいと思いますその後、アプリをビルドすることで
R.generated.swift
のファイルが上記で指定したディレクトリに自動生成されます。自動生成されたファイルは.project
への追加は自動でされないので、手動で追加してあげましょう。その後、自動生成ファイルをgitで管理する意味はないので
.gitignore*.generated.swift
を追加してあげることでひとまず導入は終了となります。
記述方法のサンプル
これはドキュメントから引っ張ってきたものなんですが、
通常で書く場合
let icon = UIImage(named: "settings-icon") let font = UIFont(name: "San Francisco", size: 42) let color = UIColor(named: "indicator highlight") let viewController = CustomViewController(nibName: "CustomView", bundle: nil) let string = String(format: NSLocalizedString("welcome.withName"))
R.swift
で記述した場合let icon = R.image.settingsIcon() let font = R.font.sanFrancisco(size: 42) let color = R.color.indicatorHighlight() let viewController = CustomViewController(nib: R.nib.customView) let string = R.string.localizable.welcomeWithName("Arthur Dent")とこのような違いがあります。すべて
R.
が始まる共通点がありますね。SwiftGen
導入方法
R.swift
に対して導入はSwiftGen
の方が少し複雑です。
まず初めに導入方法を選ぶ必要がある訳ですが、今回はR.swift
のときにCocoaPodsで導入の説明を行ったので揃えた方が違いを理解しやすいと思うのでCocoaPods
を使った場合での説明を行っていこうと思います。Podfileへ以下の一行を追加します。
Podfilepod 'SwiftGen'その後、Terminal上で以下のコマンドを叩きます。
pod install次に設定ファイルを作成します。このファイルの設定を記述するのがこの導入を難しくしています。
touch swiftgen.yml
作成した
swiftgen.yml
を編集していく訳ですが、まず初めにドキュメントに書かれているサンプルを見ていきましょうswiftgen.ymlstrings: inputs: Resources/Base.lproj filter: .+\.strings$ outputs: - templateName: structured-swift4 output: Generated/strings.swift xcassets: inputs: - Resources/Images.xcassets - Resources/MoreImages.xcassets outputs: - templateName: swift4 output: Generated/assets-images.swiftこれがどのような設定をしているのか説明していきます。
このサンプルでは
Base.lproj
のファイルからの生成.xcassets
のファイルからの生成を行っています。もちろん、
SwiftGen
ではそれ以外のStoryboard
とかも生成できる訳ですが、それは別途記述が必要です。さらに具体的にどのように書いているか見ていくと、
inputs
とoutputs
が定義されていて
inputs
は生成したいファイルの指定outputs
は生成したもの出力先気になるのは
filter
とtemplateName
というものが存在することです。filterとは
本来であればinputsの中のファイルの指定は
Base.lproj
ではなく、Base.lproj/Localizable.strings
である方が自然です。しかし、多言語化対応をしているとおそらくLocalizable.strings
は対応している言語分あるはずなので指定が面倒なのでfilter: .+\.strings$
と記述してあげることですべてのLocalizable.strings
をinputsの対象に含めることができますtemplateNameとは
SwiftGen
のなかで指定することのできるtemplateName
は以下です。
structured-swift4
flat-swift4
swift4
structured-swift3
flat-swift3
swift3
があります。現在
Swift5
で開発している人が多いからswift5
は?って思う人もいるかもしれませんが現時点では存在しません(swift4
->swift5
でそんなに影響を受けてないからだと思います)
string
の指定をするときにはswift4
やswift3
の指定は行えないので注意してください。
flat
かstructured
を選択するときは構造的にしたいかしたくないかで選びましょう。string
の場合、多言語化対応が多いと思うのでstructured
を推奨します。この選択肢以外にも自分でよりカスタマイズしたい方は別の方法が存在します。
それはStencilを使うことで実現可能です。元々上記のstructured-swift4
やflat-swift4
もStencilを使って作成されたテンプレートです。気になった方はぜひ見てみてください。(この記事ではこの内容にこれ以上Stencilについては触れません)記述方法のサンプル
通常で書く場合
let icon = UIImage(asset: Asset.Exotic.icon) let displayRegular = UIFont(font: FontFamily.SanFrancisco.regular, size: 42.0) let title = UIColor(named: .articleBody) let string = String(format: NSLocalizedString("welcome.with_name"))
SwiftGen
で記述した場合let icon = Asset.Exotic.banana.image let displayRegular = FontFamily.SFNSDisplay.regular.font(size: 42.0) let title = ColorName.articleBody.color let message = L10n.Welcome.withNameR.swiftとSwiftGenの比較
特徴を並べていきます。
R.swift
- 導入が楽
- リソースが単一
- buildするタイミングで毎回自動生成が走る
SwiftGen
-swiftgen.yml
ファイルでカスタマイズできるので拡張性がある
- リソースが複数
- 差分を見て生成するかどうかを見てくれる今回この記事を書くきっかけにもなったのですが、私はどちらも週数間の運用をしました。
その中でSwiftGenの方がいいと思ったので途中で乗り換えました
理由としては
UIView
はR.string
やSwiftGen
で管理する理由がそこまでないので、網羅性のあるR.swift
の強みが生きなかった- メインで使うlocalizeStringが長いので画面名とか役割とかを付け足すと
R.swift
だと長くなってしまう
R.swift
だと:R.string.localizable.hoge
SwiftGen
だと:L10n.hoge
- 設定する側を
R.swift
だとスネークケースで書くとスネークケースのままで生成されてしまうが、SwiftGen
だとスネークケースで書いたものをキャメルケースで生成してくれる- 末尾に()がつくかどうか
- コードを書いている時の保管として
()なし
、()
、(Void)
の3つの選択肢が出てくるのでめんどいの4つが主なものです。
僕は、特に中2つを利用したいがためにSwiftGenを採用しました。特にlocalizeStringの長さの話はすごく重要だと思っています。
良くあるケースとして{ViewControllerの名前}.{UIViewの名前}.{enumの名前}.title
とかを指定した場合は可読性が下がりやすいのでなるべく呼び出すときのデフォルトのクラス名は少ない方が嬉しいです。
また、スネークケース、キャメルケースの話も、長く運用する上では大事だと思っています。Localizable.Strings
でキャメルケースを書くのは自分はあまり好みには思えませんでした。
これらは実際にどっちも運用したことがないと気づけない話なので参考になれば嬉しいです。一番最初に言った、どちらを導入するか迷っている方でSwiftGenを導入することを大きなコストと捉えていない方はSwiftGenを導入するべきはこれらが理由です。
R.swift
は導入しやすい分、で細かいところに届きにくい設計になってしまいます。長期的な運用を考えるとそこのリスクはとるのは難しいかもしれません。逆に言えば、これらをメリットと感じないのであれば
R.swift
を使う方がいいと言えます。どっちを選ぶべきなのか
最終的には当たり前ですが、好きな方を選べばいいという話に落ち着きます。しかし、この記事を見ている方は長期運用を想定した上で文字列管理をしようとしている方が多いのではないでしょうか。めんどくさい作業が発生するので安易に乗り換えることはできないので将来性を加味して選ぶ必要があると思います(経験談)
参考資料