안녕하세요 새해 첫 글로 신입 개발자 시리즈를 적고있는 킴스캐슬입니다
저번글은 저번주에 들어온 신입이 문서화를 하자고 했다 였는데 그때는 정말 저번주에 들어온 신입이었고 오늘 기준으로는 저번달에 들어온 신입이어서 제목을 이렇게 지었습니다 ㅎㅎ...
소제목은 swiftUI 애니메이션 라이브러리 제작기
정도가 되겠네요
문서화와 마찬가지로 이번에도 팀에 아이디어를 제안했던 과정을 한번 적어볼까합니다
다만, 제 아이디어가 실제로 채택될지는 잘 모르겠고 단순한 아이디어를 구현하는 과정을 적기때문에 앞으로는 어떻게 될지 모르는 아이디어라는거... 제안했다는 사실에 의의를 두고 그 제안을 어떻게 구체화하고 실현했는지, 그 과정에서 어떤 문제점과 피드백이 있었는지의 과정
을 봐주시면 좋겠다고 말씀드리고 싶습니다
그럼 시작해보겠습니다
우선 이 이야기가 나오게된 배경에 대해서 먼저 말씀드리면 좋을것같습니다
대부분의 팀이 그렇겠지만 기본적으로 가지고 있는 디자인시스템은 존재합니다. 예를들면 color, font 아니면 기본적인 버튼이나 바텀시트 등등 말이죠. (UI관련)컴포넌트관련된 디자인시스템모듈이 있으니 필요할때 가져다 쓰면 UI를 짤때 큰 도움이 됩니다. 레고처럼 말이죠
팀내에서 기존코드 리팩터링(+재구성 및 구현) 및 추가기능 및 서비스의 경우엔 본격적으로 swiftUI의 사용을 고려하고 있고 실제로 신규 서비스의 경우엔 full swiftUI로 구현을 하고 있습니다. 신규 프로젝트가 아닌 기존 프로젝트도 최근에 몇 개의 View는 swiftUI로 구현하거나 refactoring하는 등 swiftUI의 비율을 점차 늘려가고 있습니다
그러다가 위에서 말한 full swiftUI로 구현된 신규프로젝트를 담당하신 팀원분이 프로젝트를 개발하시고 출시하신 후에 관련 내용들을 자세하게 공유하시는 자리를 가지게 되었습니다
제가 집중을 했던 부분은 Lottie이외에 swiftUI자체의 animation을 통해서 구현된 부분이 꽤나 많았다는 것이었습니다
예를들어서 이런 코드가 있었습니다
Task {
try await Task.sleep(nanoseconds: 340_000_000)
withAnimation(.linear(duration: 0.04)) {
xOffset = 22.0
}
try await Task.sleep(nanoseconds: 40_000_000)
withAnimation(.linear(duration: 0.04)) {
xOffset = -16.92
}
try await Task.sleep(nanoseconds: 40_000_000)
withAnimation(.linear(duration: 0.07)) {
xOffset = 15.12
}
try await Task.sleep(nanoseconds: 70_000_000)
withAnimation(.linear(duration: 0.04)) {
xOffset = -0.92
}
}
처음에 이 코드를 봤을 때 드는 생각은 이랬습니다
sleep이 있고 animation이 있네? 그럼 sleep일때 일정시간 멈췄다가 animation을 하고 다시 sleep으로 멈췄다가 animation을 하겠네?
근데 막상 실제 동작을 보니 양옆으로 떨리는 애니메이션이었습니다
멈추는 동작이 없는 애니메이션이다보니 sleep이 왜있는거지?
라는 생각이 가장 먼저 들었습니다
나중에 알고보니 withAnimation자체가 비동기로 동작하기때문에 특정 animation이 끝나고 다음 animation이 동작하도록 동기적으로 실행시기키 위해서는 중간중간 인위적으로 sleep을 넣어줘야한다는걸 알게되었습니다
네...이런것도 몰랐네요 저라는 사람은...
위 코드를 보고 몇가지 생각이 떠올랐습니다
- 특정 코드들이 계속해서 반복되고 있음
- withAnimation은 비동기적으로 동작하기에 특정 애니메이션이 끝나고 다음 애니메이션을 실행시키기 위해서는 중간중간 적절한 정도의 Task.sleep이 필요함
2
번에서의 적절한정도의 시간을 sleep한다는 부분에서적절한
의 기준이 굉장히 주관적임- nanoseconds자체가 정확히 몇초인지 몇점몇초인지를 바로 알기가 어려워서 코드 해석하기 까다로움
만약, 디자이너분이 아래와같이 디자인 명세를 줬다고 가정해보겠습니다
(될지 안될지는 모르겠고 우선 말해보세요;;)
이 이미지를 처음에는 가운데기준으로 오른쪽으로 20만큼 0.5초동안 갔다가 0.2초 쉬고 다시 가운데기준으로 왼쪽으로 -20만큼 0.7초 동안 갔다가 0.2초 쉬게 해주세요
만약 이렇게 디자인 명세가 나왔을 때 아래와같이 로직을 짜야합니다
withAnimation(.linear(duration: 0.5초)) {
xOffset = 20.0
}
try await Task.sleep(nanoseconds: 0.5초+0.2초)
withAnimation(.linear(duration: 0.5초)) {
xOffset = -(20.0+20.0)
}
try await Task.sleep(nanoseconds: 0.7초+0.2초)
굉장히 간단한 로직이지만 withanimation이 비동기라는점과 예시코드에는 몇초로 표현했지만 저게 nanoseconds로 표현되었다면 0.9초가 900_000_000로 표현될거고 숫자가 의미하는 바를 직관적으로 이해하기는 더 어려워질 수 있다고 생각했습니다. 추가적으로 0.7초 애니메이션 후 0.2초간 기다리기
라는 명세에 대한 코드로 결과적으로는 0.9라는 값이 코드에들어있는것도 이해를 어렵게하는 간극을 만드는 원인이 될 수도 있겠다는 생각을 했습니다
그러다 문득 Toss에서 애니메이션 관련 디자인시스템관련 영상을 봤던 기억이 떠올랐습니다(Toss Design System에 관련된 이야기였고 TDS라고 하겠습니다)
토스ㅣSLASH 23 - Rally로 3분 만에 애니메이션 완성하기
실제로 사용하고 있는 방식에대한 캡쳐이미지는 아래와 같습니다(출처는 토스 공식 유튜브 채널입니다)
위 이미지를 보면 두개의 애니메이션이 순서대로 동작한다는걸 직관적으로 알 수 있습니다
0.8초동안 y축 -10으로 움직이면서 동시에 10%만큼 커지고
기본시간(0.5초라고 해보죠)만큼 y축 0으로 움직이면서 동시에 원래 크기로 돌아온다
이전 코드처럼 왔다갔다하는 코드지만 직관적으로 이해할 수 있다는걸 알 수 있습니다
단순한 애니메이션이라면 코드자체의 반복이 적어진다!라고 말하긴 어렵겠지만 복잡한 애니메이션일수록 코드의 반복을 줄일 수 있지 않을까라는 생각이 들었습니다
swiftUI에 맞게 어떤 이미지나 UI요소를 위와같이 직관적으로 짜고 읽을 수 있는 방법이 있다면 쉽게 짜고 쉽게 읽을 수 있지 않을까?
라는 생각과 함께 아이디어를 구체화 해보기로 했습니다
(번뜩!)
위에서 언급한 TDS의 Rally의 형태로 애니메이션을 조정하려면 어떻게 해야할까 게다가 swiftUI에서 이걸 적용하려면 어떻게 해야할까를 고민하는 것이 첫 시작점이었습니다
최종적으로 만들고 싶은 형태는 아래와 같습니다
- moveX라는 동작을 하면 offsetX값이 변하면서 애니메이션이 되었으면 좋겠다
(왜냐면 moveX가 좀더 직관적인 동작을 표현해주니까)- moveY라는 동작을 하면 offsetY값이 변하면서 애니메이션이 되었으면 좋겠다
(왜냐면 moveY가 좀더 직관적인 동작을 표현해주니까)- opacity라는 동작을 하면 opacity라는 값이 변하면서 애니메이션이 되었으면 좋겠다
- 모등 동작이 동기적으로(1번 끝나면 2번시작, 2번 끝나면 3번시작)동작하면 좋겠다
(왜냐면 코드를 그렇게 읽고 쓰고 그렇게 동작하기를 기대하기 때문에)
기본적으로 swiftUI에서 애니메이션의 동작은 위 이미지처럼 이뤄집니다. 어떤 객체(여기서는 흰색 사각형이라고 해볼까요?)가 @State로 선언된 Source of Truth를 바라보고 있고 어디선가 withAnimation으로 @State를 바꾸면 duration(특정시간만큼) 값이 바뀌면서 애니메이션이 동작하게 됩니다
TDS처럼 애니메이션을 동작시키기 위해서는 우선 @State를 한곳에서 바꿀 수 있게 만드는 객체가 필요하겠다고 생각했습니다
위 이미지처럼 흰색 사각형의 변경을 이끌어내는 변수들을 Binding<T>
타입으로 가지고 있는 객체를 만들어 흰색 사각형 관련 응집화를 도와주기도 하면서 객체에 변화를 명령하면 자동으로 Binding변수에 값을 변경하고 이게 State에 변경으로 이어지면서 실제 애니메이션을 동작시킬 수 있습니다
특정 객체의 애니메이션 관련된 변수, 로직을 담당하는 새로운 객체가 생김으로써 코드의 응집도와 관심사 분리를 할 수 있을것이라고 생각했습니다
이전에 말했던것처럼 withAnimation자체가 async(비동기)로 동작하기에 Task.sleep을 통해 sync하게 동작하도록 만들어야하지만 그 로직조차도 애니메이션 담당객체만 알고 있으면 되기때문에 사용하는 쪽에서는 직관적으로 애니메이션을 선언해 나갈 수 있습니다
이 아이디어를 적용한 애니메이션 라이브러리가 version 1.0 이었습니다
네이밍 고민을 많이했었는데 motion이라는 워딩이 들어가야 뭔가 좀더 느낌이 살아서 KiTmotion이라는 네이밍을 우선적으로 지어봤습니다(앞에 KiT는 팀내에서 사용하는 접두사 같은겁니다)
위에서 말한 아이디어를 하나하나 구현해보겠습니다
우선 모션을 다룰 객체를 만들고 인자로는 특정 객체(위 예시로는 흰색 사각형)에 애니메이션을 줄 수 있는 변수들을 Binding으로 받습니다
private var offsetState: Binding<CGSize>
private var opacityState: Binding<Double>?
private var steps: [Step] = []
init(
offset: Binding<CGSize>,
opacity: Binding<Double>? = nil
) {
self.offsetState = offset
self.opacityState = opacity
}
그리고 step이라는걸 만들었는데 각각의 애니메이션의 동작을 정의해줍니다
제가 생각했을 때 필수적으로 필요한 인자는 아래 세가지였습니다
아주 간단하게 아래와같은 구조체로 표현할 수 있습니다
private struct Step {
let action: () -> Void
let duration: TimeInterval
let wait: TimeInterval
}
즉 여기서 어떤 동작을 메서드로 실행하면 이러한 Step들을 list에 쌓아두고 실행해!
라고 하면 한번에 list에 있는 동작들을 하나하나 동기적으로 실행하게 하면되지 않을까? 라는 아이디어를 구현시키면 되겠다는 생각을 했습니다
list에 들어간 Step객체에 들어간 action클로저내에서 적절한 binding변수들을 바꿔주면 외부 state의 값을 변화시키면서 애니메이션을 동작시킬 수 있을 것 같다는 생각을 함께 했습니다
@discardableResult
func offset(to value: CGSize, duration: TimeInterval = 0.5, wait: TimeInterval = 0.0) -> Motion {
self.steps.append(.init(action: {self.offsetState.wrappedValue = value}, duration: duration, wait: wait))
return self
}
@discardableResult
func opacity(to value: Double, duration: TimeInterval = 0.5, wait: TimeInterval = 0.0) -> Motion {
self.steps.append(.init(action: {self.opacityState?.wrappedValue = value}, duration: duration, wait: wait))
return self
}
메서드를 실행할때마다 받은 paramter를 기반으로 Step객체를 만들어서 append를 계속 해줍니다. 여기까지는 애니메이션들을 계속 offset메서드를 통해 받은 인자들을 통해서 offset관련 동작들을 정의해준 Step객체를 만들어줍니다
위 메서드를 보면 받은 value를 self.offsetState.wrappedValue = value
코드처럼 offsetState의 wrappedValue에 넣어줍니다. 결국 offset메서드를 통해서는 offset변수를 바꿔주고 opacity메서드를 통해선 opacity변수를 바꿔주기때문에 메서드명만 직관적으로 바꿔놓으면 알아서 관련 변수를 바꿔줘서 직관적으로 코드를 선언하고 동작시킬 수 있습니다
실제로 동작시키는 코드를 어떻게해야할까 고민을 했는데 선언 시점과 동작시점이 다를 수 있기도하고 애니메이션이 끝나면 완료후에 어떤 동작을 해야할 수 도있어서 run이라는 메서드를 통해서 애니메이션의 시작과 끝을 명시할 수 있도록 만들어야겠다고 생각했습니다
우선 코드를 잘짜고 못짜고를 떠나서 돌아가게끔
만 만들어보겠다가 목표였기에 run메서드를 아래와같이 구현해봤습니다
/// for문을 돌면서 async타입의 애니메이션은 따로모으면서 실행시간을 쭉 가져온다?
func run(completionHandler: (() -> Void)? = nil) {
Task {
for step in steps {
withAnimation(.linear(duration: step.duration)) {
step.action()
}
try? await Task.sleep(nanoseconds: UInt64(step.wait * 1_000_000_000 + step.duration * 1_000_000_000))
}
}
}
duration의 시간만큼 sleep을 하기에 각 step들이 동기적으로 동작하게끔 만들 수 있고 wait만큼의 sleep을 추가해줘서 step사이사이의 대기시간을 만들어 줄 수 있습니다
최종적으로는 아래와같은 version 1.0을 완성했습니다
(코드 퀄리티는 모르겠고 돌아가면 장땡이지라는 마인드...)
final class Motion {
private struct Step {
let action: () -> Void
let duration: TimeInterval
let wait: TimeInterval
}
private var offsetState: Binding<CGSize>
private var opacityState: Binding<Double>?
private var steps: [Step] = []
init(
offset: Binding<CGSize>,
opacity: Binding<Double>? = nil
) {
self.offsetState = offset
self.opacityState = opacity
}
@discardableResult
func offset(to value: CGSize, duration: TimeInterval = 0.5, wait: TimeInterval = 0.0) -> Motion {
self.steps.append(.init(action: {self.offsetState.wrappedValue = value}, duration: duration, wait: wait))
return self
}
@discardableResult
func opacity(to value: Double, duration: TimeInterval = 0.5, wait: TimeInterval = 0.0) -> Motion {
self.steps.append(.init(action: {self.opacityState?.wrappedValue = value}, duration: duration, wait: wait))
return self
}
func run(completionHandler: (() -> Void)? = nil) {
Task {
for step in steps {
withAnimation(.linear(duration: step.duration)) {
step.action()
}
try? await Task.sleep(nanoseconds: UInt64(step.wait * 1_000_000_000 + step.duration * 1_000_000_000))
}
}
}
}
실제로 사용해볼까요??
우선 기본적인 swiftUI의 뷰에서 globe이미지를 옮겨보겠습니다
offset이랑 opacity두가지 변수만 가지고 애니메이션을 구현한다고 해볼게요
struct ContentView: View {
@State var imageOffset: CGSize = .init(width: 0, height: 0)
@State var imageOpacity: Double = 1
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
.opacity(imageOpacity)
.offset(imageOffset)
Text("Hello, world!")
}
.padding()
}
}
시점은 onAppear되는 시점에 자동으로 원하는 애니메이션을 동작시킨다고 해봅시다
.onAppear {
Motion(offset: $imageOffset, opacity: $imageOpacity)
.offset(to: .init(width: 100, height: 100), duration: 2, wait: 2)
.opacity(to: 0.5, duration: 2, wait: 2)
.run {
print("애니메이션끝남")
}
}
Motion(여기서는 image관련 애니메이션 변수들을 들고있는 객체겠네요)이 들고있는 변수를 각 메서드에 맞게 변형시켜줄 수 있습니다
코드만 봐도 두가지 애니메이션이 차례대로 동작함을 알 수 있습니다
실제로 그렇게 동작하는지 확인해보겠습니다
2초 동안 움직이고 2초동안 가만히 있었다가 2초동안 투명도가 낮아지고 2초기다렸다가 애니메이션이끝남
이 출력되는걸 확인 할 수 있습니다
근데 여기까지만 하면 정말 정말 심플한 애니메이션만 가능합니다. 그러다보니 한가지 의견이 있었습니다
진짜로 비동기적으로 애니메이션이 동작하게는 못할까요?
예를들면 움직이면서 동시에 불투명도가 변하게 할 수 있는 기능말이죠
동기적으로 만들기 위해서 Task.sleep을 넣었는데 오히려 필요없는 상황이 생긴거죠. 이를 해결하기 위해 어떻게해야할까를 고민하게되었습니다
처음 든 생각은 combine의 merge처럼 동시에 실행되어야할 동작들을 list형태로 받아볼까?
였습니다
(이거면 되지 않을까? 안댐말구~)
예를들어 아래와같이 3개의 동작이 동시에 실행된다면 어떻게 해야할까를 생각해봤습니다
- 3초 동안 A동작 실행
- 2초 동안 B동작 실행
- 1초 동안 C동작 실행
최종적으로는 3초동안 ABC 동작을 동시에 실행하는데 동시에 실행될 동작중에 가장 시간이 긴 동작 시간을 기준으로 설정하면 되겠구나 라는 결론을 내릴 수 있습니다
merge메서드 내에 list형태로 구현된 step들은 동시에 실행시키고 그게 아닌 것들은 이전 로직대로 Task.sleep으로 끝나고 실행될 수 있도록 하면되겠다는 아이디어를 그려볼 수 있었고 이대로 한번 구현을 해봤습니다
그전에, offset을 width와 height로 설정하는건 좀 덜 직관적인거같아 offset을 offsetX와 offsetY로 분리하는작업을 먼저 했음을 미리 알려드립니다!
우선 Motion객체 내에서 MergeStep이라는 enum을 만들어줬습니다 이 case들은 실제 동시에 동작시킬거라는 flag를 넣어서 Step으로 변환해 step list에 append해주기 위한 enum입니다
말이 이해가 안되실 수 있는데 코드를 보시면 아~ 하실겁니다!
enum MergeStep {
case offsetX(to: Double, duration: TimeInterval)
case offsetY(to: Double, duration: TimeInterval)
case opacity(to: Double, duration: TimeInterval)
}
이렇게 현재 정의된 x이동, y이동, 불투명도 변경을 동시에 바꿀수있는 case를 정의해주고 이제 이런 비동기 애니메이션을 list행태로 인자로 받는 메서드를 정의해보겠습니다
mergeFlow라는 메서드를 정의하기전에 제가 생각한 부분은 step에 list형태로 들어있는 동작들을 for문을 통해서 시간계산을 하고 동작을 계산하는데 이게 sync일지 async일지에대한 flag가 있어야 시간계산 및 동작을 계산할 수 있는 최소한의 근거가 될 수 있다고 생각해 Step에 하나의 변수를 추가했습니다
private struct Step {
let action: () -> Void
let duration: TimeInterval
let wait: TimeInterval
var isAsync: Bool = false
}
처음에 어떻게 써야할지에대한 생각이 있었다기보다는 이게 없으면 이게 동시에 동작될 애니메이션인지 순서대로 동작될 애니메이션인지를 모를거같아서 우선 추가를 해봤습니다
그리고 mergeFlow라는 메서드를 만들어봤습니다
@discardableResult
func mergeFlow(wait: TimeInterval = 0, _ element: [MergeStep]) -> Motion {
for asynStep in element {
switch asynStep {
case .opacity(let to, let duration):
self.steps.append(.init(action: {self.opacityState?.wrappedValue = to}, duration: duration, wait: wait, isAsync: true))
case .offsetX(to: let to, duration: let duration):
self.steps.append(.init(action: {self.offsetXState?.wrappedValue = to}, duration: duration, wait: wait, isAsync: true))
case .offsetY(to: let to, duration: let duration):
self.steps.append(.init(action: {self.offsetYState?.wrappedValue = to}, duration: duration, wait: wait, isAsync: true))
}
}
return self
}
mergeFlow라는 메서드의 인자로 받은 MergeStep들을 Step형태로 변환해서 list내부에 넣어주는 메서드입니다
제가 가장 찜찜한 부분입니다. 확장성이 전혀 고려되지 않고 비슷한 로직과 형태라서 좀더 좋은 코드를 만들수 있을거같은데 이부분은 돌아가게끔
구현
에 집중된 로직이기에 추후에 좋은 코드로 고쳐볼까 합니다 ㅎㅎ... 조언주시면 감사드리겠습니다
그러면 이렇게 들어간 Step들을 run메서드에서 잘 처리를 해줘야겠죠?
func run(completionHandler: (() -> Void)? = nil) {
Task {
var asnycGroupDuration: TimeInterval = 0
var asyncGroupWaitAfter: TimeInterval = 0
var endAsyncGroup: Bool = false
for step in steps {
endAsyncGroup = !step.isAsync
if asnycGroupDuration > 0 && endAsyncGroup {
try? await Task.sleep(nanoseconds: UInt64(asnycGroupDuration * 1_000_000_000 + asyncGroupWaitAfter * 1_000_000_000) )
asnycGroupDuration = 0
asyncGroupWaitAfter = 0
}
withAnimation(.linear(duration: step.duration)) {
step.action()
}
if step.isAsync {
asyncGroupWaitAfter = step.wait
asnycGroupDuration = max(asnycGroupDuration, step.duration)
} else {
try? await Task.sleep(nanoseconds: UInt64(step.wait * 1_000_000_000 + step.duration * 1_000_000_000))
}
}
if asnycGroupDuration > 0 {
try? await Task.sleep(nanoseconds: UInt64(asnycGroupDuration * 1_000_000_000))
}
completionHandler?()
}
}
간단하게 설명을 해보면 이 코드는 우선 비동기 작업의 그룹을 관리하기 위해 asnycGroupDuration, asyncGroupWaitAfter, endAsyncGroup와 같은 변수를 설정합니다. asnycGroupDuration은 비동기 작업이 진행되는 동안의 최대 시간을 추적하고, asyncGroupWaitAfter는 비동기 작업이 끝난 후 기다려야 할 추가 시간을 추적합니다
각 step을 처리할 때, 만약 현재 step이 비동기 작업이 아니라면 이전에 실행된 비동기 그룹의 작업이 모두 끝날 때까지 대기하도록 처리를 했습니다. 비동기 작업이 끝난 후에는 추가적으로 설정된 시간을 기다린 후 다음 작업을 진행합니다
각 작업은 withAnimation을 사용하여 지정된 애니메이션 효과로 실행되는데 비동기 작업이 있는 경우, 그 step의 대기 시간과 지속 시간을 저장하고, 비동기 작업이 아닌 경우에는 주어진 대기 시간과 지속 시간만큼 기다리며 작업을 진행합니다. 마지막으로 모든 작업이 끝나면 completionHandler를 호출하여 모든 작업이 완료되었다는걸 알려줄 수 있습니다
자, 그러면 이렇게 만든 애니메이션 객체를 통해 아래와같은 동작을 명령해보겠습니다
.onAppear {
Motion(offsetX: $imageOffsetX, opacity: $imageOpacity)
.mergeFlow([.offsetX(to: 100, duration: 1), .opacity(to: 0.5, duration: 1)])
.mergeFlow([.offsetX(to: 0, duration: 1), .opacity(to: 1, duration: 1)])
.offsetX(to: 100, wait: 2)
.run {
print("애니메이션끝남")
}
}
코드를 보면
- 1초동안 x를 100만큼 이동시키면서 불투명도를 0.5로 바꾸는 애니메이션을 실행
- 1초동안 x를 0으로 옮기면서 불투명도를 1로바꾸는 애니메이션을 실행
- x와 y를 100으로 옮기고 2초대기
- 애니메이션 완료 메세지 출력
한번 결과물을 보겠습니다
(해달라는대로 다 해줬잖아!!! 왜 그러는건데!)
뭔가 결과물이 이상합니다... 원하는 순서와 방식으로 동작하지 않음을 알 수 있습니다... 왜그럴까요???
우선 결과물로부터 하나씩 추적해봅시다
분명 x좌표가 100으로 이동한 후에 다시 0으로 이동해야하는데 이동하지 않는걸 봐서는 1번동작이 끝난 후 2번동작이 실행되지 않았음을 알 수 있습니다
왜그런가 곰곰히 생각해보면
mergeFlow에 들어간 4개의 동작 [.offsetX(to: 100, duration: 1), .opacity(to: 0.5, duration: 1)]
여기서 두개와 [.offsetX(to: 0, duration: 1), .opacity(to: 1, duration: 1)]
여기의 두개가 for문으로 isAsnyc가 true인채로 들어가기때문에 이 4개의 동작이 모두 동시에
실행되었다는걸 알 수 있습니다
즉, mergeFlow가 끝나고 mergeFlow를 동작시키지 못하게됩니다. mergeFlow가 연속적으로 선언되면 모두다 한번에 동작되는걸 알 수 있습니다
여기서 한번의 mergeFlow가 끝나면 isAsnyc를 한번 false로 끊어줄 필요가 있다는걸 알 수 있고 아래와 같이 mergeFlow가 한번 끝난다는걸 명시해줄 수 있는 코드를 추가해봤습니다
@discardableResult
func mergeFlow(wait: TimeInterval = 0, _ element: [MergeStep]) -> Motion {
for asynStep in element {
switch asynStep {
case .opacity(let to, let duration):
self.steps.append(.init(action: {self.opacityState?.wrappedValue = to}, duration: duration, wait: wait, isAsync: true))
case .offsetX(to: let to, duration: let duration):
self.steps.append(.init(action: {self.offsetXState?.wrappedValue = to}, duration: duration, wait: wait, isAsync: true))
case .offsetY(to: let to, duration: let duration):
self.steps.append(.init(action: {self.offsetYState?.wrappedValue = to}, duration: duration, wait: wait, isAsync: true))
}
}
self.steps.append(.init(action: {}, duration: 0, wait: 0, isAsync: false))
return self
}
한번의 mergeFlow들의 동작이 들어가면 self.steps.append(.init(action: {}, duration: 0, wait: 0, isAsync: false))
이 코드를 통해 한번의 async 동작이 끝나고 다음 동작을 동기적으로 동작시킬 수 있는 flag로써 동작할 수 있습니다
이렇게 코드를 추가하고 다시 실행시키면
원하는 대로 mergeFlow사이가 동기적으로 동작하는걸 확인 할 수 있습니다
KiTmotion의 ver 1.1까지의 과정을 글로 담아봤습니다ㅎㅎ
(길다 그죠...)
불편했던 부분을 개선하기위한 아이디어 스케치부터 구현, 추가적인 기능에 대한 확장, 그 과정에서 발생했던 문제 해결과정을 다시 정리하다보니 좀더 잘 만들 순 없을까라는 생각이 많이 들지만 우선 돌아는가는 코드를 만들어냈다는것만으로도 큰 한걸음이지 않을까 싶습니다
물론 이렇게 만든 코드가 팀원분들이 기꺼이 사용해 주실지, 지금 이 방식이 어떤 문제를 직면하게될지 이로인해 어떤 변경점과 확장이 생길지는 모르겠지만 저는 유명한 디자인시스템은 과거에는 이러한 시작점을 지나왔다는 생각을 합니다
사용하면서 기능을 확장하고 유지보수를 하면서 다양한 case를 커버할 수 있는 디자인시스템이 되어나갈수 있었으면 좋겠다는 생각이 들고 그러한 경험을 예전부터 해보고 싶었던 사람으로서 혹여나 팀내에서 채택되지 않더라도 개인프로젝트에 한번 적용을 해보면서 디벨롭 시켜보면 좋겠다라는 생각을 가지고 있습니다
언젠간 같이 공부하던 개발자 친구가 어떤 개발자가 되고 싶은지를 고민해보면 좋을거같다라는 조언을 해준적이 있습니다. 그때 저는 개발자를 위한 개발자
라는 답변을 했던 기억이 있는데 내가 만든 도구로 개발자들이 개발을 하는데 있어서 편해지는 경험에 좀더 높은 가치를 두는사람이 아닐까라는 생각을 했던것같습니다
앞으로 KiTmotion(가제)이 발전하는 과정을 포스팅할 수 있기를 바라며 오늘 글은 여기서 마무리해보겠습니다!
그럼 20000!