mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
869 lines
33 KiB
Swift
869 lines
33 KiB
Swift
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,
|
|
environment: context.environment,
|
|
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 = transition.withAnimation(.none)
|
|
}
|
|
|
|
let context = view.context(component: component)
|
|
EnvironmentBuilder._environment = context.erasedEnvironment
|
|
let environmentResult = environment()
|
|
EnvironmentBuilder._environment = nil
|
|
context.erasedEnvironment = environmentResult
|
|
|
|
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 _scale: CGFloat?
|
|
var _opacity: CGFloat?
|
|
var _cornerRadius: CGFloat?
|
|
var _clipsToBounds: Bool?
|
|
var _shadow: Shadow?
|
|
|
|
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 scale(_ scale: CGFloat) -> _UpdatedChildComponent {
|
|
self._scale = scale
|
|
return self
|
|
}
|
|
|
|
@discardableResult public func opacity(_ opacity: CGFloat) -> _UpdatedChildComponent {
|
|
self._opacity = opacity
|
|
return self
|
|
}
|
|
|
|
@discardableResult public func cornerRadius(_ cornerRadius: CGFloat) -> _UpdatedChildComponent {
|
|
self._cornerRadius = cornerRadius
|
|
return self
|
|
}
|
|
|
|
@discardableResult public func clipsToBounds(_ clipsToBounds: Bool) -> _UpdatedChildComponent {
|
|
self._clipsToBounds = clipsToBounds
|
|
return self
|
|
}
|
|
|
|
@discardableResult public func shadow(_ shadow: Shadow?) -> _UpdatedChildComponent {
|
|
self._shadow = shadow
|
|
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
|
|
}
|
|
|
|
let viewContext = view.context(component: component)
|
|
EnvironmentBuilder._environment = viewContext.erasedEnvironment
|
|
let environmentResult = environment()
|
|
EnvironmentBuilder._environment = nil
|
|
viewContext.erasedEnvironment = environmentResult
|
|
|
|
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 = transition.withAnimation(.none)
|
|
}
|
|
|
|
let viewContext = view.context(component: component)
|
|
EnvironmentBuilder._environment = viewContext.erasedEnvironment
|
|
let environmentResult = environment()
|
|
EnvironmentBuilder._environment = nil
|
|
viewContext.erasedEnvironment = environmentResult
|
|
|
|
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.isUserInteractionEnabled = true
|
|
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, state: State, environment: Environment<EnvironmentType>, 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)
|
|
|
|
if let scale = updatedChild._scale {
|
|
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
|
|
updatedChild.view.center = updatedChild._position ?? CGPoint()
|
|
updatedChild.view.transform = CGAffineTransform(scaleX: scale, y: scale)
|
|
} else {
|
|
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
|
|
}
|
|
|
|
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
|
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
|
|
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0
|
|
if let shadow = updatedChild._shadow {
|
|
updatedChild.view.layer.shadowColor = shadow.color.withAlphaComponent(1.0).cgColor
|
|
updatedChild.view.layer.shadowRadius = shadow.radius
|
|
updatedChild.view.layer.shadowOpacity = Float(shadow.color.alpha)
|
|
updatedChild.view.layer.shadowOffset = shadow.offset
|
|
} else {
|
|
updatedChild.view.layer.shadowColor = nil
|
|
updatedChild.view.layer.shadowRadius = 0.0
|
|
updatedChild.view.layer.shadowOpacity = 0.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()
|
|
}
|
|
|
|
static func StoredActionSlot<Arguments>(_ argumentsType: Arguments.Type) -> ActionSlot<Arguments> {
|
|
return ActionSlot<Arguments>()
|
|
}
|
|
}
|
|
|
|
public struct Shadow {
|
|
public let color: UIColor
|
|
public let radius: CGFloat
|
|
public let offset: CGSize
|
|
|
|
public init(
|
|
color: UIColor,
|
|
radius: CGFloat,
|
|
offset: CGSize
|
|
) {
|
|
self.color = color
|
|
self.radius = radius
|
|
self.offset = offset
|
|
}
|
|
}
|