The lifecycle of a SwiftUI view.
Bài đăng này đã không được cập nhật trong 4 năm
-
Một trong những điều khác biệt của
SwiftUIvới những người tiền nhiệm nhưUIKit,AppKitlà cácviewchủ yếu được khai báo dưới dạngvalue typenhưstructthay vìclass. -
Đây là một trong những thay đổi trong thiết kế kiến trúc khiến
APISwiftUIhoạt động nhẹ nhàng, linh hoạt. Thay đổi này đôi khi khiến cho nhữngdevelopertrong đó 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
SwiftUItrong 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,AppKittrong các project mới.
1/ Vai trò của property body:
-
Property bodytrongView Protocolcó 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ệcupdatecủaviewcũng nhưrendering cycle. -
Trong
UIKit,AppKitchúng ta sử dụng nhữngmethodnhưviewDidLoadhaylayoutSubviewsđể 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ớiSwiftUIbody propertycó thể render lại view mà lại không sử dụng nhữngmethodtrên. -
body propertycho phép chúng tarender viewdựa trênstatehiện tại của nó và hệ thống sẽ dựa vàostatehiện tại của của nó để xem xét việc có cầnrenderlại view không. Ví dụ như khi build mộtUIKitViewControllerchúng ta thườngtriggercácupdatecủamodelvớimethodviewWillAppearđể đảm bảo rằngviewControllerluôn luônrenderlạiviewvớidatamới nhất:
class ArticleViewController: UIViewController {
private let viewModel: ArticleViewModel
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.update()
}
}
- Khi chuyển sang
SwiftUIthay vì ý tưởngrenderlạiviewmỗi khiviewWillAppearthì chúng ta triển khai việcviewModelsẽ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à
bodycủaviewsẽ bị so sánh ngay khi cácviewModelcó sự thay đổi nghĩa là chúng ta sẽ gây ra rất nhiều việcupdatekhông cần thiết củamodel. -
Dễ nhận thấy rằng
body propertykhông phải là là nơi thuận tiện để xử lý những việcupdatekhô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ỉnhonAppeartương tự nhưviewWillAppeartrongcontrolelr:
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 cyclelà một vấn đề quan trọng mà mỗidevelopercần lưu tâm khi làm việc vớiview. Thực tế chúng ta dễ nhận thấy cácviewtrongSwiftUIcòn không có một vòng đời đúng nghĩa vì chúng ta sử dụngvalue typemà không phải làreference type. -
Khi chúng ta muốn thay đổi một
ArticleViewvà update lại viewModel mỗi khiappđượcresumemỗi khi chuyển vềbackgroundthay 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 quaNotificationCenterkhi 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
ArticleViewvào cácviewkhác. Để diễn đạt trường hợp này chúng ta cùng tạo ra nhiềuArticleViewvaluenhưArticleListViewbằng việc sử dụngListvà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
)
)
)
}
}
}
-
NavigationLinksẽ yêu cầu chúng ta cung cấp chi tiếtdestinationcho từngviewđến và chúng ta đãsetuptrongNotificationCenterkhi khởi tạoArticleView. Cácobservationsẽ đượcactivengay lập tức mặc dù cácviewcòn chưa đượcrender. -
Do đó chúng ta nên triển khai các
functioncàng nhỏ càng tốt và cácArticleViewsẽ được update khiappđược chuyển vềforegroundthay vìupdatetừngArticleViewModelmột. -
Để thực hiện triển khai trên chúng ta cần đến
onReceivethay 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 cancellablenữ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
SwiftUIview đượ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 saoSwiftUIyêu cầu chúng ta cần tạo ra cácviewtrướ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,AppKitvào sử dụng cùng SwiftUI thông quaProtocolUIViewPresentablevà chúng ta sẽ chịu trách nghiệm tạo vàupdatecácinstancecủaviewđang được hiển trị và sử dụng. -
Để minh họa chúng ta cùng
renderNSAttributedStringbằng cách sử dụnginstanceUIKitnhư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
UILabelbằng cáchassignnó 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
updatelại view vớiupdateUIViewmethod,labelsẽ tiếp tụcrenderattributedTextgiống như cũ vàassignlại chomakeUIViewmặc dùstringđã đượcupdate.
- Trong trường hợp chúng ta khởi tạo
-
Cùng khỏi tạo
UILabelvớimakeUIViewmethod. Chúng ta luônassignlạistringcủalabelvớiattributedTextpropertymỗ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
UILabelcủa chúng ta cũng có thể tái sử dụng vàattributedTextluôn luôn đượcupdatevớiwrapperstringproperty.
All rights reserved