ios-horizontalmenu/Sources/ContainerLifeCycleControlle...

215 lines
7.7 KiB
Swift

//
// ContainerLifeCycleController.swift
// TPGHorizontalMenu
//
// Created by David Livadaru on 15/03/2017.
// Copyright © 2017 3Pillar Global. All rights reserved.
//
import Foundation
import GameplayKit
/// An object which is reponsible with the life cycle control of menu child view controllers.
class ContainerLifeCycleController {
/// Enumeration if possible states of object.
///
/// - inProgress: enters when users scrolls between menu items.
/// - selecting: enters when user selected a menu item.
/// - changing: enters when menu view controlelr is loading.
/// - notChanging: enters when there is no need to perform any changes.
private enum State {
case inProgress(from: Int, to: Int, progress: CGFloat)
case selecting(from: Int, to: Int, progress: CGFloat)
case changing(toIndex: Int)
case notChanging
}
unowned let menuDataSource: MenuDataSource
private var stateMachines: [Int : GKStateMachine] = [:]
private var state: State = .notChanging {
didSet {
updateState(from: oldValue, to: state)
}
}
init(menuDataSource: MenuDataSource) {
self.menuDataSource = menuDataSource
}
func reload() {
stateMachines = [:]
for index in 0..<menuDataSource.screens.count {
let stateMachine = createStateMachine(for: index)
stateMachine.enter(ViewDidLoadState.self)
stateMachines[index] = stateMachine
}
}
func prepareMachine(for index: Int) {
guard menuDataSource.isValid(index: index), stateMachines[index] == nil else { return }
let stateMachine = createStateMachine(for: index)
stateMachine.enter(ViewDidLoadState.self)
stateMachines[index] = stateMachine
}
func removeMachine(for index: Int) {
guard menuDataSource.isValid(index: index), stateMachines[index] != nil else { return }
stateMachines[index] = nil
}
func updateContainter(using transition: ScrollTransition) {
state = createState(from: transition)
}
func update(currentIndex index: Int) {
state = .changing(toIndex: index)
}
func startAppearanceForCurrentIndex() {
startAppearance(for: menuDataSource.currentIndex)
}
func endAppearanceForCurrentIndex() {
endAppearance(for: menuDataSource.currentIndex)
}
func startDisappearanceForCurrentIndex() {
startDisappearance(for: menuDataSource.currentIndex)
}
func endDisappearanceForCurrentIndex() {
endDisappearance(for: menuDataSource.currentIndex)
}
// MARK: Private functionality
private func createStateMachine(for index: Int) -> GKStateMachine {
var states: [GKState] = []
if let viewController = menuDataSource.screens[index] {
states = [ViewDidLoadState(viewController: viewController),
ViewWillAppearState(viewController: viewController),
AppearanceProgressState(viewController: viewController),
ViewDidAppearState(viewController: viewController),
ViewWillDisappearState(viewController: viewController),
DisappearanceProgressState(viewController: viewController),
ViewDidDisappearState(viewController: viewController)]
}
return GKStateMachine(states: states)
}
private func createState(from transition: ScrollTransition) -> State {
let fromIndexIsValid = menuDataSource.isValid(index: transition.fromIndex)
let toIndexIsValid = menuDataSource.isValid(index: transition.toIndex)
switch (fromIndexIsValid, toIndexIsValid) {
case (true, true) where transition.kind == .selection:
return .selecting(from: transition.fromIndex, to: transition.toIndex,
progress: transition.progress)
case (true, true):
return .inProgress(from: transition.fromIndex, to: transition.toIndex,
progress: transition.progress)
case (false, true):
return .changing(toIndex: transition.toIndex)
default:
return .notChanging
}
}
private func updateState(from old: State, to new: State) {
switch (old, new) {
case (.inProgress(let oldFrom, let oldTo, _), .inProgress(_, let newTo, _)) where newTo > oldTo:
if newTo == oldFrom { break }
endDisappearance(for: oldFrom)
case (.inProgress(let oldFrom, let oldTo, _), .changing(let index)) where oldTo == index:
endDisappearance(for: oldFrom)
default:
break
}
switch new {
case .inProgress(let fromIndex, let toIndex, let progress):
startAppearance(for: toIndex)
updateAppearance(for: toIndex, progress: progress)
startDisappearance(for: fromIndex)
updateDisappearance(for: fromIndex, progress: progress)
case .changing(let index):
startAppearance(for: index)
endAppearance(for: index)
case .selecting(let from, let to, let progress):
switch progress {
case 0.0:
startAppearance(for: to)
startDisappearance(for: from)
case 1.0:
endAppearance(for: to)
endDisappearance(for: from)
default:
updateAppearance(for: to, progress: progress)
updateDisappearance(for: from, progress: progress)
}
default:
break
}
}
private func startAppearance(for index: Int) {
guard let machine = stateMachines[index] else { return }
if machine.canEnterState(ViewWillAppearState.self) {
machine.enter(ViewWillAppearState.self)
}
}
private func updateAppearance(for index: Int, progress: CGFloat) {
guard let machine = stateMachines[index] else { return }
if machine.canEnterState(AppearanceProgressState.self) {
machine.enter(AppearanceProgressState.self)
if let currentState = machine.currentState as? AppearanceProgressState {
currentState.update(progress: progress)
}
if progress == 1.0 {
endAppearance(for: index)
}
}
}
private func endAppearance(for index: Int) {
guard let machine = stateMachines[index] else { return }
if machine.canEnterState(ViewDidAppearState.self) {
machine.enter(ViewDidAppearState.self)
}
}
private func startDisappearance(for index: Int) {
guard let machine = stateMachines[index] else { return }
if machine.canEnterState(ViewWillDisappearState.self) {
machine.enter(ViewWillDisappearState.self)
}
}
private func updateDisappearance(for index: Int, progress: CGFloat) {
guard let machine = stateMachines[index] else { return }
if machine.canEnterState(DisappearanceProgressState.self) {
machine.enter(DisappearanceProgressState.self)
if let currentState = machine.currentState as? DisappearanceProgressState {
currentState.update(progress: progress)
}
if progress == 1.0 {
endDisappearance(for: index)
}
}
}
private func endDisappearance(for index: Int) {
guard let machine = stateMachines[index] else { return }
if machine.canEnterState(ViewDidDisappearState.self) {
machine.enter(ViewDidDisappearState.self)
}
if machine.canEnterState(ViewDidLoadState.self) {
machine.enter(ViewDidLoadState.self)
}
}
}