Keajaiban SwiftUI atau tentang Function builders


Sudahkah Anda mencoba menambahkan lebih dari 10 tampilan ke VStack?


 var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") // ...    3  10 . . . Text("Placeholder11") } } 

Saya mencoba - tidak mengkompilasi. Ya, saya juga terkejut pada awalnya dan terjun ke studi tentang forum Swift dan github. Hasil penelitian saya adalah - "masih belum bisa dikompilasi Β―\_(ツ)_/Β― ". Tapi tunggu, mari kita lihat mengapa.


Pembangun fungsi


Untuk memulai, ada baiknya memahami bagaimana sintaksis seperti itu tersedia secara umum. Dasar dari penciptaan elemen deklaratif yang begitu tidak biasa bagi kita adalah mekanisme Function Builder .
Di github dalam swift-evolution terdapat proposal dari John McCall dan Doug Gregor - Function builder (Proposal: SE-XXXX) , di mana mereka menggambarkan secara detail masalah apa yang mereka hadapi, mengapa diputuskan untuk menggunakan Functions Builder dan apa itu. seperti itu.


Jadi apa ini?


Singkatnya, ini sulit untuk dijelaskan, tetapi singkatnya - ini adalah mekanisme yang memungkinkan Anda untuk membuat daftar argumen, beberapa konten dalam tubuh harta, dan memberikan hasil umum dari semua ini.
Kutipan dari Proposal: SE-XXXX :


Gagasan utamanya adalah kita mengambil hasil dari ekspresi, termasuk ekspresi bertingkat seperti jika dan beralih, dan membentuknya menjadi satu hasil, yang menjadi nilai balik dari fungsi saat ini. "Build" ini dikendalikan oleh function builder, yang merupakan atribut khusus.

Asli
ide dasarnya adalah bahwa kita mengambil hasil ekspresi "diabaikan" dari blok pernyataan - termasuk dalam posisi bersarang seperti badan pernyataan dan penukaran simpanan - dan membangunnya menjadi nilai hasil tunggal yang menjadi nilai balik dari fungsi saat ini. Cara koleksi ini dilakukan dikontrol oleh afunction builder, yang hanya merupakan tipe baru tipe atribut kustom;

Mekanisme seperti ini memungkinkan Anda untuk menulis kode seperti pohon deklaratif, tanpa karakter tanda baca yang tidak perlu:


 let myBody = body { let chapter = spellOutChapter ? "Chapter" : "" div { if useChapterTitles { h1(chapter + "1. Loomings.") } p { "Call me Ishmael. Some years ago" } p { "There is now your insular city" } } } 

Ini tersedia berkat atribut @_functionBuilder baru. Atribut ini menandai beberapa pembangun, bisa berupa struktur. Pembangun ini mengimplementasikan sejumlah metode khusus. Selanjutnya, pembangun ini digunakan dengan sendirinya, sebagai atribut pengguna dalam berbagai situasi.
Di bawah ini saya akan menunjukkan cara kerjanya dan bagaimana mengatur kode tersebut.


Kenapa ini?


Jadi Apple ingin melakukan dukungan untuk DSL bahasa khusus domain- built-in.
John McCall dan Doug Gregor menunjukkan bahwa kode seperti itu jauh lebih mudah untuk dibaca dan ditulis - ini menyederhanakan sintaksisnya, membuatnya lebih ringkas dan, sebagai hasilnya, kode tersebut menjadi lebih didukung. Pada saat yang sama, mereka mencatat bahwa solusi mereka bukan DSL universal.
Solusi ini berfokus pada serangkaian masalah tertentu, termasuk menggambarkan struktur linier dan pohon seperti XML, JSON, Lihat hierarki, dll.


Bagaimana cara mengatasinya?


Anda dapat membuat pembangun fungsi Anda sendiri, lebih mudah bagi saya untuk memahami cara kerjanya. Pertimbangkan contoh primitif pembangun yang menggabungkan string.


 // 1.  Builder @_functionBuilder struct MyBuilder { static func buildBlock(_ atrs: String...) -> String { return atrs.reduce("", + ) } } 

 // 2.          func stringsReduce(@MyBuilder block: () -> String) -> String { return block() } 

 // 3.     let result = stringsReduce { "1" "2" } print(result) // "12" 

Di bawah tenda, ini akan berhasil seperti ini:


 let result = stringsReduce { return MyBuilder.build("1", "2") } 

Adalah penting bahwa dalam implementasi pembangun, metode harus persis statis, dengan nama spesifik dan dengan tipe parameter tertentu dari daftar ini . Anda hanya dapat mengubah jenis dan nama parameter input.


 static func buildBlock(_ <*atrs*>: <*String*>...) -> <*String*> 

Nama metode khusus akan dicari dalam pembangun dan diganti pada tahap kompilasi. Dan jika metode ini tidak ditemukan, kesalahan kompilasi akan terjadi.
Dan ini sihir. Ketika Anda mengimplementasikan pembangun, kompiler tidak akan memberi tahu Anda apa pun. Itu tidak akan mengatakan tentang metode yang tersedia, itu tidak akan membantu dengan pelengkapan otomatis. Hanya ketika Anda menulis kode klien yang tidak dapat diproses oleh pembuat ini Anda mendapatkan kesalahan tidak jelas.
Sejauh ini, satu-satunya solusi yang saya temukan adalah dipandu oleh daftar metode .
Jadi mengapa kita perlu metode lain? Nah, misalnya, untuk mendukung kode tersebut dengan cek


 stringsReduce { if .random() { //   Bool "one string" } else { "another one" } "fixed string" } 

Untuk mendukung sintaks ini, builder perlu mengimplementasikan metode buildEither(first:/second:)


 static func buildEither(first: String) -> String { return first } static func buildEither(second: String) -> String { return second } 

Tanggapan masyarakat


Yang lucu adalah bahwa ini belum ada di Swift 5.1, yaitu permintaan tarik dengan fitur ini belum dituangkan, namun demikian, Apple telah menambahkannya ke Xcode 11 beta. Dan di Function builders β†’ Pitches β†’ Swift Forum, Anda dapat melihat reaksi komunitas terhadap proposal ini.


ViewBuilder


Sekarang kembali ke VStack dan lihat dokumentasi init inisialisasi nya (alignment: spacing: content :) .
Ini terlihat seperti ini:


 init(alignment: HorizontalAlignment = .center, spacing: ? = nil, @ViewBuilder content: () -> Content) 

Dan sebelum tabel konten ada atribut kustom @ViewBuilder
Dinyatakan sebagai berikut:


 @_functionBuilder public struct ViewBuilder { /// Builds an empty view from an block containing no statements, `{ }`. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through /// unmodified. public static func buildBlock<Content>(_ content: Content) -> Content where Content : View } 

Atribut pengguna adalah @ _functionBuilder , yang terdaftar pada awal deklarasi.


Dan jika Anda melihat melalui dokumentasi lebih rendah, Anda dapat melihat banyak metode buildBlock statis yang berbeda dalam jumlah argumen.


Ini berarti kode tampilan


 var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") Text("Placeholder3") } } 

di bawah tenda dikonversi menjadi seperti itu


  var body: some View { VStack(alignment: .leading) { ViewBuilder.buildBlock(Text("Placeholder1"), Text("Placeholder2"), Text("Placeholder3")) } } 

Yaitu memenuhi metode buildBlock (:: _ :) builder.


Dari seluruh daftar, metode dengan jumlah argumen maksimum adalah buildBlock orang ini (:::::::: :) :) (10 argumen):


 extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View } 

Dan karenanya, kembali ke contoh asli, ketika Anda mencoba menaikkan VStack dan sebelas tampilan di dalamnya, kompiler mencoba menemukan metode buildBlock buildBlock , yang memiliki 11 argumen input. Tetapi tidak ada metode seperti itu: maka kesalahan kompilasi.
Ini berlaku untuk semua koleksi yang menggunakan inisialisasi dengan atribut @ViewBuilder di penginisialisasi: V | H | Z-Stack, Daftar, Grup dan lainnya, di dalamnya Anda dapat mendeklarasikan lebih dari satu tampilan sebagai enumerasi.
Dan ini menyedihkan.


MEM (maaf, saya tidak menemukan meme yang layak)


Bagaimana menjadi


Kita dapat menghindari batasan ini menggunakan ForEach


 struct TestView : View { var body: some View { VStack { ForEach(texts) { i in Text(Β«\(i)Β») } } } var texts: [Int] { var result: [Int] = [] for i in 0...150 { result.append(i) } return result } } 

Atau koleksi bersarang:


 var body: some View { VStack { VStack { Text("Placeholder_1") Text("Placeholder_2") //   8 } Group { Text("11") Text("12") //   8 } } } 

Tapi keputusan seperti itu terlihat seperti tongkat penyangga dan hanya ada harapan untuk masa depan yang lebih cerah. Tapi seperti apa masa depan ini?
Swift sudah memiliki parameter Variadic . Ini adalah kemampuan suatu metode untuk menerima argumen melalui penghitungan. Misalnya, metode print yang diketahui oleh semua orang memungkinkan Anda untuk menulis print(1, 2) dan print(1, 2, 3, 4) dan ini tanpa kelebihan metode yang tidak perlu.


 print(items: Any...) 

Tetapi fitur bahasa ini tidak cukup, karena metode buildBlock menerima argumen umum yang berbeda sebagai input.
Menambahkan generik Variadic akan menyelesaikan masalah ini. Generik variadic memungkinkan Anda untuk abstrak dari banyak tipe generik, misalnya, sesuatu seperti ini:


  static func buildBlock<…Component>(Component...) -> TupleView<(Component...)> where Component: View 

Dan Apple wajib menambahkan ini. Semuanya bertumpu pada mekanisme ini sekarang. Dan menurut saya mereka tidak berhasil menyelesaikannya dengan WWDC 2019 (tapi ini hanya spekulasi).

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


All Articles