Refreshable view in SwiftUI.

Apple đã giới thiệu một API mới trong SwiftUI tại WWDC21 cho phép chúng ta có thể gắn action refresh cho bất kỳ view nào. Điều đó đồng nghĩa Apple đã hỗ trợ trực tiếp chúng ta cho cơ chế refresh rất phổ biến là pull-to-refresh. Bài viết này chúng ta sẽ cùng tìm hiểu

  • Apple đã giới thiệu một API mới trong SwiftUI tại WWDC21 cho phép chúng ta có thể gắn action refresh cho bất kỳ view nào. Điều đó đồng nghĩa Apple đã hỗ trợ trực tiếp chúng ta cho cơ chế refresh rấ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ủa API mới này cũng như cùng xây dựng cơ chế refresh riêng biệt.

1/ Sức mạnh của async/await:

  • Apple đã giới thiệu cho chúng ta một pattern đó là async/await trong SwiftUI để thông báo cho chúng ta biết khi nào operation``refresh hoàn tất. Do đó để bắt đầu áp dụng APIrefreshing mới chúng ta sẽ sử dụng từ khóa async khi khai báo một function để có thể trigger mỗi khi refresh action tiến hành.

  • Giả sử một ứng dụng có tính năng bookmarking và chúng tôi đã có BookmarkListViewModel chịu trách nhiệm cung cấp data cho UI. Để cho phép data đó được refresh chúng ta cần một methodreload hoạt động asynchorous lần lượt gọi DatabaseController để fetch về một array các Bookmark.

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ụng refreshable trong BookmarkListview như sau:
structBookmarkList:View{@ObservedObjectvar viewModel:BookmarkListViewModelvar body:someView{List(viewModel.bookmarks){ bookmark in...}.refreshable {await viewModel.reload()}}}
  • Với thay đổi trên, ListUI của chúng ta đã hỗ trợ cơ thế pull-to-refresh. SwiftUI sẽ tự động ẩn và hiển thị spinner khi refresh đang hoạt động và còn đảm bảo rằng không có hành động refresh nào khác hoạt động đồng thời.

  • Thêm vào đó Swift còn hỗ trợ chúng ta cơ chế first class function để chúng ta có thể truyền methodreload từ viewModel một cách trực tiếp giúp chúng ta có thể implementation mộ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 loading thì chúng ta thường sẽ phải để tâm nhiều đến việc xử lý error vì đây là điều rất dễ xảy ra. Lấy ví dụ cụ thể hơn thì khi APIloadAllModels thực hiện throws function thì chúng ta thường kèm theo từ khóa try để xử lý bất kỳ error nào. Trong SwiftUI ta 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óa throws khi khai báo function:
classBookmarkListViewModel:ObservableObject{...funcreload()asyncthrows{
        bookmarks =tryawait databaseController.loadAllModels(
            ofType:Bookmark.self)}}
  • Tuy nhiên cách làm trên khiến code BookmarkList không được thực thi cho đến khi refreshable đượ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ần wrap method reload lại trong do/catch để có thể bắt được error khi 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ả state của chúng ta(bao gồm error) trong viewModel. Chúng ta cần di chuyển do/catch lên trên trong viewModel như 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 view của chúng ta trở nên đơn giản hơn hẳn vì method reload giờ có thể throw error dễ dàng và chi tiết hơn nhiêu vì nó là một phần trong viewModel. Nhưng ở đây chúng ta sẽ cần thêm một error property để có thể sử dụng hiển thị các error xả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ế refresh như sau. Khi chúng ta được truyền cho một RefreshActionvalue, chúng ta sẽ cần setpropertyisPerforming thành true khi mà actionrefresh đang tiến hành cũng như cho phép chúng ta theo dõi state của các UIrefreshing chú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 RetryButton cho phép chúng ta có thể Retry khi mà action refresh kết thúc hoặc xảy ra lỗi. Chúng ta sẽ cần một refreshenviremonet value ở đây cho phép chúng ta có thể access bất kỳ RefreshAction nào được inject trong view hierachy để sử dụng refreshable. Chúng ta có thể truyền bất kỳ action nào một instance mới RefreshActionPerformer như 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 SwiftUI cho phép chúng ta có thể thêm vào action thông qua biến enviremonet là một quyền năng rất mạnh mẽ – tương đương việc chúng ta có thể tự define một action riêng lẻ để có thể sử dụng cho bất kỳ view nào trong view hierachy. Khi không có sự thay đổi nào trong BookmarkList view, nếu chúng ta chỉ thêm một RetryButton vào trong ErrorView thì nó cũng tiến hành action refreshing y như UI List vì đơn giản là action này có thể sử dụng cho bất kỳ view nào trong view-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

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ