Проблема с непреднамеренным перемещением по NavigationStack в SwiftUI

Вопросы и ответы

Я создал BlinkingCircle, который просто мигает бесконечно в SwiftUI.
Когда я открываю этот вид в NavigationStack, он начинает двигаться вверх и вниз.
Я не могу понять, почему круг движется.
Пожалуйста, помогите.

Вот минимальный код, который воспроизводит проблему, а также запись этой проблемы.

import SwiftUI

struct ContentView: View {
    @State private var pushedViewIDs: [String] = []

    var body: some View {
        NavigationStack(path: $pushedViewIDs) {
            Form {
                NavigationLink(value: "blinking-circle") {
                    Label("Показать мигающий круг", systemImage: "arrow.forward")
                }
            }
            .navigationTitle("Приложение")
            .navigationDestination(for: String.self) { pushedViewID in
                if pushedViewID == "blinking-circle" {
                    BlinkingCircle()
                }
            }
        }
    }
}

struct BlinkingCircle: View {
    @State private var isTranslucent = false

    var body: some View {
        Circle()
            .frame(width: 200, height: 200)
            .opacity(isTranslucent ? 0.3 : 1) // Если я закомментирую это, он все равно движется.
            .onAppear {
                withAnimation(.linear(duration: 1).repeatForever()) {
                    isTranslucent.toggle()
                }
            }
    }
}

#Preview {
    ContentView()
}

запись экрана

Когда круг только что был добавлен (до onAppear), его положение немного выше, чем должно быть (подозреваю, что это связано с тем, что начальное положение не учитывает навигационную панель).

Только после этого он позиционируется на правильном месте, немного опускаясь. Поэтому withAnimation также анимирует это изменение положения.

Как упомянул workingdog support Ukraine в комментариях, такое поведение больше не наблюдается в iOS 18, поэтому это, вероятно, ошибка.

Один из способов исправить это — ограничить область анимации только opacity.

struct BlinkingCircle: View {
    @State private var isTranslucent = false

    var body: some View {
        Circle()
            .frame(width: 200, height: 200)
            .animation(nil, value: isTranslucent) // эта строка отключает анимацию до этой точки
            .opacity(isTranslucent ? 0.3 : 1)
            .animation(.linear(duration: 1).repeatForever(), value: isTranslucent)
            .onAppear {
                isTranslucent.toggle()
            }
    }
}

На iOS 17 вы также можете сделать так:

struct BlinkingCircle: View {
    @State private var isTranslucent = false

    var body: some View {
        Circle()
            .frame(width: 200, height: 200)
            .animation(.linear(duration: 1).repeatForever()) { content in
                content.opacity(isTranslucent ? 0.3 : 1)
            }
            .onAppear {
                isTranslucent.toggle()
            }
    }
}

Ответ

Проблема, с которой вы столкнулись, заключается в том, что при использовании анимации в SwiftUI, особенно в этой ситуации с NavigationStack, у нас могут возникнуть непреднамеренные изменения положения вьюх. Это происходит из-за того, что при первом отображении вашего представления оно может не учитывать навигационный бар, что приводит к смещению круговой фигуры.

Когда вы используете анимацию для изменения прозрачности круга, это может также неосознанно повлиять на положение самой фигуры, поскольку SwiftUI пытается сгладить анимацию. Вот несколько решений, которые могут помочь решить эту проблему.

1. Ограничение области анимации

Первый способ – ограничить область анимации только на изменение прозрачности. Вы можете сделать это, установив animation(nil, value: isTranslucent) для самого круга:

struct BlinkingCircle: View {
    @State private var isTranslucent = false

    var body: some View {
        Circle()
            .frame(width: 200, height: 200)
            .animation(nil, value: isTranslucent) // это отключает анимацию до этой точки
            .opacity(isTranslucent ? 0.3 : 1)
            .animation(.linear(duration: 1).repeatForever(), value: isTranslucent)
            .onAppear {
                isTranslucent.toggle()
            }
    }
}

С помощью этой конструкции вы отключаете анимацию для изменения положения круга и применяете анимацию только к его прозрачности.

2. Использование анимации в iOS 17

Если вы работаете на iOS 17, вы можете использовать более специфический подход, чтобы избежать непреднамеренного смещения:

struct BlinkingCircle: View {
    @State private var isTranslucent = false

    var body: some View {
        Circle()
            .frame(width: 200, height: 200)
            .animation(.linear(duration: 1).repeatForever()) { content in
                content.opacity(isTranslucent ? 0.3 : 1)
            }
            .onAppear {
                isTranslucent.toggle()
            }
    }
}

Здесь мы используем функции анимации на уровне отображения, которые более точно управляют тем, что именно анимируется, что также предотвращает нежелательное смещение.

Заключение

Эти подходы помогут устранить непреднамеренное смещение вашей круговой фигуры при использовании анимаций в SwiftUI. Таким образом, вы получите желаемый эффект мигания, не меняя положения объекта. Попробуйте оба способа и посмотрите, какой из них работает лучше в вашем случае.

Оцените материал
Добавить комментарий

Капча загружается...