Perusahaan Seluler iOS Baru. Bagian # 1: Pembuatan Kode untuk Sumber Daya

Halo semuanya!


Nama saya Dmitry. Kebetulan saya adalah pemimpin tim di tim 13 pengembang iOS selama dua tahun terakhir. Dan bersama-sama kami bekerja pada aplikasi Tinkoff Business .


Saya ingin berbagi dengan Anda pengalaman kami tentang cara merilis aplikasi pada saat yang tidak terduga dengan set fitur maksimum atau perbaikan bug dan masih belum menjadi abu-abu.


Saya akan memberi tahu Anda tentang praktik dan pendekatan yang membantu tim secara signifikan mempercepat pengembangan dan pengujian dan secara signifikan mengurangi jumlah stres, bug, masalah dengan rilis yang tidak dijadwalkan atau mendesak. #MakeReleaseWithoutStress .


Ayo pergi!


Deskripsi masalah


Bayangkan situasi berikut.


Ada rilis lain. Itu didahului oleh pengujian regresi, penguji kembali menemukan tempat di mana alih-alih teks dalam aplikasi, ID baris ditampilkan.


Bug pelokalan

Ini adalah salah satu masalah kami yang paling sering kami temui.


Anda mungkin tidak mengalami masalah ini jika Anda tidak memiliki aplikasi yang dilokalkan dalam bahasa lain, atau semua pelokalan ditulis langsung dalam kode tanpa menggunakan file Localizable.strings.


Tetapi Anda mungkin menghadapi masalah lain yang kami akan bantu Anda selesaikan:


  • Aplikasi macet karena Anda salah menentukan nama gambar dan membuat paksa membuka
    UIImage(named: "NotExist")! 
  • Aplikasi macet jika storyboard tidak ditambahkan ke target
  • Aplikasi macet jika Anda membuat pengontrol dari storyboard dengan ID yang tidak ada
  • Aplikasi macet jika Anda membuat pengontrol dari storyboard dengan ID yang ada, tetapi dilemparkan ke kelas yang salah
  • Perilaku yang tidak dapat diprediksi jika Anda menggunakan font dalam kode yang tidak ditambahkan ke info.plist, atau jika file font tidak ditandai dengan target: crash dimungkinkan, atau dimungkinkan untuk mendapatkan font standar alih-alih yang Anda butuhkan. Pengembang Apple: Font Kustom , Stackoverflow: crash
  • Aplikasi macet jika storyboard menunjukkan ke controller kelas yang tidak ada
  • Sekelompok kode monoton yang membuat ikon, font, pengontrol, tampilan
  • Tidak ada gambar, ikon dalam runtime, meskipun nama gambar ada di storyboard, tetapi tidak dalam aset
  • Papan cerita menggunakan font yang tidak ada di info.plist
  • ID baris muncul di aplikasi, alih-alih dilokalkan di tempat yang tak terduga, karena menghapus garis di Localizable.strings (menganggapnya tidak digunakan)
  • Hal lain yang saya lupa sebutkan, atau belum kita jumpai.

Alasan → Efek


Mengapa ini semua terjadi?


Ada kode program yang mengkompilasi. Jika Anda menulis sesuatu yang salah (secara sintaksis, atau nama fungsi yang salah saat memanggil), maka proyek Anda tidak akan dirakit. Ini bisa dimengerti, jelas dan logis.


Tetapi bagaimana dengan hal-hal seperti sumber daya?


Mereka tidak dikompilasi, mereka hanya ditambahkan ke bundel setelah kode dikompilasi. Dalam hal ini, sejumlah besar masalah dapat terjadi dalam runtime, misalnya, kasus yang dijelaskan di atas - dengan string dalam pelokalan.


Cari solusinya


Kami berpikir tentang bagaimana masalah tersebut diselesaikan secara umum, dan bagaimana kami dapat memperbaikinya. Saya ingat salah satu konferensi Cocoaheads di mail.ru. Ada pembicaraan tentang membandingkan alat pembuat kode.


Setelah melihat sekali lagi apa alat-alat ini (perpustakaan / kerangka kerja) tentang semua, kami akhirnya menemukan apa yang dibutuhkan.


Pada saat yang sama, pendekatan serupa telah digunakan oleh pengembang untuk Android selama bertahun-tahun. Google memikirkan mereka dan menjadikannya alat yang luar biasa. Tetapi Apple, bahkan Xcode stabil, tidak dapat melakukan ...


Yang tersisa hanyalah mencari tahu instrumen mana yang harus dipilih: Natalie , SwiftGen atau R.swift ?


Natalie tidak memiliki dukungan pelokalan, diputuskan untuk segera meninggalkannya. SwiftGen dan R.swift memiliki kemampuan yang sangat mirip. Kami memilih R.swift, hanya berdasarkan jumlah bintang, mengetahui bahwa setiap saat kami dapat berubah ke SwiftGen.


Bagaimana R.swift bekerja


Skrip tahap pembuatan pra-kompilasi diluncurkan, dijalankan melalui struktur proyek dan menghasilkan file bernama R.generated.swift , yang perlu ditambahkan ke proyek (kami akan memberi tahu Anda lebih lanjut tentang cara melakukan ini di akhir).


File memiliki struktur berikut:


 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: "") } } } } 

Penggunaan:


 let str = R.string.localizable.card_actions_activate_apple_pay() print(str) >  Apple Pay 

"Mengapa Rswift.StringResource membutuhkan Rswift.StringResource ?", Anda bertanya. Saya sendiri tidak mengerti mengapa membuatnya, tetapi, seperti yang penulis jelaskan, diperlukan untuk yang berikut: tautan .


Aplikasi dunia nyata


Penjelasan kecil dari konten di bawah ini:


* Itu - mereka menggunakan pendekatan untuk sementara waktu, pada akhirnya, mereka meninggalkannya
* Ini telah menjadi - pendekatan yang kita gunakan saat menulis kode baru
* Itu tidak, tetapi Anda dapat memilikinya - suatu pendekatan yang tidak pernah ada dalam aplikasi kami, tetapi saya bertemu di berbagai proyek, pada saat-saat yang jauh, ketika saya belum bekerja di Tinkoff.ru.


Lokalisasi


Kami mulai menggunakan R.swift untuk pelokalan, ini menyelamatkan kami dari masalah yang kami tulis di awal. Sekarang, jika id di lokalisasi telah berubah, maka proyek tidak akan dirakit.


* Ini hanya berfungsi jika Anda mengubah id di semua pelokalan ke yang lain. Jika string tetap berada di salah satu pelokalan, maka pada saat kompilasi akan ada peringatan bahwa id ini tidak dilokalkan dalam semua bahasa.


Peringatan

Tidak di sana, tetapi Anda mungkin memiliki:
 final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } } 

Itu:
 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 } } 

Itu menjadi:
 titleLabel.text = R.string.localizable.newsTitle() 

Gambar


Sekarang, jika kita mengganti nama sesuatu di * .xcassets, dan tidak mengubah kode, maka proyek tidak akan dirakit.


Itu:
 imageView.image = UIImage(named: "NotExist") //     imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash 

Itu menjadi:
 imageView.image = R.image.tinkoffLogo() //     

Papan cerita


Itu:
 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() ¯\_(ツ)_/¯} 

Itu menjadi:
 guard let vc = R.storyboard.someStoryboard.someViewController() else { //    -  ,  Fabric  Firebase //    fatalError() ¯\_(ツ)_/¯ } 

Dan sebagainya.


Storyboard Validasi


R.validate () adalah alat yang luar biasa yang mengalahkan tangan (atau lebih tepatnya, hanya melempar kesalahan ke dalam catch block) jika Anda melakukan sesuatu yang salah di storyboard atau file xib.
Sebagai contoh:


  • Menunjukkan nama gambar, yang tidak ada dalam proyek
  • Mereka menunjukkan font, dan kemudian berhenti menggunakannya dan menghapusnya dari proyek (dari info.plist)

Penggunaan:


 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 } } 

Dan sekarang Anda siap membeli dua!


Diam dan ambil uangku!

Bagaimana cara mengimplementasikannya?


* Sistem berbasis komponen - wiki , konsep pengembangan kode di mana komponen (satu set layar / modul dihubungkan bersama) dikembangkan di lingkungan tertutup (dalam kasus kami, di pod lokal) untuk mengurangi koherensi basis kode. Banyak orang tahu pendekatan di backend, yang didasarkan pada konsep ini - layanan microser.


* Monolith - wiki , konsep pengembangan kode, di mana seluruh basis kode terletak pada satu repositori, dan kode tersebut terkait erat. Konsep ini cocok untuk proyek-proyek kecil dengan serangkaian fungsi yang terbatas.


Jika Anda sedang mengembangkan aplikasi monolitik atau hanya menggunakan dependensi pihak ketiga, maka Anda beruntung (tetapi ini tidak akurat). Ikuti tutorial dan lakukan semuanya dengan ketat.


Ini bukan kasus kami. Kami terlibat. Karena kami menggunakan sistem berbasis komponen, menanamkan R.swift di aplikasi utama, kami memutuskan untuk menanamkannya di pod lokal (yang merupakan komponen).


Karena pembaruan pelokalan, gambar, dan semua elemen yang memengaruhi file R.generated.swift, ada banyak konflik dalam file yang dihasilkan saat digabungkan ke cabang umum. Dan untuk menghindari ini, Anda harus menghapus R.generated.swift dari repositori git. Penulis juga merekomendasikan untuk melakukan ini .


Tambahkan baris berikut ke .gitignore .


 # R.Swift generated files *.generated.swift 

Selain itu, jika Anda tidak ingin membuat kode untuk beberapa sumber daya, Anda selalu dapat menggunakan mengabaikan file individual atau seluruh folder:


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

deskripsi .rswiftignore


Seperti dalam proyek utama, penting bagi kita untuk tidak menambahkan file R.generated.swift dari pod lokal ke repositori git. Kami mulai mempertimbangkan opsi bagaimana hal ini dapat dilakukan:


  • alias pada R.generated.swift sehingga file (alias, misalnya: R.swift) ditambahkan ke proyek, dan kemudian, ketika mengkompilasi dengan referensi, file nyata tersedia. Tapi cocoapod itu pintar, dan tidak diizinkan melakukannya
  • di podspec pada fase pra-kompilasi, tambahkan file R.generated.swift ke proyek itu sendiri menggunakan skrip, tetapi kemudian akan ditambahkan hanya sebagai file dalam sistem file, dan file tersebut tidak akan muncul dalam proyek
  • Pilihan lainnya kurang lebih rapi
  • sihir di podfile


    Ajaib
    Ajaib

     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 



  • dan opsi lain ... masih menambahkan R.generated.swift ke git

Kami sementara memilih opsi: "magic in the Podfile", meskipun faktanya ada beberapa kekurangan:


  • Itu hanya dapat diluncurkan dari root proyek (meskipun cocoapods dapat diluncurkan dari hampir semua folder dalam proyek)
  • Semua pod harus memiliki folder bernama Sumber (walaupun ini tidak penting jika podnya sudah beres)
  • Dia aneh dan tidak bisa dipahami, tetapi cepat atau lambat dia harus mendukung (ini masih penopang)
  • Jika beberapa pustaka pihak ketiga ada di folder dengan “LocalPods” di jalurnya, maka itu akan mencoba untuk menambahkan file R.generated.swift di sana atau akan crash dengan kesalahan

prep_command


Hidup beberapa lama dengan naskah dan penderitaan, saya memutuskan untuk mempelajari topik ini lebih luas dan menemukan pilihan lain.
Di Podspec ada prep_command , yang dimaksudkan hanya untuk membuat dan memodifikasi sumber, yang kemudian akan ditambahkan ke proyek.


* Berita - nama pod, yang harus diganti dengan nama pod lokal Anda
* sentuh - perintah untuk membuat file. Argumen adalah jalur relatif ke file (termasuk nama file dengan ekstensi)


Selanjutnya kita akan melakukan penipuan dengan News.podspec


Script ini disebut pertama kali pod install dijalankan dan menambahkan file yang kita butuhkan ke folder sumber di perapian.


 Pod::Spec.new do |s| # ... generated_file_path = "News/Sources/R.generated.swift" s.prepare_command = <<-CMD touch "#{generated_file_path}" CMD # ... end 

Berikutnya adalah "tipuan dengan telinga" - kita perlu membuat panggilan ke skrip R.swift untuk perapian lokal.


 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 

Benar, ada satu "tetapi." prepare_command tidak berfungsi dengan pod lokal, atau lebih tepatnya berfungsi, tetapi dalam beberapa kasus khusus. Ada diskusi tentang topik ini di Github .


Kematian


* Fatality - wiki , hit terakhir di Mortal Kombat.


Setelah sedikit riset, saya menemukan solusi lain - gabungan dari pendekatan c prepare_command dan pre_install .


Modifikasi kecil sihir dari 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 

Dan skrip yang sama yang tidak berjalan untuk perapian lokal


 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 

Pada akhirnya, itu berfungsi seperti yang kita harapkan.


Akhirnya!


PS:


Saya mencoba membuat perintah kustom lain alih-alih prepare_command , tetapi pod lib lint (perintah untuk memvalidasi konten podspec dan perapian sendiri) bersumpah pada variabel tambahan dan tidak lulus.


Perapian non-lokal


Di pod jarak jauh (yang masing-masing di repositori mereka sendiri), Anda tidak memerlukan semua keajaiban skrip ini, yang dijelaskan di atas, karena di sana basis kode terikat erat dengan versi ketergantungan.


Cukup dengan menanamkan Contoh (proyek yang dihasilkan setelah pod lib membuat perintah <Name>) skrip R.swift itu sendiri dan menambahkan R.generated.swift ke paket pustaka (bawah). Jika proyek tidak memiliki Contoh, maka Anda harus menulis skrip yang akan mirip dengan yang saya kutip.


PS:


Ada klarifikasi kecil:
R.swift + Xcode 10 + sistem build baru + build inkremental! = <3
Informasi lebih lanjut tentang masalah di halaman utama perpustakaan atau di sini
R.swift v4.0.0 tidak bekerja dengan cocoapods 1.6.0 :(
Saya pikir segera semua masalah akan diperbaiki.


Kesimpulan


Anda selalu perlu menjaga kualitas bar setinggi mungkin. Ini sangat penting untuk aplikasi yang bekerja dengan keuangan.


Dalam hal ini, Anda tidak perlu membebani pengujian dan menemukan bug sedini mungkin. Dalam kasus kami, ini adalah saat pengembang menyusun kode, atau saat uji coba untuk Tarik Permintaan. Dengan demikian, kami menemukan kurangnya lokalisasi bukan oleh tatapan penuh perhatian dari penguji atau oleh tes otomatis, tetapi oleh proses yang biasa membangun aplikasi.


Anda juga perlu mempertimbangkan fakta bahwa ini adalah alat pihak ketiga yang terkait dengan struktur proyek dan mem-parsing isinya. Jika struktur file proyek berubah, maka alat harus diubah.
Kami mengambil risiko ini dan, dalam hal ini, selalu siap untuk mengubah alat ini ke yang lain atau menulis milik Anda sendiri.


Dan keuntungan dari R.swift adalah sejumlah besar jam kerja yang dapat dihabiskan tim untuk hal-hal yang jauh lebih penting: fitur baru, solusi teknis baru, peningkatan kualitas, dan sebagainya. R.swift sepenuhnya mengembalikan jumlah waktu yang dihabiskan untuk integrasinya, bahkan dengan mempertimbangkan kemungkinan penggantiannya di masa depan dengan solusi serupa lainnya.


R. cepat


Bonus


Anda dapat bermain-main dengan contoh untuk segera melihat dengan mata kepala sendiri keuntungan dari pembuatan kode untuk sumber daya. Kode sumber proyek "untuk bermain-main": GitHub .


Terima kasih banyak untuk membaca artikel atau hanya pergi ke tempat ini, saya senang dalam hal apapun)


Itu saja.

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


All Articles