SwiftUI自定义分段控件:保留切换动画并解决视图加载卡顿
问题解决:保留分段控件动画同时避免视图切换卡顿
问题原因
当你用withAnimation包裹selectedIndex的修改时,SwiftUI会将这个动画应用到所有依赖于selectedIndex的视图更新,包括ViewOne和ViewTwo的切换。如果这两个视图的初始化/渲染成本较高,就会导致动画期间的卡顿。
解决方案
将分段控件的动画作用域限制在控件内部,不让外部的视图切换继承该动画。以下是两种简洁的实现方式:
方式一:用.animation修饰符绑定控件内部动画
移除onTapGesture中的withAnimation,改为在自定义分段控件的根视图上添加.animation修饰符,仅让控件内部UI响应selectedIndex变化的动画:
修改后的CustomSegmentedControl代码:
import SwiftUI struct SegmentedControlItem { var name: String? = nil var iconString: String? = nil } struct CustomSegmentedControl: View { @Binding var selectedIndex: Int let options: [SegmentedControlItem] var body: some View { ZStack(alignment: .center) { GeometryReader { geo in RoundedRectangle(cornerRadius: 16) .foregroundColor(.white) .cornerRadius(16) .padding(4) .frame(width: geo.size.width / CGFloat(options.count)) .shadow(color: Color("Shadow Color").opacity(0.1), radius: 4, x: 0, y: 2) .offset(x: geo.size.width / CGFloat(options.count) * CGFloat(selectedIndex), y: 0) } .frame(height: 48) HStack(spacing: 0) { ForEach((0..<options.count), id: \.self) { index in HStack(spacing: 6) { if let iconString = options[index].iconString { Image(systemName: iconString) } if let name = options[index].name { Text(name) } } .font(.custom("Inter-Medium", size: 16)) .foregroundColor(selectedIndex == index ? Color("Primary Color") : Color.black.opacity(0.5)) .frame(maxWidth: .infinity) .frame(height: 48) .contentShape(Rectangle()) .onTapGesture { // 直接修改selectedIndex,不再用withAnimation包裹 selectedIndex = index } } } } .background( RoundedRectangle(cornerRadius: 18) .fill(Color.wireframe) ) // 仅让当前控件内部的UI响应selectedIndex变化的动画 .animation(.easeInOut(duration: 0.150), value: selectedIndex) } } #Preview { CustomSegmentedControl( selectedIndex: .constant(0), options: [ SegmentedControlItem(name: "One"), SegmentedControlItem(name: "Two") ] ) }
方式二:用本地状态变量隔离动画作用域
在CustomSegmentedControl内部创建一个本地状态变量,用于控制控件的动画效果,再同步到绑定的selectedIndex,这样外部视图不会感知到内部的动画:
修改后的CustomSegmentedControl代码:
import SwiftUI struct SegmentedControlItem { var name: String? = nil var iconString: String? = nil } struct CustomSegmentedControl: View { @Binding var selectedIndex: Int @State private var localSelectedIndex: Int // 本地状态控制内部动画 let options: [SegmentedControlItem] // 初始化时同步外部选中状态到本地 init(selectedIndex: Binding<Int>, options: [SegmentedControlItem]) { self._selectedIndex = selectedIndex self._localSelectedIndex = State(initialValue: selectedIndex.wrappedValue) } var body: some View { ZStack(alignment: .center) { GeometryReader { geo in RoundedRectangle(cornerRadius: 16) .foregroundColor(.white) .cornerRadius(16) .padding(4) .frame(width: geo.size.width / CGFloat(options.count)) .shadow(color: Color("Shadow Color").opacity(0.1), radius: 4, x: 0, y: 2) // 使用本地状态控制偏移动画 .offset(x: geo.size.width / CGFloat(options.count) * CGFloat(localSelectedIndex), y: 0) } .frame(height: 48) HStack(spacing: 0) { ForEach((0..<options.count), id: \.self) { index in HStack(spacing: 6) { if let iconString = options[index].iconString { Image(systemName: iconString) } if let name = options[index].name { Text(name) } } .font(.custom("Inter-Medium", size: 16)) // 使用本地状态控制文字颜色 .foregroundColor(localSelectedIndex == index ? Color("Primary Color") : Color.black.opacity(0.5)) .frame(maxWidth: .infinity) .frame(height: 48) .contentShape(Rectangle()) .onTapGesture { withAnimation(.easeInOut(duration: 0.150)) { localSelectedIndex = index } // 同步到外部绑定变量,无动画 selectedIndex = index } } } } .background( RoundedRectangle(cornerRadius: 18) .fill(Color.wireframe) ) // 同步外部状态变化到本地 .onChange(of: selectedIndex) { newIndex in localSelectedIndex = newIndex } } } #Preview { CustomSegmentedControl( selectedIndex: .constant(0), options: [ SegmentedControlItem(name: "One"), SegmentedControlItem(name: "Two") ] ) }
效果说明
两种方式都能保留分段控件的滑动/颜色切换动画,同时让ViewOne和ViewTwo的切换瞬间完成,不会因为动画导致卡顿。如果需要给视图切换添加独立的动画,可以单独在视图切换处添加.transition或withAnimation,与分段控件的动画互不干扰。
内容的提问来源于stack exchange,提问作者user2343632
相关产品推荐
相关产品推荐

