-
Một trong những điều khác biệt của
SwiftUI
với những người tiền nhiệm nhưUIKit
,AppKit
là cácview
chủ yếu được khai báo dưới dạngvalue type
nhưstruct
thay vìclass
. -
Đây là một trong những thay đổi trong thiết kế kiến trúc khiến
API
SwiftUI
hoạt động nhẹ nhàng, linh hoạt. Thay đổi này đôi khi khiến cho nhữngdeveloper
trong đó có tôi thường xuyên nhầm lẫn vì các kiến thức lập trình hướng đối tượng đã sử dụng từ trước. -
Vì vậy trong bài viết này chúng ta hãy cùng dành thời gian để nghiên cữu một cách cẩn thận, kỹ lưỡng hơn về ý nghĩa, cách sử dụng của
SwiftUI
trong cách khai báo, tương tác với UI và hơn nữa là tìm cách để có thể tìm ra các phương pháp mới tốt hơn việc sử dụngUIKit
,AppKit
trong các project mới.
1/ Vai trò của property body:
-
Property body
trongView Protocol
có lẽ là điều khó hiểu nhất trongSwiftUI
đặc biệt là khi nó liên quan mật thiết đến việcupdate
củaview
cũng nhưrendering cycle
. -
Trong
UIKit
,AppKit
chúng ta sử dụng nhữngmethod
nhưviewDidLoad
haylayoutSubviews
để nhận biết các event của hệ thống cũng như xử lý một đoạn logic trong khi vớiSwiftUI
body property
có thể render lại view mà lại không sử dụng nhữngmethod
trên. -
body property
cho phép chúng tarender view
dựa trênstate
hiện tại của nó và hệ thống sẽ dựa vàostate
hiện tại của của nó để xem xét việc có cầnrender
lại view không. Ví dụ như khi build mộtUIKit
ViewController
chúng ta thườngtrigger
cácupdate
củamodel
vớimethod
viewWillAppear
để đảm bảo rằngviewController
luôn luônrender
lạiview
vớidata
mới nhất:
class ArticleViewController: UIViewController {
private let viewModel: ArticleViewModel
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.update()
}
}
- Khi chuyển sang
S
wiftUIthay vì ý tưởng
renderlại
viewmỗi khi
viewWillAppearthì chúng ta triển khai việc
viewModelsẽ
updatekhi xử lý
body property` mới nhất:
struct ArticleView: View {
@ObservedObject var viewModel: ArticleViewModel
var body: some View {
viewModel.update()
return VStack {
Text(viewModel.article.text)
...
}
}
}
-
Tuy nhiên có vấn đề với việc triển khai bên trên là
body
củaview
sẽ bị so sánh ngay khi cácviewModel
có sự thay đổi nghĩa là chúng ta sẽ gây ra rất nhiều việcupdate
không cần thiết củamodel
. -
Dễ nhận thấy rằng
body property
không phải là là nơi thuận tiện để xử lý những việcupdate
không cần thiết trên mà thay vào đóSwiftUI
đã cung cấp một vài tính năng tương tự như trongUIKit
,AppKit
. Chúng ta đang nói đến tùy chỉnhonAppear
tương tự nhưviewWillAppear
trongcontrolelr
:
struct ArticleView: View {
@ObservedObject var viewModel: ArticleViewModel
var body: some View {
VStack {
Text(viewModel.article.text)
...
}
.onAppear(perform: viewModel.update)
}
}
2/ The initializer problem:
-
Life cycle
là một vấn đề quan trọng mà mỗideveloper
cần lưu tâm khi làm việc vớiview
. Thực tế chúng ta dễ nhận thấy cácview
trongSwiftUI
còn không có một vòng đời đúng nghĩa vì chúng ta sử dụngvalue type
mà không phải làreference type
. -
Khi chúng ta muốn thay đổi một
ArticleView
và update lại viewModel mỗi khiapp
đượcresume
mỗi khi chuyển vềbackground
thay vì mỗi khiviewappear
. Một cách để chúng ta thực hiện điều đó là theo dõi mỗi đối tượng thông quaNotificationCenter
khi khởi tạo như sau:
struct ArticleView: View {
@ObservedObject var viewModel: ArticleViewModel
private var cancellable: AnyCancellable?
init(viewModel: ArticleViewModel) {
self.viewModel = viewModel
cancellable = NotificationCenter.default.publisher(
for: UIApplication.willEnterForegroundNotification
)
.sink { _ in
viewModel.update()
}
}
var body: some View {
VStack {
Text(viewModel.article.text)
...
}
}
}
- Triển khai trên hoạt động bình thường cho đến khi chúng ta những
ArticleView
vào cácview
khác. Để diễn đạt trường hợp này chúng ta cùng tạo ra nhiềuArticleView
value
nhưArticleListView
bằng việc sử dụngList
vàNavigationLink
:
struct ArticleListView: View {
@ObservedObject var store: ArticleStore
var body: some View {
List(store.articles) { article in
NavigationLink(article.title,
destination: ArticleView(
viewModel: ArticleViewModel(
article: article,
store: store
)
)
)
}
}
}
-
NavigationLink
sẽ yêu cầu chúng ta cung cấp chi tiếtdestination
cho từngview
đến và chúng ta đãsetup
trongNotificationCenter
khi khởi tạoArticleView
. Cácobservation
sẽ đượcactive
ngay lập tức mặc dù cácview
còn chưa đượcrender
. -
Do đó chúng ta nên triển khai các
function
càng nhỏ càng tốt và cácArticleView
sẽ được update khiapp
được chuyển vềforeground
thay vìupdate
từngArticleViewModel
một. -
Để thực hiện triển khai trên chúng ta cần đến
onReceive
thay vì sử dụngNotificationCenter
để thao dõi quá trìnhview
được khởi tạo. Thêm vào đó chúng ta không còn cầnCombine cancellable
nữa:
struct ArticleView: View {
@ObservedObject var viewModel: ArticleViewModel
var body: some View {
VStack {
Text(viewModel.article.text)
...
}
.onReceive(NotificationCenter.default.publisher(
for: UIApplication.willEnterForegroundNotification
)) { _ in
viewModel.update()
}
}
}
- Khi
SwiftUI
view được khởi tạo không đồng nghĩa với việc nó sẽ được hiển thị lên hoặc sử dụng. Đó là lí do tại saoSwiftUI
yêu cầu chúng ta cần tạo ra cácview
trước thay vì khởi tạo từng view một.
3/ Ensuring that UIKit and AppKit views can be properly reused:
-
Chúng ta có thể đưa
UIKit
,AppKit
vào sử dụng cùng SwiftUI thông quaProtocol
UIViewPresentable
và chúng ta sẽ chịu trách nghiệm tạo vàupdate
cácinstance
củaview
đang được hiển trị và sử dụng. -
Để minh họa chúng ta cùng
render
NSAttributedString
bằng cách sử dụnginstance
UIKit
nhưUILabel
:
struct AttributedText: UIViewRepresentable {
var string: NSAttributedString
private let label = UILabel()
func makeUIView(context: Context) -> UILabel {
label.attributedText = string
return label
}
func updateUIView(_ view: UILabel, context: Context) {
// No-op
}
}
-
Tuy nhiên để sử dụng triển khai bên trên chúng ta cần giải quyết 2 vấn đề lớn
- Trong trường hợp chúng ta khởi tạo
UILabel
bằng cáchassign
nó cho property nghĩa là chúng ta không thể tái khởi tạo klaij nó mỗi khistruct
được khởi tạo lại. - Không
update
lại view vớiupdateUIView
method
,label
sẽ tiếp tụcrender
attributedText
giống như cũ vàassign
lại chomakeUIView
mặc dùstring
đã đượcupdate
.
- Trong trường hợp chúng ta khởi tạo
-
Cùng khỏi tạo
UILabel
vớimakeUIView
method. Chúng ta luônassign
lạistring
củalabel
vớiattributedText
property
mỗi lầnupdateUIView
được gọi:
struct AttributedText: UIViewRepresentable {
var string: NSAttributedString
func makeUIView(context: Context) -> UILabel {
UILabel()
}
func updateUIView(_ view: UILabel, context: Context) {
view.attributedText = string
}
}
- Với cách trên thì cuối cùng
UILabel
của chúng ta cũng có thể tái sử dụng vàattributedText
luôn luôn đượcupdate
vớiwrapper
string
property
.