Как остановить SwiftUI DragGesture от анимации вложенных представлений

#swiftui #draggesture

#swiftui #перетаскивание

Вопрос:

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

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

 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 для дочернего представления, которая фактически останавливает перемещение дочерних элементов во время перетаскивания.

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

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

Ответ №1:

Теоретически 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) {
    }
}
  

Ответ №2:

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

 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 также для нажатой кнопки, что я сейчас и сделал с пользовательской кнопкой и перетаскиванием.

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

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

https://www.youtube.com/watch?v=3krC2c56ceQamp;list=PLpGHT1n4-mAtTj9oywMWoBx0dCGd51_yGamp;index=11

Комментарии:

1. Если вы добавляете .animation(.spring()) Text("CONTAINER") , вы видите проблему при перетаскивании

2. Я обновляю вопрос с помощью кода кнопки реального мира, который находится в контейнере

Ответ №3:

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

 struct Container: View {
    
    @Binding var bottomState: CGFloat

              .
              .
              .
              .
}
  

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

 Container(bottomState: $bottomState)
  

Теперь, на ваш Container взгляд, вам просто нужно объявить, что вам не нужна анимация во время bottomState изменения:

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

В .animation(nil, value: bottomState) , by nil вы запрашиваете у no SwiftUI анимацию, в то время value как of 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 она не изменяется.