Swift UI - 动画 - 糟糕的表现
我正在尝试使用 Swift UI 显示带有动画的自动滚动文本(选取框?)。
当鼠标悬停在文本上时,动画停止(这就是我存储动画当前状态的原因)。
使用最新的 M1 MBP 之一,这个简单的动画占用了高达 10% 的 CPU,我正在尝试了解原因。 Swift UI 不是为像这样的动画而设计的吗?还是我做错了什么?最后,它只是一个移动 x 偏移的动画。
这是我的字幕的代码。
import SwiftUI
private enum MarqueeState {
case idle
case animating
}
struct GeometryBackground: View {
var body: some View {
GeometryReader { geometry in
Color.clear.preference(key: WidthKey.self, value: geometry.size.width)
}
}
}
struct WidthKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
extension View {
func myOffset(x: CGFloat, y: CGFloat) -> some View {
return modifier(_OffsetEffect(offset: CGSize(width: x, height: y)))
}
func myOffset(_ offset: CGSize) -> some View {
return modifier(_OffsetEffect(offset: offset))
}
}
struct PausableOffsetX: GeometryEffect {
@Binding var currentOffset: CGFloat
@Binding var contentWidth: CGFloat
private var targetOffset: CGFloat = 0.0;
var animatableData: CGFloat {
get { targetOffset }
set { targetOffset = newValue }
}
init(targetOffset: CGFloat, currentOffset: Binding<CGFloat>, contentWidth: Binding<CGFloat>) {
self.targetOffset = targetOffset
self._currentOffset = currentOffset
self._contentWidth = contentWidth
}
public func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.currentOffset = targetOffset
}
let relativeOffset = targetOffset.truncatingRemainder(dividingBy: contentWidth)
let transform = CGAffineTransform(translationX: relativeOffset, y: 0)
return ProjectionTransform(transform)
}
}
struct Marquee<Content: View> : View {
@State private var isOver: Bool = false
private var content: () -> Content
@State private var state: MarqueeState = .idle
@State private var contentWidth: CGFloat = 0
@State private var isAppear = false
@State private var targetOffsetX: CGFloat = 0
@State private var currentOffsetX: CGFloat
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
self.currentOffsetX = 0
}
private func getAnimation() -> Animation {
let duration = contentWidth / 30
print("animation with duration of ", duration)
return Animation.linear(duration: duration).repeatForever(autoreverses: false);
}
public var body : some View {
GeometryReader { proxy in
HStack(alignment: .center, spacing: 0) {
if isAppear {
content()
.overlay(GeometryBackground())
.fixedSize()
content()
.overlay(GeometryBackground())
.fixedSize()
}
}
.modifier(PausableOffsetX(targetOffset: targetOffsetX, currentOffset: $currentOffsetX, contentWidth: $contentWidth))
.onPreferenceChange(WidthKey.self, perform: { value in
if value != self.contentWidth {
self.contentWidth = value
print("Content width = \(value)")
resetAnimation()
}
})
.onAppear {
self.isAppear = true
resetAnimation()
}
.onDisappear {
self.isAppear = false
}
.onHover(perform: { isOver in
self.isOver = isOver
checkAnimation()
})
}
.frame(width: 400)
.clipped()
}
private func getOffsetX() -> CGFloat {
switch self.state {
case .idle:
return self.currentOffsetX
case .animating:
return -self.contentWidth + currentOffsetX
}
}
private func checkAnimation() {
if isOver{
if self.state != .idle {
pauseAnimation()
}
} else {
if self.state != .animating {
resumeAnimation()
}
}
}
private func pauseAnimation() {
withAnimation(.linear(duration: 0)) {
self.state = .idle
self.targetOffsetX = getOffsetX()
}
}
private func resumeAnimation() {
print("Resume animation");
withAnimation(getAnimation()) {
self.state = .animating
self.targetOffsetX = getOffsetX()
}
}
private func resetAnimation() {
withAnimation(.linear(duration: 0)) {
self.currentOffsetX = 0
self.targetOffsetX = 0
self.state = .idle
}
resumeAnimation()
}
}
我们可以按如下方式使用它:
Marquee {
Text("Hello, world! Hello, world! Hello, world! Hello, world!").padding().fixedSize()
}.frame(width: 300)
编辑 我最终使用了 Core Animation,而不是 Swift UI 中内置的动画。对 CPU/能源的影响绝对为零。因此,我不建议使用 Swift UI 动画来实现持久或持久的动画。
I'm trying to display an automatic scrolling text (marquee?) with an animation using Swift UI.
When the mouse is over the text, the animation stops (that's why I store the current state of the animation).
Using one of the latest M1 MBP, this simple animation is using up to 10% of CPU and I'm trying to understand why. Is Swift UI not made for animations like this one or am I doing something wrong? At the end, it's just an animation moving the x offset.
Here is the code of my Marquee.
import SwiftUI
private enum MarqueeState {
case idle
case animating
}
struct GeometryBackground: View {
var body: some View {
GeometryReader { geometry in
Color.clear.preference(key: WidthKey.self, value: geometry.size.width)
}
}
}
struct WidthKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
extension View {
func myOffset(x: CGFloat, y: CGFloat) -> some View {
return modifier(_OffsetEffect(offset: CGSize(width: x, height: y)))
}
func myOffset(_ offset: CGSize) -> some View {
return modifier(_OffsetEffect(offset: offset))
}
}
struct PausableOffsetX: GeometryEffect {
@Binding var currentOffset: CGFloat
@Binding var contentWidth: CGFloat
private var targetOffset: CGFloat = 0.0;
var animatableData: CGFloat {
get { targetOffset }
set { targetOffset = newValue }
}
init(targetOffset: CGFloat, currentOffset: Binding<CGFloat>, contentWidth: Binding<CGFloat>) {
self.targetOffset = targetOffset
self._currentOffset = currentOffset
self._contentWidth = contentWidth
}
public func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.currentOffset = targetOffset
}
let relativeOffset = targetOffset.truncatingRemainder(dividingBy: contentWidth)
let transform = CGAffineTransform(translationX: relativeOffset, y: 0)
return ProjectionTransform(transform)
}
}
struct Marquee<Content: View> : View {
@State private var isOver: Bool = false
private var content: () -> Content
@State private var state: MarqueeState = .idle
@State private var contentWidth: CGFloat = 0
@State private var isAppear = false
@State private var targetOffsetX: CGFloat = 0
@State private var currentOffsetX: CGFloat
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
self.currentOffsetX = 0
}
private func getAnimation() -> Animation {
let duration = contentWidth / 30
print("animation with duration of ", duration)
return Animation.linear(duration: duration).repeatForever(autoreverses: false);
}
public var body : some View {
GeometryReader { proxy in
HStack(alignment: .center, spacing: 0) {
if isAppear {
content()
.overlay(GeometryBackground())
.fixedSize()
content()
.overlay(GeometryBackground())
.fixedSize()
}
}
.modifier(PausableOffsetX(targetOffset: targetOffsetX, currentOffset: $currentOffsetX, contentWidth: $contentWidth))
.onPreferenceChange(WidthKey.self, perform: { value in
if value != self.contentWidth {
self.contentWidth = value
print("Content width = \(value)")
resetAnimation()
}
})
.onAppear {
self.isAppear = true
resetAnimation()
}
.onDisappear {
self.isAppear = false
}
.onHover(perform: { isOver in
self.isOver = isOver
checkAnimation()
})
}
.frame(width: 400)
.clipped()
}
private func getOffsetX() -> CGFloat {
switch self.state {
case .idle:
return self.currentOffsetX
case .animating:
return -self.contentWidth + currentOffsetX
}
}
private func checkAnimation() {
if isOver{
if self.state != .idle {
pauseAnimation()
}
} else {
if self.state != .animating {
resumeAnimation()
}
}
}
private func pauseAnimation() {
withAnimation(.linear(duration: 0)) {
self.state = .idle
self.targetOffsetX = getOffsetX()
}
}
private func resumeAnimation() {
print("Resume animation");
withAnimation(getAnimation()) {
self.state = .animating
self.targetOffsetX = getOffsetX()
}
}
private func resetAnimation() {
withAnimation(.linear(duration: 0)) {
self.currentOffsetX = 0
self.targetOffsetX = 0
self.state = .idle
}
resumeAnimation()
}
}
And we can use it as follow:
Marquee {
Text("Hello, world! Hello, world! Hello, world! Hello, world!").padding().fixedSize()
}.frame(width: 300)
EDIT
I ended up using Core Animation instead of the one built in Swift UI. The cpu / Energy impact is an absolute zero. So I wouldn't recommend using Swift UI animation for long lasting or persistant animations.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
改进建议包括优化视图层次结构、避免不必要的状态更改、优化图像、利用硬件加速、解决内存管理问题、使用 Instruments 进行分析、在各种设备上进行测试、考虑使用 Core Animation 进行复杂动画以及确保使用最新的 SwiftUI版本和 Xcode。
Suggestions for improvement include optimizing the view hierarchy, avoiding unnecessary state changes, optimizing images, utilizing hardware acceleration, addressing memory management issues, profiling with Instruments, testing on various devices, considering Core Animation for complex animations, and ensuring the use of the latest SwiftUI versions and Xcode.