Hiểu onChange của SwiftUI
Bắt đầu từ iOS 14, SwiftUI cung cấp công cụ sửa đổi onChange cho chế độ xem. Bằng cách sử dụng onChange, chúng ta có thể quan sát các giá trị cụ thể trong chế độ xem và kích hoạt hành động khi chúng thay đổi. Bài viết này sẽ giới thiệu các đặc điểm, cách sử dụng, biện pháp phòng ngừa và các giải pháp thay thế của onChange.
Cách sử dụng onChange
Định nghĩa của onChange như sau:
func onChange<V>(of value: V, perform action: @escaping (V) -> Void) -> some View where V : Equatable
struct OnChangeDemo:View{
@State var t = 0
var body: some View{
Button("change"){
t += 1
}
.onChange(of: t, perform: { value in
print(value)
})
}
}
Việc đóng cửa có thể thực hiện các tác dụng phụ hoặc sửa đổi nội dung có thể thay đổi khác trong chế độ xem.
Giá trị được truyền cho bao đóng (chẳng hạn như giá trị trên) là không thay đổi. Nếu cần sửa đổi, hãy sửa đổi trực tiếp giá trị có thể thay đổi trong dạng xem (t).
Việc đóng onChange chạy trên luồng chính và nên tránh thực hiện các tác vụ chạy dài.
Cách lấy OldValue của giá trị được quan sát
onChange cho phép chúng tôi nắm bắt giá trị cũ (oldValue) của giá trị được quan sát thông qua một bao đóng. Ví dụ:
struct OldValue: View {
@State var t = 1
var body: some View {
Button("change") {
t = Int.random(in: 1...5)
}
.onChange(of: t) { [t] newValue in
let oldValue = t
if newValue % oldValue == 2 {
print("余值为 2")
} else {
print("不满足条件")
}
}
}
}
Đối với các loại cấu trúc, nên sử dụng một thể hiện của cấu trúc khi chụp và không thể chụp trực tiếp các thuộc tính trong cấu trúc. Ví dụ:
struct OldValue1:View{
@State var data = MyData()
var body: some View{
Button("change"){
data.t = Int.random(in: 1...5)
}
.onChange(of: data.t){ [data] newValue in
let oldValue = data.t
if newValue % oldValue == 2 {
print("余值为 2")
} else {
print("不满足条件")
}
}
}
}
struct MyData{
var t = 0
}
Fields may only be captured by assigning to a specific name
Những giá trị nào có thể được quan sát bởi onChange
Bất kỳ loại nào phù hợp với giao thức Equatable đều có thể được quan sát bởi onChange. Đối với các giá trị tùy chọn, chỉ loại Bao bọc cần tuân theo Equatable.
Thông thường, chúng tôi sử dụng onChange để quan sát các thay đổi trong dữ liệu được bao bọc bởi @State, @StateObject hoặc @ObservableObject. Tuy nhiên, trong một số tình huống cụ thể, chúng ta cũng có thể sử dụng onChange để quan sát dữ liệu không phải là nguồn chính xác cho chế độ xem. Ví dụ:
struct NonStateDemo: View {
let store = Store.share
@State var id = UUID()
var body: some View {
VStack {
Button("refresh") {
id = UUID()
}
.id(id)
.onChange(of: store.date) { value in
print(value)
}
}
}
}
class Store {
var date = Date()
var cancellables = Set<AnyCancellable>()
init(){
Timer.publish(every: 3, on: .current, in: .common)
.autoconnect()
.assign(to: \.date, on: self)
.store(in: &cancellables)
}
static let share = Store()
}
Ví dụ này có vẻ hơi vô nghĩa, nhưng nó cung cấp cái nhìn sâu sắc về các đặc điểm của onChange.
Đặc điểm của onChange
Khi onChange được giới thiệu, hầu hết mọi người coi nó như một triển khai của didSet cho @State. Tuy nhiên, có sự khác biệt đáng kể giữa hai.
didSet gọi hoạt động trong bao đóng khi giá trị thay đổi , bất kể giá trị mới có khác giá trị cũ hay không. Ví dụ,
class MyStore{
var i = 0{
didSet {
print("oldValue:\(oldValue),newValue:\(i)")
}
}
}
let store = MyStore()
store.i = 0
//oldValue:0,newValue:0
Trong ví dụ từ phần trước, mặc dù ngày trong Cửa hàng thay đổi ba giây một lần nhưng nó không khiến chế độ xem bị vẽ lại. onChange chỉ được kích hoạt khi nút được nhấp để buộc chế độ xem được vẽ lại.
Nếu nút được bấm nhiều lần trong vòng ba giây, bảng điều khiển sẽ không in thêm thông tin về thời gian.
Các thay đổi trong giá trị được quan sát không kích hoạt onChange. onChange chỉ được kích hoạt mỗi khi chế độ xem được vẽ lại. Sau khi onChange được kích hoạt, nó sẽ so sánh các thay đổi trong giá trị được quan sát. Chỉ khi các giá trị mới và cũ khác nhau, các hoạt động trong bao đóng onChange mới được gọi.
Câu hỏi thường gặp về onChange
Có thể đặt bao nhiêu onChange trong một chế độ xem?
Nhiều như mong muốn. Tuy nhiên, vì việc đóng onChange chạy trên luồng chính, tốt hơn hết là hạn chế sử dụng onChange để tránh ảnh hưởng đến hiệu quả hiển thị của chế độ xem.
Thứ tự thực hiện của nhiều onChange là gì?
Tuân thủ nghiêm ngặt thứ tự hiển thị của cây xem. Trong đoạn mã sau, thứ tự thực hiện onChange là từ trong ra ngoài:
struct ContentView: View {
@State var text = ""
var body: some View {
VStack {
Button("Change") {
text += "1"
}
.onChange(of: text) { _ in
print("TextField1")
}
.onChange(of: text) { _ in
print("TextField2")
}
}
.onChange(of: text, perform: { _ in
print("VStack")
})
}
}
// Output:
// TextField1
// TextField2
// VStack
Trong một chu kỳ kết xuất, việc quan sát cùng một giá trị với nhiều sự kiện onChange sẽ dẫn đến các giá trị cũ và mới giống nhau, bất kể thứ tự xảy ra của chúng. Giá trị sẽ không thay đổi do các sửa đổi được thực hiện bởi các sự kiện onChange trước đó trong chuỗi.
struct InOneLoop: View {
@State var t = 0
var body: some View {
VStack {
Button("change") {
t += 1 // t = 1
}
// onChange1
.onChange(of: t) { [t] newValue in
print("onChange1: old:\(t) new:\(newValue)")
self.t += 1
}
// onChange2
.onChange(of: t) { [t] newValue in
print("onChange2 old:\(t) new:\(newValue)")
}
}
}
}
render looponChange1: old:3 new:4onChange2 old:3 new:4render looponChange1: old:4 new:5onChange2 old:4 new:5render looponChange(of: Int) action tried to update multiple times per frame.
Tại sao onChange báo lỗi
Trong đoạn mã trên, ở cuối đầu ra, chúng tôi nhận được thông báo lỗi cho biết onChange(of: Int) action tried to update multiple times per frame..
Điều này là do, do việc sửa đổi giá trị được quan sát trong onChange, việc sửa đổi sẽ làm mới lại chế độ xem, gây ra một vòng lặp vô hạn. SwiftUI có một cơ chế bảo vệ để tránh đóng băng ứng dụng, điều này buộc phải làm gián đoạn quá trình thực thi onChange.
Đối với số lần lặp lại vòng lặp được phép, không có thỏa thuận rõ ràng. Trong ví dụ trên, các thay đổi do Nút kích hoạt thường bị giới hạn ở 2 lần, trong khi các thay đổi do onAppear kích hoạt có thể vào khoảng 6–7 lần.
struct LoopTest: View {
@State var t = 0
var body: some View {
let _ = print("frame")
VStack {
Text("\(t)")
.onChange(of: t) { _ in
t += 1
print(t)
}
.onAppear(perform: { t += 1 })
}
}
}
frame 2 frame 3 frame 4 frame 5 frame 6 frame 7 frame onChange(of: Int) action tried to update multiple times per frame.
Các lựa chọn thay thế cho onChange
Trong phần này, chúng tôi sẽ giới thiệu một số triển khai tương tự như onChange, nhưng với các hành vi, đặc điểm và tình huống phù hợp khác nhau.
nhiệm vụ(id:)
SwiftUI 3.0 giới thiệu công cụ sửa đổi tác vụ, chạy không đồng bộ nội dung của bao đóng khi chế độ xem xuất hiện và khởi động lại tác vụ khi giá trị id thay đổi.
Khi đơn vị tác vụ trong phần đóng tác vụ đủ đơn giản, nó hoạt động tương tự như onChange, tương đương với sự kết hợp của onAppear và onChange.
struct AsyncTest: View {
@State var t: CGFloat = 0
var body: some View {
let _ = print("frame")
VStack {
Text("\(t)")
.task(id: t) {
t += 1
print(t)
}
}
}
}
frame1.0frame2.0...
Phiên bản Kết hợp của onChange
Trước khi phát hành onChange, hầu hết mọi người đã sử dụng khung Kết hợp để đạt được hiệu quả tương tự.
import Combine
struct CombineVersion: View {
@State var t = 0
var body: some View {
VStack {
Button("change") {
t += 1
}
}
.onAppearAndOnChange(of: t, perform: { value in
print(value)
})
}
}
public extension View {
func onAppearAndOnChange<V>(of value: V, perform action: @escaping (_ newValue: V) -> Void) -> some View where V: Equatable {
onReceive(Just(value), perform: action)
}
}
struct CombineVersion: View {
@State var t = 0
@State var n = 0
var body: some View {
VStack {
Text("\(n)")
Button("change n"){
n += 1
t += 0
}
}
.onAppearAndOnChange(of: t, perform: { value in
print("combine \(t)")
})
.onChange(of: t){ value in
print("onChange \(t)")
}
}
}
Đôi khi, hành vi này chính xác là những gì chúng ta cần.
onChange để ràng buộc
Cách tiếp cận này chỉ có thể được áp dụng cho dữ liệu thuộc loại Binding. Bằng cách thêm một lớp logic trong Tập hợp ràng buộc, chúng tôi có thể phản hồi những thay đổi về nội dung.
extension Binding {
func didSet(_ didSet: @escaping (Value) -> Void) -> Binding<Value> {
Binding(get: { wrappedValue },
set: { newValue in
self.wrappedValue = newValue
didSet(newValue)
})
}
}
struct BindingVersion2: View {
@State var text = ""
var body: some View {
Form {
TextField("text:", text: $text.didSet { print($0) })
}
}
}
Ví dụ: chúng tôi cũng có thể thực hiện kiểm tra trước dữ liệu mới để xác định xem có nên sửa đổi dữ liệu gốc hay không.值:
extension Binding {
func conditionSet(_ condition: @escaping (Value) -> Bool) -> Binding<Value> {
Binding(get: { wrappedValue },
set: { newValue in
if condition(newValue) {
self.wrappedValue = newValue
}
})
}
}
struct BindingVersion3: View {
@State var text = ""
var body: some View {
Form {
Text(text)
TextField("text:", text: $text.conditionSet { text in
return text.count < 5
})
}
}
}
Hàm onChange cung cấp cho chúng ta sự tiện lợi cho việc xử lý logic trong view. Điều quan trọng là phải hiểu các đặc điểm và hạn chế của nó, đồng thời chọn các tình huống thích hợp để sử dụng nó. Trong các tình huống cần thiết, hãy tách xử lý logic khỏi chế độ xem để đảm bảo hiệu quả hiển thị.
Tôi hy vọng bài viết này có thể hữu ích cho bạn. Bạn cũng có thể liên lạc với tôi qua Twitter , kênh Discord hoặc bảng tin trên blog của tôi .

![Dù sao thì một danh sách được liên kết là gì? [Phần 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































