Refreshable view in SwiftUI.
Bài đăng này đã không được cập nhật trong 4 năm
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.
class BookmarkListViewModel: ObservableObject {
@Published private(set) var bookmarks: [Bookmark]
private let databaseController: DatabaseController
...
func reload() 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:
struct BookmarkList: View {
@ObservedObject var viewModel: BookmarkListViewModel
var body: some View {
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:
struct BookmarkList: View {
@ObservedObject var viewModel: BookmarkListViewModel
var body: some View {
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:
class BookmarkListViewModel: ObservableObject {
...
func reload() async throws {
bookmarks = try await 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.
struct BookmarkList: View {
@ObservedObject var viewModel: BookmarkListViewModel
@State private var error: Error?
var body: some View {
List(viewModel.bookmarks) { bookmark in
...
}
.overlay(alignment: .top) {
if error != nil {
ErrorView(error: $error)
}
}
.refreshable {
do {
try await 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:
class BookmarkListViewModel: ObservableObject {
@Published private(set) var bookmarks: [Bookmark]
@Published var error: Error?
...
func reload() async {
do {
bookmarks = try await 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:
struct BookmarkList: View {
@ObservedObject var viewModel: BookmarkListViewModel
var body: some View {
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:
class RefreshActionPerformer: ObservableObject {
@Published private(set) var isPerforming = false
func perform(_ action: RefreshAction) async {
guard !isPerforming else { return }
isPerforming = true
await action()
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:
struct RetryButton: View {
var title: LocalizedStringKey = "Retry"
@Environment(\.refresh) private var action
@StateObject private var actionPerformer = RefreshActionPerformer()
var body: some View {
if let 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:
struct ErrorView: View {
@Binding var error: Error?
var body: some View {
if let error = error {
VStack {
Text(error.localizedDescription)
.bold()
HStack {
Button("Dismiss") {
self.error = nil
}
RetryButton()
}
}
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
All rights reserved