mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Initial implementation of channel overscroll navigation
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension Transition.Appear {
|
||||
static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.Appear {
|
||||
return Transition.Appear { component, view, transition in
|
||||
if scale {
|
||||
transition.animateScale(view: view, from: 0.01, to: 1.0)
|
||||
}
|
||||
if alpha {
|
||||
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func scaleIn() -> Transition.Appear {
|
||||
return Transition.Appear { component, view, transition in
|
||||
transition.animateScale(view: view, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.AppearWithGuide {
|
||||
static func `default`(scale: Bool = false, alpha: Bool = false) -> Transition.AppearWithGuide {
|
||||
return Transition.AppearWithGuide { component, view, guide, transition in
|
||||
if scale {
|
||||
transition.animateScale(view: view, from: 0.01, to: 1.0)
|
||||
}
|
||||
if alpha {
|
||||
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
}
|
||||
transition.animatePosition(view: view, from: CGPoint(x: guide.x - view.center.x, y: guide.y - view.center.y), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.Disappear {
|
||||
static let `default` = Transition.Disappear { view, transition, completion in
|
||||
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.DisappearWithGuide {
|
||||
static func `default`(alpha: Bool = true) -> Transition.DisappearWithGuide {
|
||||
return Transition.DisappearWithGuide { stage, view, guide, transition, completion in
|
||||
switch stage {
|
||||
case .begin:
|
||||
if alpha {
|
||||
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: guide.x - view.bounds.width / 2.0, y: guide.y - view.bounds.height / 2.0), size: view.bounds.size), completion: { _ in
|
||||
if !alpha {
|
||||
completion()
|
||||
}
|
||||
})
|
||||
case .update:
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: guide.x - view.bounds.width / 2.0, y: guide.y - view.bounds.height / 2.0), size: view.bounds.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition.Update {
|
||||
static let `default` = Transition.Update { component, view, transition in
|
||||
let frame = component.size.centered(around: component._position ?? CGPoint())
|
||||
if view.frame != frame {
|
||||
transition.setFrame(view: view, frame: frame)
|
||||
}
|
||||
let opacity = component._opacity ?? 1.0
|
||||
if view.alpha != opacity {
|
||||
transition.setAlpha(view: view, alpha: opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
798
submodules/ComponentFlow/Source/Base/CombinedComponent.swift
Normal file
798
submodules/ComponentFlow/Source/Base/CombinedComponent.swift
Normal file
@@ -0,0 +1,798 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private func updateChildAnyComponent<EnvironmentType>(
|
||||
id: _AnyChildComponent.Id,
|
||||
component: AnyComponent<EnvironmentType>,
|
||||
view: UIView,
|
||||
availableSize: CGSize,
|
||||
transition: Transition
|
||||
) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
|
||||
if !parentContext.updateContext.updatedViews.insert(id).inserted {
|
||||
preconditionFailure("Child component can only be processed once")
|
||||
}
|
||||
|
||||
let context = view.context(component: component)
|
||||
var isEnvironmentUpdated = false
|
||||
var isStateUpdated = false
|
||||
var isComponentUpdated = false
|
||||
var availableSizeUpdated = false
|
||||
|
||||
if context.environment.calculateIsUpdated() {
|
||||
context.environment._isUpdated = false
|
||||
isEnvironmentUpdated = true
|
||||
}
|
||||
|
||||
if context.erasedState.isUpdated {
|
||||
context.erasedState.isUpdated = false
|
||||
isStateUpdated = true
|
||||
}
|
||||
|
||||
if context.erasedComponent != component {
|
||||
isComponentUpdated = true
|
||||
}
|
||||
context.erasedComponent = component
|
||||
|
||||
if context.layoutResult.availableSize != availableSize {
|
||||
context.layoutResult.availableSize = availableSize
|
||||
availableSizeUpdated = true
|
||||
}
|
||||
|
||||
let isUpdated = isEnvironmentUpdated || isStateUpdated || isComponentUpdated || availableSizeUpdated
|
||||
|
||||
if !isUpdated, let size = context.layoutResult.size {
|
||||
return _UpdatedChildComponent(
|
||||
id: id,
|
||||
component: component,
|
||||
view: view,
|
||||
context: context,
|
||||
size: size
|
||||
)
|
||||
} else {
|
||||
let size = component._update(
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
context.layoutResult.size = size
|
||||
|
||||
return _UpdatedChildComponent(
|
||||
id: id,
|
||||
component: component,
|
||||
view: view,
|
||||
context: context,
|
||||
size: size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public class _AnyChildComponent {
|
||||
fileprivate enum Id: Hashable {
|
||||
case direct(Int)
|
||||
case mapped(Int, AnyHashable)
|
||||
}
|
||||
|
||||
fileprivate var directId: Int {
|
||||
return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
public final class _ConcreteChildComponent<ComponentType: Component>: _AnyChildComponent {
|
||||
fileprivate var id: Id {
|
||||
return .direct(self.directId)
|
||||
}
|
||||
|
||||
public func update(component: ComponentType, @EnvironmentBuilder environment: () -> Environment<ComponentType.EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
if !parentContext.updateContext.configuredViews.insert(self.id).inserted {
|
||||
preconditionFailure("Child component can only be configured once")
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
let view: ComponentType.View
|
||||
if let current = parentContext.childViews[self.id] {
|
||||
// TODO: Check if the type is the same
|
||||
view = current.view as! ComponentType.View
|
||||
} else {
|
||||
view = component.makeView()
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
let context = view.context(component: component)
|
||||
EnvironmentBuilder._environment = context.erasedEnvironment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
return updateChildAnyComponent(
|
||||
id: self.id,
|
||||
component: AnyComponent(component),
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _ConcreteChildComponent where ComponentType.EnvironmentType == Empty {
|
||||
func update(component: ComponentType, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _UpdatedChildComponentGuide {
|
||||
fileprivate let instance: _ChildComponentGuide
|
||||
|
||||
fileprivate init(instance: _ChildComponentGuide) {
|
||||
self.instance = instance
|
||||
}
|
||||
}
|
||||
|
||||
public final class _ChildComponentGuide {
|
||||
fileprivate var directId: Int {
|
||||
return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
|
||||
fileprivate var id: _AnyChildComponent.Id {
|
||||
return .direct(self.directId)
|
||||
}
|
||||
|
||||
public func update(position: CGPoint, transition: Transition) -> _UpdatedChildComponentGuide {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
|
||||
let previousPosition = parentContext.guides[self.id]
|
||||
|
||||
if parentContext.updateContext.configuredGuides.updateValue(_AnyCombinedComponentContext.UpdateContext.ConfiguredGuide(previousPosition: previousPosition ?? position, position: position), forKey: self.id) != nil {
|
||||
preconditionFailure("Child guide can only be configured once")
|
||||
}
|
||||
|
||||
for disappearingView in parentContext.disappearingChildViews {
|
||||
if disappearingView.guideId == self.id {
|
||||
disappearingView.transitionWithGuide?(
|
||||
stage: .update,
|
||||
view: disappearingView.view,
|
||||
guide: position,
|
||||
transition: transition,
|
||||
completion: disappearingView.completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return _UpdatedChildComponentGuide(instance: self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _UpdatedChildComponent {
|
||||
fileprivate let id: _AnyChildComponent.Id
|
||||
fileprivate let component: _TypeErasedComponent
|
||||
fileprivate let view: UIView
|
||||
fileprivate let context: _TypeErasedComponentContext
|
||||
|
||||
public let size: CGSize
|
||||
|
||||
var _removed: Bool = false
|
||||
var _position: CGPoint?
|
||||
var _opacity: CGFloat?
|
||||
|
||||
fileprivate var transitionAppear: Transition.Appear?
|
||||
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
||||
fileprivate var transitionDisappear: Transition.Disappear?
|
||||
fileprivate var transitionDisappearWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)?
|
||||
fileprivate var transitionUpdate: Transition.Update?
|
||||
fileprivate var gestures: [Gesture] = []
|
||||
|
||||
fileprivate init(
|
||||
id: _AnyChildComponent.Id,
|
||||
component: _TypeErasedComponent,
|
||||
view: UIView,
|
||||
context: _TypeErasedComponentContext,
|
||||
size: CGSize
|
||||
) {
|
||||
self.id = id
|
||||
self.component = component
|
||||
self.view = view
|
||||
self.context = context
|
||||
self.size = size
|
||||
}
|
||||
|
||||
@discardableResult public func appear(_ transition: Transition.Appear) -> _UpdatedChildComponent {
|
||||
self.transitionAppear = transition
|
||||
self.transitionAppearWithGuide = nil
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func appear(_ transition: Transition.AppearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent {
|
||||
self.transitionAppear = nil
|
||||
self.transitionAppearWithGuide = (transition, guide.instance.id)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func disappear(_ transition: Transition.Disappear) -> _UpdatedChildComponent {
|
||||
self.transitionDisappear = transition
|
||||
self.transitionDisappearWithGuide = nil
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func disappear(_ transition: Transition.DisappearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent {
|
||||
self.transitionDisappear = nil
|
||||
self.transitionDisappearWithGuide = (transition, guide.instance.id)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func update(_ transition: Transition.Update) -> _UpdatedChildComponent {
|
||||
self.transitionUpdate = transition
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func removed(_ removed: Bool) -> _UpdatedChildComponent {
|
||||
self._removed = removed
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func position(_ position: CGPoint) -> _UpdatedChildComponent {
|
||||
self._position = position
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func opacity(_ opacity: CGFloat) -> _UpdatedChildComponent {
|
||||
self._opacity = opacity
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
|
||||
self.gestures.append(gesture)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public final class _EnvironmentChildComponent<EnvironmentType>: _AnyChildComponent {
|
||||
fileprivate var id: Id {
|
||||
return .direct(self.directId)
|
||||
}
|
||||
|
||||
func update(component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
if !parentContext.updateContext.configuredViews.insert(self.id).inserted {
|
||||
preconditionFailure("Child component can only be configured once")
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
let view: UIView
|
||||
if let current = parentContext.childViews[self.id] {
|
||||
// Check if the type is the same
|
||||
view = current.view
|
||||
} else {
|
||||
view = component._makeView()
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
return updateChildAnyComponent(
|
||||
id: self.id,
|
||||
component: component,
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _EnvironmentChildComponent where EnvironmentType == Empty {
|
||||
func update(component: AnyComponent<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _EnvironmentChildComponent {
|
||||
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType {
|
||||
return self.update(component: AnyComponent(component), environment: environment, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType, EnvironmentType == Empty {
|
||||
return self.update(component: AnyComponent(component), environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _EnvironmentChildComponentFromMap<EnvironmentType>: _AnyChildComponent {
|
||||
private let id: Id
|
||||
|
||||
fileprivate init(id: Id) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public func update(component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
let parentContext = _AnyCombinedComponentContext.current
|
||||
if !parentContext.updateContext.configuredViews.insert(self.id).inserted {
|
||||
preconditionFailure("Child component can only be configured once")
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
let view: UIView
|
||||
if let current = parentContext.childViews[self.id] {
|
||||
// Check if the type is the same
|
||||
view = current.view
|
||||
} else {
|
||||
view = component._makeView()
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment
|
||||
let _ = environment()
|
||||
EnvironmentBuilder._environment = nil
|
||||
|
||||
return updateChildAnyComponent(
|
||||
id: self.id,
|
||||
component: component,
|
||||
view: view,
|
||||
availableSize: availableSize,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension _EnvironmentChildComponentFromMap where EnvironmentType == Empty {
|
||||
func update(component: AnyComponent<EnvironmentType>, availableSize: CGSize, transition: Transition) -> _UpdatedChildComponent {
|
||||
return self.update(component: component, environment: {}, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class _EnvironmentChildComponentMap<EnvironmentType, Key: Hashable> {
|
||||
private var directId: Int {
|
||||
return Int(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
|
||||
public subscript(_ key: Key) -> _EnvironmentChildComponentFromMap<EnvironmentType> {
|
||||
get {
|
||||
return _EnvironmentChildComponentFromMap<EnvironmentType>(id: .mapped(self.directId, key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class CombinedComponentContext<ComponentType: Component> {
|
||||
fileprivate let escapeGuard = EscapeGuard()
|
||||
|
||||
private let context: ComponentContext<ComponentType>
|
||||
public let view: UIView
|
||||
|
||||
public let component: ComponentType
|
||||
public let availableSize: CGSize
|
||||
public let transition: Transition
|
||||
private let addImpl: (_ updatedComponent: _UpdatedChildComponent) -> Void
|
||||
|
||||
public var environment: Environment<ComponentType.EnvironmentType> {
|
||||
return self.context.environment
|
||||
}
|
||||
public var state: ComponentType.State {
|
||||
return self.context.state
|
||||
}
|
||||
|
||||
fileprivate init(
|
||||
context: ComponentContext<ComponentType>,
|
||||
view: UIView,
|
||||
component: ComponentType,
|
||||
availableSize: CGSize,
|
||||
transition: Transition,
|
||||
add: @escaping (_ updatedComponent: _UpdatedChildComponent) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.view = view
|
||||
self.component = component
|
||||
self.availableSize = availableSize
|
||||
self.transition = transition
|
||||
self.addImpl = add
|
||||
}
|
||||
|
||||
public func add(_ updatedComponent: _UpdatedChildComponent) {
|
||||
self.addImpl(updatedComponent)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CombinedComponent: Component {
|
||||
typealias Body = (CombinedComponentContext<Self>) -> CGSize
|
||||
|
||||
static var body: Body { get }
|
||||
}
|
||||
|
||||
private class _AnyCombinedComponentContext {
|
||||
class UpdateContext {
|
||||
struct ConfiguredGuide {
|
||||
var previousPosition: CGPoint
|
||||
var position: CGPoint
|
||||
}
|
||||
|
||||
var configuredViews: Set<_AnyChildComponent.Id> = Set()
|
||||
var updatedViews: Set<_AnyChildComponent.Id> = Set()
|
||||
var configuredGuides: [_AnyChildComponent.Id: ConfiguredGuide] = [:]
|
||||
}
|
||||
|
||||
private static var _current: _AnyCombinedComponentContext?
|
||||
static var current: _AnyCombinedComponentContext {
|
||||
return self._current!
|
||||
}
|
||||
|
||||
static func push(_ context: _AnyCombinedComponentContext) -> _AnyCombinedComponentContext? {
|
||||
let previous = self._current
|
||||
|
||||
precondition(context._updateContext == nil)
|
||||
context._updateContext = UpdateContext()
|
||||
self._current = context
|
||||
|
||||
return previous
|
||||
}
|
||||
|
||||
static func pop(_ context: _AnyCombinedComponentContext, stack: _AnyCombinedComponentContext?) {
|
||||
precondition(context._updateContext != nil)
|
||||
context._updateContext = nil
|
||||
|
||||
self._current = stack
|
||||
}
|
||||
|
||||
class ChildView {
|
||||
let view: UIView
|
||||
var index: Int
|
||||
var transition: Transition.Disappear?
|
||||
var transitionWithGuide: (Transition.DisappearWithGuide, _AnyChildComponent.Id)?
|
||||
|
||||
var gestures: [UInt: UIGestureRecognizer] = [:]
|
||||
|
||||
init(view: UIView, index: Int) {
|
||||
self.view = view
|
||||
self.index = index
|
||||
}
|
||||
|
||||
func updateGestures(_ gestures: [Gesture]) {
|
||||
var validIds: [UInt] = []
|
||||
for gesture in gestures {
|
||||
validIds.append(gesture.id.id)
|
||||
if let current = self.gestures[gesture.id.id] {
|
||||
gesture.update(gesture: current)
|
||||
} else {
|
||||
let gestureInstance = gesture.create()
|
||||
self.gestures[gesture.id.id] = gestureInstance
|
||||
self.view.addGestureRecognizer(gestureInstance)
|
||||
}
|
||||
}
|
||||
var removeIds: [UInt] = []
|
||||
for id in self.gestures.keys {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
if let gestureInstance = self.gestures.removeValue(forKey: id) {
|
||||
self.view.removeGestureRecognizer(gestureInstance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DisappearingChildView {
|
||||
let view: UIView
|
||||
let guideId: _AnyChildComponent.Id?
|
||||
let transition: Transition.Disappear?
|
||||
let transitionWithGuide: Transition.DisappearWithGuide?
|
||||
let completion: () -> Void
|
||||
|
||||
init(
|
||||
view: UIView,
|
||||
guideId: _AnyChildComponent.Id?,
|
||||
transition: Transition.Disappear?,
|
||||
transitionWithGuide: Transition.DisappearWithGuide?,
|
||||
completion: @escaping () -> Void
|
||||
) {
|
||||
self.view = view
|
||||
self.guideId = guideId
|
||||
self.transition = transition
|
||||
self.transitionWithGuide = transitionWithGuide
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
var childViews: [_AnyChildComponent.Id: ChildView] = [:]
|
||||
var childViewIndices: [_AnyChildComponent.Id] = []
|
||||
var guides: [_AnyChildComponent.Id: CGPoint] = [:]
|
||||
var disappearingChildViews: [DisappearingChildView] = []
|
||||
|
||||
private var _updateContext: UpdateContext?
|
||||
var updateContext: UpdateContext {
|
||||
return self._updateContext!
|
||||
}
|
||||
}
|
||||
|
||||
private final class _CombinedComponentContext<ComponentType: CombinedComponent>: _AnyCombinedComponentContext {
|
||||
var body: ComponentType.Body?
|
||||
}
|
||||
|
||||
private var UIView_CombinedComponentContextKey: Int?
|
||||
|
||||
private extension UIView {
|
||||
func getCombinedComponentContext<ComponentType: CombinedComponent>(_ type: ComponentType.Type) -> _CombinedComponentContext<ComponentType> {
|
||||
if let context = objc_getAssociatedObject(self, &UIView_CombinedComponentContextKey) as? _CombinedComponentContext<ComponentType> {
|
||||
return context
|
||||
} else {
|
||||
let context = _CombinedComponentContext<ComponentType>()
|
||||
objc_setAssociatedObject(self, &UIView_CombinedComponentContextKey, context, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
return context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Transition {
|
||||
final class Appear {
|
||||
private let f: (_UpdatedChildComponent, UIView, Transition) -> Void
|
||||
|
||||
public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) {
|
||||
self.f(component, view, transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class AppearWithGuide {
|
||||
private let f: (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void
|
||||
|
||||
public init(_ f: @escaping (_UpdatedChildComponent, UIView, CGPoint, Transition) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, guide: CGPoint, transition: Transition) {
|
||||
self.f(component, view, guide, transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class Disappear {
|
||||
private let f: (UIView, Transition, @escaping () -> Void) -> Void
|
||||
|
||||
public init(_ f: @escaping (UIView, Transition, @escaping () -> Void) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(view: UIView, transition: Transition, completion: @escaping () -> Void) {
|
||||
self.f(view, transition, completion)
|
||||
}
|
||||
}
|
||||
|
||||
final class DisappearWithGuide {
|
||||
public enum Stage {
|
||||
case begin
|
||||
case update
|
||||
}
|
||||
|
||||
private let f: (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void
|
||||
|
||||
public init(_ f: @escaping (Stage, UIView, CGPoint, Transition, @escaping () -> Void) -> Void
|
||||
) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(stage: Stage, view: UIView, guide: CGPoint, transition: Transition, completion: @escaping () -> Void) {
|
||||
self.f(stage, view, guide, transition, completion)
|
||||
}
|
||||
}
|
||||
|
||||
final class Update {
|
||||
private let f: (_UpdatedChildComponent, UIView, Transition) -> Void
|
||||
|
||||
public init(_ f: @escaping (_UpdatedChildComponent, UIView, Transition) -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: Transition) {
|
||||
self.f(component, view, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension CombinedComponent {
|
||||
func makeView() -> UIView {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let context = view.getCombinedComponentContext(Self.self)
|
||||
|
||||
let storedBody: Body
|
||||
if let current = context.body {
|
||||
storedBody = current
|
||||
} else {
|
||||
storedBody = Self.body
|
||||
context.body = storedBody
|
||||
}
|
||||
|
||||
let viewContext = view.context(component: self)
|
||||
|
||||
var nextChildIndex = 0
|
||||
var addedChildIds = Set<_AnyChildComponent.Id>()
|
||||
|
||||
let contextStack = _AnyCombinedComponentContext.push(context)
|
||||
|
||||
let escapeStatus: EscapeGuard.Status
|
||||
let size: CGSize
|
||||
do {
|
||||
let bodyContext = CombinedComponentContext<Self>(
|
||||
context: viewContext,
|
||||
view: view,
|
||||
component: self,
|
||||
availableSize: availableSize,
|
||||
transition: transition,
|
||||
add: { updatedChild in
|
||||
if !addedChildIds.insert(updatedChild.id).inserted {
|
||||
preconditionFailure("Child component can only be added once")
|
||||
}
|
||||
|
||||
let index = nextChildIndex
|
||||
nextChildIndex += 1
|
||||
|
||||
if let previousView = context.childViews[updatedChild.id] {
|
||||
precondition(updatedChild.view === previousView.view)
|
||||
|
||||
if index != previousView.index {
|
||||
assert(index < previousView.index)
|
||||
for i in index ..< previousView.index {
|
||||
if let moveView = context.childViews[context.childViewIndices[i]] {
|
||||
moveView.index += 1
|
||||
}
|
||||
}
|
||||
context.childViewIndices.remove(at: previousView.index)
|
||||
context.childViewIndices.insert(updatedChild.id, at: index)
|
||||
previousView.index = index
|
||||
view.insertSubview(previousView.view, at: index)
|
||||
}
|
||||
|
||||
previousView.updateGestures(updatedChild.gestures)
|
||||
previousView.transition = updatedChild.transitionDisappear
|
||||
previousView.transitionWithGuide = updatedChild.transitionDisappearWithGuide
|
||||
|
||||
(updatedChild.transitionUpdate ?? Transition.Update.default)(component: updatedChild, view: updatedChild.view, transition: transition)
|
||||
} else {
|
||||
for i in index ..< context.childViewIndices.count {
|
||||
if let moveView = context.childViews[context.childViewIndices[i]] {
|
||||
moveView.index += 1
|
||||
}
|
||||
}
|
||||
|
||||
context.childViewIndices.insert(updatedChild.id, at: index)
|
||||
let childView = _AnyCombinedComponentContext.ChildView(view: updatedChild.view, index: index)
|
||||
context.childViews[updatedChild.id] = childView
|
||||
|
||||
childView.updateGestures(updatedChild.gestures)
|
||||
childView.transition = updatedChild.transitionDisappear
|
||||
childView.transitionWithGuide = updatedChild.transitionDisappearWithGuide
|
||||
|
||||
view.insertSubview(updatedChild.view, at: index)
|
||||
|
||||
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
|
||||
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
||||
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
||||
guard let viewContext = viewContext else {
|
||||
return
|
||||
}
|
||||
viewContext.state.updated(transition: transition)
|
||||
}
|
||||
|
||||
if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide {
|
||||
guard let guide = context.updateContext.configuredGuides[transitionAppearWithGuide.1] else {
|
||||
preconditionFailure("Guide should be configured before using")
|
||||
}
|
||||
transitionAppearWithGuide.0(
|
||||
component: updatedChild,
|
||||
view: updatedChild.view,
|
||||
guide: guide.previousPosition,
|
||||
transition: transition
|
||||
)
|
||||
} else if let transitionAppear = updatedChild.transitionAppear {
|
||||
transitionAppear(
|
||||
component: updatedChild,
|
||||
view: updatedChild.view,
|
||||
transition: transition
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
escapeStatus = bodyContext.escapeGuard.status
|
||||
size = storedBody(bodyContext)
|
||||
}
|
||||
|
||||
assert(escapeStatus.isDeallocated, "Body context should not be stored for later use")
|
||||
|
||||
if nextChildIndex < context.childViewIndices.count {
|
||||
for i in nextChildIndex ..< context.childViewIndices.count {
|
||||
let id = context.childViewIndices[i]
|
||||
if let childView = context.childViews.removeValue(forKey: id) {
|
||||
let view = childView.view
|
||||
let completion: () -> Void = { [weak context, weak view] in
|
||||
view?.removeFromSuperview()
|
||||
|
||||
if let context = context {
|
||||
for i in 0 ..< context.disappearingChildViews.count {
|
||||
if context.disappearingChildViews[i].view === view {
|
||||
context.disappearingChildViews.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let transitionWithGuide = childView.transitionWithGuide {
|
||||
guard let guide = context.updateContext.configuredGuides[transitionWithGuide.1] else {
|
||||
preconditionFailure("Guide should be configured before using")
|
||||
}
|
||||
context.disappearingChildViews.append(_AnyCombinedComponentContext.DisappearingChildView(
|
||||
view: view,
|
||||
guideId: transitionWithGuide.1,
|
||||
transition: nil,
|
||||
transitionWithGuide: transitionWithGuide.0,
|
||||
completion: completion
|
||||
))
|
||||
view.isUserInteractionEnabled = false
|
||||
transitionWithGuide.0(
|
||||
stage: .begin,
|
||||
view: view,
|
||||
guide: guide.position,
|
||||
transition: transition,
|
||||
completion: completion
|
||||
)
|
||||
} else if let simpleTransition = childView.transition {
|
||||
context.disappearingChildViews.append(_AnyCombinedComponentContext.DisappearingChildView(
|
||||
view: view,
|
||||
guideId: nil,
|
||||
transition: simpleTransition,
|
||||
transitionWithGuide: nil,
|
||||
completion: completion
|
||||
))
|
||||
view.isUserInteractionEnabled = false
|
||||
simpleTransition(
|
||||
view: view,
|
||||
transition: transition,
|
||||
completion: completion
|
||||
)
|
||||
} else {
|
||||
childView.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
context.childViewIndices.removeSubrange(nextChildIndex...)
|
||||
}
|
||||
|
||||
if addedChildIds != context.updateContext.updatedViews {
|
||||
preconditionFailure("Updated and added child lists do not match")
|
||||
}
|
||||
|
||||
context.guides.removeAll()
|
||||
for (id, guide) in context.updateContext.configuredGuides {
|
||||
context.guides[id] = guide.position
|
||||
}
|
||||
|
||||
_AnyCombinedComponentContext.pop(context, stack: contextStack)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public extension CombinedComponent {
|
||||
static func Child<Environment>(environment: Environment.Type) -> _EnvironmentChildComponent<Environment> {
|
||||
return _EnvironmentChildComponent<Environment>()
|
||||
}
|
||||
|
||||
static func ChildMap<Environment, Key: Hashable>(environment: Environment.Type, keyedBy keyType: Key.Type) -> _EnvironmentChildComponentMap<Environment, Key> {
|
||||
return _EnvironmentChildComponentMap<Environment, Key>()
|
||||
}
|
||||
|
||||
static func Child<ComponentType: Component>(_ type: ComponentType.Type) -> _ConcreteChildComponent<ComponentType> {
|
||||
return _ConcreteChildComponent<ComponentType>()
|
||||
}
|
||||
|
||||
static func Guide() -> _ChildComponentGuide {
|
||||
return _ChildComponentGuide()
|
||||
}
|
||||
}
|
||||
203
submodules/ComponentFlow/Source/Base/Component.swift
Normal file
203
submodules/ComponentFlow/Source/Base/Component.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ObjectiveC
|
||||
|
||||
public class ComponentLayoutResult {
|
||||
var availableSize: CGSize?
|
||||
var size: CGSize?
|
||||
}
|
||||
|
||||
public protocol _TypeErasedComponentContext: AnyObject {
|
||||
var erasedEnvironment: _Environment { get }
|
||||
var erasedState: ComponentState { get }
|
||||
|
||||
var layoutResult: ComponentLayoutResult { get }
|
||||
}
|
||||
|
||||
class AnyComponentContext<EnvironmentType>: _TypeErasedComponentContext {
|
||||
var erasedComponent: AnyComponent<EnvironmentType> {
|
||||
get {
|
||||
preconditionFailure()
|
||||
} set(value) {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
var erasedState: ComponentState {
|
||||
preconditionFailure()
|
||||
}
|
||||
var erasedEnvironment: _Environment {
|
||||
return self.environment
|
||||
}
|
||||
|
||||
let layoutResult: ComponentLayoutResult
|
||||
var environment: Environment<EnvironmentType>
|
||||
|
||||
init(environment: Environment<EnvironmentType>) {
|
||||
self.layoutResult = ComponentLayoutResult()
|
||||
self.environment = environment
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentContext<ComponentType: Component>: AnyComponentContext<ComponentType.EnvironmentType> {
|
||||
override var erasedComponent: AnyComponent<ComponentType.EnvironmentType> {
|
||||
get {
|
||||
return AnyComponent(self.component)
|
||||
} set(value) {
|
||||
self.component = value.wrapped as! ComponentType
|
||||
}
|
||||
}
|
||||
|
||||
var component: ComponentType
|
||||
let state: ComponentType.State
|
||||
|
||||
override var erasedState: ComponentState {
|
||||
return self.state
|
||||
}
|
||||
|
||||
init(component: ComponentType, environment: Environment<ComponentType.EnvironmentType>, state: ComponentType.State) {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
super.init(environment: environment)
|
||||
}
|
||||
}
|
||||
|
||||
private var UIView_TypeErasedComponentContextKey: Int?
|
||||
|
||||
extension UIView {
|
||||
func context<EnvironmentType>(component: AnyComponent<EnvironmentType>) -> AnyComponentContext<EnvironmentType> {
|
||||
return self.context(typeErasedComponent: component) as! AnyComponentContext<EnvironmentType>
|
||||
}
|
||||
|
||||
func context<ComponentType: Component>(component: ComponentType) -> ComponentContext<ComponentType> {
|
||||
return self.context(typeErasedComponent: component) as! ComponentContext<ComponentType>
|
||||
}
|
||||
|
||||
func context(typeErasedComponent component: _TypeErasedComponent) -> _TypeErasedComponentContext{
|
||||
if let context = objc_getAssociatedObject(self, &UIView_TypeErasedComponentContextKey) as? _TypeErasedComponentContext {
|
||||
return context
|
||||
} else {
|
||||
let context = component._makeContext()
|
||||
objc_setAssociatedObject(self, &UIView_TypeErasedComponentContextKey, context, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
return context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ComponentState {
|
||||
var _updated: ((Transition) -> Void)?
|
||||
var isUpdated: Bool = false
|
||||
|
||||
public final func updated(transition: Transition = .immediate) {
|
||||
self.isUpdated = true
|
||||
self._updated?(transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class EmptyComponentState: ComponentState {
|
||||
}
|
||||
|
||||
public protocol _TypeErasedComponent {
|
||||
func _makeView() -> UIView
|
||||
func _makeContext() -> _TypeErasedComponentContext
|
||||
func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize
|
||||
func _isEqual(to other: _TypeErasedComponent) -> Bool
|
||||
}
|
||||
|
||||
public protocol Component: _TypeErasedComponent, Equatable {
|
||||
associatedtype EnvironmentType = Empty
|
||||
associatedtype View: UIView = UIView
|
||||
associatedtype State: ComponentState = EmptyComponentState
|
||||
|
||||
func makeView() -> View
|
||||
func makeState() -> State
|
||||
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize
|
||||
}
|
||||
|
||||
public extension Component {
|
||||
func _makeView() -> UIView {
|
||||
return self.makeView()
|
||||
}
|
||||
|
||||
func _makeContext() -> _TypeErasedComponentContext {
|
||||
return ComponentContext<Self>(component: self, environment: Environment<EnvironmentType>(), state: self.makeState())
|
||||
}
|
||||
|
||||
func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return self.update(view: view as! Self.View, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
func _isEqual(to other: _TypeErasedComponent) -> Bool {
|
||||
if let other = other as? Self {
|
||||
return self == other
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Component where Self.View == UIView {
|
||||
func makeView() -> UIView {
|
||||
return UIView()
|
||||
}
|
||||
}
|
||||
|
||||
public extension Component where Self.State == EmptyComponentState {
|
||||
func makeState() -> State {
|
||||
return EmptyComponentState()
|
||||
}
|
||||
}
|
||||
|
||||
public class ComponentGesture {
|
||||
public static func tap(action: @escaping() -> Void) -> ComponentGesture {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public class AnyComponent<EnvironmentType>: _TypeErasedComponent, Equatable {
|
||||
fileprivate let wrapped: _TypeErasedComponent
|
||||
|
||||
public init<ComponentType: Component>(_ component: ComponentType) where ComponentType.EnvironmentType == EnvironmentType {
|
||||
self.wrapped = component
|
||||
}
|
||||
|
||||
public static func ==(lhs: AnyComponent<EnvironmentType>, rhs: AnyComponent<EnvironmentType>) -> Bool {
|
||||
return lhs.wrapped._isEqual(to: rhs.wrapped)
|
||||
}
|
||||
|
||||
public func _makeView() -> UIView {
|
||||
return self.wrapped._makeView()
|
||||
}
|
||||
|
||||
public func _makeContext() -> _TypeErasedComponentContext {
|
||||
return self.wrapped._makeContext()
|
||||
}
|
||||
|
||||
public func _update(view: UIView, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
return self.wrapped._update(view: view, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
public func _isEqual(to other: _TypeErasedComponent) -> Bool {
|
||||
return self.wrapped._isEqual(to: other)
|
||||
}
|
||||
}
|
||||
|
||||
public final class AnyComponentWithIdentity<Environment>: Equatable {
|
||||
public let id: AnyHashable
|
||||
public let component: AnyComponent<Environment>
|
||||
|
||||
public init<IdType: Hashable>(id: IdType, component: AnyComponent<Environment>) {
|
||||
self.id = AnyHashable(id)
|
||||
self.component = component
|
||||
}
|
||||
|
||||
public static func == (lhs: AnyComponentWithIdentity<Environment>, rhs: AnyComponentWithIdentity<Environment>) -> Bool {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.component != rhs.component {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
219
submodules/ComponentFlow/Source/Base/Environment.swift
Normal file
219
submodules/ComponentFlow/Source/Base/Environment.swift
Normal file
@@ -0,0 +1,219 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class Empty: Equatable {
|
||||
static let shared: Empty = Empty()
|
||||
|
||||
public static func ==(lhs: Empty, rhs: Empty) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public class _Environment {
|
||||
fileprivate var data: [Int: _EnvironmentValue] = [:]
|
||||
var _isUpdated: Bool = false
|
||||
|
||||
func calculateIsUpdated() -> Bool {
|
||||
if self._isUpdated {
|
||||
return true
|
||||
}
|
||||
for (_, item) in self.data {
|
||||
if let parentEnvironment = item.parentEnvironment, parentEnvironment.calculateIsUpdated() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate func set<T: Equatable>(index: Int, value: EnvironmentValue<T>) {
|
||||
if let current = self.data[index] {
|
||||
self.data[index] = value
|
||||
if current as! EnvironmentValue<T> != value {
|
||||
self._isUpdated = true
|
||||
}
|
||||
} else {
|
||||
self.data[index] = value
|
||||
self._isUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum EnvironmentValueStorage<T> {
|
||||
case direct(T)
|
||||
case reference(_Environment, Int)
|
||||
}
|
||||
|
||||
public class _EnvironmentValue {
|
||||
fileprivate let parentEnvironment: _Environment?
|
||||
|
||||
fileprivate init(parentEnvironment: _Environment?) {
|
||||
self.parentEnvironment = parentEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
@dynamicMemberLookup
|
||||
public final class EnvironmentValue<T: Equatable>: _EnvironmentValue, Equatable {
|
||||
private var storage: EnvironmentValueStorage<T>
|
||||
|
||||
fileprivate var value: T {
|
||||
switch self.storage {
|
||||
case let .direct(value):
|
||||
return value
|
||||
case let .reference(environment, index):
|
||||
return (environment.data[index] as! EnvironmentValue<T>).value
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate init(_ value: T) {
|
||||
self.storage = .direct(value)
|
||||
|
||||
super.init(parentEnvironment: nil)
|
||||
}
|
||||
|
||||
fileprivate init(environment: _Environment, index: Int) {
|
||||
self.storage = .reference(environment, index)
|
||||
|
||||
super.init(parentEnvironment: environment)
|
||||
}
|
||||
|
||||
public static func ==(lhs: EnvironmentValue<T>, rhs: EnvironmentValue<T>) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
// TODO: follow the reference chain for faster equality checking
|
||||
return lhs.value == rhs.value
|
||||
}
|
||||
|
||||
public subscript<V>(dynamicMember keyPath: KeyPath<T, V>) -> V {
|
||||
return self.value[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
public class Environment<T>: _Environment {
|
||||
private let file: StaticString
|
||||
private let line: Int
|
||||
|
||||
public init(_ file: StaticString = #file, _ line: Int = #line) {
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
}
|
||||
|
||||
public extension Environment where T == Empty {
|
||||
static let value: Environment<Empty> = {
|
||||
let result = Environment<Empty>()
|
||||
result.set(index: 0, value: EnvironmentValue(Empty()))
|
||||
return result
|
||||
}()
|
||||
}
|
||||
|
||||
public extension Environment {
|
||||
subscript(_ t1: T.Type) -> EnvironmentValue<T> where T: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2>(_ t1: T1.Type) -> EnvironmentValue<T1> where T == (T1, T2), T1: Equatable, T2: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2>(_ t2: T2.Type) -> EnvironmentValue<T2> where T == (T1, T2), T1: Equatable, T2: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 1)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3>(_ t1: T1.Type) -> EnvironmentValue<T1> where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3>(_ t2: T2.Type) -> EnvironmentValue<T2> where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 1)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3>(_ t3: T3.Type) -> EnvironmentValue<T3> where T == (T1, T2, T3), T1: Equatable, T2: Equatable, T3: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 2)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t1: T1.Type) -> EnvironmentValue<T1> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 0)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t2: T2.Type) -> EnvironmentValue<T2> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 1)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t3: T3.Type) -> EnvironmentValue<T3> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 2)
|
||||
}
|
||||
|
||||
subscript<T1, T2, T3, T4>(_ t4: T4.Type) -> EnvironmentValue<T4> where T == (T1, T2, T3, T4), T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable {
|
||||
return EnvironmentValue(environment: self, index: 3)
|
||||
}
|
||||
}
|
||||
|
||||
@resultBuilder
|
||||
public struct EnvironmentBuilder {
|
||||
static var _environment: _Environment?
|
||||
private static func current<T>(_ type: T.Type) -> Environment<T> {
|
||||
return self._environment as! Environment<T>
|
||||
}
|
||||
|
||||
public struct Partial<T: Equatable> {
|
||||
fileprivate var value: EnvironmentValue<T>
|
||||
}
|
||||
|
||||
public static func buildBlock() -> Environment<Empty> {
|
||||
let result = self.current(Empty.self)
|
||||
result.set(index: 0, value: EnvironmentValue(Empty.shared))
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildExpression<T: Equatable>(_ expression: T) -> Partial<T> {
|
||||
return Partial<T>(value: EnvironmentValue(expression))
|
||||
}
|
||||
|
||||
public static func buildExpression<T: Equatable>(_ expression: EnvironmentValue<T>) -> Partial<T> {
|
||||
return Partial<T>(value: expression)
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable>(_ t1: Partial<T1>) -> Environment<T1> {
|
||||
let result = self.current(T1.self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable, T2: Equatable>(_ t1: Partial<T1>, _ t2: Partial<T2>) -> Environment<(T1, T2)> {
|
||||
let result = self.current((T1, T2).self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
result.set(index: 1, value: t2.value)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable, T2: Equatable, T3: Equatable>(_ t1: Partial<T1>, _ t2: Partial<T2>, _ t3: Partial<T3>) -> Environment<(T1, T2, T3)> {
|
||||
let result = self.current((T1, T2, T3).self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
result.set(index: 1, value: t2.value)
|
||||
result.set(index: 2, value: t3.value)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func buildBlock<T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable>(_ t1: Partial<T1>, _ t2: Partial<T2>, _ t3: Partial<T3>, _ t4: Partial<T4>) -> Environment<(T1, T2, T3, T4)> {
|
||||
let result = self.current((T1, T2, T3, T4).self)
|
||||
result.set(index: 0, value: t1.value)
|
||||
result.set(index: 1, value: t2.value)
|
||||
result.set(index: 2, value: t3.value)
|
||||
result.set(index: 3, value: t4.value)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct ZeroEquatable<T>: Equatable {
|
||||
public var wrappedValue: T
|
||||
|
||||
public init(_ wrappedValue: T) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public static func ==(lhs: ZeroEquatable<T>, rhs: ZeroEquatable<T>) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
270
submodules/ComponentFlow/Source/Base/Transition.swift
Normal file
270
submodules/ComponentFlow/Source/Base/Transition.swift
Normal file
@@ -0,0 +1,270 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private extension UIView {
|
||||
static var animationDurationFactor: Double {
|
||||
return 1.0
|
||||
}
|
||||
}
|
||||
|
||||
@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate {
|
||||
private let keyPath: String?
|
||||
var completion: ((Bool) -> Void)?
|
||||
|
||||
init(animation: CAAnimation, completion: ((Bool) -> Void)?) {
|
||||
if let animation = animation as? CABasicAnimation {
|
||||
self.keyPath = animation.keyPath
|
||||
} else {
|
||||
self.keyPath = nil
|
||||
}
|
||||
self.completion = completion
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
|
||||
if let anim = anim as? CABasicAnimation {
|
||||
if anim.keyPath != self.keyPath {
|
||||
return
|
||||
}
|
||||
}
|
||||
if let completion = self.completion {
|
||||
completion(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeSpringAnimation(keyPath: String) -> CASpringAnimation {
|
||||
let springAnimation = CASpringAnimation(keyPath: keyPath)
|
||||
springAnimation.mass = 3.0;
|
||||
springAnimation.stiffness = 1000.0
|
||||
springAnimation.damping = 500.0
|
||||
springAnimation.duration = 0.5
|
||||
springAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
return springAnimation
|
||||
}
|
||||
|
||||
private extension CALayer {
|
||||
func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
|
||||
switch curve {
|
||||
case .spring:
|
||||
let animation = makeSpringAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = .forwards
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
let k = Float(UIView.animationDurationFactor)
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
animation.speed = speed * Float(animation.duration / duration)
|
||||
animation.isAdditive = additive
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
return animation
|
||||
default:
|
||||
let k = Float(UIView.animationDurationFactor)
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
let animation = CABasicAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.duration = duration
|
||||
animation.timingFunction = curve.asTimingFunction()
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = .forwards
|
||||
animation.speed = speed
|
||||
animation.isAdditive = additive
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
return animation
|
||||
}
|
||||
}
|
||||
|
||||
func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) {
|
||||
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, duration: duration, delay: delay, curve: curve, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
self.add(animation, forKey: additive ? nil : keyPath)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Transition.Animation.Curve {
|
||||
func asTimingFunction() -> CAMediaTimingFunction {
|
||||
switch self {
|
||||
case .easeInOut:
|
||||
return CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
case .spring:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Transition {
|
||||
public enum Animation {
|
||||
public enum Curve {
|
||||
case easeInOut
|
||||
case spring
|
||||
}
|
||||
|
||||
case none
|
||||
case curve(duration: Double, curve: Curve)
|
||||
}
|
||||
|
||||
public var animation: Animation
|
||||
private var _userData: [Any] = []
|
||||
|
||||
public func userData<T>(_ type: T.Type) -> T? {
|
||||
for item in self._userData {
|
||||
if let item = item as? T {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func withUserData(_ userData: Any) -> Transition {
|
||||
var result = self
|
||||
result._userData.append(userData)
|
||||
return result
|
||||
}
|
||||
|
||||
public static var immediate: Transition = Transition(animation: .none)
|
||||
|
||||
public static func easeInOut(duration: Double) -> Transition {
|
||||
return Transition(animation: .curve(duration: duration, curve: .easeInOut))
|
||||
}
|
||||
|
||||
public init(animation: Animation) {
|
||||
self.animation = animation
|
||||
}
|
||||
|
||||
public func setFrame(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||
if view.frame == frame {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
switch self.animation {
|
||||
case .none:
|
||||
view.frame = frame
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousPosition = view.center
|
||||
let previousBounds = view.bounds
|
||||
view.frame = frame
|
||||
|
||||
self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion)
|
||||
self.animateBounds(view: view, from: previousBounds, to: view.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if view.alpha == alpha {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
switch self.animation {
|
||||
case .none:
|
||||
view.alpha = alpha
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousAlpha = view.alpha
|
||||
view.alpha = alpha
|
||||
self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: fromValue as NSNumber,
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: fromValue as NSNumber,
|
||||
to: toValue as NSNumber,
|
||||
keyPath: "opacity",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: NSValue(cgPoint: fromValue),
|
||||
to: NSValue(cgPoint: toValue),
|
||||
keyPath: "position",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
break
|
||||
case let .curve(duration, curve):
|
||||
view.layer.animate(
|
||||
from: NSValue(cgRect: fromValue),
|
||||
to: NSValue(cgRect: toValue),
|
||||
keyPath: "bounds",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user