Изучаем переходы в SwiftUI

Туториалы

Изучаем переходы в SwiftUI

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


Запуск перехода

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

struct BasicTransitionView: View {
    @State var showText = false
    
    var body: some View {
        VStack {
            if (self.showText) {
                Text("HELLO WORLD")
            }
            Button(action: {
                withAnimation() {
                    self.showText.toggle()
                }
            }) {
                Text("Change me")
            }
        }
    }
}

Есть Text, который отображается только в том случае, если showText является истинным. Важно отметить:

  • Изменение состояния должно выполняться в блоке withAnimation(). Если вы явно укажете здесь анимацию, это повлияет на временную кривую перехода и продолжительность.
  • По умолчанию переход - постепенное появление/исчезновение. Ниже мы увидим, как это изменить.
  • Обратите внимание, как layout изменяется и адаптируется при дополнительном элементе, заставляя кнопку перемещаться немного ниже. Это поведение полностью справедливо для нашей ситуации с VStack, просто имейте в виду, что вставленный/удаленный элемент может повлиять на соседние элементы.

Переход может запускаться также при изменении .id() отдельного view. В такой ситуации SwiftUI удаляет старый вью и создает новый, который может запускать переход как для старых, так и для новых вью.

Базовые переходы

Чтобы изменить переход по умолчанию, мы можем установить новый, используя модификатор view компонента .transition. Уже доступно несколько типов преобразований для базовых переходов:

  • .scale
  • .move
  • .offset
  • .slide
  • .opacity

В нашем примере позвольте мне продемонстрировать использование move перехода, который сдвигает добавляемый/удаляемый элемент от/к левому краю его фрейма. Нам просто нужно применить .transition (.move (edge: .leading)) к нашему текстовому вью.

Не стесняйтесь экспериментировать с другими типами.

Объединение переходов

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

  • комбинация переходов
  • переход, созданный из любого модификатора вью

Вы можете объединить два перехода, используя метод .combine (with : ), который возвращает новый переход, являющийся уже результатом применения обоих переходов.
Количество комбинаций переходов не ограничено, поэтому, например, эта комбинация прозрачности, масштабирования и перемещения перехода отлично выглядит:

.transition( AnyTransition.move(edge: .leading).combined(with: AnyTransition.opacity).combined(with: .scale) )

 

Асимметричные переходы

Если вам нужно выполнить другой переход при удалении, нежели при вставке, можно создать асимметричный переход, используя статическую функцию asymmetric(...), например

.transition( AnyTransition.asymmetric(insertion: .scale, removal: .opacity))

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

Пользовательские переходы

Творческая часть начинается с возможности определить свой собственный переход на основе View модификаторов. Позвольте мне начать с очень простого примера, чтобы продемонстрировать основы.

Вот настраиваемый вью модификатор , который маскирует вью в прямоугольный вид с закругленными углами. Ожидается, что параметр value будет иметь значения от 0 до 1, в значении 0 целиком обрезает вью, 1 отображает полностью вью.

struct ClipEffect: ViewModifier {
    var value: CGFloat
    
    func body(content: Content) -> some View {
        content
            .clipShape(RoundedRectangle(cornerRadius: 100*(1-value)).scale(value))
    }
}

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

.transition(AnyTransition.modifier(active: ClipEffect(value: 0), identity: ClipEffect(value: 1)))

Примечание: Чтобы упростить повторное использование в будущем, может быть полезно определить ваши переходы как статические свойства AnyTransition:

extension AnyTransition {
    static var clipTransition: AnyTransition {
        .modifier(
            active: ClipEffect(value: 0),
            identity: ClipEffect(value: 1)
        )
    }
}

При использовании в нашем примере .transition(.clipTransition), результат выглядит так:


Обратите внимание:

  • модификатор перехода зависит от двух состояний: active и identity. identity применяется, когда вью полностью вставлено в иерархию вью, и становится активным, когда элемент отсутствует.
  • во время перехода SwiftUI интерполирует между этими двумя состояниями, поэтому тип модификатора active и модификатора identity должен быть одинаковым! (В противном случае Xcode будет жаловаться).

Освоение переходов

Пока мы рассмотрели довольно простые вещи, так что, возможно, вы ещё не осознали, насколько все это действенно. Выполнение перехода из настраиваемых модификаторов элементов позволяет вам раскрыть свой творческий потенциал, и с небольшой помощью Geometry Effect, AnimatableModifier или режимов наложения вы можете создавать нестандартные переходы. Позвольте мне продемонстрировать несколько примеров:

Подсчёт перехода

Удобно для экранов результатов или достижений, подсчитать value для текстового вью с помощью этого простого AnimatableModifier:

struct CountUpEffect: AnimatableModifier {
    var value: CGFloat
    
    var animatableData: CGFloat {
        get { return value }
        set { value = newValue }
    }
    
    func body(content: Content) -> some View {
        Text("\(self.value, specifier: "%.1f")")
            .font(.title)
            .foregroundColor(self.value<100 ? .primary : .red)
    }
}

В этом случае нам нужно предоставить параметр самому переходу, поэтому я подготовил простое расширение, которое предложит нам правильный переход:

extension AnyTransition {
    static func countUpTransition(value: CGFloat)->AnyTransition {
        return AnyTransition.modifier(active: CountUpEffect(value: 0), identity: CountUpEffect(value: value))
    }
}

И для упражнения мы объединим его с переходом .scale применительно к элементу:

.transition(AnyTransition.countUpTransition(value: self.textValue).combined(with: .scale))

Разбор контента

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

И снова позвольте мне начать с самого эффекта:

struct SlidingDoorEffect: ViewModifier {
    let shift: CGFloat
    
    func body(content: Content) -> some View {
        let c = content
        return ZStack {
            c.clipShape(HalfClipShape(left: false)).offset(x: -shift, y: 0)
            c.clipShape(HalfClipShape(left: true)).offset(x: shift, y: 0)
        }
    }
}

struct HalfClipShape: Shape {
    var left: Bool
        
    func path(in rect: CGRect) -> Path {
        // shape covers lef or right part of rect
        return Path { path in
            let width = rect.width
            let height = rect.height
            
            let startx:CGFloat = left ? 0 : width/2
            let shapeWidth:CGFloat = width/2
            
            path.move(to: CGPoint(x: startx, y: 0))
            
            path.addLines([
                CGPoint(x: startx+shapeWidth, y: 0),
                CGPoint(x: startx+shapeWidth, y: height),
                CGPoint(x: startx, y: height),
                CGPoint(x: startx, y: 0)
            ])
        }
    }
}

Этот эффект дублирует содержимое вью в ZStack и маскирует/обрезает только левую часть нижнего слоя и правую часть верхнего слоя.
Обе копии содержимого позже раздвигаются в зависимости от параметра сдвига.

Text("HELLO WORLD")
	.padding()
    .background(Color.orange)
    .transition(AnyTransition.modifier(active: SlidingDoorEffect(shift: 170), identity: SlidingDoorEffect(shift:

Вы можете сойти с ума, смешивая несколько вхождений контента, поэтому имейте в виду, что

  • смешивание множества (сотен) слоев контента может привести к проблемам с производительностью. identity модификатор остается примененным даже после завершения перехода.
  • хотя я называю слои содержимого копиями, это разные элементы одного и того же типа!
  • если контент анимирован, он останется живым в течение всего перехода
  • некоторые разрезы могут быть видны даже после завершения перехода. Если вы столкнулись с такой ситуацией, решением может быть обрезка подобластей с небольшим (1-2 пикселя) перекрытием.

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

Задание

Лучший способ узнать что-то новое - это практиковаться. Так что по этой теме у меня для вас есть задача. Реализуйте переход, который увеличивает вью при удалении.

Пример:

Как это сделать?

  1. начать с Geometry Effect, который анимирует вью вдоль кривой
  2. подготовить форму для маскирования подобластей контента
  3. создать эффект взрыва, который умножает контент на множество подобластей, маскируя каждую из них соответствующей clipShape и смещая, используя эффект из первого шага
  4. обернуть новый эффект в переход
  5. получить результат

Статья в оригинале

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: