Как запретить SwiftUI DragGesture анимировать подпредставления

Я создаю настраиваемое модальное окно, и когда я перетаскиваю модальное окно, любые подпредставления, к которым прикреплена анимация, анимируются, пока я перетаскиваю. Как мне этого не допустить?

Я думал о передаче @EnvironmentObject с isDragging флаг, но он не очень масштабируемый (и не работает с настраиваемыми ButtonStyles)

       import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
            .showModal(isShowing: .constant(true))
    }
}

extension View {
    func showModal(isShowing: Binding<Bool>) -> some View {
        ViewOverlay(isShowing: isShowing, presenting: { self })
    }
}

struct ViewOverlay<Presenting>: View where Presenting: View {
    @Binding var isShowing: Bool
    
    let presenting: () -> Presenting
    
    @State var bottomState: CGFloat = 0
    
    var body: some View {
        ZStack(alignment: .center) {
            presenting().blur(radius: isShowing ? 1 : 0)
            VStack {
                if isShowing {
                    Container()
                        .background(Color.red)
                        .offset(y: bottomState)
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    bottomState = value.translation.height
                                }
                                .onEnded { _ in
                                    if bottomState > 50 {
                                        withAnimation {
                                            isShowing = false
                                        }
                                    }
                                    bottomState = 0
                                })
                        .transition(.move(edge: .bottom))
                }
            }
        }
    }
}

struct Container: View {
    var body: some View {
// I want this to not animate when dragging the modal
        Text("CONTAINER")
            .frame(maxWidth: .infinity, maxHeight: 200)
            .animation(.spring())
    }
}


ОБНОВИТЬ:

       extension View {
    func animationsDisabled(_ disabled: Bool) -> some View {
        transaction { (tx: inout Transaction) in
            tx.animation = tx.animation
            tx.disablesAnimations = disabled
        }
    }
}


Container()
   .animationsDisabled(isDragging || bottomState > 0)

В реальной жизни Контейнер содержит кнопку с анимацией в нажатом состоянии.

       struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.9 : 1)
            .animation(.spring())
    }
}

В дочернее представление добавлена ​​функция animationsDisabled, которая фактически останавливает движение детей во время перетаскивания.

Чего он не делает, так это остановки анимации, когда существо изначально скользит или отклоняется.

Есть ли способ узнать, когда представление по существу не перемещается / не меняется?

3 ответа

Решение

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

В любом случае... здесь возможен обходной путь - нарушить видимость анимации, вставив другой хост-контроллер посередине.

Протестировано с Xcode 12 / iOS 14

       struct ViewOverlay<Presenting>: View where Presenting: View {
    @Binding var isShowing: Bool
    
    let presenting: () -> Presenting
    
    @State var bottomState: CGFloat = 0
    
    var body: some View {
        ZStack(alignment: .center) {
            presenting().blur(radius: isShowing ? 1 : 0)
            VStack {
                    Color.clear
                if isShowing {
                        HelperView {
                    Container()
                        .background(Color.red)
                        }
                        .offset(y: bottomState)
                        .gesture(
                             DragGesture()
                                  .onChanged { value in
                                        bottomState = value.translation.height
                                  }
                                  .onEnded { _ in
                                        if bottomState > 50 {
                                             withAnimation {
                                                  isShowing = false
                                             }
                                        }
                                        bottomState = 0
                                  })
                        .transition(.move(edge: .bottom))
                }
                    Color.clear
            }
        }
    }
}

struct HelperView<Content: View>: UIViewRepresentable {
    let content: () -> Content
    func makeUIView(context: Context) -> UIView {
        let controller = UIHostingController(rootView: content())
        return controller.view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

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

import SwiftUI

struct ContentView: View {
    @State var isShowing = false
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onTapGesture(count: 1, perform: {
                withAnimation(.spring()) {
                    self.isShowing.toggle()
                }
            })
            .showModal(isShowing: self.$isShowing)
    }
}

extension View {
    func showModal(isShowing: Binding<Bool>) -> some View {
        ViewOverlay(isShowing: isShowing, presenting: { self })
    }
}

struct ViewOverlay<Presenting>: View where Presenting: View {
    @Binding var isShowing: Bool
    
    let presenting: () -> Presenting
    
    @State var bottomState: CGFloat = 0
    @State var isDragging = false
    var body: some View {
        ZStack(alignment: .center) {
            presenting().blur(radius: isShowing ? 1 : 0)
            VStack {
                if isShowing {
                    Container()
                        .background(Color.red)
                        .offset(y: bottomState)
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    isDragging = true
                                    bottomState = value.translation.height
                                    
                                }
                                .onEnded { _ in
                                    isDragging = false
                                    if bottomState > 50 {
                                        withAnimation(.spring()) {
                                            isShowing = false
                                        }
                                    }
                                    bottomState = 0
                                })
                        .transition(.move(edge: .bottom))
                }
            }
        }
    }
}

struct Container: View {
    var body: some View {
        CustomButton(action: {}, label: {
            Text("Pressme")
        })
        .frame(maxWidth: .infinity, maxHeight: 200)
    }
}

struct CustomButton<Label >: View where Label: View {
    @State var isPressed = false
    var action: () -> ()
    var label: () -> Label
    var body: some View {
        label()
            .scaleEffect(self.isPressed ? 0.9 : 1.0)
        .gesture(DragGesture(minimumDistance: 0).onChanged({_ in
            withAnimation(.spring()) {
                self.isPressed = true
            }
        }).onEnded({_ in
            withAnimation(.spring()) {
                self.isPressed = false
                action()
            }
        }))
    }
}

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

В этом разница между явной и неявной анимацией.

Посмотрите это видео, где подробно рассматривается эта тема:

https://www.youtube.com/watch?v=3krC2c56ceQ&list=PLpGHT1n4-mAtTj9oywMWoBx0dCGd51_yG&index=11

В Container, объявите привязку var, чтобы можно было передать bottomState к Container Посмотреть:

struct Container: View {
    
    @Binding var bottomState: CGFloat

              .
              .
              .
              .
}

Не забудьте пройти bottomState на ваш Container Смотрите, где бы вы его ни использовали:

Container(bottomState: $bottomState)

Теперь в твоем Container View, вам просто нужно объявить, что вам не нужна анимация, пока bottomState меняется:

Text("CONTAINER")
            .frame(maxWidth: .infinity, maxHeight: 200)
            .animation(nil, value: bottomState) // You Need To Add This
            .animation(.spring())

В .animation(nil, value: bottomState), по nil вы просите SwiftUI no анимации, а value из bottomState меняется.

Этот подход протестирован с использованием Xcode 12 GM, iOS 14.0.1. Вы должны использовать модификаторы Textв том порядке, в котором я их поставил. это означает, что это будет работать:

.animation(nil, value: bottomState)
.animation(.spring())

но это не сработает:

.animation(.spring())
.animation(nil, value: bottomState)

Я также убедился, что добавление .animation(nil, value: bottomState) отключит анимацию только тогда, когда bottomState меняется, а анимация .animation(.spring()) всегда должно работать, если bottomState не меняется.

Другие вопросы по тегам