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,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
}
}
}