Я обнаружил очень странную проблему (КОТОРАЯ ПРОИСХОДИТ ТОЛЬКО НА РЕАЛЬНЫХ УСТРОЙСТВАХ), связанную с использованием @NameSpace и MatchedGeometryEffect с использованием NavigationStack. Проблема в том, что если я перехожу с первого экрана на второй экран (на втором экране у меня View с MatchedGeometryEffect), то получаю странный баг, когда анимация MatchedGeometryEffect начинает работать очень плохо (ТОЛЬКО НА РЕАЛЬНОМ УСТРОЙСТВЕ), она такое ощущение, что количество кадров анимации падает до минимума, но в симуляторе или предварительном просмотре все работает нормально, как и должно быть. Однако если я использую экран без NavigationStack, такой проблемы с анимацией не будет. С чем это может быть связано? И как это можно исправить? Чтобы обнаружить проблему с анимацией, требуется всего пара строк кода, но чтобы понять, в чем проблема, ушел целый день.
Первый просмотр
struct NameSpaceTest2Navigation: View {
@State private var nameSpaceTest2Path: [String] = []
var body: some View {
NavigationStack(path: $nameSpaceTest2Path) {
Button(action: {
nameSpaceTest2Path.append("nameSpaceTest2")
}, label: {
Text("Button")
})
.navigationDestination(for: String.self) { path in
NameSpaceTest2()
}
}
}
}
Второй просмотр
struct NameSpaceTest2: View {
@State private var selection: Int = 0
var body: some View {
TabView(selection: $selection) {
ForEach(0..<5, id: \.self) { _ in
Color.white
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.overlay(alignment: .top) {
NameSpaceTest2Header(selection: $selection)
}
}
}
Третий вид
struct NameSpaceTest2Header: View {
@Binding var selection: Int
@Namespace var sectionUnderline
var body: some View {
ScrollViewReader { scrollReader in //START: ScrollViewReader
ScrollView(.horizontal, showsIndicators: false) { //START: ScrollView
HStack(spacing: 0) { //START: HStack
ForEach(0..<5, id: \.self) { index in
VStack { //START: VStack
Text("Name: \(index)")
.foregroundStyle(Color.green)
.padding(.horizontal, 24)
.padding(.vertical, 15)
.overlay(alignment: .bottom) {
if selection == index {
Rectangle()
.fill(Color.red)
.frame(height: 5)
.matchedGeometryEffect(id: "sectionUnderline", in: sectionUnderline, properties: .frame)
.transition(.scale(scale: 1))
}
}
.animation(.smooth, value: selection)
} //END: VStack
.onTapGesture {
withAnimation(.smooth) {
scrollReader.scrollTo(index)
selection = index
}
}
.tag(index)
}
} //END: HStack
} //END: ScrollView
.onChange(of: selection, perform: { value in
withAnimation(.smooth) {
scrollReader.scrollTo(value, anchor: .center)
}
})
} //END: ScrollViewReader
}
}
Попробуйте повторить этот код и запустить его на реальном устройстве с NavigationStack, и вы получите ошибку с анимацией, она будет дергаться, как будто у нее 5-10 кадров в секунду.
Затем попробуйте запустить Second View без стека навигации, и вы получите плавную анимацию, какой она и должна быть.
В чем может быть проблема ? Как вернуть плавную анимацию?
Еще раз: тестировать следует только на реальном устройстве.
Я попробовал обернуть select = index в DispacthQueue.main.ascync, но это не сработало. Я попробовал изменить анимацию, но это тоже не сработало. Если я установлю анимацию, я увижу, как прямоугольник меняет размер практически покадрово. Проверил работоспособность в Инструментах, ничего сильно нагруженного не происходит. Не ожидал, что с этим будут проблемы. Не представляю, в чем может быть проблема, ожидал, что анимация будет плавной, как на симуляторе





Мне не удалось воспроизвести проблему с заикающейся анимацией, у меня ваш код работает даже на реальном устройстве (iPhone 8 под управлением iOS 16).
В любом случае, я бы предложил следующие изменения:
HStack, которая всегда будет видна..matchedGeometryEffect, поэтому у них есть isSource: true.isSource: false.selection в качестве идентификатора для сопоставления..animation с Text на панель.VStack, заключающий каждую метку, является избыточным и его можно удалить.Все изменения касаются содержания горизонтали ScrollView в NameSpaceTest2Header:
// NameSpaceTest2Header
HStack(spacing: 0) { //START: HStack
ForEach(0..<5, id: \.self) { index in
Text("Name: \(index)")
.foregroundStyle(.green)
.padding(.horizontal, 24)
.padding(.vertical, 15)
.matchedGeometryEffect(id: index, in: sectionUnderline, isSource: true)
.onTapGesture {
withAnimation(.smooth) {
scrollReader.scrollTo(index)
selection = index
}
}
.tag(index)
}
} //END: HStack
.overlay(alignment: .bottom) {
Rectangle()
.fill(.red)
.frame(height: 5)
.matchedGeometryEffect(id: selection, in: sectionUnderline, isSource: false)
.animation(.smooth, value: selection)
}
Вполне возможно, что проблема возникает потому, что TabView обычно используется в качестве родительского элемента для NavigationStack, а не наоборот. В вашем случае вы используете страничный TabView, поэтому, если приведенные выше изменения не помогают и проблема все еще сохраняется, вы можете попробовать изменить TabView на страничный ScrollView. Для этого необходимо использовать scrollTargetLayout, scrollPosition и scrollTargetBehavior(.paging). Ответ на вопрос SwiftUI — безопасная зона TabView показывает пример.
Кстати, возможно, вам захочется взглянуть на ответ на вопрос Как я могу динамически выбирать объекты с помощью ScrollView? (это тоже был мой ответ). При этом используется аналогичный маркер выбора, но с той разницей, что если вы прокручиваете метки вверху, выбор автоматически переключается, чтобы полоса не исчезла за пределами экрана. В вашей версии отсутствует это автоматическое переключение - если прокручивать метки, то полоску можно прокрутить за пределы экрана (но, возможно, вы не против).
Спасибо большое, я сделал как вы сказали. Я переместил Rectangle в HStack, добавил matchedGeometryEffect в Text. Я запустил его на реальном устройстве и лаги анимации исчезли.