Код: В этом коде, когда я помещаю GeometryReader
внутрь ZStack
, анимация isSpecialTransitionNeeded
не работает. Раньше она работала. Почему? Как сделать так, чтобы она работала? Пожалуйста, дайте совет.
struct DashboardBottomView: View {
@State private var stringArray = [String]()
@State private var xOffset = CGFloat.zero
@GestureState private var dragOffset = CGFloat.zero
@State private var selectedTab: String?
let backgroundHeight: CGFloat = 90
let labelSpacing: CGFloat = 27
@State private var previousTab: String?
@State private var selectedIndexTab: Int? = nil
@ViewBuilder
private func viewForSelectedTab() -> some View {
switch selectedTab {
case "Dashboard":
DashboardView(viewModel: viewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case "Attendance":
AttendanceSwipeView(viewModel: viewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case "Feed":
DashboardFeedView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
default:
DashboardView(viewModel: viewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private var isSpecialTransitionNeeded: Bool {
if let previousTab {
return (previousTab == "Feed" && selectedTab == "Attendance") ||
(previousTab == "Attendance" && selectedTab == "Dashboard")
} else {
return false
}
}
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
if isSpecialTransitionNeeded {
viewForSelectedTab()
.transition(
.asymmetric(
insertion: .swipeMovement,
removal: .opacity
)
)
} else {
viewForSelectedTab()
.transition(.opacity)
}
}
.background(.gray)
.animation(.easeInOut(duration: 1), value: selectedTab)
.toolbar(.hidden, for: .navigationBar)
ZStack{
Image("Vector 19")
.resizable()
.scaledToFill() // Настройте масштабирование по мере необходимости
.edgesIgnoringSafeArea(.all)
GeometryReader { outer in
let w = outer.size.width
let halfWidth = w / 2
let curveHeight = backgroundHeight * 0.1
let slopeLen = sqrt((halfWidth * halfWidth) + (curveHeight * curveHeight))
let arcRadius = (slopeLen * slopeLen) / (2 * curveHeight)
let arcAngle = 4 * asin((slopeLen / 2) / arcRadius)
let totalItemsWidth = CGFloat(stringArray.count - 1) * labelSpacing
let needsScrolling = totalItemsWidth > w
let startOffset = needsScrolling ? 0 : (w - totalItemsWidth) / 2
HStack(spacing: labelSpacing) {
ForEach(Array(stringArray.enumerated()), id: \.offset) { index, item in
Text(item)
.lineLimit(1)
.fixedSize()
.hidden()
.onTapGesture {
previousTab = selectedTab // Сохранить предыдущую вкладку
selectedTab = item // Обновить выбранную вкладку
print("Выбрано элемент \(item) с индексом \(index)")
}
.overlay {
GeometryReader { proxy in
let midX = proxy.frame(in: .global).midX
let offsetFromMiddle = midX - (w / 2)
CurvedText(string: item, radius: arcRadius)
.foregroundStyle(.white)
.offset(y: -arcRadius)
.rotationEffect(.radians((offsetFromMiddle / w) * arcAngle))
.offset(x: -offsetFromMiddle, y: arcRadius)
.onAppear {
if selectedTab == item && xOffset == .zero {
xOffset = -offsetFromMiddle
}
}
.onTapGesture {
previousTab = selectedTab
selectedTab = item
print("Выбрано элемент \(item) с индексом \(index)")
let itemMidX = proxy.frame(in: .global).midX
let offsetFromMiddle = itemMidX - (w / 2)
withAnimation(.easeInOut) {
xOffset -= offsetFromMiddle
}
}
.onChange(of: dragOffset) { newVal in
if newVal == 0 {
let halfWidth = (proxy.size.width + labelSpacing) / 2
if (abs(offsetFromMiddle) < halfWidth) ||
(index == 0 && offsetFromMiddle > 0) ||
(index == stringArray.count - 1 && offsetFromMiddle < 0) {
previousTab = selectedTab
selectedTab = item
// print("selectedTab... \(selectedTab)")
xOffset -= offsetFromMiddle
}
}
}
}
}
}
}
.offset(x: xOffset + dragOffset + startOffset)
.animation(.easeInOut, value: xOffset)
.gesture(
DragGesture(minimumDistance: 0)
.updating($dragOffset) { val, state, trans in
state = val.translation.width
}
.onEnded { val in
xOffset += val.translation.width
}
)
.padding(.top, curveHeight + 26)
.frame(width: w)
}
.padding(.top, -25)
tabViewForDashboard(selectedIndex: $selectedIndexTab, showSideMenu: $showSideMenu)
}
.frame(height: 150)
.padding(.top, -30)
}
}
}
Ответ
Когда вы добавляете GeometryReader
внутри ZStack
в вашем коде SwiftUI и анимация isSpecialTransitionNeeded
перестает работать, это связано с тем, как View
обновляются в SwiftUI и как они обрабатывают анимации. Вот как можно решить эту проблему:
Причины проблемы:
-
Изменение состояния: Когда вы оборачиваете ваш контент в
GeometryReader
, он может влиять на обновление представления и анимации. В частности, изменения, происходящие вGeometryReader
, могут вызывать перерисовку всего представления и, как следствие, сбивать анимации. - Отсутствие четкого
update
: SwiftUI не всегда может отследить каждое изменение состояния, если у вас есть несколько зависимостей и вложенные виджеты, особенно если используетсяGeometryReader
.
Решение:
Чтобы исправить проблему с анимацией, можно использовать следующее:
-
Создайте отдельные представления: Вместо того чтобы помещать все ваши представления внутри
GeometryReader
, вынесите его логику в отдельные представления. Это поможет SwiftUI легче отслеживать изменения состояния. - Используйте
withAnimation
: Если вы явно модифицируете состояние, которое влияет на отображение переходов, убедитесь, что вы используетеwithAnimation
при измененииselectedTab
.
Вот пример, как можно реорганизовать ваш код:
struct DashboardBottomView: View {
@State private var stringArray = [String]()
@State private var xOffset = CGFloat.zero
@GestureState private var dragOffset = CGFloat.zero
@State private var selectedTab: String?
let backgroundHeight: CGFloat = 90
let labelSpacing: CGFloat = 27
@State private var previousTab: String?
@State private var selectedIndexTab: Int? = nil
private var isSpecialTransitionNeeded: Bool {
if let previousTab = previousTab {
return (previousTab == "Feed" && selectedTab == "Attendance") ||
(previousTab == "Attendance" && selectedTab == "Dashboard")
}
return false
}
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
if isSpecialTransitionNeeded {
viewForSelectedTab()
.transition(
.asymmetric(
insertion: .swipeMovement,
removal: .opacity
)
)
} else {
viewForSelectedTab()
.transition(.opacity)
}
}
.background(.gray)
.animation(.easeInOut(duration: 1), value: selectedTab)
.toolbar(.hidden, for: .navigationBar)
tabScrollView
.frame(height: 150)
}
}
@ViewBuilder
private func viewForSelectedTab() -> some View {
switch selectedTab {
case "Dashboard":
DashboardView(viewModel: viewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case "Attendance":
AttendanceSwipeView(viewModel: viewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case "Feed":
DashboardFeedView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
default:
DashboardView(viewModel: viewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private var tabScrollView: some View {
ZStack {
Image("Vector 19")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
GeometryReader { outer in
let w = outer.size.width
let totalItemsWidth = CGFloat(stringArray.count - 1) * labelSpacing
let needsScrolling = totalItemsWidth > w
let startOffset = needsScrolling ? 0 : (w - totalItemsWidth) / 2
HStack(spacing: labelSpacing) {
ForEach(Array(stringArray.enumerated()), id: \.offset) { index, item in
Text(item)
.lineLimit(1)
.fixedSize()
.hidden()
.onTapGesture {
withAnimation {
previousTab = selectedTab
selectedTab = item
}
}
}
}
.offset(x: xOffset + dragOffset + startOffset)
.animation(.easeInOut, value: xOffset)
.gesture(
DragGesture(minimumDistance: 0)
.updating($dragOffset) { val, state, trans in
state = val.translation.width
}
.onEnded { val in
xOffset += val.translation.width
}
)
}
}
}
}
Заключение
Этот код обеспечивает лучшую организацию вашего представления и может помочь вернуть анимацию isSpecialTransitionNeeded
. Отключение GeometryReader
от непосредственного взаимодействия с состоянием анимации часто помогает избежать нежелательных эффектов при работе с данными или изменениях состояния.