SwiftUI - как переопределить вложенную анимацию смещения / положения?

Рассмотрим этот простой пример:

struct TestView: View {
    @State private var enabled = false
    
    var body: some View {
        Circle()
            .foregroundColor(.red)
            .overlay(
                Circle()
                    .foregroundColor(.blue)
                    .frame(width: 50, height: 50)
                    .animation(.spring())
                
            )
            .frame(width: 100, height: 100)
            .offset(x: 0, y: enabled ? -50 : 50)
            .animation(.easeIn(duration: 1.0))
            .onTapGesture{
                enabled.toggle()
            }
            
    }
}

Это дает следующую анимацию при нажатии на получившийся круг:

введите описание изображения здесь

Вложенный круг анимируется в новое глобальное положение с помощью своей собственной функции синхронизации (пружина), в то время как внешний круг / родительский вид анимируется в новое глобальное положение с помощью функции времени easyInOut. В идеале все дочерние элементы должны анимироваться с помощью функции синхронизации родителей, если модификатор работает на уровне абстракции родительских представлений (в данном случае offset, но также и такие вещи, как position).

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

Еще одна особенность, которую я заметил, заключается в том, что в этом конкретном примере добавление модификатора animation(nil) непосредственно перед модификатором смещения прерывает анимацию на внешнем круге, несмотря на то, что .easeInOut остается прикрепленным непосредственно к модификатору смещения. Это нарушает мое понимание того, как эти модификаторы связаны, где (согласно этот источник) модификатор анимации применяется ко всем представлениям, которые он влечет за собой, до следующего вложенного модификатора анимации.


person roozbubu    schedule 02.01.2021    source источник
comment
По правде говоря, мне непонятно, чего вы пытаетесь добиться.   -  person Asperi    schedule 03.01.2021
comment
@Asperi Анимации, которые влияют на свойство родительского представления (смещение, положение), не должны запускать анимацию в дочернем представлении. В приведенном выше примере весь круг (внешний + внутренний) должен перемещаться вместе, даже если дочерние представления (т.е. внутренний круг) выполняют другие анимации.   -  person roozbubu    schedule 03.01.2021
comment
Анимация запускается измененными свойствами, но не наоборот. Таким образом, если вы измените свойство animatable, активируется текущая контекстная анимация. Если вы хотите, чтобы анимация была независимой, присоедините ее к определенным различным значениям.   -  person Asperi    schedule 03.01.2021
comment
@Asperi Привязка модификаторов анимации к разным значениям в этом случае все еще не решает проблему - я пробовал. Проблема в том, что модификаторы анимации, привязанные к значению, изменяющему одно свойство, по-прежнему будут применяться ко всем другим свойствам представления, которое они изменяют, даже если изменение этого свойства является неявным (то есть его глобальное положение на экране из-за изменения родителя. это собственное смещение, даже если локальное положение дочернего элемента вообще не меняется).   -  person roozbubu    schedule 03.01.2021
comment
@roozbubu, я обнаружил такую ​​же проблему, см. stackoverflow.com/questions/65582121/.   -  person foolbear    schedule 05.01.2021


Ответы (2)


Мне это кажется ошибкой, о которой стоит сообщить в Apple. Я нашел странный обходной путь, добавив еще один модификатор перед модификатором .offset:

struct TestView: View {
    @State private var enabled = false
    
    var body: some View {
        Circle()
            .foregroundColor(.red)
            .overlay(
                Circle()
                    .foregroundColor(.blue)
                    .frame(width: 50, height: 50)
                    .animation(.spring())
                
            )
            .frame(width: 100, height: 100)
            .scaleEffect(1) // <------- this doesn't do anything but somehow overrides the animation
            .offset(x: 0, y: enabled ? -50 : 50)
            .animation(.easeIn(duration: 1.0))
            .onTapGesture{
                enabled.toggle()
            }
            
    }
}

Я пробовал использовать scaleEffect, RotationEffect и Rotation3DEffect, и все они работают. Не все модификаторы работают. Мне действительно любопытно, почему именно эти.

Параметр .animation (nil) мне тоже кажется ошибкой.

person bze12    schedule 05.01.2021
comment
Насколько мне известно, случай .animation(nil) - это не ошибка, а просто способ работы SwiftUI (по крайней мере, сейчас). - person pawello2222; 05.01.2021

Я постараюсь прояснить некоторые моменты, которые вы упомянули в своем вопросе:

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

Рассмотрим следующий пример:

Circle()
    .overlay(
        Circle()
            .frame(width: 50, height: 50)
            .foregroundColor(.blue)
    )
    .frame(width: 100, height: 100)
    .foregroundColor(.red)

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


добавление модификатора анимации (nil) непосредственно перед модификатором смещения прерывает анимацию на внешнем круге

Это происходит точно так же, как в приведенном выше примере.

Позвонив:

Circle()
    .foregroundColor(.red)
    .overlay(
        Circle()
            .foregroundColor(.blue)
            .frame(width: 50, height: 50)
            .animation(.spring())
    )
    .frame(width: 100, height: 100)
    .animation(nil) // with `nil` animation
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

вы устанавливаете animation на nil только для родителя. Дочернее представление имеет собственный модификатор animation.

Это сработало бы, если бы внутренний Circle не имел модификаторов animation, и поэтому родительский модификатор также применялся бы к этому конкретному дочернему представлению:

Circle()
    .foregroundColor(.red)
    .overlay(
        Circle()
            .foregroundColor(.blue)
            .frame(width: 50, height: 50)
            // .animation(.spring())
    )
    .frame(width: 100, height: 100)
    .animation(nil) // now this will work for both parent and child view
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

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

пока следующий модификатор вложенной анимации не станет здесь ключевым. Настройка animation(nil) работает до, пока не будет найден другой модификатор animation.


как переопределить вложенную анимацию смещения / положения?

Самый простой вариант - просто удалить вложенный модификатор .animation(.spring()).

Если это невозможно, вы можете создать одно представление из обоих кругов, например используя scaleEffect как @ bze12, предложенный в их ответе.

Из документации scaleEffect:

Масштабирует визуализированный вывод этого представления на заданные значения вертикального и горизонтального размера относительно точки привязки.

Это означает, что приведенный ниже код:

Circle()
    .foregroundColor(.red)
    .overlay(
        Circle()
            .foregroundColor(.blue)
            .frame(width: 50, height: 50)
            .animation(.spring())
    )
    .frame(width: 100, height: 100)
    .scaleEffect()
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

можно также читать как:

scaledView
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

Как видите, теперь есть только один модификатор animation, и он применяется к получившемуся масштабированному виду.

person pawello2222    schedule 05.01.2021