Điều gì kích hoạt DSL của SwiftUI?


88

Có vẻ như SwiftUIkhung công tác mới của Apple sử dụng một loại cú pháp mới có hiệu quả xây dựng một bộ tuple, nhưng có một cú pháp khác:

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

Đang cố gắng giải quyết cú pháp này thực sự là gì , tôi phát hiện ra rằng trình VStackkhởi tạo được sử dụng ở đây lấy kiểu đóng của kiểu () -> Content làm tham số thứ hai, trong đó Contentmột tham số chung phù hợp với Viewđiều đó được suy ra thông qua kiểu đóng. Để tìm hiểu loại Contentđược suy ra, tôi đã thay đổi mã một chút, duy trì chức năng của nó:

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

    return test
}

Với điều này, testcho thấy bản thân nó thuộc loại VStack<TupleView<(Text, Text)>>, nghĩa Contentlà thuộc loại TupleView<Text, Text>. Nhìn lên TupleView, tôi thấy đó là một loại trình bao bọc có nguồn gốc từ SwiftUIchính nó mà chỉ có thể được khởi tạo bằng cách chuyển qua tuple mà nó nên quấn.

Câu hỏi

Bây giờ tôi đang tự hỏi làm thế nào trên thế giới hai Texttrường hợp trong ví dụ này được chuyển đổi thành a TupleView<(Text, Text)>. Điều này có bị tấn công SwiftUIvà do đó cú pháp Swift thông thường không hợp lệ không? TupleViewlà một SwiftUIloại hỗ trợ giả định này. Hay đây là cú pháp Swift hợp lệ? Nếu có, làm thế nào người ta có thể sử dụng nó bên ngoài SwiftUI?



Câu trả lời:


109

Như Martin nói , nếu bạn xem tài liệu về VStack's init(alignment:spacing:content:), bạn có thể thấy rằng content:tham số có thuộc tính @ViewBuilder:

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

Thuộc tính này đề cập đến ViewBuilderloại, nếu bạn nhìn vào giao diện đã tạo, sẽ giống như:

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

Các @_functionBuilderthuộc tính là một phần của một tính năng không chính thức gọi là " xây dựng chức năng ", đã được dốc trên Swift tiến hóa ở đây , và thực hiện đặc biệt dành cho các phiên bản của Swift mà tàu với Xcode 11, cho phép nó được sử dụng trong SwiftUI.

Việc đánh dấu một kiểu @_functionBuildercho phép nó được sử dụng như một thuộc tính tùy chỉnh trên các khai báo khác nhau, chẳng hạn như hàm, thuộc tính được tính toán và trong trường hợp này là các tham số của kiểu hàm. Các khai báo được chú thích như vậy sử dụng trình tạo hàm để chuyển đổi các khối mã:

  • Đối với các hàm có chú thích, khối mã được chuyển đổi là phần thực hiện.
  • Đối với các thuộc tính tính toán có chú thích, khối mã được chuyển đổi là getter.
  • Đối với các tham số được chú thích của kiểu hàm, khối mã được chuyển đổi là bất kỳ biểu thức đóng nào được chuyển cho nó (nếu có).

Cách thức mà trình tạo hàm chuyển đổi mã được xác định bằng cách triển khai các phương thức của trình tạo, chẳng hạn như buildBlock, nhận một tập hợp các biểu thức và hợp nhất chúng thành một giá trị duy nhất.

Ví dụ: ViewBuildertriển khai buildBlocktừ 1 đến 10 Viewtham số phù hợp, hợp nhất nhiều chế độ xem thành một 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

    // ...
}

Điều này cho phép một tập hợp các biểu thức khung nhìn trong một bao đóng được chuyển tới trình VStackkhởi tạo của 's được chuyển thành một lệnh gọi buildBlockcó cùng số đối số. Ví dụ:

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

được chuyển đổi thành một cuộc gọi đến buildBlock(_:_:):

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

dẫn đến loại kết quả không rõ ràng some View được thỏa mãn bởi TupleView<(Text, Text)>.

Bạn sẽ lưu ý rằng ViewBuilderchỉ xác định buildBlocktối đa 10 tham số, vì vậy nếu chúng tôi cố gắng xác định 11 lượt xem phụ:

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

chúng tôi gặp lỗi trình biên dịch, vì không có phương pháp trình tạo nào để xử lý khối mã này (lưu ý rằng vì tính năng này vẫn đang trong quá trình làm việc, các thông báo lỗi xung quanh nó sẽ không hữu ích).

Trong thực tế, tôi không tin rằng mọi người sẽ thường xuyên gặp phải hạn chế này, ví dụ: ví dụ trên sẽ được phục vụ tốt hơn bằng cách sử dụng ForEachchế độ xem thay thế:

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

Tuy nhiên, nếu bạn cần nhiều hơn 10 dạng xem được xác định tĩnh, bạn có thể dễ dàng giải quyết hạn chế này bằng cách sử dụng dạng Groupxem:

  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 cũng triển khai các phương thức xây dựng hàm khác như:

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
}

Điều này cung cấp cho nó khả năng xử lý các câu lệnh if:

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

được chuyển thành:

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

(phát ra lệnh gọi 1 đối số dư thừa để ViewBuilder.buildBlocklàm rõ).


3
ViewBuilderchỉ xác định buildBlocktối đa 10 tham số - có nghĩa là var body: some Viewkhông được có nhiều hơn 11 lượt xem phụ?
LinusGeffarth

1
@LinusGeffarth Trong thực tế, tôi không nghĩ rằng mọi người sẽ thường xuyên gặp phải hạn chế này, vì họ có thể sẽ muốn sử dụng thứ gì đó như ForEachchế độ xem thay thế. Tuy nhiên, bạn có thể sử dụng Groupchế độ xem để giải quyết hạn chế này, tôi đã chỉnh sửa câu trả lời của mình để hiển thị điều đó.
Hamish

3
@MandisaW - bạn có thể nhóm các chế độ xem thành các chế độ xem của riêng mình và sử dụng lại chúng. Tôi không thấy có vấn đề gì với nó. Tôi thực sự đang ở WWDC ngay bây giờ và đã nói chuyện với một trong những kỹ sư tại phòng thí nghiệm SwiftUI - anh ấy nói rằng đó là hạn chế của Swift hiện tại và họ đã chọn 10 là một con số hợp lý. Một khi những thứ chung chung khác nhau được đưa vào Swift, chúng ta sẽ có thể có nhiều „lượt xem phụ” như chúng ta muốn.
Losiowaty

1
Có thể thú vị hơn, điểm của các phương thức buildEither là gì? Có vẻ như bạn cần triển khai cả hai và cả hai đều có cùng kiểu trả về, tại sao mỗi kiểu không trả về kiểu được đề cập?
Gusutafu


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.