Làm cho preview SwiftUI thật sự dùng được
Suốt khoảng một năm tôi không dùng preview của SwiftUI. Chúng hết giờ, chúng sập trên những view cần đối tượng environment, và chạy lại cả ứng dụng thì nhanh hơn ngồi chờ một cái preview build xong.
Việc làm cho chúng chạy được là ba thay đổi về cấu trúc, và vòng phản hồi thiết kế mà nó tạo ra thì đáng giá hơn hẳn thời gian bỏ ra.
Vì sao chúng hỏng
Thời gian build. Một preview biên dịch cái module chứa view đó cộng mọi thứ nó phụ thuộc vào. Trong một ứng dụng khối nguyên liền thì đó là cả ứng dụng, mỗi lần. Đây là nguyên nhân lớn nhất và việc chia module là cách chữa lớn nhất.
Thiếu phụ thuộc. Một view đọc @Environment(Library.self) sẽ sập trong preview trừ khi preview
tiêm nó vào. Mọi thứ gọi tới một singleton mong đợi một ứng dụng đã được cấu hình cũng vậy.
Làm việc thật lúc dựng đối tượng. Một view mà bộ khởi tạo của nó mở một cơ sở dữ liệu hay khởi động một request mạng thì cũng làm đúng thế trong preview, mà preview thì không có mạng và không có stack nào được cấu hình.
Cách chữa một: để view nhận vào dữ liệu của nó
Thay đổi có đòn bẩy cao nhất. Một view nhận thứ nó cần qua tham số thì preview dễ như bỡn:
// khó preview: tự đi lấy dữ liệu
struct ProfileView: View {
@State private var profile: Profile?
let userID: User.ID
var body: some View {
…
.task { profile = await api.profile(for: userID) }
}
}
// preview dễ dàng: được cho sẵn dữ liệu
struct ProfileContent: View {
let profile: Profile
var body: some View { … }
}
Hãy tách cái container đi lấy dữ liệu khỏi cái view trình bày. Container thì mỏng và không đáng preview; view trình bày mới là nơi toàn bộ phần thiết kế sống, và nó preview được với một giá trị viết thẳng:
#Preview {
ProfileContent(profile: .sample)
}
Sự tách bạch đó là thiết kế tốt độc lập với chuyện preview — nó vẫn là cú chẻ khiến view kiểm thử được và tái sử dụng được — và đó là lập luận tôi sẽ đưa ra ngay cả với người chẳng bao giờ mở preview.
Cách chữa hai: dữ liệu mẫu dưới dạng thuộc tính static
extension Profile {
static let sample = Profile(
id: UUID(),
name: "Nguyễn Tuấn Anh",
bio: "Lập trình viên iOS ở Hà Nội",
followers: 1_284
)
static let longName = Profile(
id: UUID(),
name: "Một cái tên cố tình dài quá mức để không thể vừa trên một dòng",
bio: "",
followers: 0
)
}
Cái thứ hai còn quan trọng hơn cái thứ nhất. Dữ liệu mẫu nên bao gồm những trường hợp làm vỡ bố cục — chuỗi rỗng, con số khổng lồ, tấm ảnh bị thiếu, cái tên viết bằng một hệ chữ có kích thước khác. Đó là những trường hợp mà nếu không thì bạn tìm thấy trong một ảnh chụp màn hình do người dùng gửi.
Mẹo
Hãy đặt dữ liệu mẫu sau #if DEBUG để nó không được phát hành, và giữ nó cạnh mô hình chứ đừng
để trong preview. Một khi nó tồn tại, các bài test cũng dùng nó, và cái mẫu từng tái hiện một lỗi
bố cục trở thành dữ liệu cố định cho bài test ngăn lỗi ấy.
Cách chữa ba: preview các trạng thái, đừng preview cả màn hình
Một preview duy nhất cho đường đi thuận lợi là cái ít hữu ích nhất. Preview mọi trạng thái mới là chỗ có giá trị:
#Preview("Đã tải") {
ProfileContent(profile: .sample)
}
#Preview("Tên dài") {
ProfileContent(profile: .longName)
}
#Preview("Chế độ tối") {
ProfileContent(profile: .sample)
.preferredColorScheme(.dark)
}
#Preview("Trợ năng XXL") {
ProfileContent(profile: .sample)
.environment(\.dynamicTypeSize, .accessibility5)
}
Cái cuối cùng đã bắt được nhiều lỗi bố cục hơn tất cả các preview khác cộng lại. Chữ ở cỡ trợ năng lớn nhất làm vỡ những bố cục trông vẫn ổn ở cỡ mặc định, và kiểm tra nó tốn một cái preview thay vì phải đổi một thiết lập hệ thống rồi khởi chạy lại.
Những thứ khiến chúng nhanh
Chia module. Một preview trong một module lá thì biên dịch module đó, không phải cả ứng dụng. Đây là khác biệt giữa một preview hai giây và một preview bốn mươi giây.
Giữ các khối #Preview nhỏ. Preview cả một ngăn xếp điều hướng thì biên dịch cả ngăn xếp điều
hướng. Hãy preview cái lá.
Tránh singleton trong các view được preview. Bất cứ thứ gì với tới .shared đều lôi cả đồ thị
phụ thuộc vào bản build của preview.
@Previewable cho trạng thái. Với một view cần một binding, cái này tránh được một view bọc:
#Preview {
@Previewable @State var isOn = true
ToggleRow(title: "Thông báo", isOn: $isOn)
}
Nó đã thay đổi điều gì
Tôi mong đợi việc lặp nhanh hơn. Thứ tôi nhận được là một kiểu làm việc khác.
Dựng một màn hình trong preview với sáu trạng thái nằm cạnh nhau nghĩa là thiết kế trạng thái rỗng, trạng thái lỗi và trạng thái đang nạp cùng lúc với đường đi thuận lợi — thay vì thêm chúng vào sau, khi có ai đó báo rằng màn hình trắng trơn khi mạng kém.
Việc đảo thứ tự đó mới là giá trị thật. Trạng thái rỗng thôi là một việc nghĩ sau, vì nó nằm ngay trên màn hình cạnh trạng thái đầy dữ liệu suốt cả quá trình tôi làm việc.