อะไรเปิดใช้ DSL ของ SwiftUI


89

ดูเหมือนว่าSwiftUIเฟรมเวิร์กใหม่ของ Apple จะใช้ไวยากรณ์รูปแบบใหม่ที่สร้างทูเพิลได้อย่างมีประสิทธิภาพ แต่มีไวยากรณ์อื่น:

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

พยายามจัดการสิ่งที่ไวยากรณ์นี้เป็นจริงฉันพบว่าตัวVStackเริ่มต้นที่ใช้ที่นี่ใช้การปิดประเภท() -> Content เป็นพารามิเตอร์ที่สองโดยที่Contentพารามิเตอร์ทั่วไปที่สอดคล้องกับViewที่อนุมานผ่านการปิด หากต้องการทราบว่าประเภทContentใดที่อนุมานได้ฉันได้เปลี่ยนรหัสเล็กน้อยโดยยังคงใช้งานได้:

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

    return test
}

ด้วยวิธีนี้testเผยให้เห็นตัวเองไปเป็นชนิดVStack<TupleView<(Text, Text)>>มีความหมายว่าเป็นประเภทContent TupleView<Text, Text>มองขึ้นไปTupleViewฉันพบว่ามันเป็นประเภทกระดาษห่อหุ้มที่มีต้นกำเนิดจากSwiftUIตัวมันเองซึ่งสามารถเริ่มต้นได้โดยการส่งทูเปิลที่ควรห่อ

คำถาม

ตอนนี้ฉันสงสัยว่าในโลกทั้งสองTextอินสแตนซ์ในตัวอย่างนี้ถูกแปลงเป็นTupleView<(Text, Text)>ไฟล์. สิ่งนี้ถูกแฮ็กSwiftUIและทำให้ไวยากรณ์ของ Swift ปกติไม่ถูกต้องหรือไม่ TupleViewการเป็นSwiftUIประเภทสนับสนุนสมมติฐานนี้ หรือไวยากรณ์ Swiftนี้ถูกต้องหรือไม่ ถ้าใช่จะใช้ภายนอกได้SwiftUIอย่างไร?


3
developer.apple.com/documentation/swiftui/vstack/3278367-initแสดงให้เห็นว่ามี“กำหนดเองแอตทริบิวต์” developer.apple.com/documentation/swiftui/viewbuilder@ViewBuilder
Martin R

1
กล่าวถึงในฟอรั่มสวิฟท์ที่นี่forums.swift.org/t/pitch-introduce-custom-attributes/21335และนี่forums.swift.org/t/pitch-static-custom-attributes-round-2/22938
Martin R

คำตอบ:


111

ดังที่มาร์ตินกล่าวหากคุณดูเอกสารประกอบVStackของinit(alignment:spacing:content:)คุณจะเห็นว่าcontent:พารามิเตอร์มีแอตทริบิวต์@ViewBuilder:

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

แอตทริบิวต์นี้อ้างถึงViewBuilderประเภทซึ่งหากคุณดูอินเทอร์เฟซที่สร้างขึ้นดูเหมือนว่า:

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

@_functionBuilderแอตทริบิวต์เป็นส่วนหนึ่งของคุณลักษณะที่ไม่เป็นทางการเรียกว่า " ผู้สร้างฟังก์ชั่น " ซึ่งได้รับเสียงแหลมวิวัฒนาการของสวิฟท์ที่นี่และดำเนินการมาเป็นพิเศษสำหรับรุ่นของสวิฟท์ที่มาพร้อมกับ Xcode 11 ปล่อยให้มันถูกนำมาใช้ใน SwiftUI

การทำเครื่องหมายประเภท@_functionBuilderช่วยให้สามารถใช้เป็นแอตทริบิวต์ที่กำหนดเองในการประกาศต่างๆเช่นฟังก์ชันคุณสมบัติที่คำนวณและในกรณีนี้พารามิเตอร์ของประเภทฟังก์ชัน การประกาศคำอธิบายประกอบดังกล่าวใช้ตัวสร้างฟังก์ชันเพื่อแปลงบล็อกโค้ด:

  • สำหรับฟังก์ชันที่มีคำอธิบายประกอบบล็อกของโค้ดที่ได้รับการแปลงคือการนำไปใช้งาน
  • สำหรับคุณสมบัติที่คำนวณด้วยคำอธิบายประกอบบล็อกของโค้ดที่ถูกแปลงคือ getter
  • สำหรับพารามิเตอร์ที่มีคำอธิบายประกอบของประเภทฟังก์ชันบล็อกของโค้ดที่ถูกแปลงคือนิพจน์การปิดใด ๆ ที่ส่งผ่านไปยัง (ถ้ามี)

วิธีที่ตัวสร้างฟังก์ชันแปลงโค้ดถูกกำหนดโดยการใช้งานเมธอดตัวสร้างเช่นbuildBlockซึ่งใช้ชุดของนิพจน์และรวมเข้าเป็นค่าเดียว

ตัวอย่างเช่นViewBuilderการใช้พารามิเตอร์ที่สอดคล้องกันbuildBlock1 ถึง 10 รายการViewโดยรวมหลายมุมมองไว้ในรายการเดียว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

    // ...
}

สิ่งนี้ช่วยให้ชุดของนิพจน์มุมมองภายในการปิดที่ส่งผ่านไปยังVStackผู้เริ่มต้นของสามารถเปลี่ยนเป็นการเรียกไปbuildBlockที่ใช้อาร์กิวเมนต์จำนวนเท่ากัน ตัวอย่างเช่น:

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

ถูกเปลี่ยนเป็นการเรียกร้องให้buildBlock(_:_:):

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

ส่งผลให้ในประเภทผลขุ่น ถูกความพึงพอใจโดยsome ViewTupleView<(Text, Text)>

คุณจะทราบว่าViewBuilderกำหนดbuildBlockพารามิเตอร์ได้สูงสุด 10 รายการเท่านั้นดังนั้นหากเราพยายามกำหนดมุมมองย่อย 11 รายการ:

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

เราได้รับข้อผิดพลาดของคอมไพเลอร์เนื่องจากไม่มีวิธีการสร้างในการจัดการบล็อกโค้ดนี้ (โปรดทราบว่าเนื่องจากคุณลักษณะนี้ยังคงอยู่ระหว่างดำเนินการข้อความแสดงข้อผิดพลาดที่อยู่รอบ ๆ จึงไม่เป็นประโยชน์)

ในความเป็นจริงฉันไม่เชื่อว่าผู้คนจะพบกับข้อ จำกัด นี้บ่อยครั้งเช่นตัวอย่างข้างต้นจะได้รับการบริการที่ดีกว่าโดยใช้ForEachมุมมองแทน

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

อย่างไรก็ตามหากคุณต้องการมุมมองที่กำหนดแบบคงที่มากกว่า 10 มุมมองคุณสามารถแก้ไขข้อ จำกัด นี้ได้อย่างง่ายดายโดยใช้Groupมุมมอง:

  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 ยังใช้วิธีการสร้างฟังก์ชันอื่น ๆ เช่น:

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
}

สิ่งนี้ทำให้สามารถจัดการกับคำสั่ง if:

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

ซึ่งเปลี่ยนเป็น:

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

(ส่งเสียงเรียก 1 อาร์กิวเมนต์ซ้ำซ้อนเพื่อViewBuilder.buildBlockความชัดเจน)


4
ViewBuilderกำหนดbuildBlockพารามิเตอร์ได้สูงสุด 10 พารามิเตอร์นั่นหมายความว่าvar body: some Viewต้องมีมุมมองย่อยไม่เกิน 11 รายการใช่หรือไม่
LinusGeffarth

1
@LinusGeffarth ในความเป็นจริงฉันไม่คิดว่าผู้คนจะเจอข้อ จำกัด นี้บ่อยนักเพราะพวกเขาน่าจะต้องการใช้อะไรบางอย่างเช่นForEachมุมมองแทน อย่างไรก็ตามคุณสามารถใช้Groupมุมมองเพื่อแก้ไขข้อ จำกัด นี้ได้ฉันได้แก้ไขคำตอบเพื่อแสดงให้เห็นว่า
Hamish

3
@MandisaW - คุณสามารถจัดกลุ่มมุมมองเป็นมุมมองของคุณเองและนำมาใช้ซ้ำได้ ฉันไม่เห็นปัญหากับมัน ตอนนี้ฉันอยู่ที่ WWDC และได้พูดคุยกับวิศวกรคนหนึ่งในห้องทดลอง SwiftUI เขาบอกว่าตอนนี้มันเป็นข้อ จำกัด ของ Swift และพวกเขาใช้ 10 เป็นตัวเลขที่สมเหตุสมผล เมื่อมีการนำตัวแปรทั่วไปมาใช้ใน Swift แล้วเราจะสามารถมี "มุมมองย่อย" ได้มากเท่าที่เราต้องการ
Losiowaty

1
อาจจะน่าสนใจกว่านี้ประเด็นของ buildEither คืออะไร? ดูเหมือนว่าคุณต้องใช้ทั้งสองอย่างและทั้งคู่มีประเภทผลตอบแทนเหมือนกันทำไมพวกเขาไม่ส่งคืนประเภทที่เป็นปัญหา
Gusutafu

1
ติดตามความคิดเห็นของฉันเกี่ยวกับข้อบกพร่องของ ASTPrinter สิ่งนี้จะได้รับการแก้ไขในมาสเตอร์เมื่อรวมตัวสร้างฟังก์ชัน PRแล้ว
Hamish

13

มีการอธิบายสิ่งที่คล้ายคลึงกันในวิดีโอมีอะไรใหม่ใน Swift WWDCในส่วนเกี่ยวกับ DSL (เริ่มเวลา ~ 31: 15) แอตทริบิวต์ถูกตีความโดยคอมไพเลอร์และแปลเป็นรหัสที่เกี่ยวข้อง:

ใส่คำอธิบายภาพที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.