Appleđã giới thiệu mộtAPImới trongSwiftUItạiWWDC21cho phép chúng ta có thể gắnaction refreshcho bất kỳviewnào. Điều đó đồng nghĩaAppleđã hỗ trợ trực tiếp chúng ta cho cơ chếrefreshrất phổ biến làpull-to-refresh. Bài viết này chúng ta sẽ cùng tìm hiểu cơ chế hoạt động củaAPImới này cũng như cùng xây dựng cơ chếrefreshriêng biệt.
1/ Sức mạnh của async/await:
-
Appleđã giới thiệu cho chúng ta mộtpatternđó làasync/awaittrongSwiftUIđể thông báo cho chúng ta biết khi nàooperation``refreshhoàn tất. Do đó để bắt đầu áp dụngAPIrefreshingmới chúng ta sẽ sử dụng từ khóaasynckhi khai báo mộtfunctionđể có thểtriggermỗi khirefresh actiontiến hành. -
Giả sử một ứng dụng có tính năng
bookmarkingvà chúng tôi đã cóBookmarkListViewModelchịu trách nhiệm cung cấpdatachoUI. Để cho phépdatađó đượcrefreshchúng ta cần mộtmethodreloadhoạt độngasynchorouslần lượt gọiDatabaseControllerđểfetchvề mộtarraycácBookmark.
classBookmarkListViewModel:ObservableObject{@Publishedprivate(set)var bookmarks:[Bookmark]privatelet databaseController:DatabaseController...funcreload()async{
bookmarks =await databaseController.loadAllModels(
ofType:Bookmark.self)}}
- Chúng ta đã có một
async functionđược gọi đểrefreshview data. Tiếp đó chúng ta sẽ sử dụngrefreshabletrongBookmarkListviewnhư sau:
structBookmarkList:View{@ObservedObjectvar viewModel:BookmarkListViewModelvar body:someView{List(viewModel.bookmarks){ bookmark in...}.refreshable {await viewModel.reload()}}}
-
Với thay đổi trên,
ListUIcủa chúng ta đã hỗ trợ cơ thếpull-to-refresh.SwiftUIsẽ tự động ẩn và hiển thịspinnerkhirefreshđang hoạt động và còn đảm bảo rằng không có hành độngrefreshnào khác hoạt động đồng thời. -
Thêm vào đó
Swiftcòn hỗ trợ chúng ta cơ chếfirst class functionđể chúng ta có thể truyềnmethodreloadtừviewModelmột cách trực tiếp giúp chúng ta có thểimplementationmột cách gọn nhẹ hơn:
structBookmarkList:View{@ObservedObjectvar viewModel:BookmarkListViewModelvar body:someView{List(viewModel.bookmarks){ bookmark in...}.refreshable(action: viewModel.reload)}}
2/ Xử lý error:
- Khi thực hiện action
loadingthì chúng ta thường sẽ phải để tâm nhiều đến việc xử lýerrorvì đây là điều rất dễ xảy ra. Lấy ví dụ cụ thể hơn thì khiAPIloadAllModelsthực hiệnthrows functionthì chúng ta thường kèm theo từ khóatryđể xử lý bất kỳerrornào. TrongSwiftUIta có một cách khác để thực hiện điều đó bằng cách thêm vào trực tiếp từ khóathrowskhi khai báofunction:
classBookmarkListViewModel:ObservableObject{...funcreload()asyncthrows{
bookmarks =tryawait databaseController.loadAllModels(
ofType:Bookmark.self)}}
- Tuy nhiên cách làm trên khiến code
BookmarkListkhông được thực thi cho đến khirefreshableđược tùy chỉnh lại đễ hoạt động nhưnon-throwing async closure. Để thực hiện điều đó chúng ta cầnwrapmethodreloadlại trongdo/catchđể có thể bắt đượcerrorkhi chúng ta cho hiện thịErrorView.
structBookmarkList:View{@ObservedObjectvar viewModel:BookmarkListViewModel@Stateprivatevar error:Error?var body:someView{List(viewModel.bookmarks){ bookmark in...}.overlay(alignment:.top){if error !=nil{ErrorView(error: $error)}}.refreshable {do{tryawait viewModel.reload()
error =nil}catch{self.error = error
}}}}
- Cách triển khai trên chưa thực sự tối ưu để có thể đóng gói tất cả
statecủa chúng ta(bao gồmerror) trongviewModel. Chúng ta cần di chuyểndo/catchlên trên trongviewModelnhư sau:
classBookmarkListViewModel:ObservableObject{@Publishedprivate(set)var bookmarks:[Bookmark]@Publishedvar error:Error?...funcreload()async{do{
bookmarks =tryawait databaseController.loadAllModels(
ofType:Bookmark.self)
error =nil}catch{self.error = error
}}}
- Chúng ta đã làm cho
viewcủa chúng ta trở nên đơn giản hơn hẳn vì methodreloadgiờ có thểthrow errordễ dàng và chi tiết hơn nhiêu vì nó là một phần trongviewModel. Nhưng ở đây chúng ta sẽ cần thêm một errorpropertyđể có thể sử dụng hiển thị cácerrorxảy ra vì nhiều lý do khác:
structBookmarkList:View{@ObservedObjectvar viewModel:BookmarkListViewModelvar body:someView{List(viewModel.bookmarks){ bookmark in...}.overlay(alignment:.top){if viewModel.error !=nil{ErrorView(error: $viewModel.error)}}.refreshable {await viewModel.reload()}}}
3/ Tự tùy chỉnh logic refreshing:
- Chúng ta sữ tự thực hiện tùy chỉnh cơ chế
refreshnhư sau. Khi chúng ta được truyền cho mộtRefreshActioncóvalue, chúng ta sẽ cầnsetpropertyisPerformingthànhtruekhi màactionrefreshđang tiến hành cũng như cho phép chúng ta theo dõistatecủa cácUIrefreshingchúng ta mong muốn:
classRefreshActionPerformer:ObservableObject{@Publishedprivate(set)var isPerforming =falsefuncperform(_ action:RefreshAction)async{guard!isPerforming else{return}
isPerforming =trueawaitaction()
isPerforming =false}}
- Công việc tiếp theo chúng ta thực hiện là xây dựng
RetryButtoncho phép chúng ta có thểRetrykhi mà actionrefreshkết thúc hoặc xảy ra lỗi. Chúng ta sẽ cần mộtrefreshenviremonet valueở đây cho phép chúng ta có thểaccessbất kỳRefreshActionnào đượcinjecttrongview hierachyđể sử dụngrefreshable. Chúng ta có thể truyền bất kỳ action nào mộtinstancemớiRefreshActionPerformernhư sau:
structRetryButton:View{var title:LocalizedStringKey="Retry"@Environment(.refresh)privatevar action
@StateObjectprivatevar actionPerformer =RefreshActionPerformer()var body:someView{iflet action = action {Button(
role:nil,
action:{await actionPerformer.perform(action)},
label:{ZStack{if actionPerformer.isPerforming {Text(title).hidden()ProgressView()}else{Text(title)}}}).disabled(actionPerformer.isPerforming)}}}
- Thực tế thì việc
SwiftUIcho phép chúng ta có thể thêm vàoactionthông qua biếnenviremonetlà một quyền năng rất mạnh mẽ – tương đương việc chúng ta có thể tựdefinemộtactionriêng lẻ để có thể sử dụng cho bất kỳ view nào trongview hierachy. Khi không có sự thay đổi nào trongBookmarkListview, nếu chúng ta chỉ thêm mộtRetryButtonvào trongErrorViewthì nó cũng tiến hành actionrefreshingy như UIListvì đơn giản làactionnày có thể sử dụng cho bất kỳ view nào trongview-hierachy:
structErrorView:View{@Bindingvar error:Error?var body:someView{iflet error = error {VStack{Text(error.localizedDescription).bold()HStack{Button("Dismiss"){self.error =nil}RetryButton()}}.padding().background(Color.red).foregroundColor(.white).cornerRadius(10)}}}
Nguồn: viblo.asia
