Ngầm định và tường minh
Hai cách gắn animation, và làm cho giá trị của bạn nội suy được
Có hai chỗ để gắn một animation: vào thay đổi, hoặc vào view.
Tường minh bọc quanh thay đổi trạng thái và chạy animation cho mọi thứ chịu ảnh hưởng:
withAnimation(.spring(duration: 0.4)) {
isExpanded.toggle()
}
Ngầm định đánh dấu một view là sẽ chạy animation khi một giá trị cụ thể thay đổi:
CardView()
.rotationEffect(.degrees(isExpanded ? 180 : 0))
.animation(.spring(duration: 0.4), value: isExpanded)
Khác biệt nằm ở phạm vi. withAnimation chạy animation cho mọi view trong ứng dụng phụ thuộc vào
thay đổi đó; .animation(_:value:) chỉ chạy cho view này, và chỉ khi isExpanded đổi.
Hãy ưu tiên ngầm định khi một view có ý riêng về cách nó nên chuyển động — một mũi tên luôn xoay bằng đúng một đường spring, dùng ở đâu cũng vậy. Ưu tiên tường minh khi một cử chỉ phải làm nhiều thứ chuyển động cùng nhau, và trong thực tế đây là trường hợp phổ biến hơn.
Tham số value: giờ không còn là tùy chọn
Bản .animation(_:) nguyên gốc — không có value — chạy animation cho mọi thay đổi mà view nhìn
thấy được. Nó bị bỏ đi vì gần như lúc nào cũng sai: một view vẽ lại vì lý do chẳng liên quan cũng sẽ
chạy animation cho vị trí của nó, nên danh sách trượt qua trượt lại khi có cú cuộn và chữ mờ chồng
lên nhau khi một con số ở chỗ khác đổi.
.animation(.default) // đã bỏ: chạy animation cho mọi thứ, khó lường
.animation(.default, value: isOn) // chỉ chạy khi isOn đổi
Nếu bạn đang làm trong một codebase vẫn còn cách viết cũ, đó là thứ đáng sửa ngay khi nhìn thấy — nó là nguồn đáng tin cậy của những animation chẳng ai yêu cầu.
Mẹo
.animation(nil, value:) tắt animation cho cả một cây con. Nó là công cụ cho đứa con duy nhất
cần nhảy phựt trong khi mọi thứ quanh nó chạy animation, và nó dễ lập luận hơn nhiều so với việc
cố thu hẹp phạm vi một withAnimation để loại trừ thứ gì đó.
Thứ gì thật sự đang được nội suy
SwiftUI chỉ chạy animation được cho một giá trị biết cách nằm ở lưng chừng. Đó là protocol
Animatable, và nó có đúng một yêu cầu:
protocol Animatable {
associatedtype AnimatableData: VectorArithmetic
var animatableData: AnimatableData { get set }
}
CGFloat, Double, CGSize, CGPoint, Color, EdgeInsets và Angle đều tuân thủ, và đó là lý
do các độ dịch, độ mờ, khung và màu chạy animation miễn phí. Thứ gì không tuân thủ thì không nội suy
được, và những thay đổi của nó xảy ra tức thì ngay giữa một animation vốn mượt mà.
Đó là câu trả lời cho “sao phần chữ nhảy phựt trong khi mọi thứ khác trượt?” — một String không có
điểm lưng chừng.
Làm cho một giá trị tự định nghĩa nội suy được
Trường hợp điều này thật sự quan trọng là một Shape được điều khiển bởi một con số. Không có
animatableData, hình đó vẽ lại đúng một lần ở cuối thay vì được vẽ lại ở từng bước:
struct Wedge: Shape {
var endAngle: Angle
var animatableData: Double {
get { endAngle.degrees }
set { endAngle = .degrees(newValue) }
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
radius: rect.width / 2,
startAngle: .zero, endAngle: endAngle, clockwise: false)
return path
}
}
Giờ Wedge(endAngle: .degrees(progress * 360)) quét dần, vì SwiftUI đặt animatableData sáu mươi
lần một giây và gọi path(in:) cho từng giá trị.
Với hai giá trị cùng lúc, hãy dùng AnimatablePair:
var animatableData: AnimatablePair<Double, Double> {
get { AnimatablePair(width, height) }
set { width = newValue.first; height = newValue.second }
}
Transaction
Một transaction là ngữ cảnh animation đi kèm theo một thay đổi. withAnimation là một cách viết
tiện, nó đặt một Animation vào transaction hiện tại; đọc transaction ra cho phép một view biết mình
đang được chạy animation thế nào và đáp lại:
.transaction { transaction in
transaction.animation = transaction.disablesAnimations ? nil : .spring
}
Công dụng thực tế là ghi đè một animation bị áp từ trên xuống. Một view cha bọc mọi thứ trong
withAnimation(.easeInOut(duration: 2)) sẽ lôi view của bạn đi theo; một khối .transaction trên
view của bạn là cách nó từ chối, và đó là thứ duy nhất có tác dụng khi bạn không kiểm soát được chỗ
gọi.
Cảnh báo
.transaction áp lên mọi thay đổi chảy qua view đó, khiến nó thành thứ công cụ thô nhất trong
chương này. Hãy với tới .animation(_:value:) trước và để dành .transaction cho trường hợp
animation được đặt bởi đoạn code bạn không sửa được.