Kontrol Sumberdaya. Kustomisasi SwiftGen

Mungkin, di setiap proyek iOS besar, proyek yang tahan lama dapat menemukan ikon yang tidak digunakan di mana pun, atau akses ke kunci pelokalan yang sudah tidak ada sejak lama. Paling sering, situasi seperti itu muncul dari kurangnya perhatian, dan otomatisasi adalah obat terbaik untuk kekurangan perhatian.


Di tim iOS HeadHunter, kami memberikan perhatian besar untuk mengotomatisasi tugas-tugas rutin yang mungkin dihadapi pengembang. Dengan artikel ini kami ingin memulai siklus cerita tentang alat dan pendekatan yang menyederhanakan pekerjaan kami sehari-hari.


Beberapa waktu lalu, kami berhasil mengendalikan sumber daya aplikasi menggunakan utilitas SwiftGen. Tentang cara mengkonfigurasinya, bagaimana hidup dengannya, dan bagaimana utilitas ini membantu menggeser cek untuk relevansi sumber daya dengan kompiler, dan kita akan berbicara tentang cat.



SwiftGen adalah utilitas yang memungkinkan Anda menghasilkan kode Swift untuk mengakses berbagai sumber daya proyek Xcode, di antaranya:


  • font
  • warna
  • papan cerita;
  • string lokalisasi;
  • aset.

Setiap orang dapat menulis kode serupa untuk menginisialisasi gambar atau string pelokalan:


logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current ) 

Untuk menunjukkan nama gambar atau kunci lokalisasi, kami menggunakan string literal. Apa yang ditulis di antara tanda kutip ganda tidak divalidasi oleh kompiler atau lingkungan pengembangan (Xcode). Di situlah letak serangkaian masalah berikut:


  • Anda dapat membuat kesalahan ketik;
  • Anda mungkin lupa memperbarui penggunaan dalam kode setelah mengedit atau menghapus kunci / gambar.

Mari kita lihat bagaimana kita dapat meningkatkan kode ini dengan SwiftGen.


Untuk tim kami, generasi hanya relevan untuk string dan aset, dan mereka akan dibahas dalam artikel. Generasi untuk jenis sumber daya lainnya serupa dan, jika diinginkan, mudah dikuasai secara mandiri.


Implementasi Proyek


Pertama, Anda perlu menginstal SwiftGen. Kami memilih untuk menginstalnya melalui CocoaPods sebagai cara mudah untuk mendistribusikan utilitas di antara semua anggota tim. Tapi ini bisa dilakukan dengan cara lain, yang dijelaskan secara rinci dalam dokumentasi . Dalam kasus kami, semua yang perlu dilakukan adalah menambahkan pod 'SwiftGen' , dan kemudian menambahkan fase build baru ( Build Phase ) yang akan meluncurkan SwiftGen sebelum membangun proyek.


 "$PODS_ROOT"/SwiftGen/bin/swiftgen 

Penting untuk menjalankan SwiftGen sebelum memulai fase Compile Sources untuk menghindari kesalahan saat mengkompilasi proyek.



Sekarang kami siap untuk mengadaptasi SwiftGen ke proyek kami.


Konfigurasikan SwiftGen


Pertama-tama, Anda perlu mengonfigurasi templat yang dengannya kode untuk mengakses sumber daya akan dihasilkan. Utilitas sudah berisi seperangkat templat untuk menghasilkan kode, semuanya dapat dilihat di github dan, pada prinsipnya, mereka siap digunakan. Template ditulis dalam bahasa Stensil , Anda mungkin akrab dengannya jika Anda menggunakan Sourcery atau dimainkan dengan Kitura . Jika diinginkan, masing-masing templat dapat disesuaikan dengan panduan masing-masing.
Misalnya, ambil templat yang dibuat enum untuk mengakses string pelokalan. Tampak bagi kami bahwa standar itu terlalu berlebihan dan dapat disederhanakan. Contoh sederhana dengan komentar penjelasan ada di bawah spoiler.


Contoh template
 {#      #} {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} {#    #} {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} _ p{{forloop.counter}}: {{type}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} p{{forloop.counter}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {#       enum        #} {% macro recursiveBlock table item sp %} {{sp}}{% for string in item.strings %} {{sp}}{% if not param.noComments %} {{sp}}/// {{string.translation}} {{sp}}{% endif %} {{sp}}{% if string.types %} {{sp}}{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { {{sp}} return localize("{{string.key}}", {% call argumentsBlock string.types %}) {{sp}}} {{sp}}{% else %} {{sp}}{{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = localize("{{string.key}}") {{sp}}{% endif %} {{sp}}{% endfor %} {{sp}}{% for child in item.children %} {{sp}}{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {{sp}}{% set sp2 %}{{sp}} {% endset %} {{sp}}{% call recursiveBlock table child sp2 %} {{sp}}} {{sp}}{% endfor %} {% endmacro %} import Foundation {#   enum #} {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} {{accessModifier}} enum {{enumName}} { {% if tables.count > 1 %} {% for table in tables %} {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {% call recursiveBlock table.name table.levels " " %} } {% endfor %} {% else %} {% call recursiveBlock tables.first.name tables.first.levels " " %} {% endif %} } {#  enum Localization         #} extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } } 

Lebih mudah untuk menyimpan file template itu sendiri di root proyek, misalnya, di folder SwiftGen/Templates sehingga template ini tersedia untuk semua orang yang bekerja di proyek.
Utilitas ini mendukung konfigurasi melalui file YAML swiftgen.yml , di mana Anda dapat menentukan jalur ke file sumber, templat, dan parameter tambahan. Buat di root proyek di folder Swiftgen , dan kemudian kelompokkan file lain yang terkait dengan skrip ke folder yang sama.
Untuk proyek kami, file ini mungkin terlihat seperti ini:


 xcassets: - paths: ../SwiftGenExample/Assets.xcassets templatePath: Templates/ImageAssets.stencil output: ../SwiftGenExample/Image.swift params: enumName: Image publicAccess: 1 noAllValues: 1 strings: - paths: ../SwiftGenExample/en.lproj/Localizable.strings templatePath: Templates/LocalizableStrings.stencil output: ../SwiftGenExample/Localization.swift params: enumName: Localization publicAccess: 1 noComments: 0 

Bahkan, jalur ke file dan template ditunjukkan di sana, serta parameter tambahan yang diteruskan ke konteks template.
Karena file tersebut bukan di root proyek, kita perlu menentukan path ke sana ketika memulai Swiftgen. Ubah skrip startup kami:


 "$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml 

Sekarang proyek kami dapat dirakit. Setelah perakitan, dua file Localization.swift dan Image.swift akan muncul di folder proyek di sepanjang jalur yang ditentukan dalam Image.swift . Mereka perlu ditambahkan ke proyek Xcode. Dalam kasus kami, file yang dihasilkan berisi yang berikut ini:


Untuk string:
 public enum Localization { public enum Languages { public enum ObjectiveC { /// General-purpose, object-oriented programming language that adds Smalltalk-style messaging to the C programming language public static let description = localize("languages.objective-c.description") /// https://en.wikipedia.org/wiki/Objective-C public static let link = localize("languages.objective-c.link") /// Objective-C public static let name = localize("languages.objective-c.name") } public enum Swift { /// General-purpose, multi-paradigm, compiled programming language developed by Apple Inc. for iOS, macOS, watchOS, tvOS, and Linux public static let description = localize("languages.swift.description") /// https://en.wikipedia.org/wiki/Swift_(programming_language) public static let link = localize("languages.swift.link") /// Swift public static let name = localize("languages.swift.name") } } public enum MainScreen { /// Language public static let title = localize("main-screen.title") public enum Button { /// View in Wikipedia public static let title = localize("main-screen.button.title") } } } extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } } 

Untuk gambar:
 public enum Image { public enum Logos { public static var objectiveC: UIImage { return image(named: "ObjectiveC") } public static var swift: UIImage { return image(named: "Swift") } } private static func image(named name: String) -> UIImage { let bundle = Bundle(for: BundleToken.self) guard let image = UIImage(named: name, in: bundle, compatibleWith: nil) else { fatalError("Unable to load image named \(name).") } return image } } private final class BundleToken {} 

Sekarang Anda dapat mengganti semua penggunaan string pelokalan dan inisialisasi gambar dari bentuk UIImage(named: "") dengan apa yang kami hasilkan. Ini akan memudahkan kita untuk melacak perubahan atau menghapus kunci string pelokalan. Dalam setiap kasus ini, proyek tidak akan berkumpul sampai semua kesalahan yang terkait dengan perubahan telah diperbaiki.
Setelah perubahan, kode kami terlihat seperti ini:


  let logos = Image.Logos.self let localization = Localization.self private func setupWithLanguage(_ language: ProgrammingLanguage) { switch language { case .Swift: logoImageView.image = logos.swift nameLabel.text = localization.Languages.Swift.name descriptionLabel.text = localization.Languages.Swift.description wikiUrl = localization.Languages.Swift.link.toURL() case .ObjectiveC: logoImageView.image = logos.objectiveC nameLabel.text = localization.Languages.ObjectiveC.name descriptionLabel.text = localization.Languages.ObjectiveC.description wikiUrl = localization.Languages.ObjectiveC.link.toURL() } } 

Menyiapkan proyek dalam Xcode


Ada satu masalah dengan file yang dihasilkan: mereka dapat diubah secara manual oleh kesalahan, dan karena mereka ditimpa dari awal dengan setiap kompilasi, perubahan ini mungkin hilang. Untuk menghindari hal ini, Anda dapat mengunci file untuk ditulis setelah SwiftGen skrip SwiftGen .
Ini dapat dicapai dengan menggunakan chmod . Kami menulis ulang Build Phase kami dengan meluncurkan SwiftGen sebagai berikut:


 PODSROOT="$1" OUTPUT_FILES=() COUNTER=0 while [ $COUNTER -lt ${SCRIPT_OUTPUT_FILE_COUNT} ]; do tmp="SCRIPT_OUTPUT_FILE_$COUNTER" OUTPUT_FILES+=(${!tmp}) COUNTER=$[$COUNTER+1] done for file in "${OUTPUT_FILES[@]}" do if [ -f $file ] then chmod a=rw "$file" fi done $PODSROOT/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml for file in "${OUTPUT_FILES[@]}" do chmod a=r "$file" done 

Skripnya cukup sederhana. Sebelum memulai pembuatan, jika file ada, kami memberi mereka izin menulis. Setelah menjalankan skrip, kami memblokir kemampuan untuk memodifikasi file.
Untuk kemudahan mengedit dan memeriksa skrip untuk ditinjau, lebih mudah untuk menempatkannya di file runswiftgen.sh terpisah. Versi final skrip dengan modifikasi kecil dapat ditemukan di sini . Sekarang Build Phase kami akan terlihat seperti ini: kami meneruskan path ke folder Pods ke input skrip:


 "$SRCROOT"/SwiftGen/runswiftgen.sh "$PODS_ROOT" 

Kami membangun kembali proyek, dan sekarang ketika Anda mencoba untuk memodifikasi file yang dihasilkan secara manual, peringatan muncul:



Jadi, folder dengan Swiftgen sekarang berisi file konfigurasi, skrip untuk mengunci file dan meluncurkan Swiftgen dan folder dengan template yang disesuaikan. Lebih mudah untuk menambahkannya ke proyek untuk pengeditan lebih lanjut jika perlu.



Dan karena file Localization.swift dan Image.swift dihasilkan secara otomatis, Anda dapat menambahkannya ke .gitignore sehingga Anda tidak perlu menyelesaikan konflik setelah git merge lagi.


Saya juga ingin menarik perhatian pada kebutuhan untuk menentukan File Input / Output untuk fase SwiftGen jika proyek Anda menggunakan Xcode New Build System (ini adalah sistem build default dengan Xcode 10). Dalam daftar File Input, Anda harus menentukan semua file berdasarkan kode yang dihasilkan. Dalam kasus kami, ini adalah file skrip itu sendiri, konfigurasi, templat, .strings, dan .xcassets:


 $(SRCROOT)/SwiftGen/runswiftgen.sh $(SRCROOT)/SwiftGen/swiftgen.yml $(SRCROOT)/SwiftGen/Templates/ImageAssets.stencil $(SRCROOT)/SwiftGen/Templates/LocalizableStrings.stencil $(SRCROOT)/SwiftGenExample/en.lproj/Localizable.strings $(SRCROOT)/SwiftGenExample/Assets.xcassets 

Dalam File Output kami menempatkan file di mana SwiftGen menghasilkan kode:


 $(SRCROOT)/SwiftGenExample/Image.swift $(SRCROOT)/SwiftGenExample/Localization.swift 

Indikasi file-file ini diperlukan agar sistem build dapat memutuskan apakah akan menjalankan skrip, tergantung pada ada atau tidaknya perubahan pada file, dan pada tahap apa dalam perakitan proyek skrip ini harus dieksekusi.


Ringkasan


SwiftGen adalah alat yang baik untuk melindungi dari kecerobohan kami saat bekerja dengan sumber daya proyek. Dengan itu, kami dapat secara otomatis menghasilkan kode untuk mengakses sumber daya aplikasi dan mengalihkan sebagian pekerjaan untuk memeriksa relevansi sumber daya dengan penyusun, yang berarti menyederhanakan pekerjaan kami sedikit. Selain itu, kami menyiapkan proyek Xcode sehingga pekerjaan lebih lanjut dengan alat ini lebih mudah.


Pro:


  1. Lebih mudah untuk mengontrol sumber daya proyek.
  2. Kesalahan kesalahan ketik dikurangi, menjadi mungkin untuk menggunakan penggantian otomatis.
  3. Kesalahan diperiksa pada tahap kompilasi.

Cons:


  1. Tidak ada dukungan untuk Localizable.stringsdict.
  2. Sumber daya yang tidak digunakan tidak diperhitungkan.

Contoh lengkapnya bisa dilihat di github

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


All Articles