Function builder trong Swift
Bài đăng này đã không được cập nhật trong 5 năm
- 
Function buidlerlà tính năng được ưa thích nhất kể từ khi ra mắt cùngSwiftUItrong versionSwift 5.1. Nó ngay lập tức là một phần quan trọng trong việc triển khaiSwiftUItrong khi chưa đượcApplephát triển hoàn chỉnh.
- 
Ở bài viết này chúng ta sẽ cùng tìm hiểu kỹ hơn về tính năng function buidlernày để xem nó mang lại cho cho chúng ta những lợi ích thế nào trong việc triển khaicodetrongSwiftUI.
- 
Lưu ý: Chúng ta cần nói rõ với nhau là function buidlerđược giới thiệu ở phiển bảnSwift 5.1. Việc sử dụngFunction buidlertrong môi trường production tùy thuộc lớn vào project bạn đang phát triển đang hỗ trợ sử dụng versionSwiftnào. Hãy chắc chắn versionSwiftcủaprojectbạn đang phát triển trước khi áp dụngfunction buidler.
1: Setting:
- Cách tốt nhất với bản thân tôi khi tìm hiểu cách thức mà tính năng Swifthoạt động là thực hànhcodeví dụ nào đó do đó chúng ta sẽ cùng cần một ví dụ với đầy đủ cácsetting, chúng ta sẽ dùngSettingnhư sau:
struct Setting {
    var name: String
    var value: Value
}
extension Setting {
    enum Value {
        case bool(Bool)
        case int(Int)
        case string(String)
        case group([Setting])
    }
}
- 
Chúng ta sử dụng associated typecho các giá trị củaenumđể đảm bảo việc cho việc cácinstance settingsẽ có đầy đủ cáctypekhác nhau.
- 
Thông qua việc tạo groupchúng ta có thể tạo ra cácnested settingnhư sau:
let settings = [
    Setting(name: "Offline mode", value: .bool(false)),
    Setting(name: "Search page size", value: .int(25)),
    Setting(name: "Experimental", value: .group([
        Setting(name: "Default name", value: .string("Untitled")),
        Setting(name: "Fluid animations", value: .bool(true))
    ]))
]
2: Sử dụng builder cho function:
- Điều cốt lõi của Function buildertrongSwiftlà giúp cho việcbuildcáccontenttrongfunctionthành cácsingle value. TrongSwiftUIchúng ta sử dụng tính năng trên cho việctransformcáccontentcủa một hoặc nhiềucontainer(HStackhoặcVStack) trong mộtviewbằng cách sử dụngtype(of:):
import SwiftUI
let stack = VStack {
    Text("Hello")
    Text("World")
    Button("I'm a button") {}
}
// Prints 'VStack<TupleView<(Text, Text, Button<Text>)>>'
print(type(of: stack))
- 
SwiftUIsử dụng cácfunctionkhác nhau trong việcimplementationsnhưViewBuildervàSceneBuilder. Nhưng việc chúng ta không thể tham khảosource codecho cáctypenày nên chúng ta sẽbuildcácFunction buidlercho các setting trongAPInày:
- 
Như tính năng property-wrappers-in-swift , một function buidlersẽ đảm nhiệm việcimplementcác loại type bình thường trongSwiftđược giới thiệu với cú pháp@_functionBuilder. Cácmethodđặc biệt đã từng được dùng đểimplementcáccaselàm việc củaFunction Builder. Chúng ta tham khảo cách dùngbuildBlocksau:
@_functionBuilder
struct SettingsBuilder {
    static func buildBlock() -> [Setting] { [] }
}
- Cách return typetrên trongfunction ([Setting])phải chỉ định thêmtypemàbuildercó thể sử dụng. Chúng ta sẽ chọn cáchimplement APInhưglobal functionđể sử dụng cácSettingsBuildercho bất kỳclosurenào được truyền đến:
func makeSettings(@SettingsBuilder _ content: () -> [Setting]) -> [Setting] {
    content()
}
- Với cách codetrên chúng ta có thể gọimakeSettingsvới mộttrailing closuretrống và nhận về mộtarraytrống:
let settings = makeSettings {}
- APImới của chúng ta vẫn chưa thực sự hữu dụng, nó mới chỉ cho chúng ta thấy một ít khả năng cũng như cách làm việc của nó- Function Builder. Chúng ta sẽ tìm hiều kỹ hơn ở đầu mục tiếp theo:
3: Build các values:
- Để kích hoạt SettingsBuildercủa chúng ta đểacceptcácinputthì việc của chúng ta cần làm là khai báo bổ sungbuildBlockvới cácargumentphù hợp vớiinputcho kết quà mà chúng ta muốn nhận về. Chúng ta sẽimplementmộtmethodvới list các valueSettingvàreturnvề mộtarray:
extension SettingsBuilder {
    static func buildBlock(_ settings: Setting...) -> [Setting] {
        settings
    }
}
- Với buildBlockmới vừa được khai báo, chúng ta sẽ cho phép fillmakeSettingsgọi đến cácSettingvalue và cácfunction buildersẽ tích hợp cho các value trongarrayvàreturn:
let settings = makeSettings {
    Setting(name: "Offline mode", value: .bool(false))
    Setting(name: "Search page size", value: .int(25))
    Setting(name: "Experimental", value: .group([
        Setting(name: "Default name", value: .string("Untitled")),
        Setting(name: "Fluid animations", value: .bool(true))
    ]))
}
- Chúng ta đã đạt được một sự cải tiến nhè nhẹ ở đây, bây giờ thì hãy quay về với SwiftUIvà thêm vào cácfunction buildervới khả năng mạnh mẽ trong công việc định nghĩagroup. Chúng ta sẽ cần thêm một typeSettingGroupcóannotateslà@SettingsBuilderđể kết nối vớifunction buidler:
struct SettingsGroup {
    var name: String
    var settings: [Setting]
    init(name: String,
         @SettingsBuilder builder: () -> [Setting]) {
        self.name = name
        self.settings = builder()
    }
}
- Cách triển khai trên cho phép chúng ta định nghĩa groupgiống cách chúng ta định nghĩa cácsettingbằng cách kết nối mỗiSettingvớiclosurenhư sau:
SettingsGroup(name: "Experimental") {
    Setting(name: "Default name", value: .string("Untitled"))
    Setting(name: "Fluid animations", value: .bool(true))
}
- 
Nếu chúng ta cứ khăng khăng sử dụng makeSettings closurethì chúng ta sẽ bị báo lỗi vì cácmethod function buildersẽ đang đợi rất nhiều giá trịSettingvàSettingGroupmới của chúng ta là mộttypehoàn toàn khác.
- 
Để xử lý errortrên chúng ta cần thêm cácprototolđể có thể chia sẻ giữaSettingvàSettingGroup:
protocol SettingsConvertible {
    func asSettings() -> [Setting]
}
extension Setting: SettingsConvertible {
    func asSettings() -> [Setting] { [self] }
}
extension SettingsGroup: SettingsConvertible {
    func asSettings() -> [Setting] {
        [Setting(name: name, value: .group(settings))]
    }
}
- Công việc tiếp theo cần làm là modifycácfunction buidlercủabuildBlockđểacceptcácSettingsConvertibleđược khởi tạo hơn là sử dụng giá trị màSettingđược trả về thông qua map:
extension SettingsBuilder {
    static func buildBlock(_ values: SettingsConvertible...) -> [Setting] {
        values.flatMap { $0.asSettings() }
    }
}
- Bây giờ chúng ta đã có đầy đủ phương tiện để sử dụng đầy đủ tính năng của SwiftUIhỗ trợ bằng cách thiết laapk cácgroupnhư sau:
let settings = makeSettings {
    Setting(name: "Offline mode", value: .bool(false))
    Setting(name: "Search page size", value: .int(25))
    SettingsGroup(name: "Experimental") {
        Setting(name: "Default name", value: .string("Untitled"))
        Setting(name: "Fluid animations", value: .bool(true))
    }
}
4: Các điều kiện sử dụng Function Builder:
- Chúng ta có thể thêm vào các conditionalđể hỗ trợ việc sử dụngFunction Builder. Bản thânSwiftđã hỗ trợ tất cả các loạiconditionalchúng ta cần sử dụng, tuy nhiên trong trường hợp vớiSettingsBuilderhiện tại đangimplementchúng ta sẽ gặp phải error nếu chúng ta cố gắng làm như sau:
let shouldShowExperimental: Bool = ...
let settings = makeSettings {
    Setting(name: "Offline mode", value: .bool(false))
    Setting(name: "Search page size", value: .int(25))
    // Compiler error: Closure containing control flow statement
    // cannot be used with function builder 'SettingsBuilder'.
    if shouldShowExperimental {
        SettingsGroup(name: "Experimental") {
            Setting(name: "Default name", value: .string("Untitled"))
            Setting(name: "Fluid animations", value: .bool(true))
        }
    }
}
- 
Đoạn codetrên cho chúng ta thấy cáchexecutedcủa cácfunction buildervớiannotatedkhông được thực hiện giống với cách thông dụng mà chúng ta thường sử dụng vì các tiến trình xử lý cần được định nghĩa rõ ràng bỏibuilderbao gồm cảconditionalnhưif statement:
- 
Chúng ta cần thêm các statementđểsortvà xử lý vấn đề trên. Chúng ta có thêm mộtmethodđược giới thiệu trong API làbuidlIf. Tác dụng của nó làmapmỗiif statement:
// Here we extend Array to make it conform to our SettingsConvertible
// protocol, in order to be able to return an empty array from our
// 'buildIf' implementation in case a nil value was passed:
extension Array: SettingsConvertible where Element == Setting {
    func asSettings() -> [Setting] { self }
}
extension SettingsBuilder {
    static func buildIf(_ value: SettingsConvertible?) -> SettingsConvertible {
        value ?? []
    }
}
- if statementở trên bây giờ đã có thể hoạt động chính xác với mong đợi của chúng ta nhưng chúng ta vẫn cần thêm các- if/elseđể có thể bổ sung- method buildEithervới các các- argumentphù hợp với các- if/else statement:
extension SettingsBuilder {
    static func buildEither(first: SettingsConvertible) -> SettingsConvertible {
        first
    }
    static func buildEither(second: SettingsConvertible) -> SettingsConvertible {
        second
    }
}
- Chúng ta thêm vào elsechoif statementđầu tiên của chúng ta từ trước nên chúng ta có ví dụ cho việc cácusersẽrequestcácsettingmà trước đó chưa được hiển thị:
let settings = makeSettings {
    Setting(name: "Offline mode", value: .bool(false))
    Setting(name: "Search page size", value: .int(25))
    if shouldShowExperimental {
        SettingsGroup(name: "Experimental") {
            Setting(name: "Default name", value: .string("Untitled"))
            Setting(name: "Fluid animations", value: .bool(true))
        }
    } else {
        Setting(name: "Request experimental access", value: .bool(false))
    }
}
- 
Cho đến tận Swift 5.3, cácmethod buildEithermới được hỗ trợswift statementđể sử dụng cho việcbuild contextmà không cần thêm cácbuild method.
- 
Chúng ta sẽ cố cải tiến đoạn code trên thêm một chút cho biến shouldShowExperimentaltrongenumbằng cách hỗ trợ việcaccessnhiều level khác nhau. Đơn giản là chúng ta có thể chuyển đổi qua lại trongenumvớimakeSettings closurevà trình biên dịch sẽ tự động chuyển đếnmethod buildEither:
enum UserAccessLevel {
    case restricted
    case normal
    case experimental
}
let accesssLevel: UserAccessLevel = ...
let settings = makeSettings {
    Setting(name: "Offline mode", value: .bool(false))
    Setting(name: "Search page size", value: .int(25))
    switch accesssLevel {
    case .restricted:
        Setting.Empty()
    case .normal:
        Setting(name: "Request experimental access", value: .bool(false))
    case .experimental:
        SettingsGroup(name: "Experimental") {
            Setting(name: "Default name", value: .string("Untitled"))
            Setting(name: "Fluid animations", value: .bool(true))
        }
    }
}
- Bổ sung thêm một chú ý là type Setting.Emptyvới mỗiswitch statementtrongcase``.restrictedcó thể sẽ cần sử dụng thêmbreak keyworkvới cácfunction buildersử dụngswitch statement. Như cách sử dụngEmptyViewtrongSwiftUI, cácSetting APImới đã có thêmSetting.Emptycho trường hợp này:
extension Setting {
    struct Empty: SettingsConvertible {
        func asSettings() -> [Setting] { [] }
    }
}
All rights reserved
 
  
 