UIViewRepresentable làm cho tử tế
Bọc một view UIKit cho SwiftUI là bốn dòng protocol và một sai lầm thật sự nguy hiểm. Đây là hình dạng chạy được, và kiểu hỏng đã lấy của tôi một ngày.
Ba phần
struct TextEditor: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = .preferredFont(forTextStyle: .body)
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
if textView.text != text { // cái guard quan trọng
textView.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
final class Coordinator: NSObject, UITextViewDelegate {
@Binding var text: String
init(text: Binding<String>) {
_text = text
}
func textViewDidChange(_ textView: UITextView) {
text = textView.text
}
}
}
makeUIViewchạy một lần. Phần cấu hình một lần nằm ở đây.updateUIViewchạy ở mọi lần SwiftUI cập nhật. Hãy coi nó là một đường chạy nóng.makeCoordinatortạo ra đối tượng sở hữu các delegate, target và closure — bất cứ thứ gì mà UIKit cần một tham chiếu bền vững tới.
Cái vòng lặp vô hạn
Cái guard if textView.text != text không phải sự cẩn thận cho gọn gàng. Không có nó:
- Người dùng gõ →
textViewDidChange→textthay đổi - SwiftUI vẽ lại →
updateUIViewchạy → gántextView.text - Gán
textlên mộtUITextViewcó thể bắn delegate lần nữa → quay lại bước 1
Triệu chứng hoặc là ứng dụng đơ, hoặc một cảnh báo lúc chạy “Modifying state during view update”,
hoặc — tệ nhất — con trỏ nhảy về cuối đoạn chữ ở mỗi phím gõ, vì gán text sẽ đặt lại vùng chọn.
Luôn so sánh trước khi gán trong updateUIView. Mọi thuộc tính, mọi lần. Đó là quy tắc quan
trọng nhất trong protocol này.
Cảnh báo
Ngay cả khi có cái guard, việc gán text vẫn làm con trỏ dịch chỗ. Nếu các giá trị thật sự khác
nhau nhưng người dùng đang gõ dở, hãy lưu rồi khôi phục vùng chọn:
let selection = textView.selectedRange
textView.text = text
textView.selectedRange = selectionCoordinator là đối tượng sống lâu
Cái struct của bạn được tạo lại liên tục — nó là một view SwiftUI. Coordinator được tạo một lần
cho mỗi danh tính view và sống sót, thứ khiến nó thành mái nhà đúng đắn cho bất cứ gì có vòng đời:
final class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
private var cancellables: Set<AnyCancellable> = []
private var lastRegion: MKCoordinateRegion?
init(_ parent: MapView) {
self.parent = parent
}
}
Hãy để ý var parent được gán lại trong updateUIView — vì cái struct là mới ở mỗi lần, một
coordinator giữ cái struct cũ là đang giữ những binding cũ và những closure cũ. Đây là con bug phổ
biến thứ hai trong protocol này:
func updateUIView(_ mapView: MKMapView, context: Context) {
context.coordinator.parent = self // làm mới cái struct đã bắt
…
}
Kích thước
Mặc định một representable thì tham lam — nó lấy hết chỗ được mời. sizeThatFits cho bạn quyền kiểm
soát:
func sizeThatFits(_ proposal: ProposedViewSize,
uiView: UITextView,
context: Context) -> CGSize? {
let width = proposal.width ?? UIView.layoutFittingCompressedSize.width
let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
return CGSize(width: width, height: size.height)
}
Trả về nil nghĩa là “dùng hành vi mặc định”. Phương thức này là thứ khiến một UILabel được bọc
hành xử như một Text thay vì lấp đầy màn hình.
Dọn dẹp
dismantleUIView là static và chạy khi view bị gỡ đi:
static func dismantleUIView(_ uiView: MKMapView, coordinator: Coordinator) {
uiView.delegate = nil
coordinator.stopUpdates()
}
Hãy dùng nó cho bất cứ thứ gì nếu không sẽ tiếp tục chạy — một đồng hồ, một trình quản lý vị trí, một observer. Nó là static vì cái struct đã biến mất từ lâu vào lúc nó được gọi, và đó là một lời nhắc tốt về việc những kiểu này thật ra là gì.
Khi nào đừng bọc
Hai trường hợp mà lớp bọc là câu trả lời sai.
Cả một màn hình. Bọc một UIViewController cho cả một màn hình nghĩa là việc điều hướng, vòng
đời và trạng thái của SwiftUI đang đánh nhau với của UIKit. UIHostingController theo chiều ngược
lại thường sạch hơn — hãy giữ màn hình đó trong UIKit rồi nhúng các view SwiftUI vào trong nó.
Thứ mà SwiftUI giờ đã có. TextEditor, Map, PhotosPicker, ShareLink và WebView đều tồn
tại sẵn. Bọc thứ tương đương bên UIKit cho một tính năng mà bản gốc đã lo được là chuốc lấy gánh nặng
bảo trì mà chẳng được lợi gì.
Những trường hợp tốt là những cái không có thứ tương đương trong SwiftUI và có bề mặt API nhỏ:
PDFView, MFMailComposeViewController, AVPlayerLayer, một control bên thứ ba. Lớp bọc nhỏ, một
coordinator, các lần cập nhật đều có guard.