新しいiOSモバイルエンタープライズ。 パート1:リソースのコード生成

みなさんこんにちは!



私の名前はドミトリーです。 私は過去2年間、13人のiOS開発者のチームのチームリーダーであることがたまたまありました。 そして、一緒にTinkoff Businessアプリケーションに取り組んでいます。







最大の機能セットまたはバグ修正を使用して、予期しない瞬間にアプリケーションをリリースし、それでも灰色にならない方法に関する経験を共有したいと思います。







チームが開発とテストを大幅に加速し、予定外のリリースや緊急リリースに伴うストレス、バグ、問題の量を大幅に削減するのに役立つプラクティスとアプローチについて説明します。 #MakeReleaseWithoutStress







行こう!



問題の説明



次の状況を想像してください。







別のリリースがあります。 回帰テストが先行し、テスターは再びアプリケーション内のテキストの代わりに行IDを表示する場所を見つけました。







ローカリゼーションのバグ

これは、私たちが遭遇した最も頻繁な問題の1つでした。







アプリケーションを別の言語にローカライズしていない場合、またはすべてのローカライズがLocalizable.stringsファイルを使用せずにコード内の行に直接記述されている場合、この問題は発生しません。







ただし、解決に役立つ他の問題が発生する場合があります。









理由→効果



なぜこれがすべて起こっているのですか?







コンパイルするプログラムコードがあります。 間違った記述をした場合(構文的に、または呼び出されたときに関数名が正しくない場合)、プロジェクトは単にアセンブルされません。 これは理解可能で、明白で論理的です。







しかし、リソースのようなものはどうですか?







これらはコンパイルされず、コードのコンパイル後にバンドルに追加されるだけです。 この点で、実行時に多数の問題が発生する可能性があります。たとえば、前述のローカライズの文字列の場合などです。







ソリューションを検索する



このような問題が一般的にどのように解決され、どのように修正できるかを考えました。 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ブロックにエラーを投げる素晴らしいツールです。

例:









使用法:







 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"
      
      





.rswiftignoreの説明







メインプロジェクトと同様に、ローカルポッドからR.generated.swiftファイルをgitリポジトリに追加しないことが重要でした。 私たちはこれをどのように行うかの選択肢を検討し始めました。











いくつかの欠点があったにもかかわらず、一時的に「Podfileの魔法」というオプションに決めました。









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は、統合に費やされた時間を完全に返し、将来的に同様の別のソリューションに置き換えられる可能性も考慮しました。







R.スウィフト







ボーナス



例を試して、リソースのコード生成による利益をすぐに自分の目で確認できます。 「 遊び回る 」プロジェクトのソースコード: GitHub







記事を読んでくれたり、この場所に移動してくれてありがとう。



それだけです。




All Articles