Как запретить SwiftUI DragGesture анимировать подпредставления
Я создаю настраиваемое модальное окно, и когда я перетаскиваю модальное окно, любые подпредставления, к которым прикреплена анимация, анимируются, пока я перетаскиваю. Как мне этого не допустить?
Я думал о передаче
@EnvironmentObject
с
isDragging
флаг, но он не очень масштабируемый (и не работает с настраиваемыми
ButtonStyle
s)
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
не меняется.