바탕화면에 있는 앱을 꾹 누르고 있으면 앱이 덜덜 떨리면서 수정 상태에 들어가고 드래그가 가능해진다. 이른바 진동 애니매이션이다. 이를 구현해보고자 한다.
결론부터 말하면, 진동하면서 드래그로 이동하는 행위는 구현에 실패했다.
삽질 이후의 결과물은 '크기 조작 + 드래그 기능' 부터 진행된다.
진동 애니매이션 자체를 구현하는 것은 생각보다 간단하다. animation 기능을 이용해서 움직임을 부여하면 된다. 진동 움직임은 단순하게 좌우로만 흔들리는 정도로만 구현해보자.
ShakingView 라는 이름으로 파일을 하나 만들어준다.
진동 상태에 돌입했는지를 판단하기 위한 변수를 하나 만들어준다.
struct ShakingView: View {
@State private var is_shaking = false
var body: some View {
Text("Shake Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
이제 이놈이 움직일 수 있도록 만들어야 한다.
실제 위치가 움직이는 것이 아니라, 움직이는 것처럼 모습만 보여주는 것으로 충분하므로 offset 을 이용한다. offset 은 대상의 실제 위치는 그대로이지만 그림자가 움직이는 것이라고 이해하면 편하다.
좌우로만 흔들거니까 x값만 변동을 주면 된다.
offset은 진동 여부에 따라 위치를 움직일지 말지를 결정해주면 된다.
다음으로 animation 부분이다.
struct ShakingView: View {
@State private var is_shaking = false
var body: some View {
Text("Shake Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.offset(x: is_shaking ? -10 : 0)
.animation(
Animation.linear(duration: 0.1)
.repeatForever(autoreverses: true)
)
.onTapGesture {
self.is_shaking.toggle()
}
}
}
이제 위에서 만든 놈을 1초 동안 누르고 있으면 진동 상태가 되도록 만들어보자. 이는 LongPressGesture 를 이용해서 구현할 수 있다.
LongPressGesture 에 대해 흔히 하는 오해가 있는데, 이 제스쳐는 드래그를 통해 움직이는데 사용되는 기능이 아니다. LongPressGesture 는 minimumDuration 라는 변수를 받는다. 이는 사용자가 얼마나 누르고 있어야 기능을 발동할지를 결정한다. minimumDuration 에 다다르면 제스쳐는 딱 한 번만 실행된다. 연속적으로 실행되는 것이 아니다!
따라서 해당 기능은 진동 애니매이션을 키는 용도로만 사용한다.
struct ShakingView: View {
@State private var is_shaking = false
var body: some View {
let longPress = LongPressGesture(minimumDuration: 1.0)
.onEnded { _ in
withAnimation(Animation
.linear(duration: 0.1)
.repeatForever(autoreverses: true)) {
self.is_shaking = true
}
}
Text("Shake Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.offset(x: is_shaking ? -10 : 0)
.gesture(longPress)
}
}
여러 가지 노력을 해봤지만 오프셋을 동시에 조작하는 행위는 불가능했다.
위에 실험 결과 모두, 드래그 시도 시, 진동이 멈추는 이슈가 발생했다.
결론은 오프셋 조작 애니매이션은 하나만 걸 수 있다. 정확히 말하자면, 각각의 제스쳐는 다른 역할을 심어줘야한다. 서로 다른 제스쳐가 같은 종류의 애니매이션을 수행하게 하는 일은 어렵고 비효율적이다.
위에서 삽질을 하면서 알게 된 점은 제스쳐를 동시에 처리할 수 있는 기능이 존재한다는 것이다. 다른 종류의 작업만 걸어준다면 제스쳐는 동시 수행이 가능하다. 따라서, 이번에는 꾹 눌렀을 때 객체의 크기가 커지고, 드래그로 움직이는 기능을 만들어보겠다.
먼저, 꾹 눌렀을 때와, 드래그 할 때의 상태를 확인할 변수를 설정한다.
struct ShakingView: View {
@State var longPressing = false
@State var position: CGSize = .zero
var body: some View {
Text("Shake Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
다음으로 꾹 누를 때와, 드래그 할 때 처리할 로직을 작성한다.
let longPress = LongPressGesture()
.onChanged { _ in
withAnimation {
self.longPressing = true
}
}
.onEnded { _ in
withAnimation {
self.longPressing = false
}
}
let drag = DragGesture()
.onChanged { value in
self.position = value.translation
}
.onEnded { _ in
withAnimation {
self.longPressing = false
self.position = .zero
}
}
두 개의 애니매이션을 연속적으로 처리하도록 합쳐준다.
simultaneously 를 이용하면 동시에 처리가 가능하다!
let continueGesture = longPress.simultaneously(with: drag)
마지막으로 애니매이션 변수에 따른 크기 변화 및 위치 변화면 객체에 적용해주면 끝이다.
struct ShakingView: View {
@State var longPressing = false
@State var position: CGSize = .zero
var body: some View {
let longPress = LongPressGesture()
.onChanged { _ in
withAnimation {
self.longPressing = true
}
}
.onEnded { _ in
withAnimation {
self.longPressing = false
}
}
let drag = DragGesture()
.onChanged { value in
self.position = value.translation
}
.onEnded { _ in
withAnimation {
self.longPressing = false
self.position = .zero
}
}
let continueGesture = longPress.simultaneously(with: drag)
Text("Shake Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.offset(position)
.scaleEffect(longPressing ? 1.2 : 1.0)
.gesture(continueGesture)
}
}