Function builder trong Swift
Bài đăng này đã không được cập nhật trong 4 năm
-
Function buidler
là tính năng được ưa thích nhất kể từ khi ra mắt cùngSwiftUI
trong versionSwift 5.1
. Nó ngay lập tức là một phần quan trọng trong việc triển khaiSwiftUI
trong khi chưa đượcApple
phá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 buidler
này để xem nó mang lại cho cho chúng ta những lợi ích thế nào trong việc triển khaicode
trongSwiftUI
. -
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 buidler
trong 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 versionSwift
nào. Hãy chắc chắn versionSwift
củaproject
bạ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
Swift
hoạt động là thực hànhcode
ví 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ùngSetting
như 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 type
cho các giá trị củaenum
để đảm bảo việc cho việc cácinstance setting
sẽ có đầy đủ cáctype
khác nhau. -
Thông qua việc tạo
group
chúng ta có thể tạo ra cácnested setting
như 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 builder
trongSwift
là giúp cho việcbuild
cáccontent
trongfunction
thành cácsingle value
. TrongSwiftUI
chúng ta sử dụng tính năng trên cho việctransform
cáccontent
của một hoặc nhiềucontainer
(HStack
hoặcVStack
) trong mộtview
bằ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))
-
SwiftUI
sử dụng cácfunction
khác nhau trong việcimplementations
nhưViewBuilder
vàSceneBuilder
. Nhưng việc chúng ta không thể tham khảosource code
cho cáctype
này nên chúng ta sẽbuild
cácFunction buidler
cho các setting trongAPI
này: -
Như tính năng property-wrappers-in-swift , một
function buidler
sẽ đảm nhiệm việcimplement
cá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 đểimplement
cáccase
làm việc củaFunction Builder
. Chúng ta tham khảo cách dùngbuildBlock
sau:
@_functionBuilder
struct SettingsBuilder {
static func buildBlock() -> [Setting] { [] }
}
- Cách
return type
trên trongfunction ([Setting])
phải chỉ định thêmtype
màbuilder
có thể sử dụng. Chúng ta sẽ chọn cáchimplement API
nhưglobal function
để sử dụng cácSettingsBuilder
cho bất kỳclosure
nào được truyền đến:
func makeSettings(@SettingsBuilder _ content: () -> [Setting]) -> [Setting] {
content()
}
- Với cách
code
trên chúng ta có thể gọimakeSettings
với mộttrailing closure
trống và nhận về mộtarray
trống:
let settings = makeSettings {}
API
mớ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
SettingsBuilder
của chúng ta đểaccept
cácinput
thì việc của chúng ta cần làm là khai báo bổ sungbuildBlock
với cácargument
phù hợp vớiinput
cho kết quà mà chúng ta muốn nhận về. Chúng ta sẽimplement
mộtmethod
với list các valueSetting
vàreturn
về mộtarray
:
extension SettingsBuilder {
static func buildBlock(_ settings: Setting...) -> [Setting] {
settings
}
}
- Với
buildBlock
mới vừa được khai báo, chúng ta sẽ cho phép fillmakeSettings
gọi đến cácSetting
value và cácfunction builder
sẽ tích hợp cho các value trongarray
và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
SwiftUI
và thêm vào cácfunction builder
vớ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 typeSettingGroup
cóannotates
là@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
group
giống cách chúng ta định nghĩa cácsetting
bằng cách kết nối mỗiSetting
vớiclosure
như 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 closure
thì chúng ta sẽ bị báo lỗi vì cácmethod function builder
sẽ đang đợi rất nhiều giá trịSetting
vàSettingGroup
mới của chúng ta là mộttype
hoàn toàn khác. -
Để xử lý
error
trên chúng ta cần thêm cácprototol
để có thể chia sẻ giữaSetting
và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à
modify
cácfunction buidler
củabuildBlock
đểaccept
cá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
SwiftUI
hỗ trợ bằng cách thiết laapk cácgroup
như 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ạiconditional
chúng ta cần sử dụng, tuy nhiên trong trường hợp vớiSettingsBuilder
hiện tại đangimplement
chú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
code
trên cho chúng ta thấy cáchexecuted
của cácfunction builder
vớiannotated
khô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ỏibuilder
bao gồm cảconditional
nhưif statement
: -
Chúng ta cần thêm các
statement
đểsort
và 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àmap
mỗ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ácif/else
để có thể bổ sungmethod buildEither
với các cácargument
phù hợp với cácif/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
else
choif statement
đầu tiên của chúng ta từ trước nên chúng ta có ví dụ cho việc cácuser
sẽrequest
cácsetting
mà 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 buildEither
mới được hỗ trợswift statement
để sử dụng cho việcbuild context
mà 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
shouldShowExperimental
trongenum
bằng cách hỗ trợ việcaccess
nhiều level khác nhau. Đơn giản là chúng ta có thể chuyển đổi qua lại trongenum
vớimakeSettings closure
và 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.Empty
với mỗiswitch statement
trongcase``.restricted
có thể sẽ cần sử dụng thêmbreak keywork
với cácfunction builder
sử dụngswitch statement
. Như cách sử dụngEmptyView
trongSwiftUI
, cácSetting API
mới đã có thêmSetting.Empty
cho trường hợp này:
extension Setting {
struct Empty: SettingsConvertible {
func asSettings() -> [Setting] { [] }
}
}
All rights reserved