みなさんこんにちは!
私の名前はドミトリーです。 私は過去2年間、13人のiOS開発者のチームのチームリーダーであることがたまたまありました。 そして、一緒にTinkoff Businessアプリケーションに取り組んでいます。
最大の機能セットまたはバグ修正を使用して、予期しない瞬間にアプリケーションをリリースし、それでも灰色にならない方法に関する経験を共有したいと思います。
チームが開発とテストを大幅に加速し、予定外のリリースや緊急リリースに伴うストレス、バグ、問題の量を大幅に削減するのに役立つプラクティスとアプローチについて説明します。 #MakeReleaseWithoutStress 。
行こう!
問題の説明
次の状況を想像してください。
別のリリースがあります。 回帰テストが先行し、テスターは再びアプリケーション内のテキストの代わりに行IDを表示する場所を見つけました。
これは、私たちが遭遇した最も頻繁な問題の1つでした。
アプリケーションを別の言語にローカライズしていない場合、またはすべてのローカライズがLocalizable.stringsファイルを使用せずにコード内の行に直接記述されている場合、この問題は発生しません。
ただし、解決に役立つ他の問題が発生する場合があります。
- 画像の名前を誤って指定し、強制的にラップを解除したため、アプリケーションがクラッシュします
UIImage(named: "NotExist")!
- ストーリーボードがターゲットに追加されない場合、アプリケーションがクラッシュします
- 存在しないIDでストーリーボードからコントローラーを作成すると、アプリケーションがクラッシュします
- 既存のIDでストーリーボードからコントローラーを作成したが、間違ったクラスにキャストすると、アプリケーションがクラッシュします
- info.plistに追加されていないコードでフォントを使用する場合、またはフォントファイルにtargetのマークが付いていない場合、予期しない動作:クラッシュが発生するか、必要なフォントの代わりに標準フォントを取得することができます。 開発者Apple:カスタムフォント 、 Stackoverflow:crash
- ストーリーボードが存在しないクラスをコントローラーに示すと、アプリケーションがクラッシュします
- アイコン、フォント、コントローラー、ビューを作成する単調なコードの束
- ランタイムには画像やアイコンはありませんが、画像の名前はストーリーボードにありますが、アセットにはありません
- ストーリーボードはinfo.plistにないフォントを使用しています
- Localizable.stringsの行を削除したため、予期しない場所にローカライズする代わりに、行IDがアプリケーションに表示されます(使用されていないと考えられます)
- 私が言及するのを忘れた、またはまだ遭遇していない何か。
理由→効果
なぜこれがすべて起こっているのですか?
コンパイルするプログラムコードがあります。 間違った記述をした場合(構文的に、または呼び出されたときに関数名が正しくない場合)、プロジェクトは単にアセンブルされません。 これは理解可能で、明白で論理的です。
しかし、リソースのようなものはどうですか?
これらはコンパイルされず、コードのコンパイル後にバンドルに追加されるだけです。 この点で、実行時に多数の問題が発生する可能性があります。たとえば、前述のローカライズの文字列の場合などです。
ソリューションを検索する
このような問題が一般的にどのように解決され、どのように修正できるかを考えました。 mail.ruのCocoaheads会議の1つを思い出しました。 コード生成ツールの比較についての講演がありました。
これらのツール(ライブラリ/フレームワーク)が何であるかをもう一度見て、最終的に必要なものが見つかりました。
同時に、Androidの開発者も同様のアプローチを何年も使用しています。 Googleはそれらについて考え、それらを箱から出してそのようなツールにしました。 しかし、Appleは、安定したXcodeでさえ、私たちにできない...
それを見つけるために残った-どのツールを選択するか: ナタリー 、 SwiftGenまたはR.swift ?
ナタリーはローカライズをサポートしていなかったため、すぐに放棄することにしました。 SwiftGenとR.swiftの機能は非常に似ていました。 R.swiftを選択しました。これは、星の数に基づいており、いつでもSwiftGenに変更できることを知っています。
R.swiftの仕組み
プリコンパイルビルドフェーズスクリプトが起動し、プロジェクト構造を実行し、プロジェクトに追加する必要があるR.generated.swift
というファイルを生成します(最後にこれを行う方法について詳しく説明します)。
ファイルの構造は次のとおりです。
import Foundation import Rswift import UIKit /// This `R` struct is generated and contains references to static resources. struct R: Rswift.Validatable { fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap(Locale.init) ?? Locale.current fileprivate static let hostingBundle = Bundle(for: R.Class.self) static func validate() throws { try intern.validate() } // ... /// This `R.string` struct is generated, and contains static references to 2 localization tables. struct string { /// This `R.string.localizable` struct is generated, and contains static references to 1196 localization keys. struct localizable { /// en translation: Apple Pay /// /// Locales: en, ru static let card_actions_activate_apple_pay = Rswift.StringResource(key: "card_actions_activate_apple_pay", tableName: "Localizable", bundle: R.hostingBundle, locales: ["en", "ru"], comment: nil) // ... /// en translation: Apple Pay /// /// Locales: en, ru static func card_actions_activate_apple_pay(_: Void = ()) -> String { return NSLocalizedString("card_actions_activate_apple_pay", bundle: R.hostingBundle, comment: "") } } } }
使用法:
let str = R.string.localizable.card_actions_activate_apple_pay() print(str) > Apple Pay
「なぜRswift.StringResource
が必要Rswift.StringResource
ですか?」とあなたは尋ねます。 私自身はそれを生成する理由を理解していませんが、著者が説明しているように、それは次のために必要です: リンク
実世界のアプリケーション
以下の内容の簡単な説明:
*それは-彼らはしばらくの間アプローチを使用し、最終的には彼らがそれを去った
*なった-新しいコードを書くときに使用するアプローチ
*それはそうではありませんでしたが、それは可能です-私たちのアプリケーションには存在しなかったアプローチですが、私はTinkoff.ruでまだ働いていなかった当時のさまざまなプロジェクトでそれを満たしました。
ローカリゼーション
ローカライズにR.swift
を使用し始めたので、最初に書いた問題から私たちを救うことができました。 これで、ローカライズのIDが変更された場合、プロジェクトはアセンブルされません。
*これは、すべてのローカリゼーションでIDを別のローカライズに変更した場合にのみ機能します。 ローカリゼーションのいずれかに文字列が残っている場合、コンパイル時に、このIDがすべての言語でローカライズされていないという警告が表示されます。
final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } }
extension String { public func localized(in bundle: Bundle = .main, value: String = "", comment: String = "") -> String { return NSLocalizedString(self, tableName: nil, bundle: bundle, value: value, comment: comment) } } final class NewsViewController: UIViewController { private enum Localized { static let newsTitle = "news_title".localized() } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = Localized.newsTitle } }
titleLabel.text = R.string.localizable.newsTitle()
画像
ここで、* .xcassetsの名前を変更し、コードを変更しなかった場合、プロジェクトは単にアセンブルされません。
imageView.image = UIImage(named: "NotExist") // imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash
imageView.image = R.image.tinkoffLogo() //
絵コンテ
let someStoryboardName = "SomeStoryboard" // Change to something else (eg: "somestoryboard") - get nil or crash in else let someVCIdentifier = "SomeViewController" // Change to something else (eg: "someviewcontroller") - get nil or crash in else let storyboard = UIStoryboard(name: someStoryboardName, bundle: .main) let _vc = storyboard.instantiateViewController(withIdentifier: someVCIdentifier) guard let vc = _vc as? SomeViewController else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯}
guard let vc = R.storyboard.someStoryboard.someViewController() else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯ }
などなど。
検証ストーリーボード
R.validate()は、ストーリーボードまたはxibファイルで何か間違ったことをした場合に手を打つ(または、単にcatchブロックにエラーを投げる)素晴らしいツールです。
例:
- プロジェクトにない写真の名前を示した
- 彼らはフォントを示し、それを使用を停止し、プロジェクトから削除しました(info.plistから)
使用法:
final class AppDelegate: UIResponder { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { #if DEBUG do { try R.validate() } catch { // fatalError // debug // - , production fatalError(error.localizedDescription) } #endif return true } }
これで、2つ購入する準備ができました!
実装方法
*コンポーネントベースのシステム-wiki (コード開発の概念)。コンポーネント(相互接続された画面/モジュールのセット)は、コードベースの一貫性を減らすために閉じた環境(この場合、ローカルポッド)で開発されます。 多くの人々は、この概念に基づいているバックエンドのアプローチを知っています-マイクロサービス。
* Monolith- wiki 、コード開発の概念。コードベース全体が1つのリポジトリにあり、コードは密接に関連しています。 この概念は、有限の機能セットを持つ小規模プロジェクトに適しています。
モノリシックアプリケーションを開発している場合、またはサードパーティの依存関係のみを使用している場合は、幸運です(ただし、これは正確ではありません)。 チュートリアルを実行し、厳密にすべてを行ってください。
これは私たちの場合ではありませんでした。 私たちは関与しました。 メインアプリケーションにR.swift
埋め込むこと
、コンポーネントベースのシステムを使用するため、ローカルポッド(コンポーネント)に埋め込むことにしました。
ローカライズ、画像、およびR.generated.swiftファイルに影響するすべての要素が絶えず更新されているため、共通ブランチにマージすると、生成されたファイルに多くの競合があります。 これを回避するには、gitリポジトリからR.generated.swiftを削除する必要があります。 また、著者はこれをすることを勧めます。
.gitignore
次の行を追加します。
# R.Swift generated files *.generated.swift
また、一部のリソースのコードを生成したくない場合は、個々のファイルまたはフォルダー全体を無視していつでも使用できます。
"${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore"
メインプロジェクトと同様に、ローカルポッドからR.generated.swiftファイルをgitリポジトリに追加しないことが重要でした。 私たちはこれをどのように行うかの選択肢を検討し始めました。
- R.generated.swiftのエイリアス。これにより、ファイル(エイリアス、たとえばR.swift)がプロジェクトに追加され、参照によるコンパイル時に実際のファイルが使用可能になります。 しかし、ココアポッドは賢く、そうすることは許可されていません
- プリコンパイルフェーズのpodspecで、スクリプトを使用してR.generated.swiftファイルをプロジェクト自体に追加しますが、ファイルシステムにファイルとして単純に追加され、ファイルはプロジェクトに表示されません
- 他の多かれ少なかれきちんとしたオプション
ポッドファイルの魔法
魔法
pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods' # LocalPods, , pod_target_srcroot = pod_target.pod_target_srcroot # pod_target_path = pod_target_srcroot.sub('${PODS_ROOT}/..', '.') # pod_target_sources_path = pod_target_path + '/' + pod_target.name + '/Sources' # Sources generated_file_path = pod_target_sources_path + '/R.generated.swift' # R.generated.swift File.new(generated_file_path, 'w') # R.generated.swift end end end
- そして別のオプション...まだR.generated.swiftをgitに追加します
いくつかの欠点があったにもかかわらず、一時的に「Podfileの魔法」というオプションに決めました。
- プロジェクトのルートからのみ起動できます(ただし、ココアポッドはプロジェクトのほぼすべてのフォルダーから起動できます)
- すべてのポッドにはSourcesという名前のフォルダーが必要です(ただし、ポッドが整頓されている場合、これは重要ではありません)
- それは奇妙で理解しがたいものでしたが、遅かれ早かれサポートされなければなりませんでした(それでも松葉杖です)
- いくつかのサードパーティライブラリがパスに「LocalPods」を含むフォルダーにある場合、そこにR.generated.swiftファイルを追加しようとするか、エラーでクラッシュします
prepare_command
しばらくの間台本と苦しみを抱えて生活していた私は、このトピックをより広く研究することを決め、別の選択肢を見つけました。
Podspecにはprepare_commandがあります 。これは、ソースを作成および変更するためだけのもので、プロジェクトに追加されます。
*ニュース-ポッドの名前。ローカルポッドの名前に置き換える必要があります
* touch-ファイルを作成するコマンド。 引数は、ファイルへの相対パスです(拡張子を持つファイルの名前を含む)
次に、News.podspecで詐欺を行います
このスクリプトは、 pod install
初めて実行されたときに呼び出され、必要なファイルを炉内のソースフォルダーに追加します。
Pod::Spec.new do |s| # ... generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD # ... end
次は、もう1つの「耳のフェイント」です。ローカルの囲炉裏でR.swiftを呼び出すスクリプトを作成する必要があります。
Pod::Spec.new do |s| # ... s.dependency 'R.swift' r_swift_script = '"${PODS_ROOT}/R.swift/rswift" generate "${PODS_TARGET_SRCROOT}/News/Sources"' s.script_phases = [ { :name => 'R.swift', :script => r_swift_script, :execution_position => :before_compile } ] end
確かに、1つの「しかし」があります。 prepare_command
はローカルポッドでは機能しませんが、むしろ機能しますが、特殊な場合があります。 Githubでこのトピックに関する議論があります。
死亡者
*死亡-wiki、Mortal Kombatの最終ヒット。
もう少し調査した結果、別の解決策が見つかりました-アプローチc prepare_command
とpre_install
ハイブリッド。
Podfileからの魔法の小さな変更:
pre_install do |installer| # map development pods installer.development_pod_targets.each do |target| # get only main spec and exclude subspecs spec = target.non_test_specs.first # get full podspec file path podspec_file_path = spec.defined_in_file # get podspec dir path pod_directory = podspec_file_path.parent # check if path contains local pods directory # exclude development but non local pods local_pods_directory_name = "LocalPods" if pod_directory.to_s.include? local_pods_directory_name # go to pod root directorty and run prepare command in sub-shell system("cd \"#{pod_directory}\"; #{spec.prepare_command}") end end end
そして、地元の囲炉裏で実行されなかった同じスクリプト
Pod::Spec.new do |s| # ... s.dependency 'R.swift' generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD r_swift_script = '"${PODS_ROOT}/R.swift/rswift" generate "${PODS_TARGET_SRCROOT}/News/Sources"' s.script_phases = [ { :name => 'R.swift', :script => r_swift_script, :execution_position => :before_compile } ] end
最終的に、これは期待どおりに機能します。
ついに!
PS:
prepare_command
代わりに別のカスタムコマンドを作成しようとしましたが、 pod lib lint
(podspecのコンテンツとハース自体を検証するコマンド)が余分な変数を誓ってパスしません。
非地元の囲炉裏
リモートポッド(それぞれが独自のリポジトリにあるもの)では、コードベースが依存バージョンに厳密に結び付けられているため、上記のすべてのスクリプトマジックは必要ありません。
R.swiftスクリプトをサンプル自体(pod lib create <Name>コマンドの後に生成されたプロジェクト)に埋め込み、R.generated.swiftをライブラリパッケージ(下)に追加するだけで十分です。 プロジェクトにサンプルがない場合は、私が引用したスクリプトに似たスクリプトを作成する必要があります。
PS:
ちょっとした説明があります:
R.swift + Xcode 10 +新しいビルドシステム+インクリメンタルビルド!= <3
問題の詳細については、ライブラリのメインページまたはこちらをご覧ください
R.swift v4.0.0は、cocoapods 1.6.0では動作しません:(
私はすぐにすべての問題が修正されると思います。
おわりに
常に品質バーを可能な限り高く保つ必要があります。 これは、財務で動作するアプリケーションにとって特に重要です。
テストをオーバーロードして、バグをできるだけ早く見つける必要はありません。 私たちの場合、これは開発者がコードをコンパイルした時点、またはプルリクエストのテスト実行中のいずれかです。 したがって、ローカリゼーションの欠如は、テスターや自動化されたテストの注意深い目ではなく、アプリケーションを構築する通常のプロセスによるものです。
また、これはプロジェクトの構造に関連付けられ、そのコンテンツを解析するサードパーティのツールであるという事実も考慮する必要があります。 プロジェクトファイルの構造が変更された場合、ツールを変更する必要があります。
私たちはこのリスクを負いました。その場合、このツールを他のツールと交換したり、独自のツールを作成したりする準備ができています。
また、R.swiftから得られるメリットは、チームがはるかに重要なことに費やすことができる膨大な工数です:新機能、新しい技術ソリューション、品質改善など。 R.swiftは、統合に費やされた時間を完全に返し、将来的に同様の別のソリューションに置き換えられる可能性も考慮しました。
ボーナス
例を試して、リソースのコード生成による利益をすぐに自分の目で確認できます。 「 遊び回る 」プロジェクトのソースコード: GitHub