Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

GameplayKit으로 상태표시 UI 쉽게 만들기

399 views

Published on

복잡한 절차적인 코드를 정리할 수 있는 GameplayKit의 상태 머신을 사용해봅시다.

Published in: Technology
  • Be the first to comment

GameplayKit으로 상태표시 UI 쉽게 만들기

  1. 1. GameplayKit 으로 상태표시 UI 쉽게 만들기 노수진 · 네이버웹툰 contact@soojin.ro 샘플코드 : https://github.com/nsoojin/VoiceControlSample-iOS
  2. 2. 샘플코드 : https://github.com/nsoojin/VoiceControlSample-iOS
  3. 3. 상태 UI attending detecting listening processing reporting
  4. 4. 3가지 정의 요소 - 상태 (a list of states) - 변화의 조건 (transition) - 초기 상태 (initial state) 유한한 개수의 상태 중에서 한 번에 오로지 하나의 상태만을 가질 수 있고, 어떠한 사건에 의해 한 상태에서 다른 상태로 변화할 수 있다. Finite-State Machines 유한 상태 기계
  5. 5. GameplayKit Randomization Entities and Components State Machines The Minmax Strategist Pathfinding Agents, Goals, Behaviors Rule Systems
  6. 6. GameplayKit State Machines Use this architecture to untangle complex procedural code in your gameplay designs.
  7. 7. State MachinesGameplayKit GKState - 상속 받아서, 오버라이드를 통해 상태별 동작과 전환(transition)의 조건을 정의 - State에 (1) 진입할 때, (2) 탈출할 때, 혹은 (3) State가 지속되는 동안 주기적으로 업데이트 GKStateMachine - [GKState] 를 파라미터로 넘겨서 생성 - 생성 후 초기 상태 지정
  8. 8. UI 요구사항 분석 Core Animation Lottie animationView(Lottie) dotsContainerView(CoreAnimation)
  9. 9. Core Animation internal final class DetectingState: VoiceState { private func bounce(_ dot: UIView) { let delayTime = 0.2 * Double(dot.tag - 1) let bounce = CAKeyframeAnimation(keyPath: “position.y”) bounce.beginTime = CACurrentMediaTime() + delayTime bounce.values = [0, -6, 0, 6, 0] bounce.keyTimes = [0, 0.25, 0.5, 0.75, 1.0] bounce.timingFunctions = [CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut), CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn), CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut), CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)] bounce.duration = 1.0 bounce.isAdditive = true bounce.repeatCount = .infinity dot.layer.add(bounce, forKey: DetectingState.animationKey) } }
  10. 10. Core Animation internal final class ListeningState: VoiceState { private func amplify(_ dot: UIView) { let noise = random(min: 0, max: 0.3) let animation = CASpringAnimation(keyPath: "bounds.size.height") animation.beginTime = CACurrentMediaTime() animation.toValue = dot.bounds.size.height * (regularized(magnitude) + noise) animation.initialVelocity = -15 animation.damping = 7 animation.stiffness = 750 animation.autoreverses = true animation.delegate = self animation.isRemovedOnCompletion = true dot.layer.add(animation, forKey: ListeningState.animationKey) } }
  11. 11. Lottie internal final class ProcessingState: VoiceState { private func animateProcessing() { stateView.animationView.currentProgress = 0.16 stateView.animationView.loopMode = .loop stateView.animationView.play(fromProgress: 0.16, toProgress: 0.32) } } internal final class ReportingState: VoiceState { private func animateReporting() { stateView.animationView.currentProgress = 0.32 stateView.animationView.loopMode = .loop stateView.animationView.play(fromProgress: 0.32, toProgress: 0.52) } }
  12. 12. GKState internal final class DetectingState: VoiceState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) animateBounce() } override func willExit(to nextState: GKState) { super.willExit(to: nextState) removeAnimation() } override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { case is AttendingState.Type, is ListeningState.Type: return true default: return false } } } (1) State 진입 시 (2) State 탈출 시 (3) Transition 조건
  13. 13. internal class VoiceState: GKState { unowned let stateView: VoiceStateView init(statusView: VoiceStateView) { self.stateView = statusView } override func didEnter(from previousState: GKState?) { switch self { case is AttendingState, is ListeningState, is DetectingState: stateView.animationView.isHidden = true stateView.dotsContainerView.isHidden = false case is ProcessingState, is ReportingState: stateView.animationView.isHidden = false stateView.dotsContainerView.isHidden = true default: break } } } GKState Best Practice ⚠ 바로 GKState을 상속받지 말고 공통 Superclass State로 공유 자원 및 반복 로직을 관리
  14. 14. internal class VoiceStateView: UIView { private lazy var stateMachine: GKStateMachine = { return GKStateMachine(states: [ AttendingState(statusView: self), DetectingState(statusView: self), ListeningState(statusView: self), ProcessingState(statusView: self), ReportingState(statusView: self) ]) }() override func didMoveToWindow() { state = .attending stateMachine.enter(AttendingState.self) } } GKStateMachine 진입할 수 있는 상태 초기 상태 지정 세팅 완료!
  15. 15. internal final class VoiceStateView: UIView { @discardableResult func setState(_ state: State) -> Bool { let isNextStateValid = stateMachine.canEnterState(state.classType()) if isNextStateValid { self.state = state stateMachine.enter(state.classType()) } return isNextStateValid } } 상태 변경 extension VoiceControl: JarvisDelegate { func jarvis(_ jarvis: Jarvis, didChangeState state: JarvisState) { switch state { case .detecting: voiceAgentView?.transcription = listeningStateDescription voiceAgentView?.stateView?.setState(.detecting) ... 생략 ... } } }
  16. 16. 전부 합치기 class VoiceStateView: UIView Jarvis(가칭) class VoiceControl: UIControl 👋
  17. 17. 희망 사항 - 상태뷰는 항상 view hierarchy 최상단에 위치 - 도중에도 쉽게 dismiss / cancel - 어느 화면에나 쉽게 컨트롤 추가
  18. 18. UIResponder var inputView: UIView? var inputAccessoryView: UIView?
  19. 19. UIResponder var inputView: UIView? var inputAccessoryView: UIView?
  20. 20. var inputAccessoryView: UIView? { get } inputView에 악세서리 뷰를 덧붙이고 싶을 때 사용한다. 인스턴스가 first responder가 되면 시스템이 이 뷰를 input view에 붙인 후 화면에 표시. inputView가 nil이어도 악세서리 뷰는 표시됨 ✨ 장점 - 애니메이션 : 네이티브 키보드처럼 show & hide - 레이아웃 : Safe Area 대응 용이 - 뷰 계층 : os가 별도 UIWindow로 관리 - Responder chain : becomeFirstResponder(), resignFirstResponder()
  21. 21. resignFirstResponder()becomeFirstResponder() 마지막 총 정리 class VoiceControl: UIControl override var inputAccessoryView: UIView? { return voiceStateView } private var jarvis: Jarvis private var voiceStateView: VoiceStateView class VoiceStateView: UIView var stateMachine: GKStateMachine
  22. 22. References https://github.com/nsoojin/VoiceControlSample-iOS https://developer.apple.com/documentation/gameplaykit/gkstatemachine https://developer.apple.com/documentation/uikit/uiresponder https://developer.apple.com/library/archive/documentation/General/Conceptual/GameplayKit_Guide/StateMachine.html#//apple_ref/doc/uid/TP40015172-CH7 https://en.wikipedia.org/wiki/Finite-state_machine 고맙습니다.

×