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.

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:
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.

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!

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
pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods'
- 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|
Berikutnya adalah "tipuan dengan telinga" - kita perlu membuat panggilan ke skrip R.swift untuk perapian lokal.
Pod::Spec.new do |s|
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|
Dan skrip yang sama yang tidak berjalan untuk perapian lokal
Pod::Spec.new do |s|
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.