Apa yang memungkinkan DSL SwiftUI?

89

Sepertinya SwiftUIkerangka kerja baru Apple menggunakan jenis sintaks baru yang secara efektif membangun tupel, tetapi memiliki sintaks lain:

var body: some View {
    VStack(alignment: .leading) {
        Text("Hello, World") // No comma, no separator ?!
        Text("Hello World!")
    }
}

Mencoba untuk mengatasi apa sebenarnya sintaks ini , saya menemukan bahwa VStackpenginisialisasi yang digunakan di sini mengambil penutupan tipe () -> Content sebagai parameter kedua, di mana parameter Contentgenerik yang sesuai dengan Viewyang disimpulkan melalui penutupan. Untuk mengetahui jenis apa Contentyang disimpulkan, saya mengubah kode sedikit, mempertahankan fungsinya:

var body: some View {
    let test = VStack(alignment: .leading) {
        Text("Hello, World")
        Text("Hello World!")
    }

    return test
}

Dengan ini, testmengungkapkan dirinya menjadi tipe VStack<TupleView<(Text, Text)>>, artinyaContent dari tipe TupleView<Text, Text>. Mendongak TupleView, saya menemukan itu adalah jenis pembungkus yang berasal dari SwiftUIdirinya sendiri yang hanya dapat diinisialisasi dengan meneruskan tupel yang harus dibungkus.

Pertanyaan

Sekarang saya bertanya-tanya bagaimana dua Textcontoh dalam contoh ini diubah menjadi TupleView<(Text, Text)>. Apakah ini diretas SwiftUIdan karena itu sintaks Swift reguler tidak valid? TupleViewmenjadi SwiftUItipe mendukung asumsi ini. Atau apakah ini sintaks Swift yang valid? Jika ya, bagaimana cara menggunakannya di luar SwiftUI?

fredpi
sumber

Jawaban:

111

Seperti Martin mengatakan , jika Anda melihat dokumentasi untuk VStack's init(alignment:spacing:content:), Anda dapat melihat bahwa content:parameter memiliki atribut @ViewBuilder:

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

Atribut ini mengacu pada ViewBuildertipe, yang jika Anda melihat antarmuka yang dihasilkan, terlihat seperti:

@_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 
      where Content : View
}

The @_functionBuilderatribut adalah bagian dari fitur tidak resmi disebut " fungsi pembangun ", yang telah bernada tentang evolusi Swift di sini , dan dilaksanakan khusus untuk versi Swift yang dikirimkan dengan Xcode 11, yang memungkinkan untuk digunakan dalam SwiftUI.

Menandai tipe @_functionBuildermemungkinkannya untuk digunakan sebagai atribut khusus pada berbagai deklarasi seperti fungsi, properti yang dihitung, dan, dalam hal ini, parameter tipe fungsi. Deklarasi beranotasi seperti itu menggunakan pembuat fungsi untuk mengubah blok kode:

  • Untuk fungsi beranotasi, blok kode yang diubah adalah implementasinya.
  • Untuk properti komputasi beranotasi, blok kode yang diubah adalah pengambil.
  • Untuk parameter beranotasi dari tipe fungsi, blok kode yang diubah adalah ekspresi penutupan apa pun yang diteruskan ke sana (jika ada).

Cara pembuat fungsi mengubah kode ditentukan oleh penerapan metode pembuatnya seperti buildBlock, yang mengambil sekumpulan ekspresi dan menggabungkannya menjadi satu nilai.

Misalnya, ViewBuildermenerapkan buildBlock1 hingga 10 Viewparameter yang sesuai, yang menggabungkan beberapa tampilan menjadi satu TupleView:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

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

    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) 
      -> TupleView<(C0, C1)> where C0 : View, C1 : View

    public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
      -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View

    // ...
}

Hal ini memungkinkan sekumpulan ekspresi tampilan dalam closure yang diteruskan ke VStackinisialisasi untuk diubah menjadi panggilan ke buildBlockyang menggunakan jumlah argumen yang sama. Sebagai contoh:

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Hello World!")
    }
  }
}

diubah menjadi panggilan ke buildBlock(_:_:):

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
    }
  }
}

menghasilkan jenis hasil buram some View yang dipenuhi TupleView<(Text, Text)>.

Anda akan melihat bahwa ViewBuilderhanya mendefinisikan buildBlockhingga 10 parameter, jadi jika kami mencoba untuk mendefinisikan 11 subview:

  var body: some View {
    // error: Static member 'leading' cannot be used on instance of
    // type 'HorizontalAlignment'
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
    }
  }

kami mendapatkan kesalahan kompiler, karena tidak ada metode pembuat untuk menangani blok kode ini (perhatikan bahwa karena fitur ini masih dalam proses, pesan kesalahan di sekitarnya tidak akan membantu).

Pada kenyataannya, saya tidak yakin orang-orang akan sering mengalami pembatasan ini, misalnya contoh di atas akan lebih baik disajikan menggunakan ForEachtampilan:

  var body: some View {
    VStack(alignment: .leading) {
      ForEach(0 ..< 20) { i in
        Text("Hello world \(i)")
      }
    }
  }

Namun, jika Anda memang membutuhkan lebih dari 10 tampilan yang ditentukan secara statis, Anda dapat dengan mudah mengatasi batasan ini menggunakan Grouptampilan:

  var body: some View {
    VStack(alignment: .leading) {
      Group {
        Text("Hello world")
        // ...
        // up to 10 views
      }
      Group {
        Text("Hello world")
        // ...
        // up to 10 more views
      }
      // ...
    }

ViewBuilder juga mengimplementasikan metode pembuat fungsi lain seperti:

extension ViewBuilder {
    /// Provides support for "if" statements in multi-statement closures, producing
    /// ConditionalContent for the "then" branch.
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
      -> ConditionalContent<TrueContent, FalseContent>
           where TrueContent : View, FalseContent : View

    /// Provides support for "if-else" statements in multi-statement closures, 
    /// producing ConditionalContent for the "else" branch.
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
      -> ConditionalContent<TrueContent, FalseContent>
           where TrueContent : View, FalseContent : View
}

Ini memberinya kemampuan untuk menangani pernyataan if:

  var body: some View {
    VStack(alignment: .leading) {
      if .random() {
        Text("Hello World!")
      } else {
        Text("Goodbye World!")
      }
      Text("Something else")
    }
  }

yang diubah menjadi:

  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(
        .random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
                  : ViewBuilder.buildEither(second: Text("Goodbye World!")),
        Text("Something else")
      )
    }
  }

(memancarkan panggilan 1-argumen yang berlebihan ke ViewBuilder.buildBlockuntuk kejelasan).

Hamish
sumber
4
ViewBuilderhanya menentukan buildBlockhingga 10 parameter - apakah itu berarti var body: some Viewtidak boleh memiliki lebih dari 11 subview?
LinusGeffarth
1
@LinusGeffarth Pada kenyataannya, saya rasa orang-orang tidak akan sering mengalami pembatasan ini, karena mereka cenderung ingin menggunakan sesuatu seperti ForEachtampilan. Namun Anda dapat menggunakan Grouptampilan untuk mengatasi batasan ini, saya telah mengedit jawaban saya untuk menunjukkannya.
Hamish
3
@MandisaW - Anda dapat mengelompokkan tampilan menjadi tampilan Anda sendiri dan menggunakannya kembali. Saya tidak melihat ada masalah dengan itu. Saya berada di WWDC sekarang, dan berbicara dengan salah satu insinyur di laboratorium SwiftUI - dia mengatakan bahwa itu adalah batasan dari Swift sekarang, dan mereka menggunakan 10 sebagai angka yang masuk akal. Setelah variadic generic diperkenalkan ke Swift, kita akan dapat memiliki sebanyak "subview" yang kita inginkan.
Losiowaty
1
Mungkin yang lebih menarik, apa gunanya metode buildEither? Sepertinya Anda perlu menerapkan keduanya, dan keduanya memiliki jenis pengembalian yang sama, mengapa mereka tidak mengembalikan jenis yang dimaksud?
Gusutafu
1
Menindaklanjuti komentar saya tentang bug ASTPrinter, ini akan diperbaiki pada master setelah PR pembangun fungsi telah digabungkan .
Hamish
13

Hal serupa dijelaskan di What's New in Swift WWDC video di bagian tentang DSLs (dimulai pada ~ 31: 15). Atribut diinterpretasikan oleh kompilator dan diterjemahkan ke dalam kode terkait:

masukkan deskripsi gambar di sini

Maciek Czarnik
sumber