Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,110 @@
import Foundation
import UIKit
public extension ComponentTransition.Appear {
static func `default`(scale: Bool = false, alpha: Bool = false) -> ComponentTransition.Appear {
return ComponentTransition.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() -> ComponentTransition.Appear {
return ComponentTransition.Appear { component, view, transition in
transition.animateScale(view: view, from: 0.01, to: 1.0)
}
}
}
public extension ComponentTransition.AppearWithGuide {
static func `default`(scale: Bool = false, alpha: Bool = false) -> ComponentTransition.AppearWithGuide {
return ComponentTransition.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 ComponentTransition.Disappear {
static func `default`(scale: Bool = false, alpha: Bool = true) -> ComponentTransition.Disappear {
return ComponentTransition.Disappear { view, transition, completion in
if scale {
transition.setScale(view: view, scale: 0.01, completion: { _ in
if !alpha {
completion()
}
})
}
if alpha {
transition.setAlpha(view: view, alpha: 0.0, completion: { _ in
completion()
})
}
if !alpha && !scale {
completion()
}
}
}
}
public extension ComponentTransition.DisappearWithGuide {
static func `default`(alpha: Bool = true) -> ComponentTransition.DisappearWithGuide {
return ComponentTransition.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 ComponentTransition.Update {
static let `default` = ComponentTransition.Update { component, view, transition in
let position = component._position ?? CGPoint()
let size = component.size
view.layer.anchorPoint = component._anchorPoint ?? CGPoint(x: 0.5, y: 0.5)
if let scale = component._scale {
transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: size))
transition.setPosition(view: view, position: position)
transition.setScale(view: view, scale: scale)
} else {
if view is UIScrollView {
let frame = component.size.centered(around: component._position ?? CGPoint())
if view.frame != frame {
transition.setFrame(view: view, frame: frame)
}
} else {
if component._anchorPoint != nil {
view.bounds = CGRect(origin: CGPoint(), size: size)
} else {
transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: size))
}
transition.setPosition(view: view, position: position)
}
}
let opacity = component._opacity ?? 1.0
if view.alpha != opacity {
transition.setAlpha(view: view, alpha: opacity)
}
}
}
@@ -0,0 +1,898 @@
import Foundation
import UIKit
private func updateChildAnyComponent<EnvironmentType>(
id: _AnyChildComponent.Id,
component: AnyComponent<EnvironmentType>,
view: UIView,
availableSize: CGSize,
transition: ComponentTransition
) -> _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: ComponentTransition) -> _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: ComponentTransition) -> _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: ComponentTransition) -> _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 _anchorPoint: CGPoint?
var _position: CGPoint?
var _scale: CGFloat?
var _opacity: CGFloat?
var _cornerRadius: CGFloat?
var _clipsToBounds: Bool?
var _allowsGroupOpacity: Bool?
var _shadow: Shadow?
fileprivate var transitionAppear: ComponentTransition.Appear?
fileprivate var transitionAppearWithGuide: (ComponentTransition.AppearWithGuide, _AnyChildComponent.Id)?
fileprivate var transitionDisappear: ComponentTransition.Disappear?
fileprivate var transitionDisappearWithGuide: (ComponentTransition.DisappearWithGuide, _AnyChildComponent.Id)?
fileprivate var transitionUpdate: ComponentTransition.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: ComponentTransition.Appear) -> _UpdatedChildComponent {
self.transitionAppear = transition
self.transitionAppearWithGuide = nil
return self
}
@discardableResult public func appear(_ transition: ComponentTransition.AppearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent {
self.transitionAppear = nil
self.transitionAppearWithGuide = (transition, guide.instance.id)
return self
}
@discardableResult public func disappear(_ transition: ComponentTransition.Disappear) -> _UpdatedChildComponent {
self.transitionDisappear = transition
self.transitionDisappearWithGuide = nil
return self
}
@discardableResult public func disappear(_ transition: ComponentTransition.DisappearWithGuide, guide: _UpdatedChildComponentGuide) -> _UpdatedChildComponent {
self.transitionDisappear = nil
self.transitionDisappearWithGuide = (transition, guide.instance.id)
return self
}
@discardableResult public func update(_ transition: ComponentTransition.Update) -> _UpdatedChildComponent {
self.transitionUpdate = transition
return self
}
@discardableResult public func removed(_ removed: Bool) -> _UpdatedChildComponent {
self._removed = removed
return self
}
@discardableResult public func anchorPoint(_ anchorPoint: CGPoint) -> _UpdatedChildComponent {
self._anchorPoint = anchorPoint
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 allowsGroupOpacity(_ allowsGroupOpacity: Bool) -> _UpdatedChildComponent {
self._allowsGroupOpacity = allowsGroupOpacity
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: ComponentTransition) -> _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: ComponentTransition) -> _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: ComponentTransition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType {
return self.update(component: AnyComponent(component), environment: environment, availableSize: availableSize, transition: transition)
}
func update(_ component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent {
return self.update(component: component, environment: environment, availableSize: availableSize, transition: transition)
}
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: ComponentTransition) -> _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: ComponentTransition) -> _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: ComponentTransition) -> _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: ComponentTransition
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: ComponentTransition,
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: ComponentTransition.Disappear?
var transitionWithGuide: (ComponentTransition.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: ComponentTransition.Disappear?
let transitionWithGuide: ComponentTransition.DisappearWithGuide?
let completion: () -> Void
init(
view: UIView,
guideId: _AnyChildComponent.Id?,
transition: ComponentTransition.Disappear?,
transitionWithGuide: ComponentTransition.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 ComponentTransition {
final class Appear {
private let f: (_UpdatedChildComponent, UIView, ComponentTransition) -> Void
public init(_ f: @escaping (_UpdatedChildComponent, UIView, ComponentTransition) -> Void) {
self.f = f
}
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: ComponentTransition) {
self.f(component, view, transition)
}
}
final class AppearWithGuide {
private let f: (_UpdatedChildComponent, UIView, CGPoint, ComponentTransition) -> Void
public init(_ f: @escaping (_UpdatedChildComponent, UIView, CGPoint, ComponentTransition) -> Void) {
self.f = f
}
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, guide: CGPoint, transition: ComponentTransition) {
self.f(component, view, guide, transition)
}
}
final class Disappear {
private let f: (UIView, ComponentTransition, @escaping () -> Void) -> Void
public init(_ f: @escaping (UIView, ComponentTransition, @escaping () -> Void) -> Void) {
self.f = f
}
public func callAsFunction(view: UIView, transition: ComponentTransition, completion: @escaping () -> Void) {
self.f(view, transition, completion)
}
}
final class DisappearWithGuide {
public enum Stage {
case begin
case update
}
private let f: (Stage, UIView, CGPoint, ComponentTransition, @escaping () -> Void) -> Void
public init(_ f: @escaping (Stage, UIView, CGPoint, ComponentTransition, @escaping () -> Void) -> Void
) {
self.f = f
}
public func callAsFunction(stage: Stage, view: UIView, guide: CGPoint, transition: ComponentTransition, completion: @escaping () -> Void) {
self.f(stage, view, guide, transition, completion)
}
}
final class Update {
private let f: (_UpdatedChildComponent, UIView, ComponentTransition) -> Void
public init(_ f: @escaping (_UpdatedChildComponent, UIView, ComponentTransition) -> Void) {
self.f = f
}
public func callAsFunction(component: _UpdatedChildComponent, view: UIView, transition: ComponentTransition) {
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: ComponentTransition) -> 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 ?? ComponentTransition.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.layer.anchorPoint = updatedChild._anchorPoint ?? CGPoint(x: 0.5, y: 0.5)
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 {
if updatedChild.view is UIScrollView {
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
} else {
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
if updatedChild.view.layer.anchorPoint != CGPoint(x: 0.5, y: 0.5) {
updatedChild.view.layer.position = updatedChild._position ?? CGPoint()
} else {
updatedChild.view.center = 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 allowsGroupOpacity = updatedChild._allowsGroupOpacity {
updatedChild.view.layer.allowsGroupOpacity = allowsGroupOpacity
}
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, isLocal in
guard let viewContext = viewContext else {
return
}
viewContext.state.updated(transition: transition, isLocal: isLocal)
}
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
}
}
@@ -0,0 +1,221 @@
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 {
get {
return self.environment
} set(value) {
self.environment = value as! Environment<EnvironmentType>
}
}
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
}
}
}
open class ComponentState {
open var _updated: ((ComponentTransition, Bool) -> Void)?
var isUpdated: Bool = false
public init() {
}
public final func updated(transition: ComponentTransition = .immediate, isLocal: Bool = false) {
self.isUpdated = true
self._updated?(transition, isLocal)
}
}
public final class EmptyComponentState: ComponentState {
}
public protocol _TypeErasedComponent {
func _makeView() -> UIView
func _makeContext() -> _TypeErasedComponentContext
func _update(view: UIView, availableSize: CGSize, environment: Any, transition: ComponentTransition) -> CGSize
func _isEqual(to other: _TypeErasedComponent) -> Bool
}
public protocol ComponentTaggedView: UIView {
func matches(tag: Any) -> Bool
}
public final class GenericComponentViewTag {
public init() {
}
}
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, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> 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, environment: Any, transition: ComponentTransition) -> CGSize {
let view = view as! Self.View
return self.update(view: view, availableSize: availableSize, state: view.context(component: self).state, environment: environment as! Environment<EnvironmentType>, 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 {
public 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, environment: Any, transition: ComponentTransition) -> CGSize {
return self.wrapped._update(view: view, availableSize: availableSize, environment: environment as! Environment<EnvironmentType>, 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
}
}
@@ -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>
public 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: EnvironmentValue(expression.value))
}
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
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,341 @@
import Foundation
import UIKit
public final class Button: Component {
public let content: AnyComponent<Empty>
public let minSize: CGSize?
public let hitTestEdgeInsets: UIEdgeInsets?
public let tag: AnyObject?
public let automaticHighlight: Bool
public let isEnabled: Bool
public let isExclusive: Bool
public let action: () -> Void
public let holdAction: ((UIView) -> Void)?
public let highlightedAction: ActionSlot<Bool>?
convenience public init(
content: AnyComponent<Empty>,
isEnabled: Bool = true,
automaticHighlight: Bool = true,
action: @escaping () -> Void,
highlightedAction: ActionSlot<Bool>? = nil
) {
self.init(
content: content,
minSize: nil,
hitTestEdgeInsets: nil,
tag: nil,
automaticHighlight: automaticHighlight,
isEnabled: isEnabled,
action: action,
holdAction: nil,
highlightedAction: highlightedAction
)
}
private init(
content: AnyComponent<Empty>,
minSize: CGSize? = nil,
hitTestEdgeInsets: UIEdgeInsets? = nil,
tag: AnyObject? = nil,
automaticHighlight: Bool = true,
isEnabled: Bool = true,
isExclusive: Bool = true,
action: @escaping () -> Void,
holdAction: ((UIView) -> Void)?,
highlightedAction: ActionSlot<Bool>?
) {
self.content = content
self.minSize = minSize
self.hitTestEdgeInsets = hitTestEdgeInsets
self.tag = tag
self.automaticHighlight = automaticHighlight
self.isEnabled = isEnabled
self.isExclusive = isExclusive
self.action = action
self.holdAction = holdAction
self.highlightedAction = highlightedAction
}
public func minSize(_ minSize: CGSize?) -> Button {
return Button(
content: self.content,
minSize: minSize,
hitTestEdgeInsets: self.hitTestEdgeInsets,
tag: self.tag,
automaticHighlight: self.automaticHighlight,
isEnabled: self.isEnabled,
isExclusive: self.isExclusive,
action: self.action,
holdAction: self.holdAction,
highlightedAction: self.highlightedAction
)
}
public func withHitTestEdgeInsets(_ hitTestEdgeInsets: UIEdgeInsets?) -> Button {
return Button(
content: self.content,
minSize: self.minSize,
hitTestEdgeInsets: hitTestEdgeInsets,
tag: self.tag,
automaticHighlight: self.automaticHighlight,
isEnabled: self.isEnabled,
isExclusive: self.isExclusive,
action: self.action,
holdAction: self.holdAction,
highlightedAction: self.highlightedAction
)
}
public func withIsExclusive(_ isExclusive: Bool) -> Button {
return Button(
content: self.content,
minSize: self.minSize,
hitTestEdgeInsets: self.hitTestEdgeInsets,
tag: self.tag,
automaticHighlight: self.automaticHighlight,
isEnabled: self.isEnabled,
isExclusive: isExclusive,
action: self.action,
holdAction: self.holdAction,
highlightedAction: self.highlightedAction
)
}
public func withHoldAction(_ holdAction: ((UIView) -> Void)?) -> Button {
return Button(
content: self.content,
minSize: self.minSize,
hitTestEdgeInsets: self.hitTestEdgeInsets,
tag: self.tag,
automaticHighlight: self.automaticHighlight,
isEnabled: self.isEnabled,
isExclusive: self.isExclusive,
action: self.action,
holdAction: holdAction,
highlightedAction: self.highlightedAction
)
}
public func tagged(_ tag: AnyObject) -> Button {
return Button(
content: self.content,
minSize: self.minSize,
hitTestEdgeInsets: self.hitTestEdgeInsets,
tag: tag,
automaticHighlight: self.automaticHighlight,
isEnabled: self.isEnabled,
isExclusive: self.isExclusive,
action: self.action,
holdAction: self.holdAction,
highlightedAction: self.highlightedAction
)
}
public static func ==(lhs: Button, rhs: Button) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.minSize != rhs.minSize {
return false
}
if lhs.hitTestEdgeInsets != rhs.hitTestEdgeInsets {
return false
}
if lhs.tag !== rhs.tag {
return false
}
if lhs.automaticHighlight != rhs.automaticHighlight {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.isExclusive != rhs.isExclusive {
return false
}
return true
}
public final class View: UIButton, ComponentTaggedView {
private let contentView: ComponentHostView<Empty>
public var content: UIView? {
return self.contentView.componentView
}
private var component: Button?
private var currentIsHighlighted: Bool = false {
didSet {
guard let component = self.component else {
return
}
if self.currentIsHighlighted != oldValue {
if component.automaticHighlight {
self.updateAlpha(transition: .immediate)
}
component.highlightedAction?.invoke(self.currentIsHighlighted)
}
}
}
private func updateAlpha(transition: ComponentTransition) {
guard let component = self.component else {
return
}
let alpha: CGFloat
if component.isEnabled {
if component.automaticHighlight {
alpha = self.currentIsHighlighted ? 0.6 : 1.0
} else {
alpha = 1.0
}
} else {
alpha = 0.3
}
transition.setAlpha(view: self.contentView, alpha: alpha)
}
private var holdActionTriggerred: Bool = false
private var holdActionTimer: Timer?
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds = self.bounds
if let hitTestEdgeInsets = self.component?.hitTestEdgeInsets {
bounds = bounds.insetBy(dx: hitTestEdgeInsets.left, dy: hitTestEdgeInsets.top)
}
return bounds.contains(point)
}
override init(frame: CGRect) {
self.contentView = ComponentHostView<Empty>()
self.contentView.isUserInteractionEnabled = false
self.contentView.layer.allowsGroupOpacity = true
super.init(frame: frame)
self.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.holdActionTimer?.invalidate()
}
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
@objc private func pressed() {
if self.holdActionTriggerred {
self.holdActionTriggerred = false
} else {
self.component?.action()
}
}
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
self.holdActionTriggerred = false
if self.component?.holdAction != nil {
self.holdActionTriggerred = true
self.component?.action()
self.holdActionTimer?.invalidate()
if #available(iOS 10.0, *) {
let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.holdActionTimer?.invalidate()
strongSelf.component?.holdAction?(strongSelf)
strongSelf.beginExecuteHoldActionTimer()
})
self.holdActionTimer = holdActionTimer
RunLoop.main.add(holdActionTimer, forMode: .common)
}
}
return super.beginTracking(touch, with: event)
}
private func beginExecuteHoldActionTimer() {
self.holdActionTimer?.invalidate()
if #available(iOS 10.0, *) {
let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.component?.holdAction?(strongSelf)
})
self.holdActionTimer = holdActionTimer
RunLoop.main.add(holdActionTimer, forMode: .common)
}
}
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
self.holdActionTimer?.invalidate()
self.holdActionTimer = nil
super.endTracking(touch, with: event)
}
override public func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
self.holdActionTimer?.invalidate()
self.holdActionTimer = nil
super.cancelTracking(with: event)
}
func update(component: Button, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let contentSize = self.contentView.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
var size = contentSize
if let minSize = component.minSize {
size.width = max(size.width, minSize.width)
size.height = max(size.height, minSize.height)
}
self.component = component
self.updateAlpha(transition: transition)
self.isEnabled = component.isEnabled
self.isExclusiveTouch = component.isExclusive
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,69 @@
import Foundation
import UIKit
public final class Circle: Component {
public let fillColor: UIColor
public let strokeColor: UIColor
public let strokeWidth: CGFloat
public let size: CGSize
public init(fillColor: UIColor = .clear, strokeColor: UIColor = .clear, strokeWidth: CGFloat = 0.0, size: CGSize) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
self.size = size
}
public static func ==(lhs: Circle, rhs: Circle) -> Bool {
if !lhs.fillColor.isEqual(rhs.fillColor) {
return false
}
if !lhs.strokeColor.isEqual(rhs.strokeColor) {
return false
}
if lhs.strokeWidth != rhs.strokeWidth {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIImageView {
var component: Circle?
var currentSize: CGSize?
func update(component: Circle, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let size = CGSize(width: min(availableSize.width, component.size.width), height: min(availableSize.height, component.size.height))
if self.currentSize != size || self.component != component {
self.currentSize = size
self.component = component
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(component.fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if component.strokeWidth > 0.0 {
context.setStrokeColor(component.strokeColor.cgColor)
context.setLineWidth(component.strokeWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.strokeWidth / 2.0, dy: component.strokeWidth / 2.0))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,105 @@
import Foundation
import UIKit
public enum HStackAlignment {
case left
case alternatingLeftRight
}
public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let spacing: CGFloat
private let alignment: HStackAlignment
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], spacing: CGFloat, alignment: HStackAlignment = .left) {
self.items = items
self.spacing = spacing
self.alignment = alignment
}
public static func ==(lhs: HStack<ChildEnvironment>, rhs: HStack<ChildEnvironment>) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.spacing != rhs.spacing {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
return true
}
public static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
return { context in
var remainingWidth: CGFloat = context.availableSize.width
var updatedChildren: [_UpdatedChildComponent] = []
for item in context.component.items {
let child = children[item.id].update(
component: item.component, environment: {
context.environment[ChildEnvironment.self]
},
availableSize: CGSize(width: remainingWidth, height: context.availableSize.height),
transition: context.transition
)
updatedChildren.append(child)
remainingWidth -= context.component.spacing + child.size.width
}
var size = CGSize(width: 0.0, height: 0.0)
switch context.component.alignment {
case .left:
for child in updatedChildren {
size.width += child.size.width
size.height = max(size.height, child.size.height)
}
size.width += context.component.spacing * CGFloat(updatedChildren.count - 1)
var nextX = 0.0
for child in updatedChildren {
context.add(child
.position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center)
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
nextX += child.size.width
nextX += context.component.spacing
}
case .alternatingLeftRight:
size.width = context.availableSize.width
for child in updatedChildren {
size.height = max(size.height, child.size.height)
}
var nextLeftX = 0.0
var nextRightX = size.width
for i in 0 ..< updatedChildren.count {
let child = updatedChildren[i]
let childFrame: CGRect
if i % 2 == 0 {
childFrame = CGRect(origin: CGPoint(x: nextLeftX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)
nextLeftX += child.size.width
nextLeftX += context.component.spacing
} else {
childFrame = CGRect(origin: CGPoint(x: nextRightX - child.size.width, y: floor((size.height - child.size.height) * 0.5)), size: child.size)
nextRightX -= child.size.width
nextRightX -= context.component.spacing
}
context.add(child
.position(child.size.centered(in: childFrame).center)
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
}
return size
}
}
}
@@ -0,0 +1,69 @@
import Foundation
import UIKit
public final class Image: Component {
public let image: UIImage?
public let tintColor: UIColor?
public let size: CGSize?
public let contentMode: UIImageView.ContentMode
public init(
image: UIImage?,
tintColor: UIColor? = nil,
size: CGSize? = nil,
contentMode: UIImageView.ContentMode = .scaleToFill
) {
self.image = image
self.tintColor = tintColor
self.size = size
self.contentMode = contentMode
}
public static func ==(lhs: Image, rhs: Image) -> Bool {
if lhs.image !== rhs.image {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.size != rhs.size {
return false
}
if lhs.contentMode != rhs.contentMode {
return false
}
return true
}
public final class View: UIImageView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.image = component.image
self.contentMode = component.contentMode
transition.setTintColor(view: self, color: component.tintColor ?? .white)
switch component.contentMode {
case .center:
return component.image?.size ?? availableSize
default:
return component.size ?? availableSize
}
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
@@ -0,0 +1,3 @@
import Foundation
import UIKit
@@ -0,0 +1,92 @@
import Foundation
import UIKit
public final class List<ChildEnvironment: Equatable>: CombinedComponent {
public enum Direction {
case horizontal
case vertical
}
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let direction: Direction
private let centerAlignment: Bool
private let appear: ComponentTransition.Appear
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], direction: Direction = .vertical, centerAlignment: Bool = false, appear: ComponentTransition.Appear = .default()) {
self.items = items
self.direction = direction
self.centerAlignment = centerAlignment
self.appear = appear
}
public static func ==(lhs: List<ChildEnvironment>, rhs: List<ChildEnvironment>) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.direction != rhs.direction {
return false
}
if lhs.centerAlignment != rhs.centerAlignment {
return false
}
return true
}
public static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
return { context in
let updatedChildren = context.component.items.map { item in
return children[item.id].update(
component: item.component, environment: {
context.environment[ChildEnvironment.self]
},
availableSize: context.availableSize,
transition: context.transition
)
}
let maxWidth: CGFloat = updatedChildren.reduce(CGFloat(0.0)) { partialResult, child in
return max(partialResult, child.size.width)
}
var nextOrigin: CGFloat = 0.0
for child in updatedChildren {
let position: CGPoint
switch context.component.direction {
case .horizontal:
position = CGPoint(x: nextOrigin + child.size.width / 2.0, y: child.size.height / 2.0)
nextOrigin += child.size.width
case .vertical:
let originX: CGFloat
if context.component.centerAlignment {
originX = maxWidth / 2.0
} else {
originX = child.size.width / 2.0
}
position = CGPoint(x: originX, y: nextOrigin + child.size.height / 2.0)
nextOrigin += child.size.height
}
context.add(child
.position(position)
.appear(context.component.appear)
)
}
switch context.component.direction {
case .horizontal:
return CGSize(width: min(context.availableSize.width, nextOrigin), height: context.availableSize.height)
case .vertical:
let width: CGFloat
if context.component.centerAlignment {
width = maxWidth
} else {
width = context.availableSize.width
}
return CGSize(width: width, height: min(context.availableSize.height, nextOrigin))
}
}
}
}
@@ -0,0 +1,70 @@
import Foundation
import UIKit
public final class Rectangle: Component {
private let color: UIColor
private let width: CGFloat?
private let height: CGFloat?
private let tag: NSObject?
public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil, tag: NSObject? = nil) {
self.color = color
self.width = width
self.height = height
self.tag = tag
}
public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.width != rhs.width {
return false
}
if lhs.height != rhs.height {
return false
}
return true
}
public final class View: UIView, ComponentTaggedView {
fileprivate var componentTag: NSObject?
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func matches(tag: Any) -> Bool {
if let componentTag = self.componentTag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
var size = availableSize
if let width = self.width {
size.width = min(size.width, width)
}
if let height = self.height {
size.height = min(size.height, height)
}
view.backgroundColor = self.color
view.componentTag = self.tag
return size
}
}
@@ -0,0 +1,412 @@
import Foundation
import UIKit
import Display
public final class RoundedRectangle: Component {
public enum GradientDirection: Equatable {
case horizontal
case vertical
}
public let colors: [UIColor]
public let cornerRadius: CGFloat?
public let gradientDirection: GradientDirection
public let stroke: CGFloat?
public let strokeColor: UIColor?
public let size: CGSize?
public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil, size: CGSize? = nil) {
self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor, size: size)
}
public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil, size: CGSize? = nil) {
self.colors = colors
self.cornerRadius = cornerRadius
self.gradientDirection = gradientDirection
self.stroke = stroke
self.strokeColor = strokeColor
self.size = size
}
public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool {
if lhs.colors != rhs.colors {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.gradientDirection != rhs.gradientDirection {
return false
}
if lhs.stroke != rhs.stroke {
return false
}
if lhs.strokeColor != rhs.strokeColor {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIImageView {
var component: RoundedRectangle?
func update(component: RoundedRectangle, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let size = component.size ?? availableSize
if self.component != component {
let cornerRadius = component.cornerRadius ?? min(size.width, size.height) * 0.5
if component.colors.count == 1, let color = component.colors.first {
let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
if let strokeColor = component.strokeColor {
context.setFillColor(strokeColor.cgColor)
} else {
context.setFillColor(color.cgColor)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize))
if let stroke = component.stroke, stroke > 0.0 {
if let _ = component.strokeColor {
context.setFillColor(color.cgColor)
} else {
context.setBlendMode(.clear)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
UIGraphicsEndImageContext()
} else if component.colors.count > 1 {
let imageSize = size
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath)
context.clip()
let colors = component.colors
let gradientColors = colors.map { $0.cgColor } as CFArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(colors.count - 1)
for i in 0 ..< colors.count {
locations.append(delta * CGFloat(i))
}
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: component.gradientDirection == .horizontal ? CGPoint(x: imageSize.width, y: 0.0) : CGPoint(x: 0.0, y: imageSize.height), options: CGGradientDrawingOptions())
if let stroke = component.stroke, stroke > 0.0 {
context.resetClip()
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: cornerRadius).cgPath)
context.setBlendMode(.clear)
context.fill(CGRect(origin: .zero, size: imageSize))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
UIGraphicsEndImageContext()
}
}
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
public final class FilledRoundedRectangleComponent: Component {
public enum CornerRadius: Equatable {
case value(CGFloat)
case minEdge
}
public let color: UIColor
public let cornerRadius: CornerRadius
public let smoothCorners: Bool
public init(
color: UIColor,
cornerRadius: CornerRadius,
smoothCorners: Bool
) {
self.color = color
self.cornerRadius = cornerRadius
self.smoothCorners = smoothCorners
}
public static func ==(lhs: FilledRoundedRectangleComponent, rhs: FilledRoundedRectangleComponent) -> Bool {
if lhs === rhs {
return true
}
if lhs.color != rhs.color {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.smoothCorners != rhs.smoothCorners {
return false
}
return true
}
public final class View: UIImageView {
private var component: FilledRoundedRectangleComponent?
private var currentCornerRadius: CGFloat?
private var cornerImage: UIImage?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func applyStaticCornerRadius() {
guard let component = self.component else {
return
}
guard let cornerRadius = self.currentCornerRadius else {
return
}
if cornerRadius == 0.0 {
if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 {
} else {
self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
}
} else {
if component.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
context.setFillColor(UIColor.white.cgColor)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
}
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
}
}
}
self.image = self.cornerImage
self.clipsToBounds = false
self.backgroundColor = nil
self.layer.cornerRadius = 0.0
}
func update(component: FilledRoundedRectangleComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
transition.setTintColor(view: self, color: component.color)
let cornerRadius: CGFloat
switch component.cornerRadius {
case let .value(value):
cornerRadius = value
case .minEdge:
cornerRadius = min(availableSize.width, availableSize.height) * 0.5
}
if self.currentCornerRadius != cornerRadius {
let previousCornerRadius = self.currentCornerRadius
self.currentCornerRadius = cornerRadius
if transition.animation.isImmediate {
self.applyStaticCornerRadius()
} else {
self.image = nil
self.clipsToBounds = true
self.backgroundColor = component.color
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = previousCornerRadius
}
if #available(iOS 13.0, *) {
if component.smoothCorners {
self.layer.cornerCurve = .continuous
} else {
self.layer.cornerCurve = .circular
}
}
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.applyStaticCornerRadius()
})
}
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
open class SolidRoundedCornersContainer: UIView {
public final class Params: Equatable {
public let size: CGSize
public let color: UIColor
public let cornerRadius: CGFloat
public let smoothCorners: Bool
public init(
size: CGSize,
color: UIColor,
cornerRadius: CGFloat,
smoothCorners: Bool
) {
self.size = size
self.color = color
self.cornerRadius = cornerRadius
self.smoothCorners = smoothCorners
}
public static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs === rhs {
return true
}
if lhs.size != rhs.size {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.smoothCorners != rhs.smoothCorners {
return false
}
return true
}
}
public let cornersView: UIImageView
private var params: Params?
private var currentCornerRadius: CGFloat?
private var cornerImage: UIImage?
override public init(frame: CGRect) {
self.cornersView = UIImageView()
super.init(frame: frame)
self.clipsToBounds = true
self.addSubview(self.cornersView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func applyStaticCornerRadius() {
guard let params = self.params else {
return
}
guard let cornerRadius = self.currentCornerRadius else {
return
}
if cornerRadius == 0.0 {
if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 {
} else {
self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.setFillColor(UIColor.clear.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
}
} else {
if params.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
}
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: nil, backgroundColor: .white)?.withRenderingMode(.alwaysTemplate)
}
}
}
self.cornersView.image = self.cornerImage
self.backgroundColor = nil
self.layer.cornerRadius = 0.0
}
public func update(params: Params, transition: ComponentTransition) {
if self.params == params {
return
}
self.params = params
transition.setTintColor(view: self.cornersView, color: params.color)
if self.currentCornerRadius != params.cornerRadius {
let previousCornerRadius = self.currentCornerRadius
self.currentCornerRadius = params.cornerRadius
if transition.animation.isImmediate {
self.applyStaticCornerRadius()
} else {
self.cornersView.image = nil
self.clipsToBounds = true
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
self.layer.cornerRadius = previousCornerRadius
}
if #available(iOS 13.0, *) {
if params.smoothCorners {
self.layer.cornerCurve = .continuous
} else {
self.layer.cornerCurve = .circular
}
}
transition.setCornerRadius(layer: self.layer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.applyStaticCornerRadius()
})
}
}
}
}
@@ -0,0 +1,110 @@
import Foundation
import UIKit
public final class Text: Component {
private final class MeasureState: Equatable {
let attributedText: NSAttributedString
let availableSize: CGSize
let size: CGSize
init(attributedText: NSAttributedString, availableSize: CGSize, size: CGSize) {
self.attributedText = attributedText
self.availableSize = availableSize
self.size = size
}
static func ==(lhs: MeasureState, rhs: MeasureState) -> Bool {
if !lhs.attributedText.isEqual(rhs.attributedText) {
return false
}
if lhs.availableSize != rhs.availableSize {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
public final class View: UIView {
private var measureState: MeasureState?
public func update(component: Text, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let attributedText = NSAttributedString(string: component.text, attributes: [
NSAttributedString.Key.font: component.font,
NSAttributedString.Key.foregroundColor: component.color
])
if let tintColor = component.tintColor {
transition.setTintColor(layer: self.layer, color: tintColor)
}
if let measureState = self.measureState {
if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize {
return measureState.size
}
}
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
let measureState = MeasureState(attributedText: attributedText, availableSize: availableSize, size: boundingRect.size)
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: measureState.size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
measureState.attributedText.draw(at: CGPoint())
UIGraphicsPopContext()
}
self.layer.contents = image.cgImage
} else {
UIGraphicsBeginImageContextWithOptions(measureState.size, false, 0.0)
measureState.attributedText.draw(at: CGPoint())
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
UIGraphicsEndImageContext()
}
self.measureState = measureState
return boundingRect.size
}
}
public let text: String
public let font: UIFont
public let color: UIColor
public let tintColor: UIColor?
public init(text: String, font: UIFont, color: UIColor, tintColor: UIColor? = nil) {
self.text = text
self.font = font
self.color = color
self.tintColor = tintColor
}
public static func ==(lhs: Text, rhs: Text) -> Bool {
if lhs.text != rhs.text {
return false
}
if !lhs.font.isEqual(rhs.font) {
return false
}
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return true
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,54 @@
import Foundation
import UIKit
public final class TransformContents<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let fixedSize: CGSize?
private let translation: CGPoint
public init(content: AnyComponent<ChildEnvironment>, fixedSize: CGSize? = nil, translation: CGPoint) {
self.content = content
self.fixedSize = fixedSize
self.translation = translation
}
public static func ==(lhs: TransformContents<ChildEnvironment>, rhs: TransformContents<ChildEnvironment>) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.fixedSize != rhs.fixedSize {
return false
}
if lhs.translation != rhs.translation {
return false
}
return true
}
public static var body: Body {
let child = Child(environment: ChildEnvironment.self)
return { context in
let child = child.update(
component: context.component.content,
environment: { context.environment[ChildEnvironment.self] },
availableSize: context.availableSize,
transition: context.transition
)
let size = context.component.fixedSize ?? child.size
var childFrame = child.size.centered(in: CGRect(origin: CGPoint(), size: size))
childFrame.origin.x += context.component.translation.x
childFrame.origin.y += context.component.translation.y
context.add(child
.position(childFrame.center)
)
return size
}
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
public enum VStackAlignment {
case left
case center
case right
}
public final class VStack<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let alignment: VStackAlignment
private let spacing: CGFloat
private let fillWidth: Bool
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], alignment: VStackAlignment = .center, spacing: CGFloat, fillWidth: Bool = false) {
self.items = items
self.alignment = alignment
self.spacing = spacing
self.fillWidth = fillWidth
}
public static func ==(lhs: VStack<ChildEnvironment>, rhs: VStack<ChildEnvironment>) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.spacing != rhs.spacing {
return false
}
if lhs.fillWidth != rhs.fillWidth {
return false
}
return true
}
public static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
return { context in
let updatedChildren = context.component.items.map { item in
return children[item.id].update(
component: item.component, environment: {
context.environment[ChildEnvironment.self]
},
availableSize: context.availableSize,
transition: context.transition
)
}
var size = CGSize(width: 0.0, height: 0.0)
if context.component.fillWidth {
size.width = context.availableSize.width
}
for child in updatedChildren {
size.height += child.size.height
size.width = max(size.width, child.size.width)
}
size.height += context.component.spacing * CGFloat(updatedChildren.count - 1)
var nextY = 0.0
for child in updatedChildren {
let childFrame: CGRect
switch context.component.alignment {
case .left:
childFrame = CGRect(origin: CGPoint(x: 0.0, y: nextY), size: child.size)
case .center:
childFrame = CGRect(origin: CGPoint(x: floor((size.width - child.size.width) * 0.5), y: nextY), size: child.size)
case .right:
childFrame = CGRect(origin: CGPoint(x: size.width - child.size.width, y: nextY), size: child.size)
}
context.add(child
.position(childFrame.center)
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
nextY += child.size.height
nextY += context.component.spacing
}
return size
}
}
}
@@ -0,0 +1,49 @@
import Foundation
import UIKit
public final class ZStack<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>]) {
self.items = items
}
public static func ==(lhs: ZStack<ChildEnvironment>, rhs: ZStack<ChildEnvironment>) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
public static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
return { context in
let updatedChildren = context.component.items.map { item in
return children[item.id].update(
component: item.component, environment: {
context.environment[ChildEnvironment.self]
},
availableSize: context.availableSize,
transition: context.transition
)
}
var size = CGSize(width: 0.0, height: 0.0)
for child in updatedChildren {
size.width = max(size.width, child.size.width)
size.height = max(size.height, child.size.height)
}
for child in updatedChildren {
context.add(child
.position(child.size.centered(in: CGRect(origin: CGPoint(), size: size)).center)
)
}
return size
}
}
}
@@ -0,0 +1,29 @@
import Foundation
import UIKit
public class Gesture {
class Id {
private var _id: UInt = 0
public var id: UInt {
return self._id
}
init() {
self._id = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
}
}
let id: Id
init(id: Id) {
self.id = id
}
func create() -> UIGestureRecognizer {
preconditionFailure()
}
func update(gesture: UIGestureRecognizer) {
preconditionFailure()
}
}
@@ -0,0 +1,59 @@
import Foundation
import UIKit
public extension Gesture {
enum LongPressGestureState {
case began
case ended
}
private final class LongPressGesture: Gesture {
private class Impl: UILongPressGestureRecognizer {
var action: (LongPressGestureState) -> Void
init(pressDuration: Double, action: @escaping (LongPressGestureState) -> Void) {
self.action = action
super.init(target: nil, action: nil)
self.minimumPressDuration = pressDuration
self.addTarget(self, action: #selector(self.onAction))
}
@objc private func onAction() {
switch self.state {
case .began:
self.action(.began)
case .ended, .cancelled:
self.action(.ended)
default:
break
}
}
}
static let id = Id()
private let pressDuration: Double
private let action: (LongPressGestureState) -> Void
init(pressDuration: Double, action: @escaping (LongPressGestureState) -> Void) {
self.pressDuration = pressDuration
self.action = action
super.init(id: Self.id)
}
override func create() -> UIGestureRecognizer {
return Impl(pressDuration: self.pressDuration, action: self.action)
}
override func update(gesture: UIGestureRecognizer) {
(gesture as! Impl).minimumPressDuration = self.pressDuration
(gesture as! Impl).action = self.action
}
}
static func longPress(duration: Double = 0.2, _ action: @escaping (LongPressGestureState) -> Void) -> Gesture {
return LongPressGesture(pressDuration: duration, action: action)
}
}
@@ -0,0 +1,59 @@
import Foundation
import UIKit
public extension Gesture {
enum PanGestureState {
case began
case updated(offset: CGPoint)
case ended(velocity: CGPoint)
}
private final class PanGesture: Gesture {
private class Impl: UIPanGestureRecognizer {
var action: (PanGestureState) -> Void
init(action: @escaping (PanGestureState) -> Void) {
self.action = action
super.init(target: nil, action: nil)
self.addTarget(self, action: #selector(self.onAction))
}
@objc private func onAction() {
switch self.state {
case .began:
self.action(.began)
case .ended, .cancelled:
self.action(.ended(velocity: self.velocity(in: self.view)))
case .changed:
let offset = self.translation(in: self.view)
self.action(.updated(offset: offset))
default:
break
}
}
}
static let id = Id()
private let action: (PanGestureState) -> Void
init(action: @escaping (PanGestureState) -> Void) {
self.action = action
super.init(id: Self.id)
}
override func create() -> UIGestureRecognizer {
return Impl(action: self.action)
}
override func update(gesture: UIGestureRecognizer) {
(gesture as! Impl).action = action
}
}
static func pan(_ action: @escaping (PanGestureState) -> Void) -> Gesture {
return PanGesture(action: action)
}
}
@@ -0,0 +1,43 @@
import Foundation
import UIKit
public extension Gesture {
private final class TapGesture: Gesture {
private class Impl: UITapGestureRecognizer {
var action: () -> Void
init(action: @escaping () -> Void) {
self.action = action
super.init(target: nil, action: nil)
self.addTarget(self, action: #selector(self.onAction))
}
@objc private func onAction() {
self.action()
}
}
static let id = Id()
private let action: () -> Void
init(action: @escaping () -> Void) {
self.action = action
super.init(id: Self.id)
}
override func create() -> UIGestureRecognizer {
return Impl(action: self.action)
}
override func update(gesture: UIGestureRecognizer) {
(gesture as! Impl).action = action
}
}
static func tap(_ action: @escaping () -> Void) -> Gesture {
return TapGesture(action: action)
}
}
@@ -0,0 +1,245 @@
import Foundation
import UIKit
public func findTaggedComponentViewImpl(view: UIView, tag: Any) -> UIView? {
if let view = view as? ComponentTaggedView {
if view.matches(tag: tag) {
return view
}
}
for subview in view.subviews {
if let result = findTaggedComponentViewImpl(view: subview, tag: tag) {
return result
}
}
return nil
}
public final class ComponentHostViewSkipSettingFrame {
public init() {
}
}
public final class ComponentHostView<EnvironmentType>: UIView {
private var currentComponent: AnyComponent<EnvironmentType>?
private var currentContainerSize: CGSize?
private var currentSize: CGSize?
public private(set) var componentView: UIView?
private(set) var isUpdating: Bool = false
public init() {
super.init(frame: CGRect())
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(transition: ComponentTransition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize {
let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize)
self.currentSize = size
return size
}
private func _update(transition: ComponentTransition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize {
precondition(!self.isUpdating)
self.isUpdating = true
precondition(containerSize.width.isFinite)
precondition(containerSize.height.isFinite)
let componentView: UIView
if let current = self.componentView {
componentView = current
} else {
componentView = component._makeView()
self.componentView = componentView
self.addSubview(componentView)
}
let context = componentView.context(component: component)
let componentState: ComponentState = context.erasedState
if updateEnvironment {
EnvironmentBuilder._environment = context.erasedEnvironment
let environmentResult = maybeEnvironment()
EnvironmentBuilder._environment = nil
context.erasedEnvironment = environmentResult
}
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()
if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
if currentContainerSize == containerSize && currentComponent == component {
self.isUpdating = false
return currentSize
}
}
self.currentComponent = component
self.currentContainerSize = containerSize
componentState._updated = { [weak self] transition, _ in
guard let strongSelf = self else {
return
}
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
preconditionFailure()
} as () -> Environment<EnvironmentType>, updateEnvironment: false, forceUpdate: true, containerSize: containerSize)
}
let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition)
if transition.userData(ComponentHostViewSkipSettingFrame.self) == nil {
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize))
}
if isEnvironmentUpdated {
context.erasedEnvironment._isUpdated = false
}
self.isUpdating = false
return updatedSize
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.alpha.isZero {
return nil
}
if !self.isUserInteractionEnabled {
return nil
}
for view in self.subviews.reversed() {
if view.isUserInteractionEnabled, view.alpha != 0.0, let result = view.hitTest(self.convert(point, to: view), with: event) {
return result
}
}
let result = super.hitTest(point, with: event)
if result != self {
return result
} else {
return nil
}
}
public func findTaggedView(tag: Any) -> UIView? {
guard let componentView = self.componentView else {
return nil
}
return findTaggedComponentViewImpl(view: componentView, tag: tag)
}
}
public final class ComponentView<EnvironmentType> {
private var currentComponent: AnyComponent<EnvironmentType>?
private var currentContainerSize: CGSize?
private var currentSize: CGSize?
public private(set) var view: UIView?
private(set) var isUpdating: Bool = false
public weak var parentState: ComponentState?
public init() {
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(transition: ComponentTransition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize {
let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize)
self.currentSize = size
return size
}
public func updateEnvironment(transition: ComponentTransition, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>) -> CGSize? {
guard let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize else {
return nil
}
let size = self._update(transition: transition, component: currentComponent, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: currentContainerSize)
self.currentSize = size
return size
}
private func _update(transition: ComponentTransition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize {
precondition(!self.isUpdating)
self.isUpdating = true
precondition(containerSize.width.isFinite)
precondition(containerSize.height.isFinite)
let componentView: UIView
if let current = self.view {
componentView = current
} else {
componentView = component._makeView()
self.view = componentView
}
let context = componentView.context(component: component)
let componentState: ComponentState = context.erasedState
if updateEnvironment {
EnvironmentBuilder._environment = context.erasedEnvironment
let environmentResult = maybeEnvironment()
EnvironmentBuilder._environment = nil
context.erasedEnvironment = environmentResult
}
var isStateUpdated = false
if componentState.isUpdated {
isStateUpdated = true
componentState.isUpdated = false
}
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()
if !forceUpdate, !isEnvironmentUpdated, !isStateUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
if currentContainerSize == containerSize && currentComponent == component {
self.isUpdating = false
return currentSize
}
}
self.currentComponent = component
self.currentContainerSize = containerSize
componentState._updated = { [weak self] transition, isLocal in
guard let strongSelf = self else {
return
}
if !isLocal, let parentState = strongSelf.parentState {
parentState.updated(transition: transition)
} else {
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
preconditionFailure()
} as () -> Environment<EnvironmentType>, updateEnvironment: false, forceUpdate: true, containerSize: containerSize)
}
}
let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition)
if isEnvironmentUpdated {
context.erasedEnvironment._isUpdated = false
}
if isStateUpdated {
context.erasedState.isUpdated = false
}
self.isUpdating = false
return updatedSize
}
public func findTaggedView(tag: Any) -> UIView? {
guard let view = self.view else {
return nil
}
return findTaggedComponentViewImpl(view: view, tag: tag)
}
}
@@ -0,0 +1,8 @@
import Foundation
import UIKit
public struct NavigationLayout: Equatable {
public var statusBarHeight: CGFloat
public var inputHeight: CGFloat
public var bottomNavigationHeight: CGFloat
}
@@ -0,0 +1,3 @@
import Foundation
import UIKit
@@ -0,0 +1,32 @@
import Foundation
public final class Action<Arguments> {
public let action: (Arguments) -> Void
public init(_ action: @escaping (Arguments) -> Void) {
self.action = action
}
public func callAsFunction(_ arguments: Arguments) {
self.action(arguments)
}
}
public final class ActionSlot<Arguments>: Equatable {
private var target: ((Arguments) -> Void)?
public init() {
}
public static func ==(lhs: ActionSlot<Arguments>, rhs: ActionSlot<Arguments>) -> Bool {
return lhs === rhs
}
public func connect(_ target: @escaping (Arguments) -> Void) {
self.target = target
}
public func invoke(_ arguments: Arguments) {
self.target?(arguments)
}
}
@@ -0,0 +1,8 @@
import Foundation
import UIKit
extension UIColor {
convenience init(rgb: UInt32) {
self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0)
}
}
@@ -0,0 +1,9 @@
import Foundation
public func Condition<R>(_ f: @autoclosure () -> Bool, _ pass: () -> R) -> R? {
if f() {
return pass()
} else {
return nil
}
}
@@ -0,0 +1,28 @@
import Foundation
final class EscapeGuard {
final class Status {
fileprivate(set) var isDeallocated: Bool = false
}
let status = Status()
deinit {
self.status.isDeallocated = true
}
}
public final class EscapeNotification: NSObject {
let deallocated: () -> Void
public init(_ deallocated: @escaping () -> Void) {
self.deallocated = deallocated
}
deinit {
self.deallocated()
}
public func keep() {
}
}
@@ -0,0 +1,8 @@
import Foundation
import UIKit
public extension UIEdgeInsets {
init(_ value: CGFloat) {
self.init(top: value, left: value, bottom: value, right: value)
}
}
@@ -0,0 +1,8 @@
import Foundation
import UIKit
public extension CGRect {
var center: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
@@ -0,0 +1,28 @@
import Foundation
import UIKit
public extension CGSize {
func centered(in rect: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self)
}
func centered(around position: CGPoint) -> CGRect {
return CGRect(origin: CGPoint(x: position.x - self.width / 2.0, y: position.y - self.height / 2.0), size: self)
}
func leftCentered(in rect: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: rect.minX, y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self)
}
func rightCentered(in rect: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: rect.maxX - self.width, y: rect.minY + floor((rect.height - self.height) / 2.0)), size: self)
}
func topCentered(in rect: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: 0.0), size: self)
}
func bottomCentered(in rect: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: rect.minX + floor((rect.width - self.width) / 2.0), y: rect.maxY - self.height), size: self)
}
}