مؤسسة iOS Mobile Enterprise الجديدة. الجزء رقم 1: إنشاء التعليمات البرمجية للموارد

مرحبا بالجميع!


اسمي دميتري. حدث ذلك أنني قائد فريق في فريق من 13 مطورًا لنظام iOS خلال العامين الماضيين. ومعاً نعمل على تطبيق Tinkoff Business .


أريد أن أشاركك تجربتنا حول كيفية إصدار التطبيق في لحظة غير متوقعة مع الحد الأقصى من الميزات أو إصلاحات الأخطاء ولا تزال غير رمادية.


سأخبرك عن الممارسات والأساليب التي ساعدت الفريق على تسريع التطوير والاختبار بشكل كبير وتقليل كمية الضغط ، والأخطاء ، والمشكلات المتعلقة بالإصدار غير المجدول أو العاجل. #MakeReleaseWithoutStress .


دعنا نذهب!


وصف المشكلة


تخيل الموقف التالي.


هناك إصدار آخر. سبقه اختبار الانحدار ، وجد المختبرون مرة أخرى مكانًا حيث يتم عرض معرف السطر بدلاً من النص في التطبيق.


علة التعريب

كانت هذه واحدة من أكثر المشاكل التي واجهتنا.


قد لا تواجه هذه المشكلة إذا لم يكن التطبيق مترجمًا بلغة أخرى ، أو تمت كتابة كل الترجمة في سطور مباشرة في التعليمات البرمجية دون استخدام ملف Localizable.strings.


ولكن قد تواجه مشاكل أخرى سنساعدك على حلها:


  • يتعطل التطبيق لأنك حددت اسم الصورة بشكل غير صحيح وقمت بفك الضغط
    UIImage(named: "NotExist")! 
  • يتعطل التطبيق إذا لم تتم إضافة لوحة العمل إلى الهدف
  • يتعطل التطبيق إذا قمت بإنشاء وحدة تحكم من لوحة العمل بمعرف غير موجود
  • يتعطل التطبيق إذا قمت بإنشاء وحدة تحكم من لوحة العمل بمعرف موجود ، ولكن تم إرسالها إلى الفئة غير الصحيحة
  • سلوك غير متوقع إذا كنت تستخدم خطًا في التعليمات البرمجية لا تتم إضافته إلى info.plist ، أو إذا لم يتم وضع علامة على ملف الخط مع الهدف: من الممكن حدوث عطل ، أو من الممكن ببساطة الحصول على خط قياسي بدلاً من الخط الذي تحتاجه. مطور آبل: خطوط مخصصة ، Stackoverflow: تحطم
  • يتعطل التطبيق إذا أشارت لوحات العمل إلى وحدة التحكم فئة غير موجودة
  • مجموعة من التعليمات البرمجية الرتيبة التي تنشئ رموزًا وخطوطًا ووحدات تحكم وطرق عرض
  • لا توجد صور ورموز في وقت التشغيل ، على الرغم من أن اسم الصورة موجود في لوحة العمل ، ولكن ليس في الأصول
  • تستخدم لوحة العمل خطًا غير موجود في info.plist
  • تظهر معرفات الصف في التطبيق ، بدلاً من أن تكون مترجمة في أماكن غير متوقعة ، بسبب حذف الأسطر في Localizable.strings (يعتقد أنها لم تستخدم)
  • شيء آخر نسيت أن أذكره ، أو لم نواجهه بعد.

السبب → التأثير


لماذا يحدث كل هذا؟


يوجد كود البرنامج الذي يجمع. إذا كتبت شيئًا خاطئًا (نحويًا أو اسم وظيفة غير صحيح عند الاتصال) ، فلن يتم تجميع مشروعك ببساطة. هذا أمر مفهوم ومفهوم ومنطقي.


ولكن ماذا عن أشياء مثل الموارد؟


لا يتم تجميعها ، يتم إضافتها ببساطة إلى الحزمة بعد تجميع الرمز. في هذا الصدد ، يمكن أن يحدث عدد كبير من المشاكل في وقت التشغيل ، على سبيل المثال ، الحالة الموضحة أعلاه - مع سلاسل في الترجمة.


ابحث عن حل


فكرنا في كيفية حل هذه المشاكل بشكل عام ، وكيف يمكننا إصلاح ذلك. تذكرت أحد مؤتمرات Cocoaheads على mail.ru. كان هناك حديث عن مقارنة أدوات توليد الكود.


بعد الاطلاع مرة أخرى على موضوع هذه الأدوات (المكتبات / الأطر) ، وجدنا أخيرًا ما هو مطلوب.


في الوقت نفسه ، تم استخدام نهج مماثل من قبل المطورين لنظام Android لسنوات. فكرت Google بها وجعلتها مثل هذه الأداة خارج الصندوق. لكن Apple ، حتى Xcode المستقرة ، لا يمكنها أن تفعلنا ...


كل ما تبقى هو معرفة الأداة التي تختارها فقط: Natalie أو SwiftGen أو R.swift ؟


لم يكن لدى ناتالي دعم التوطين ، فقد تقرر التخلي عنه على الفور. كان لدى SwiftGen و R.Swift قدرات متشابهة جدًا. لقد اخترنا R.swift ، بناءً على عدد النجوم ببساطة ، مع العلم أنه في أي وقت يمكننا التغيير إلى SwiftGen.


كيف يعمل R.swift


يتم إطلاق نص برمجي لمرحلة البناء قبل الترجمة ، ويتم تشغيله من خلال بنية المشروع R.generated.swift ملفًا يسمى R.generated.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 للتوطين ، وقد أنقذنا من المشاكل التي كتبنا عنها في البداية. الآن ، إذا تغير المعرف في الأقلمة ، فلن يتم تجميع المشروع.


* يعمل هذا فقط إذا قمت بتغيير المعرف في جميع الترجمات إلى أخرى. إذا بقيت سلسلة في أي من الترجمات ، فسيظهر تحذير عند التجميع بأن هذا المعرف غير مترجم في جميع اللغات.


تحذير

ليس هناك ، ولكن قد يكون لديك:
 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.
على سبيل المثال:


  • أشارت إلى اسم الصورة التي ليست في المشروع
  • أشاروا إلى الخط ، ثم توقفوا عن استخدامه وحذفوه من المشروع (من 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 } } 

والآن أنت مستعد لشراء اثنين!


اخرس وخذ نقودي!

كيفية التنفيذ؟


* نظام قائم على المكونات - ويكي ، مفهوم تطوير الكود حيث يتم تطوير المكونات (مجموعة من الشاشات / الوحدات المتصلة ببعضها البعض) في بيئة مغلقة (في حالتنا ، في القرون المحلية) من أجل تقليل تماسك قاعدة الكود. كثير من الناس يعرفون النهج في الواجهة الخلفية ، والذي يقوم على هذا المفهوم - الخدمات المصغرة.


* Monolith - ويكي ، مفهوم تطوير الكود ، حيث تكمن قاعدة الكود بأكملها في مستودع واحد ، ويرتبط الكود ارتباطًا وثيقًا. هذا المفهوم مناسب للمشاريع الصغيرة مع مجموعة محدودة من الوظائف.


إذا كنت تقوم بتطوير تطبيق متآلف أو تستخدم تبعيات تابعة لجهة خارجية فقط ، فأنت محظوظ (لكن هذا ليس دقيقًا). خذ البرنامج التعليمي وافعل كل شيء بدقة.


لم تكن هذه قضيتنا. لقد تدخلنا. نظرًا لأننا نستخدم نظامًا قائمًا على المكونات ، تضمين R.swift في التطبيق الرئيسي ، فقد قررنا تضمينه في القرون المحلية (وهي مكونات).


نظرًا للتحديث المستمر للترجمات والصور وجميع العناصر التي تؤثر على ملف R.generated.swift ، هناك العديد من التعارضات في الملف الذي تم إنشاؤه عند الدمج في الفرع المشترك. ولتجنب ذلك ، يجب عليك إزالة R.generated.swift من مستودع git. كما يوصي المؤلف بذلك .


أضف الأسطر التالية إلى .gitignore .


 # R.Swift generated files *.generated.swift 

أيضًا ، إذا كنت لا تريد إنشاء رمز لبعض الموارد ، فيمكنك دائمًا استخدام تجاهل الملفات الفردية أو المجلدات بأكملها:


 "${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore" 

وصف .rswiftignore


كما هو الحال في المشروع الرئيسي ، كان من المهم بالنسبة لنا عدم إضافة ملفات R.generated.swift من القرون المحلية إلى مستودع git. بدأنا النظر في خيارات لكيفية القيام بذلك:


  • الاسم المستعار على R.generated.swift بحيث تتم إضافة الملف (الاسم المستعار ، على سبيل المثال: R.swift) إلى المشروع ، وبعد ذلك ، عند الترجمة حسب المرجع ، يتوفر الملف الحقيقي. لكن cocoapods ذكية ، ولا يُسمح لها بذلك
  • في podspec في مرحلة ما قبل الترجمة ، أضف ملف R.generated.swift إلى المشروع نفسه باستخدام البرامج النصية ، ولكن بعد ذلك سيتم إضافته ببساطة كملف في نظام الملفات ، ولن يظهر الملف في المشروع
  • خيارات أخرى أكثر أو أقل مرتبة
  • السحر في podfile


    سحر
    سحر

     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 إلى بوابة

استقرنا مؤقتًا على الخيار: "السحر في Podfile" ، على الرغم من حقيقة أنه كان لديه عدد من أوجه القصور:


  • يمكن إطلاقه فقط من جذر المشروع (على الرغم من أنه يمكن تشغيل cocoapods من أي مجلد تقريبًا في المشروع)
  • يجب أن تحتوي جميع القرون على مجلد يسمى المصادر (على الرغم من أن هذا ليس حرجًا إذا كانت القرون مرتبة)
  • كان غريبًا وغير مفهوم ، ولكن عاجلاً أم آجلاً كان عليه أن يدعم (هذا لا يزال عكازاً)
  • إذا كانت هناك مكتبة تابعة لجهة خارجية في مجلد يحتوي على "LocalPods" في مسارها ، فستحاول إضافة ملف R.generated.swift هناك وإلا فسوف تتعطل مع وجود خطأ

تحضير أمر


كنت أعيش لبعض الوقت مع النص والمعاناة ، قررت دراسة هذا الموضوع على نطاق أوسع ووجدت خيارًا آخر.
في Podspec ، هناك أمر تجهيز معد ، وهو مخصص فقط لإنشاء المصادر وتعديلها ، والتي ستتم إضافتها بعد ذلك إلى المشروع.


* الأخبار - اسم الجراب ، الذي يجب استبداله باسم جرابك المحلي
* لمس - الأمر لإنشاء ملف. الوسيطة هي المسار النسبي للملف (بما في ذلك اسم الملف بالملحق)


بعد ذلك سنقوم بعمليات احتيال مع News.podspec


يُسمى هذا البرنامج النصي أول مرة pod install تشغيل 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 

التالي هو "خدعة مع آذان" أخرى - نحن بحاجة إلى إجراء مكالمة إلى النص النصي 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 

صحيح ، هناك واحد "لكن". لا يعمل prepare_command مع القرون المحلية ، أو بالأحرى يعمل ، ولكن في بعض الحالات الخاصة. هناك مناقشة حول هذا الموضوع على جيثب .


إماتة


* الوفاة - ويكي ، الضربة الأخيرة في مورتال كومبات.


بعد المزيد من البحث ، وجدت حلًا آخر - مزيج من النهج 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 

في النهاية ، يعمل كما نتوقع.


أخيرًا!


ملاحظة:


حاولت عمل أمر مخصص آخر بدلاً من prepare_command ، لكن pod lib lint (أمر للتحقق من محتوى podspec والموقد نفسه) يقسم على متغيرات إضافية ولا يمر.


المواقد غير المحلية


في القرون البعيدة (تلك الموجودة في المستودعات الخاصة بها) ، لا تحتاج إلى كل هذه البرمجة النصية ، التي تم وصفها أعلاه ، لأن قاعدة التعليمات البرمجية مرتبطة بشكل صارم بإصدار التبعية.


يكفي ببساطة تضمين المثال (مشروع تم إنشاؤه بعد الأمر pod lib بإنشاء أمر <اسم>) البرنامج النصي R.swift نفسه وإضافة R.generated.swift إلى حزمة المكتبة (أسفل). إذا لم يكن المشروع يحتوي على مثال ، فسيتعين عليك كتابة نصوص مشابهة لتلك التي ذكرتها.


ملاحظة:


هناك توضيح صغير:
R.swift + Xcode 10 + نظام بناء جديد + بناء تزايدي! = <3
مزيد من المعلومات حول المشكلة في الصفحة الرئيسية للمكتبة أو هنا
لا يعمل R.swift v4.0.0 مع cocoapods 1.6.0 :(
أعتقد أنه سيتم قريبا تصحيح جميع المشاكل.


الخلاصة


تحتاج دائمًا إلى الحفاظ على شريط الجودة على أعلى مستوى ممكن. هذا مهم بشكل خاص للتطبيقات التي تعمل مع التمويل.


في هذه الحالة ، لا تحتاج إلى زيادة التحميل على الاختبار والعثور على الأخطاء في أقرب وقت ممكن. في حالتنا ، يكون هذا إما في الوقت الذي قام فيه المطور بتجميع الشفرة ، أو أثناء التشغيل التجريبي لطلبات السحب. وبالتالي ، نجد نقصًا في التوطين ليس من خلال النظرة اليقظة للمختبرين أو من خلال الاختبارات الآلية ، ولكن من خلال العملية المعتادة لبناء التطبيق.


تحتاج أيضًا إلى مراعاة حقيقة أن هذه أداة تابعة لجهة خارجية مرتبطة ببنية المشروع وتوزع محتواها. إذا تغير هيكل ملف المشروع ، فيجب تغيير الأداة.
لقد اتخذنا هذه المخاطرة ، وفي هذه الحالة ، نحن دائمًا على استعداد لتغيير هذه الأداة إلى أي أداة أخرى أو كتابة أداة خاصة بك.


والمكاسب من R.swift هي عدد كبير من ساعات العمل التي يمكن أن يقضيها الفريق في أشياء أكثر أهمية: الميزات الجديدة والحلول التقنية الجديدة وتحسين الجودة وما إلى ذلك. أعادت R.swift تمامًا مقدار الوقت الذي تم قضاؤه في تكامله ، حتى مع الأخذ بعين الاعتبار إمكانية استبداله في المستقبل بحل مماثل آخر.


ر. سويفت


مكافأة


يمكنك اللعب بمثال لرؤية على الفور بأم عينيك الربح من توليد الشفرة للموارد. الكود المصدري للمشروع " للتلاعب ": GitHub .


شكرا جزيلا لقراءة المقال أو لتصفح هذا المكان ، يسعدني على أي حال)


هذا كل شئ.

Source: https://habr.com/ru/post/ar431148/


All Articles