#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
она не изменяется.