GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ActivityIndicatorComponent",
module_name = "ActivityIndicatorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,50 @@
import Foundation
import UIKit
import ComponentFlow
public final class ActivityIndicatorComponent: Component {
public let color: UIColor
public init(
color: UIColor
) {
self.color = color
}
public static func ==(lhs: ActivityIndicatorComponent, rhs: ActivityIndicatorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: UIActivityIndicatorView {
public init() {
super.init(style: UIActivityIndicatorView.Style.large)
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if component.color != self.color {
self.color = component.color
}
if !self.isAnimating {
self.startAnimating()
}
return CGSize(width: 22.0, height: 22.0)
}
}
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,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedStickerComponent",
module_name = "AnimatedStickerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/TelegramCore:TelegramCore",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,151 @@
import Foundation
import UIKit
import ComponentFlow
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import HierarchyTrackingLayer
import TelegramCore
public final class AnimatedStickerComponent: Component {
public struct Animation: Equatable {
public enum Source: Equatable {
case bundle(name: String)
case file(media: TelegramMediaFile)
}
public var source: Source
public var scale: CGFloat
public var loop: Bool
public init(source: Source, scale: CGFloat = 2.0, loop: Bool) {
self.source = source
self.scale = scale
self.loop = loop
}
}
public let account: Account
public let animation: Animation
public var tintColor: UIColor?
public let isAnimating: Bool
public let size: CGSize
public init(account: Account, animation: Animation, tintColor: UIColor? = nil, isAnimating: Bool = true, size: CGSize) {
self.account = account
self.animation = animation
self.tintColor = tintColor
self.isAnimating = isAnimating
self.size = size
}
public static func ==(lhs: AnimatedStickerComponent, rhs: AnimatedStickerComponent) -> Bool {
if lhs.account !== rhs.account {
return false
}
if lhs.animation != rhs.animation {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.isAnimating != rhs.isAnimating {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private var component: AnimatedStickerComponent?
private var animationNode: AnimatedStickerNode?
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var isInHierarchy: Bool = false
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isInHierarchy = true
strongSelf.animationNode?.visibility = true
}
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isInHierarchy = false
strongSelf.animationNode?.visibility = false
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AnimatedStickerComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.component?.animation != component.animation {
self.animationNode?.view.removeFromSuperview()
let animationNode = DefaultAnimatedStickerNodeImpl()
let source: AnimatedStickerNodeSource
switch component.animation.source {
case let .bundle(name):
source = AnimatedStickerNodeLocalFileSource(name: name)
case let .file(media):
source = AnimatedStickerResourceSource(account: component.account, resource: media.resource, fitzModifier: nil, isVideo: false)
}
var playbackMode: AnimatedStickerPlaybackMode = .still(.start)
if component.animation.loop {
playbackMode = .loop
} else if component.isAnimating {
playbackMode = .once
} else {
animationNode.autoplay = true
}
animationNode.setup(source: source, width: Int(component.size.width * component.animation.scale), height: Int(component.size.height * component.animation.scale), playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
animationNode.visibility = self.isInHierarchy
self.animationNode = animationNode
self.addSubnode(animationNode)
}
self.animationNode?.setOverlayColor(component.tintColor, replace: true, animated: false)
if !component.animation.loop && component.isAnimating != self.component?.isAnimating {
if component.isAnimating {
let _ = self.animationNode?.playIfNeeded()
}
}
self.component = component
let animationSize = component.size
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
if let animationNode = self.animationNode {
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
animationNode.updateLayout(size: animationSize)
}
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 {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BalancedTextComponent",
module_name = "BalancedTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Markdown:Markdown",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,226 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Markdown
public final class BalancedTextComponent: Component {
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
}
public let text: TextContent
public let balanced: Bool
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public let truncationType: CTLineTruncationType
public let maximumNumberOfLines: Int
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public let tintColor: UIColor?
public let textShadowColor: UIColor?
public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
text: TextContent,
balanced: Bool = true,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
tintColor: UIColor? = nil,
textShadowColor: UIColor? = nil,
textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.text = text
self.balanced = balanced
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.tintColor = tintColor
self.textShadowColor = textShadowColor
self.textShadowBlur = textShadowBlur
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: BalancedTextComponent, rhs: BalancedTextComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.balanced != rhs.balanced {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if lhs.textShadowBlur != rhs.textShadowBlur {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
public final class View: UIView {
private let textView: ImmediateTextView
override public init(frame: CGRect) {
self.textView = ImmediateTextView()
super.init(frame: frame)
self.addSubview(self.textView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributeSubstring(name: String, index: Int) -> (String, String)? {
return self.textView.attributeSubstring(name: name, index: index)
}
public func update(component: BalancedTextComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let attributedString: NSAttributedString
switch component.text {
case let .plain(string):
attributedString = string
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
self.textView.attributedText = attributedString
self.textView.maximumNumberOfLines = component.maximumNumberOfLines
self.textView.truncationType = component.truncationType
self.textView.textAlignment = component.horizontalAlignment
self.textView.verticalAlignment = component.verticalAlignment
self.textView.lineSpacing = component.lineSpacing
self.textView.cutout = component.cutout
self.textView.insets = component.insets
self.textView.textShadowColor = component.textShadowColor
self.textView.textShadowBlur = component.textShadowBlur
self.textView.textStroke = component.textStroke
self.textView.linkHighlightColor = component.highlightColor
self.textView.linkHighlightInset = component.highlightInset
self.textView.highlightAttributeAction = component.highlightAction
self.textView.tapAttributeAction = component.tapAction
self.textView.longTapAttributeAction = component.longTapAction
var bestSize: (availableWidth: CGFloat, info: TextNodeLayout)
let info = self.textView.updateLayoutFullInfo(availableSize)
bestSize = (availableSize.width, info)
if component.balanced && info.numberOfLines > 1 {
let measureIncrement = 8.0
var measureWidth = info.size.width
measureWidth -= measureIncrement
while measureWidth > 0.0 {
let otherInfo = self.textView.updateLayoutFullInfo(CGSize(width: measureWidth, height: availableSize.height))
if otherInfo.numberOfLines > bestSize.info.numberOfLines {
break
}
if (otherInfo.size.width - otherInfo.trailingLineWidth) < (bestSize.info.size.width - bestSize.info.trailingLineWidth) {
bestSize = (measureWidth, otherInfo)
}
measureWidth -= measureIncrement
}
let bestInfo = self.textView.updateLayoutFullInfo(CGSize(width: bestSize.availableWidth, height: availableSize.height))
bestSize = (availableSize.width, bestInfo)
}
if let tintColor = component.tintColor {
transition.setTintColor(layer: self.textView.layer, color: tintColor)
}
self.textView.frame = CGRect(origin: CGPoint(), size: bestSize.info.size)
return bestSize.info.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,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BlurredBackgroundComponent",
module_name = "BlurredBackgroundComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,62 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import ComponentDisplayAdapters
public final class BlurredBackgroundComponent: Component {
public let color: UIColor
public let tintContainerView: UIView?
public let cornerRadius: CGFloat
public init(
color: UIColor,
tintContainerView: UIView? = nil,
cornerRadius: CGFloat = 0.0
) {
self.color = color
self.tintContainerView = tintContainerView
self.cornerRadius = cornerRadius
}
public static func ==(lhs: BlurredBackgroundComponent, rhs: BlurredBackgroundComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.tintContainerView !== rhs.tintContainerView {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
return true
}
public final class View: BlurredBackgroundView {
private var tintContainerView: UIView?
private var vibrancyEffectView: UIVisualEffectView?
public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.updateColor(color: component.color, forceKeepBlur: true, transition: transition.containedViewLayoutTransition)
self.update(size: availableSize, cornerRadius: component.cornerRadius, transition: transition.containedViewLayoutTransition)
if let tintContainerView = self.tintContainerView {
transition.setFrame(view: tintContainerView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
if let vibrancyEffectView = self.vibrancyEffectView {
transition.setFrame(view: vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
return availableSize
}
}
public func makeView() -> View {
return View(color: nil, enableBlur: true)
}
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,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BundleIconComponent",
module_name = "BundleIconComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,108 @@
import Foundation
import UIKit
import ComponentFlow
import AppBundle
import Display
public final class BundleIconComponent: Component {
public let name: String
public let tintColor: UIColor?
public let maxSize: CGSize?
public let scaleFactor: CGFloat
public let shadowColor: UIColor?
public let shadowBlur: CGFloat
public let flipVertically: Bool
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) {
self.name = name
self.tintColor = tintColor
self.maxSize = maxSize
self.scaleFactor = scaleFactor
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
self.flipVertically = flipVertically
}
public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool {
if lhs.name != rhs.name {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.maxSize != rhs.maxSize {
return false
}
if lhs.scaleFactor != rhs.scaleFactor {
return false
}
if lhs.shadowColor != rhs.shadowColor {
return false
}
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
if lhs.flipVertically != rhs.flipVertically {
return false
}
return true
}
public final class View: UIImageView {
private var component: BundleIconComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BundleIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.component?.name != component.name || self.component?.tintColor != component.tintColor || self.component?.shadowColor != component.shadowColor || self.component?.shadowBlur != component.shadowBlur || self.component?.flipVertically != component.flipVertically {
var image: UIImage?
if let tintColor = component.tintColor {
image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil)
} else {
image = UIImage(bundleImageName: component.name)
}
if let imageValue = image, let shadowColor = component.shadowColor, component.shadowBlur != 0.0 {
image = generateImage(CGSize(width: imageValue.size.width + component.shadowBlur * 2.0, height: imageValue.size.height + component.shadowBlur * 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: component.shadowBlur, color: shadowColor.cgColor)
if let cgImage = imageValue.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: component.shadowBlur, y: component.shadowBlur), size: imageValue.size))
}
})
}
if component.flipVertically, let cgImage = image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down)
} else {
self.image = image
}
}
self.component = component
var imageSize = self.image?.size ?? CGSize()
if let maxSize = component.maxSize {
imageSize = imageSize.aspectFitted(maxSize)
}
if component.scaleFactor != 1.0 {
imageSize.width = floor(imageSize.width * component.scaleFactor)
imageSize.height = floor(imageSize.height * component.scaleFactor)
}
return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height))
}
}
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)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ComponentDisplayAdapters",
module_name = "ComponentDisplayAdapters",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,57 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public extension ComponentTransition.Animation.Curve {
init(_ curve: ContainedViewLayoutTransitionCurve) {
switch curve {
case .linear:
self = .linear
case .easeInOut:
self = .easeInOut
case let .custom(a, b, c, d):
self = .custom(a, b, c, d)
case .customSpring:
self = .spring
case .spring:
self = .spring
}
}
var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve {
switch self {
case .linear:
return .linear
case .easeInOut:
return .easeInOut
case .spring:
return .spring
case let .custom(a, b, c, d):
return .custom(a, b, c, d)
case .bounce:
assertionFailure()
return .spring
}
}
}
public extension ComponentTransition {
init(_ transition: ContainedViewLayoutTransition) {
switch transition {
case .immediate:
self.init(animation: .none)
case let .animated(duration, curve):
self.init(animation: .curve(duration: duration, curve: ComponentTransition.Animation.Curve(curve)))
}
}
var containedViewLayoutTransition: ContainedViewLayoutTransition {
switch self.animation {
case .none:
return .immediate
case let .curve(duration, curve):
return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve)
}
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CreditCardInputComponent",
module_name = "CreditCardInputComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Stripe:Stripe",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,172 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Stripe
public final class CreditCardInputComponent: Component {
public enum DataType {
case cardNumber
case expirationDate
}
public let dataType: DataType
public let text: String
public let textColor: UIColor
public let errorTextColor: UIColor
public let placeholder: String
public let placeholderColor: UIColor
public let updated: (String) -> Void
public init(
dataType: DataType,
text: String,
textColor: UIColor,
errorTextColor: UIColor,
placeholder: String,
placeholderColor: UIColor,
updated: @escaping (String) -> Void
) {
self.dataType = dataType
self.text = text
self.textColor = textColor
self.errorTextColor = errorTextColor
self.placeholder = placeholder
self.placeholderColor = placeholderColor
self.updated = updated
}
public static func ==(lhs: CreditCardInputComponent, rhs: CreditCardInputComponent) -> Bool {
if lhs.dataType != rhs.dataType {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.errorTextColor != rhs.errorTextColor {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.placeholderColor != rhs.placeholderColor {
return false
}
return true
}
public final class View: UIView, STPFormTextFieldDelegate, UITextFieldDelegate {
private let textField: STPFormTextField
private var component: CreditCardInputComponent?
private let viewModel: STPPaymentCardTextFieldViewModel
override init(frame: CGRect) {
self.textField = STPFormTextField(frame: CGRect())
self.viewModel = STPPaymentCardTextFieldViewModel()
super.init(frame: frame)
self.textField.backgroundColor = .clear
self.textField.keyboardType = .phonePad
self.textField.formDelegate = self
self.textField.validText = true
self.addSubview(self.textField)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func textFieldChanged(_ textField: UITextField) {
self.component?.updated(self.textField.text ?? "")
}
public func formTextFieldDidBackspace(onEmpty formTextField: STPFormTextField) {
}
public func formTextField(_ formTextField: STPFormTextField, modifyIncomingTextChange input: NSAttributedString) -> NSAttributedString {
guard let component = self.component else {
return input
}
switch component.dataType {
case .cardNumber:
self.viewModel.cardNumber = input.string
return NSAttributedString(string: self.viewModel.cardNumber ?? "", attributes: self.textField.defaultTextAttributes)
case .expirationDate:
self.viewModel.rawExpiration = input.string
return NSAttributedString(string: self.viewModel.rawExpiration ?? "", attributes: self.textField.defaultTextAttributes)
}
}
public func formTextFieldTextDidChange(_ textField: STPFormTextField) {
guard let component = self.component else {
return
}
component.updated(self.textField.text ?? "")
let state: STPCardValidationState
switch component.dataType {
case .cardNumber:
state = self.viewModel.validationState(for: .number)
case .expirationDate:
state = self.viewModel.validationState(for: .expiration)
}
self.textField.validText = true
switch state {
case .invalid:
self.textField.validText = false
case .incomplete:
break
case .valid:
break
@unknown default:
break
}
}
func update(component: CreditCardInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
switch component.dataType {
case .cardNumber:
self.textField.autoFormattingBehavior = .cardNumbers
case .expirationDate:
self.textField.autoFormattingBehavior = .expiration
}
self.textField.font = UIFont.systemFont(ofSize: 17.0)
self.textField.defaultColor = component.textColor
self.textField.errorColor = .red
self.textField.placeholderColor = component.placeholderColor
if self.textField.text != component.text {
self.textField.text = component.text
}
self.textField.attributedPlaceholder = NSAttributedString(string: component.placeholder, font: self.textField.font, textColor: component.placeholderColor)
let size = CGSize(width: availableSize.width, height: 44.0)
transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
self.component = component
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 {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PrefixSectionGroupComponent",
module_name = "PrefixSectionGroupComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,194 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public final class PrefixSectionGroupComponent: Component {
public final class Item: Equatable {
public let prefix: AnyComponentWithIdentity<Empty>
public let content: AnyComponentWithIdentity<Empty>
public init(prefix: AnyComponentWithIdentity<Empty>, content: AnyComponentWithIdentity<Empty>) {
self.prefix = prefix
self.content = content
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.prefix != rhs.prefix {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
}
public let items: [Item]
public let backgroundColor: UIColor
public let separatorColor: UIColor
public init(
items: [Item],
backgroundColor: UIColor,
separatorColor: UIColor
) {
self.items = items
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
}
public static func ==(lhs: PrefixSectionGroupComponent, rhs: PrefixSectionGroupComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: UIView
private var itemViews: [AnyHashable: ComponentHostView<Empty>] = [:]
private var separatorViews: [UIView] = []
override init(frame: CGRect) {
self.backgroundView = UIView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.layer.cornerRadius = 10.0
self.backgroundView.layer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PrefixSectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let spacing: CGFloat = 16.0
let sideInset: CGFloat = 16.0
self.backgroundView.backgroundColor = component.backgroundColor
var size = CGSize(width: availableSize.width, height: 0.0)
var validIds: [AnyHashable] = []
var maxPrefixSize = CGSize()
var prefixItemSizes: [CGSize] = []
for item in component.items {
validIds.append(item.prefix.id)
let itemView: ComponentHostView<Empty>
var itemTransition = transition
if let current = self.itemViews[item.prefix.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<Empty>()
self.itemViews[item.prefix.id] = itemView
self.addSubview(itemView)
}
let itemSize = itemView.update(
transition: itemTransition,
component: item.prefix.component,
environment: {},
containerSize: CGSize(width: size.width, height: .greatestFiniteMagnitude)
)
prefixItemSizes.append(itemSize)
maxPrefixSize.width = max(maxPrefixSize.width, itemSize.width)
maxPrefixSize.height = max(maxPrefixSize.height, itemSize.height)
}
var maxContentSize = CGSize()
var contentItemSizes: [CGSize] = []
for item in component.items {
validIds.append(item.content.id)
let itemView: ComponentHostView<Empty>
var itemTransition = transition
if let current = self.itemViews[item.content.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<Empty>()
self.itemViews[item.content.id] = itemView
self.addSubview(itemView)
}
let itemSize = itemView.update(
transition: itemTransition,
component: item.content.component,
environment: {},
containerSize: CGSize(width: size.width - maxPrefixSize.width - sideInset - spacing, height: .greatestFiniteMagnitude)
)
contentItemSizes.append(itemSize)
maxContentSize.width = max(maxContentSize.width, itemSize.width)
maxContentSize.height = max(maxContentSize.height, itemSize.height)
}
for i in 0 ..< component.items.count {
let itemSize = CGSize(width: size.width, height: max(prefixItemSizes[i].height, contentItemSizes[i].height))
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
let prefixView = itemViews[component.items[i].prefix.id]!
let contentView = itemViews[component.items[i].content.id]!
prefixView.frame = CGRect(origin: CGPoint(x: sideInset, y: itemFrame.minY + floor((itemFrame.height - prefixItemSizes[i].height) / 2.0)), size: prefixItemSizes[i])
contentView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset + maxPrefixSize.width + spacing, y: itemFrame.minY + floor((itemFrame.height - contentItemSizes[i].height) / 2.0)), size: contentItemSizes[i])
size.height += itemSize.height
if i != component.items.count - 1 {
let separatorView: UIView
if self.separatorViews.count > i {
separatorView = self.separatorViews[i]
} else {
separatorView = UIView()
self.separatorViews.append(separatorView)
self.addSubview(separatorView)
}
separatorView.backgroundColor = component.separatorColor
separatorView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset, y: itemFrame.maxY), size: CGSize(width: itemFrame.width - sideInset, height: UIScreenPixel))
}
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
if self.separatorViews.count > component.items.count - 1 {
for i in (component.items.count - 1) ..< self.separatorViews.count {
self.separatorViews[i].removeFromSuperview()
}
self.separatorViews.removeSubrange((component.items.count - 1) ..< self.separatorViews.count)
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size), 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 {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TextInputComponent",
module_name = "TextInputComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,86 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public final class TextInputComponent: Component {
public let text: String
public let textColor: UIColor
public let placeholder: String
public let placeholderColor: UIColor
public let updated: (String) -> Void
public init(
text: String,
textColor: UIColor,
placeholder: String,
placeholderColor: UIColor,
updated: @escaping (String) -> Void
) {
self.text = text
self.textColor = textColor
self.placeholder = placeholder
self.placeholderColor = placeholderColor
self.updated = updated
}
public static func ==(lhs: TextInputComponent, rhs: TextInputComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.placeholderColor != rhs.placeholderColor {
return false
}
return true
}
public final class View: UITextField, UITextFieldDelegate {
private var component: TextInputComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.delegate = self
self.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func textFieldChanged(_ textField: UITextField) {
self.component?.updated(self.text ?? "")
}
func update(component: TextInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.font = UIFont.systemFont(ofSize: 17.0)
self.textColor = component.textColor
if self.text != component.text {
self.text = component.text
}
self.attributedPlaceholder = NSAttributedString(string: component.placeholder, font: self.font, textColor: component.placeholderColor)
let size = CGSize(width: availableSize.width, height: 44.0)
self.component = component
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 {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "HierarchyTrackingLayer",
module_name = "HierarchyTrackingLayer",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,33 @@
import UIKit
private final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
private let nullAction = NullActionClass()
open class HierarchyTrackingLayer: CALayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public var isInHierarchyUpdated: ((Bool) -> Void)?
public private(set) var isInHierarchy: Bool = false {
didSet {
if self.isInHierarchy != oldValue {
self.isInHierarchyUpdated?(self.isInHierarchy)
}
}
}
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "LottieAnimationComponent",
module_name = "LottieAnimationComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/lottie-ios:Lottie",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/GZip:GZip",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,331 @@
import Foundation
import ComponentFlow
import Lottie
import AppBundle
import HierarchyTrackingLayer
import UIKit
import Display
import GZip
public final class LottieAnimationComponent: Component {
public struct AnimationItem: Equatable {
public enum StillPosition {
case begin
case end
}
public enum Mode: Equatable {
case still(position: StillPosition)
case animating(loop: Bool)
case animateTransitionFromPrevious
}
public var name: String
public var mode: Mode
public var range: (CGFloat, CGFloat)?
public var speed: CGFloat
public var waitForCompletion: Bool
public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil, speed: CGFloat = 1.0, waitForCompletion: Bool = true) {
self.name = name
self.mode = mode
self.range = range
self.speed = speed
self.waitForCompletion = waitForCompletion
}
public static func == (lhs: LottieAnimationComponent.AnimationItem, rhs: LottieAnimationComponent.AnimationItem) -> Bool {
if lhs.name != rhs.name {
return false
}
if lhs.mode != rhs.mode {
return false
}
if lhs.speed != rhs.speed {
return false
}
if let lhsRange = lhs.range, let rhsRange = rhs.range, lhsRange != rhsRange {
return false
} else if (lhs.range == nil) != (rhs.range == nil) {
return false
}
return true
}
}
public let animation: AnimationItem
public let colors: [String: UIColor]
public let tag: AnyObject?
public let size: CGSize?
public init(animation: AnimationItem, colors: [String: UIColor], tag: AnyObject? = nil, size: CGSize?) {
self.animation = animation
self.colors = colors
self.tag = tag
self.size = size
}
public func tagged(_ tag: AnyObject?) -> LottieAnimationComponent {
return LottieAnimationComponent(
animation: self.animation,
colors: self.colors,
tag: tag,
size: self.size
)
}
public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool {
if lhs.animation != rhs.animation {
return false
}
if lhs.colors != rhs.colors {
return false
}
if lhs.tag !== rhs.tag {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView, ComponentTaggedView {
private var component: LottieAnimationComponent?
//private var colorCallbacks: [LOTColorValueCallback] = []
private var animationView: AnimationView?
private var didPlayToCompletion: Bool = false
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var currentCompletion: (() -> Void)?
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self, let animationView = strongSelf.animationView else {
return
}
if case .loop = animationView.loopMode {
animationView.play { _ in
self?.currentCompletion?()
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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
}
public func playOnce() {
guard let animationView = self.animationView, let component = self.component else {
return
}
animationView.stop()
animationView.loopMode = .playOnce
if let range = component.animation.range {
animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in
self?.currentCompletion?()
})
} else {
animationView.play { [weak self] _ in
self?.currentCompletion?()
}
}
}
func update(component: LottieAnimationComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
var updatePlayback = false
var updateColors = false
if let currentComponent = self.component, currentComponent.colors != component.colors {
updateColors = true
}
var animateSize = true
var updateComponent = true
if self.component?.animation != component.animation {
if let animationView = self.animationView {
if case .animateTransitionFromPrevious = component.animation.mode, !animationView.isAnimationPlaying, !self.didPlayToCompletion {
updateComponent = false
animationView.play { [weak self] _ in
self?.currentCompletion?()
}
}
}
if let animationView = self.animationView, animationView.isAnimationPlaying && component.animation.waitForCompletion {
updateComponent = false
self.currentCompletion = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.didPlayToCompletion = true
let _ = strongSelf.update(component: component, availableSize: availableSize, transition: transition)
}
animationView.loopMode = .playOnce
} else {
self.component = component
self.animationView?.removeFromSuperview()
self.didPlayToCompletion = false
self.currentCompletion = nil
var animation: Animation?
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let maybeAnimation = Animation.filepath(url.path) {
animation = maybeAnimation
} else if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
animation = try? Animation.from(data: unpackedData, strategy: .codable)
}
if let animation {
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
switch component.animation.mode {
case .still, .animateTransitionFromPrevious:
view.loopMode = .playOnce
case let .animating(loop):
if loop {
view.loopMode = .loop
} else {
view.loopMode = .playOnce
}
}
view.animationSpeed = component.animation.speed
view.backgroundColor = .clear
view.isOpaque = false
updateColors = true
self.animationView = view
self.addSubview(view)
animateSize = false
updatePlayback = true
}
}
}
if updateComponent {
self.component = component
}
if updateColors, let animationView = self.animationView {
if let value = component.colors["__allcolors__"] {
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Colors" }) {
animationView.setValueProvider(GradientValueProvider([value.lottieColorValue, value.lottieColorValue]), keypath: AnimationKeypath(keypath: keypath))
}
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Color" }) {
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
for (key, value) in component.colors {
if key == "__allcolors__" {
continue
}
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
}
var animationSize = CGSize()
if let animationView = self.animationView, let animation = animationView.animation {
animationSize = animation.size
}
if let customSize = component.size {
animationSize = customSize
}
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
if let animationView = self.animationView {
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
if animationView.frame != animationFrame {
if !transition.animation.isImmediate && animateSize && !animationView.frame.isEmpty && animationView.frame.size != animationFrame.size {
let previouosAnimationFrame = animationView.frame
if let snapshotView = animationView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = previouosAnimationFrame
animationView.superview?.insertSubview(snapshotView, belowSubview: animationView)
transition.setPosition(view: snapshotView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY))
snapshotView.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
let scaleFactor = previouosAnimationFrame.width / animationFrame.width
transition.animateScale(view: snapshotView, from: scaleFactor, to: 1.0)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
transition.setPosition(view: animationView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY))
transition.setBounds(view: animationView, bounds: CGRect(origin: CGPoint(), size: animationFrame.size))
transition.animateSublayerScale(view: animationView, from: previouosAnimationFrame.width / animationFrame.width, to: 1.0)
animationView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
} else if animationView.frame.size == animationFrame.size {
transition.setFrame(view: animationView, frame: animationFrame)
} else {
animationView.frame = animationFrame
}
}
if updatePlayback {
if case .animating = component.animation.mode {
if !animationView.isAnimationPlaying {
if let range = component.animation.range {
animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in
self?.currentCompletion?()
})
} else {
animationView.play { [weak self] _ in
self?.currentCompletion?()
}
}
}
} else {
if case let .still(position) = component.animation.mode {
switch position {
case .begin:
animationView.currentFrame = 0.0
case .end:
animationView.currentFrame = animationView.animation?.endFrame ?? 0.0
}
}
if animationView.isAnimationPlaying {
animationView.stop()
}
}
}
}
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 {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MetalImageView",
module_name = "MetalImageView",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,252 @@
import Foundation
import UIKit
import Metal
import Display
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
open class MetalImageLayer: CALayer {
fileprivate final class TextureStoragePool {
let width: Int
let height: Int
private var items: [TextureStorage.Content] = []
init(width: Int, height: Int) {
self.width = width
self.height = height
}
func recycle(content: TextureStorage.Content) {
if self.items.count < 4 {
self.items.append(content)
} else {
print("Warning: over-recycling texture storage")
}
}
func take() -> TextureStorage.Content? {
if self.items.isEmpty {
return nil
}
return self.items.removeLast()
}
}
fileprivate final class TextureStorage {
final class Content {
#if !targetEnvironment(simulator)
let buffer: MTLBuffer
#endif
let width: Int
let height: Int
let bytesPerRow: Int
let texture: MTLTexture
init?(device: MTLDevice, width: Int, height: Int) {
if #available(iOS 12.0, *) {
let bytesPerPixel = 4
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
#if targetEnvironment(simulator)
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
#else
guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else {
return nil
}
self.buffer = buffer
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = buffer.storageMode
guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else {
return nil
}
#endif
self.texture = texture
} else {
return nil
}
}
}
private weak var pool: TextureStoragePool?
let content: Content
private var isInvalidated: Bool = false
init(pool: TextureStoragePool, content: Content) {
self.pool = pool
self.content = content
}
deinit {
if !self.isInvalidated {
self.pool?.recycle(content: self.content)
}
}
func createCGImage() -> CGImage? {
if self.isInvalidated {
return nil
}
self.isInvalidated = true
#if targetEnvironment(simulator)
guard let data = NSMutableData(capacity: self.content.bytesPerRow * self.content.height) else {
return nil
}
data.length = self.content.bytesPerRow * self.content.height
self.content.texture.getBytes(data.mutableBytes, bytesPerRow: self.content.bytesPerRow, bytesPerImage: self.content.bytesPerRow * self.content.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.content.width, height: self.content.height, depth: 1)), mipmapLevel: 0, slice: 0)
guard let dataProvider = CGDataProvider(data: data as CFData) else {
return nil
}
#else
let content = self.content
let pool = self.pool
guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.content.buffer.contents(), count: self.content.buffer.length, deallocator: .custom { [weak pool] _, _ in
guard let pool = pool else {
return
}
pool.recycle(content: content)
}) as CFData) else {
return nil
}
#endif
guard let image = CGImage(
width: Int(self.content.width),
height: Int(self.content.height),
bitsPerComponent: 8,
bitsPerPixel: 8 * 4,
bytesPerRow: self.content.bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
return image
}
}
public final class Drawable {
private weak var renderer: Renderer?
fileprivate let textureStorage: TextureStorage
public var texture: MTLTexture {
return self.textureStorage.content.texture
}
fileprivate init(renderer: Renderer, textureStorage: TextureStorage) {
self.renderer = renderer
self.textureStorage = textureStorage
}
public func present(completion: @escaping () -> Void) {
self.renderer?.present(drawable: self)
completion()
}
}
public final class Renderer {
public var device: MTLDevice?
private var storagePool: TextureStoragePool?
public var imageUpdated: ((CGImage?) -> Void)?
public var drawableSize: CGSize = CGSize() {
didSet {
if self.drawableSize != oldValue {
if !self.drawableSize.width.isZero && !self.drawableSize.height.isZero {
self.storagePool = TextureStoragePool(width: Int(self.drawableSize.width), height: Int(self.drawableSize.height))
} else {
self.storagePool = nil
}
}
}
}
public func nextDrawable() -> Drawable? {
guard let device = self.device else {
return nil
}
guard let storagePool = self.storagePool else {
return nil
}
if let content = storagePool.take() {
return Drawable(renderer: self, textureStorage: TextureStorage(pool: storagePool, content: content))
} else {
guard let content = TextureStorage.Content(device: device, width: storagePool.width, height: storagePool.height) else {
return nil
}
return Drawable(renderer: self, textureStorage: TextureStorage(pool: storagePool, content: content))
}
}
fileprivate func present(drawable: Drawable) {
if let imageUpdated = self.imageUpdated {
imageUpdated(drawable.textureStorage.createCGImage())
}
}
}
public let renderer = Renderer()
override public init() {
super.init()
self.renderer.imageUpdated = { [weak self] image in
self?.contents = image
}
}
override public init(layer: Any) {
preconditionFailure()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func action(forKey event: String) -> CAAction? {
return nullAction
}
}
open class MetalImageView: UIView {
public static override var layerClass: AnyClass {
return MetalImageLayer.self
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultilineTextComponent",
module_name = "MultilineTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Markdown:Markdown",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,194 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Markdown
public final class MultilineTextComponent: Component {
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
}
public let text: TextContent
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public let truncationType: CTLineTruncationType
public let maximumNumberOfLines: Int
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public let tintColor: UIColor?
public let textShadowColor: UIColor?
public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
text: TextContent,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
tintColor: UIColor? = nil,
textShadowColor: UIColor? = nil,
textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.text = text
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.tintColor = tintColor
self.textShadowColor = textShadowColor
self.textShadowBlur = textShadowBlur
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if lhs.textShadowBlur != rhs.textShadowBlur {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
public final class View: ImmediateTextView {
public func update(component: MultilineTextComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let attributedString: NSAttributedString
switch component.text {
case let .plain(string):
attributedString = string
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
//let previousText = self.attributedText?.string
self.attributedText = attributedString
self.maximumNumberOfLines = component.maximumNumberOfLines
self.truncationType = component.truncationType
self.textAlignment = component.horizontalAlignment
self.verticalAlignment = component.verticalAlignment
self.lineSpacing = component.lineSpacing
self.cutout = component.cutout
self.insets = component.insets
self.textShadowColor = component.textShadowColor
self.textShadowBlur = component.textShadowBlur
self.textStroke = component.textStroke
self.linkHighlightColor = component.highlightColor
self.linkHighlightInset = component.highlightInset
self.highlightAttributeAction = component.highlightAction
self.tapAttributeAction = component.tapAction
self.longTapAttributeAction = component.longTapAction
/*if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string {
if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
snapshotView.center = self.center
self.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
}*/
let size = self.updateLayout(availableSize)
if let tintColor = component.tintColor {
transition.setTintColor(layer: self.layer, color: tintColor)
} else {
self.layer.layerTintColor = nil
}
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,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultilineTextWithEntitiesComponent",
module_name = "MultilineTextWithEntitiesComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Markdown:Markdown",
"//submodules/TextFormat:TextFormat",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,350 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Markdown
import TextNodeWithEntities
import AccountContext
import AnimationCache
import MultiAnimationRenderer
public final class MultilineTextWithEntitiesComponent: Component {
public final class External {
public fileprivate(set) var layout: TextNodeLayout?
public init() {
}
}
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
}
public let external: External?
public let context: AccountContext?
public let animationCache: AnimationCache?
public let animationRenderer: MultiAnimationRenderer?
public let placeholderColor: UIColor?
public let text: TextContent
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public let truncationType: CTLineTruncationType
public let maximumNumberOfLines: Int
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public let spoilerColor: UIColor
public let textShadowColor: UIColor?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let handleSpoilers: Bool
public let manualVisibilityControl: Bool
public let resetAnimationsOnVisibilityChange: Bool
public let enableLooping: Bool
public let displaysAsynchronously: Bool
public let maxWidth: CGFloat?
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
external: External? = nil,
context: AccountContext?,
animationCache: AnimationCache?,
animationRenderer: MultiAnimationRenderer?,
placeholderColor: UIColor?,
text: TextContent,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
spoilerColor: UIColor = .black,
textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
handleSpoilers: Bool = false,
manualVisibilityControl: Bool = false,
resetAnimationsOnVisibilityChange: Bool = false,
enableLooping: Bool = true,
displaysAsynchronously: Bool = true,
maxWidth: CGFloat? = nil,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.external = external
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.placeholderColor = placeholderColor
self.text = text
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.spoilerColor = spoilerColor
self.textShadowColor = textShadowColor
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.handleSpoilers = handleSpoilers
self.manualVisibilityControl = manualVisibilityControl
self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange
self.enableLooping = enableLooping
self.displaysAsynchronously = displaysAsynchronously
self.maxWidth = maxWidth
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: MultilineTextWithEntitiesComponent, rhs: MultilineTextWithEntitiesComponent) -> Bool {
if lhs.external !== rhs.external {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.handleSpoilers != rhs.handleSpoilers {
return false
}
if lhs.manualVisibilityControl != rhs.manualVisibilityControl {
return false
}
if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange {
return false
}
if lhs.enableLooping != rhs.enableLooping {
return false
}
if lhs.displaysAsynchronously != rhs.displaysAsynchronously {
return false
}
if lhs.maxWidth != rhs.maxWidth {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
public final class View: UIView {
var spoilerTextNode: ImmediateTextNodeWithEntities?
let textNode: ImmediateTextNodeWithEntities
public override init(frame: CGRect) {
self.textNode = ImmediateTextNodeWithEntities()
super.init(frame: frame)
self.addSubview(self.textNode.view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributes(at point: CGPoint) -> (Int, [NSAttributedString.Key: Any])? {
if let result = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.textNode.frame.minX, y: point.y - self.textNode.frame.minY)) {
return result
}
return nil
}
public var isSpoilerConcealed: Bool {
if let dustNode = self.textNode.dustNode, !dustNode.isRevealed {
return true
}
return false
}
public var hasRTL: Bool {
return self.textNode.cachedLayout?.hasRTL ?? false
}
public func updateVisibility(_ isVisible: Bool) {
self.textNode.visibility = isVisible
}
public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.textNode.displaysAsynchronously = component.displaysAsynchronously
let attributedString: NSAttributedString
switch component.text {
case let .plain(string):
attributedString = string
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
let previousText = self.textNode.attributedText?.string
self.textNode.attributedText = attributedString
self.textNode.maximumNumberOfLines = component.maximumNumberOfLines
self.textNode.truncationType = component.truncationType
self.textNode.textAlignment = component.horizontalAlignment
self.textNode.verticalAlignment = component.verticalAlignment
self.textNode.lineSpacing = component.lineSpacing
self.textNode.cutout = component.cutout
self.textNode.insets = component.insets
self.textNode.textShadowColor = component.textShadowColor
self.textNode.textStroke = component.textStroke
self.textNode.linkHighlightColor = component.highlightColor
self.textNode.linkHighlightInset = component.highlightInset
self.textNode.highlightAttributeAction = component.highlightAction
self.textNode.tapAttributeAction = component.tapAction
self.textNode.longTapAttributeAction = component.longTapAction
self.textNode.spoilerColor = component.spoilerColor
self.textNode.enableLooping = component.enableLooping
self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange
if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string {
if let snapshotView = self.snapshotContentTree() {
snapshotView.center = self.center
self.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
}
if !component.manualVisibilityControl {
self.textNode.visibility = true
}
if let context = component.context, let animationCache = component.animationCache, let animationRenderer = component.animationRenderer, let placeholderColor = component.placeholderColor {
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: animationCache,
renderer: animationRenderer,
placeholderColor: placeholderColor,
attemptSynchronous: false
)
}
var constrainedSize = availableSize
if let maxWidth = component.maxWidth {
constrainedSize.width = maxWidth
}
let layoutInfo = self.textNode.updateLayoutFullInfo(constrainedSize)
self.textNode.frame = CGRect(origin: .zero, size: layoutInfo.size)
if component.handleSpoilers {
let spoilerTextNode: ImmediateTextNodeWithEntities
if let current = self.spoilerTextNode {
spoilerTextNode = current
} else {
spoilerTextNode = ImmediateTextNodeWithEntities()
spoilerTextNode.alpha = 0.0
self.spoilerTextNode = spoilerTextNode
self.textNode.dustNode?.textNode = spoilerTextNode
}
spoilerTextNode.displaySpoilers = true
spoilerTextNode.displaySpoilerEffect = false
spoilerTextNode.attributedText = attributedString
spoilerTextNode.maximumNumberOfLines = component.maximumNumberOfLines
spoilerTextNode.truncationType = component.truncationType
spoilerTextNode.textAlignment = component.horizontalAlignment
spoilerTextNode.verticalAlignment = component.verticalAlignment
spoilerTextNode.lineSpacing = component.lineSpacing
spoilerTextNode.cutout = component.cutout
spoilerTextNode.insets = component.insets
spoilerTextNode.textShadowColor = component.textShadowColor
spoilerTextNode.textStroke = component.textStroke
spoilerTextNode.isUserInteractionEnabled = false
let size = spoilerTextNode.updateLayout(constrainedSize)
spoilerTextNode.frame = CGRect(origin: .zero, size: size)
if spoilerTextNode.view.superview == nil {
self.addSubview(spoilerTextNode.view)
}
} else if let spoilerTextNode = self.spoilerTextNode {
self.spoilerTextNode = nil
spoilerTextNode.view.removeFromSuperview()
self.textNode.dustNode?.textNode = nil
}
if let external = component.external {
external.layout = layoutInfo
}
return layoutInfo.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,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PagerComponent",
module_name = "PagerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
],
visibility = [
"//visibility:public",
],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ProgressIndicatorComponent",
module_name = "ProgressIndicatorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,113 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class ProgressIndicatorComponent: Component {
public let diameter: CGFloat
public let value: Double
public let backgroundColor: UIColor
public let foregroundColor: UIColor
public init(
diameter: CGFloat,
backgroundColor: UIColor,
foregroundColor: UIColor,
value: Double
) {
self.diameter = diameter
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.value = value
}
public static func ==(lhs: ProgressIndicatorComponent, rhs: ProgressIndicatorComponent) -> Bool {
if lhs.diameter != rhs.diameter {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
public final class View: UIView {
private var currentComponent: ProgressIndicatorComponent?
private let foregroundShapeLayer: SimpleShapeLayer
public init() {
self.foregroundShapeLayer = SimpleShapeLayer()
self.foregroundShapeLayer.isOpaque = false
self.foregroundShapeLayer.backgroundColor = nil
self.foregroundShapeLayer.fillColor = nil
self.foregroundShapeLayer.lineCap = .round
super.init(frame: CGRect())
let shapeLayer = self.layer as! CAShapeLayer
shapeLayer.isOpaque = false
shapeLayer.backgroundColor = nil
shapeLayer.fillColor = nil
self.layer.addSublayer(self.foregroundShapeLayer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public static var layerClass: AnyClass {
return CAShapeLayer.self
}
func update(component: ProgressIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let lineWidth: CGFloat = 1.33
let size = CGSize(width: component.diameter, height: component.diameter)
let shapeLayer = self.layer as! CAShapeLayer
if self.currentComponent?.backgroundColor != component.backgroundColor {
shapeLayer.strokeColor = component.backgroundColor.cgColor
shapeLayer.lineWidth = lineWidth
}
if self.currentComponent?.foregroundColor != component.foregroundColor {
self.foregroundShapeLayer.strokeColor = component.foregroundColor.cgColor
self.foregroundShapeLayer.lineWidth = lineWidth
}
if self.currentComponent?.diameter != component.diameter {
let path = UIBezierPath(arcCenter: CGPoint(x: component.diameter / 2.0, y: component.diameter / 2.0), radius: component.diameter / 2.0, startAngle: -CGFloat.pi / 2.0, endAngle: 2.0 * CGFloat.pi - CGFloat.pi / 2.0, clockwise: true).cgPath
shapeLayer.path = path
self.foregroundShapeLayer.path = path
self.foregroundShapeLayer.frame = CGRect(origin: CGPoint(), size: size)
}
if self.currentComponent != nil {
let previousValue: CGFloat = self.foregroundShapeLayer.presentation()?.strokeEnd ?? self.foregroundShapeLayer.strokeEnd
self.foregroundShapeLayer.animate(from: CGFloat(previousValue) as NSNumber, to: CGFloat(component.value) as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.12)
}
self.foregroundShapeLayer.strokeEnd = CGFloat(component.value)
self.currentComponent = component
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,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ReactionButtonListComponent",
module_name = "ReactionButtonListComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ReactionImageComponent",
module_name = "ReactionImageComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/WebPBinding:WebPBinding",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,194 @@
import Foundation
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import UIKit
import WebPBinding
import RLottieBinding
import GZip
import AnimationCache
import EmojiTextAttachmentView
public let sharedReactionStaticImage = Queue(name: "SharedReactionStaticImage", qos: .default)
public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize, queue: Queue) -> Signal<EngineMediaResource.ResourceData, NoError> {
return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-\(pixelSize.width)x\(pixelSize.height)-v10", fetch: EngineMediaResource.Fetch {
return Signal { subscriber in
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start()
var customColor: UIColor?
if animation.isCustomTemplateEmoji {
customColor = nil
}
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: AnimationCacheAnimationType(file: animation), keyframeOnly: true, customColor: customColor)
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
let queue: Queue
private let frameReceived: (UIImage) -> Void
init(queue: Queue, frameReceived: @escaping (UIImage) -> Void) {
self.queue = queue
self.frameReceived = frameReceived
}
var isCancelled: Bool {
return false
}
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) {
if let renderContext = DrawingContext(size: CGSize(width: proposedWidth, height: proposedHeight), scale: 1.0, clear: true) {
let _ = drawingBlock(AnimationCacheItemDrawingSurface(
argb: renderContext.bytes.assumingMemoryBound(to: UInt8.self),
width: Int(renderContext.scaledSize.width),
height: Int(renderContext.scaledSize.height),
bytesPerRow: renderContext.bytesPerRow,
length: renderContext.length
))
if let image = renderContext.generateImage() {
self.frameReceived(image)
}
}
}
func finish() {
}
}
let innerWriter = AnimationCacheItemWriterImpl(queue: queue, frameReceived: { image in
guard let pngData = image.pngData() else {
return
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "image.png")
guard let _ = try? pngData.write(to: URL(fileURLWithPath: tempFile.path)) else {
return
}
subscriber.putNext(.moveTempFile(file: tempFile))
subscriber.putCompletion()
})
let dataDisposable = fetchFrame(AnimationCacheFetchOptions(
size: pixelSize,
writer: innerWriter,
firstFrameOnly: true
))
/*let dataDisposable = context.account.postbox.mediaBox.resourceData(animation.resource).start(next: { data in
if !data.complete {
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
return
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return
}
guard let instance = LottieInstance(data: unpackedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
return
}
instance.renderFrame(with: Int32(instance.frameCount - 1), into: renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(renderContext.size.width * renderContext.scale), height: Int32(renderContext.size.height * renderContext.scale), bytesPerRow: Int32(renderContext.bytesPerRow))
})*/
return ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
}
}
})
}
public final class ReactionImageNode: ASDisplayNode {
private var disposable: Disposable?
private let size: CGSize
private let isAnimation: Bool
private let iconNode: ASImageNode
public init(context: AccountContext, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction, displayPixelSize: CGSize) {
self.iconNode = ASImageNode()
var file: TelegramMediaFile?
var animationFile: TelegramMediaFile?
if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction {
file = availableReaction.staticIcon._parse()
animationFile = availableReaction.centerAnimation?._parse()
break
}
}
}
if let animationFile = animationFile {
self.size = animationFile.dimensions?.cgSize ?? displayPixelSize
var displaySize = self.size.aspectFitted(displayPixelSize)
displaySize.width = floor(displaySize.width * 2.0)
displaySize.height = floor(displaySize.height * 2.0)
self.isAnimation = true
super.init()
self.disposable = (reactionStaticImage(context: context, animation: animationFile, pixelSize: CGSize(width: displaySize.width * UIScreenScale, height: displaySize.height * UIScreenScale), queue: sharedReactionStaticImage)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = UIImage(data: dataValue) {
strongSelf.iconNode.image = image
}
}
}).strict()
} else if let file = file {
self.size = file.dimensions?.cgSize ?? displayPixelSize
self.isAnimation = false
super.init()
self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.iconNode.image = image
}
}
}).strict()
} else {
self.size = displayPixelSize
self.isAnimation = false
super.init()
}
self.addSubnode(self.iconNode)
}
deinit {
self.disposable?.dispose()
}
public func update(size: CGSize) {
var imageSize = self.size.aspectFitted(size)
if self.isAnimation {
imageSize.width *= 2.0
imageSize.height *= 2.0
}
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
}
}
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ReactionListContextMenuContent",
module_name = "ReactionListContextMenuContent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/ContextUI:ContextUI",
"//submodules/AvatarNode:AvatarNode",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ResizableSheetComponent",
module_name = "ResizableSheetComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,828 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ViewControllerComponent
import SwiftSignalKit
import DynamicCornerRadiusView
import TelegramPresentationData
import EdgeEffect
public final class ResizableSheetComponentEnvironment: Equatable {
public struct BoundsUpdate {
public let bounds: CGRect
public let isInteractive: Bool
}
public let theme: PresentationTheme
public let statusBarHeight: CGFloat
public let safeInsets: UIEdgeInsets
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let isDisplaying: Bool
public let isCentered: Bool
public let screenSize: CGSize
public let regularMetricsSize: CGSize?
public let dismiss: (Bool) -> Void
public let boundsUpdated: ActionSlot<BoundsUpdate>
public init(
theme: PresentationTheme,
statusBarHeight: CGFloat,
safeInsets: UIEdgeInsets,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
isDisplaying: Bool,
isCentered: Bool,
screenSize: CGSize,
regularMetricsSize: CGSize?,
dismiss: @escaping (Bool) -> Void,
boundsUpdated: ActionSlot<BoundsUpdate> = ActionSlot<BoundsUpdate>()
) {
self.theme = theme
self.statusBarHeight = statusBarHeight
self.safeInsets = safeInsets
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.isDisplaying = isDisplaying
self.isCentered = isCentered
self.screenSize = screenSize
self.regularMetricsSize = regularMetricsSize
self.dismiss = dismiss
self.boundsUpdated = boundsUpdated
}
public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.isDisplaying != rhs.isDisplaying {
return false
}
if lhs.isCentered != rhs.isCentered {
return false
}
if lhs.screenSize != rhs.screenSize {
return false
}
if lhs.regularMetricsSize != rhs.regularMetricsSize {
return false
}
return true
}
}
public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component {
public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment)
public class ExternalState {
public fileprivate(set) var contentHeight: CGFloat
public init() {
self.contentHeight = 0.0
}
}
public enum BackgroundColor: Equatable {
case color(UIColor)
}
public let content: AnyComponent<ChildEnvironmentType>
public let titleItem: AnyComponent<Empty>?
public let leftItem: AnyComponent<Empty>?
public let rightItem: AnyComponent<Empty>?
public let hasTopEdgeEffect: Bool
public let bottomItem: AnyComponent<Empty>?
public let backgroundColor: BackgroundColor
public let isFullscreen: Bool
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public init(
content: AnyComponent<ChildEnvironmentType>,
titleItem: AnyComponent<Empty>? = nil,
leftItem: AnyComponent<Empty>? = nil,
rightItem: AnyComponent<Empty>? = nil,
hasTopEdgeEffect: Bool = true,
bottomItem: AnyComponent<Empty>? = nil,
backgroundColor: BackgroundColor,
isFullscreen: Bool = false,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
) {
self.content = content
self.titleItem = titleItem
self.leftItem = leftItem
self.rightItem = rightItem
self.hasTopEdgeEffect = hasTopEdgeEffect
self.bottomItem = bottomItem
self.backgroundColor = backgroundColor
self.isFullscreen = isFullscreen
self.externalState = externalState
self.animateOut = animateOut
}
public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.titleItem != rhs.titleItem {
return false
}
if lhs.leftItem != rhs.leftItem {
return false
}
if lhs.rightItem != rhs.rightItem {
return false
}
if lhs.hasTopEdgeEffect != rhs.hasTopEdgeEffect {
return false
}
if lhs.bottomItem != rhs.bottomItem {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.isFullscreen != rhs.isFullscreen {
return false
}
if lhs.animateOut != rhs.animateOut {
return false
}
return true
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var containerCornerRadius: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
var fillingSize: CGFloat
let isTablet: Bool
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat, fillingSize: CGFloat, isTablet: Bool) {
self.containerSize = containerSize
self.containerInset = containerInset
self.containerCornerRadius = containerCornerRadius
self.bottomInset = bottomInset
self.topInset = topInset
self.fillingSize = fillingSize
self.isTablet = isTablet
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView, UIGestureRecognizerDelegate {
public final class Tag {
public init() {
}
}
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private let dimView: UIView
private let containerView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let bottomContainer: SparseContainerView
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let topEdgeEffectView: EdgeEffectView
private let bottomEdgeEffectView: EdgeEffectView
private let contentView: ComponentView<ChildEnvironmentType>
private var titleItemView: ComponentView<Empty>?
private var leftItemView: ComponentView<Empty>?
private var rightItemView: ComponentView<Empty>?
private var bottomItemView: ComponentView<Empty>?
private let backgroundHandleView: UIImageView
private var ignoreScrolling: Bool = false
private var isDismissingInteractively: Bool = false
private var dismissTranslation: CGFloat = 0.0
private var dismissStartTranslation: CGFloat?
private var dismissPanGesture: UIPanGestureRecognizer?
private var component: ResizableSheetComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ResizableSheetComponentEnvironment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.dimView = UIView()
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.layer.cornerRadius = 40.0
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 40.0
self.backgroundHandleView = UIImageView()
self.navigationBarContainer = SparseContainerView()
self.bottomContainer = SparseContainerView()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
self.bottomEdgeEffectView = EdgeEffectView()
self.bottomEdgeEffectView.clipsToBounds = true
self.bottomEdgeEffectView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.bottomEdgeEffectView.layer.cornerRadius = 40.0
self.contentView = ComponentView()
super.init(frame: frame)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.containerView.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.containerView.addSubview(self.navigationBarContainer)
self.containerView.addSubview(self.bottomContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
let dismissPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPanGesture(_:)))
dismissPanGesture.maximumNumberOfTouches = 1
dismissPanGesture.delegate = self
self.addGestureRecognizer(dismissPanGesture)
self.dismissPanGesture = dismissPanGesture
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
if let result = self.bottomContainer.hitTest(self.convert(point, to: self.bottomContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.dismissPanGesture {
let pan = gestureRecognizer as! UIPanGestureRecognizer
let velocity = pan.velocity(in: self)
if abs(velocity.y) <= abs(velocity.x) {
return false
}
}
return true
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.dismissPanGesture {
if otherGestureRecognizer === self.scrollView.panGestureRecognizer {
return true
}
}
return false
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismissAnimated()
}
}
public func dismissAnimated() {
guard let environment = self.environment else {
return
}
self.endEditing(true)
environment.dismiss(true)
}
private func updateDismissTranslation(_ translation: CGFloat) {
self.dismissTranslation = translation
self.updateScrolling(transition: .immediate)
let maxAlphaDistance = max(1.0, self.bounds.height * 0.9)
let alpha = 1.0 - min(1.0, translation / maxAlphaDistance)
self.dimView.alpha = alpha
}
private func resetDismissTranslation(animated: Bool) {
self.dismissTranslation = 0.0
if animated {
let transition: ComponentTransition = .easeInOut(duration: 0.2)
transition.setAlpha(view: self.dimView, alpha: 1.0)
self.updateScrolling(transition: transition)
} else {
self.dimView.alpha = 1.0
self.updateScrolling(transition: .immediate)
}
}
@objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
let translation = recognizer.translation(in: self)
switch recognizer.state {
case .began:
self.dismissStartTranslation = nil
case .changed:
let shouldStartDismiss = self.scrollView.contentOffset.y <= 0.0 && translation.y > 0.0
if shouldStartDismiss {
if !self.isDismissingInteractively {
self.isDismissingInteractively = true
self.dismissStartTranslation = translation.y
self.scrollView.isScrollEnabled = false
}
let start = self.dismissStartTranslation ?? translation.y
let dismissOffset = max(0.0, translation.y - start)
self.scrollView.contentOffset = .zero
self.updateDismissTranslation(dismissOffset)
} else if self.isDismissingInteractively {
let start = self.dismissStartTranslation ?? translation.y
let dismissOffset = max(0.0, translation.y - start)
self.updateDismissTranslation(dismissOffset)
}
case .ended, .cancelled:
if self.isDismissingInteractively {
let velocityY = recognizer.velocity(in: self).y
let currentOffset = self.dismissTranslation
let threshold = min(180.0, self.bounds.height * 0.25)
let shouldDismiss = currentOffset > threshold || velocityY > 1000.0
self.isDismissingInteractively = false
self.scrollView.isScrollEnabled = !component.isFullscreen
if shouldDismiss {
let animateOffset = self.bounds.height - self.backgroundLayer.frame.minY
let initialVelocity = animateOffset > 0.0 ? max(0.0, velocityY) / animateOffset : 0.0
self.animateOut(initialVelocity: initialVelocity, completion: { [weak self] in
self?.environment?.dismiss(false)
})
} else {
self.resetDismissTranslation(animated: true)
}
}
default:
break
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout, let component = self.component else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
var topOffsetFraction = self.scrollView.bounds.minY / 100.0
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
if component.isFullscreen {
topOffsetFraction = 1.0
}
let minScale: CGFloat = itemLayout.isTablet ? 1.0 : (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
let minScaledTranslation: CGFloat = itemLayout.isTablet ? 0.0 : (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
var containerTransform = CATransform3DIdentity
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
containerTransform = CATransform3DTranslate(containerTransform, 0.0, self.dismissTranslation, 0.0)
transition.setTransform(view: self.containerView, transform: containerTransform)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
if component.isFullscreen {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size))
self.scrollView.isScrollEnabled = false
} else {
self.scrollView.isScrollEnabled = !self.isDismissingInteractively
}
var bounds = self.scrollView.bounds
bounds.size.width = itemLayout.fillingSize
self.environment?.boundsUpdated.invoke(ResizableSheetComponentEnvironment.BoundsUpdate(bounds: bounds, isInteractive: self.scrollView.isTracking))
}
private var didPlayAppearanceAnimation = false
func animateIn() {
self.didPlayAppearanceAnimation = true
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: self.dimView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let initialVelocity = initialVelocity {
let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
transition.updatePosition(layer: self.scrollContentClippingView.layer, position: CGPoint(x: self.scrollContentClippingView.layer.position.x, y: self.scrollContentClippingView.layer.position.y + animateOffset), completion: { _ in
completion()
})
transition.updatePosition(layer: self.backgroundLayer, position: CGPoint(x: self.backgroundLayer.position.x, y: self.backgroundLayer.position.y + animateOffset))
transition.updatePosition(layer: self.navigationBarContainer.layer, position: CGPoint(x: self.navigationBarContainer.layer.position.x, y: self.navigationBarContainer.layer.position.y + animateOffset))
transition.updatePosition(layer: self.bottomContainer.layer, position: CGPoint(x: self.bottomContainer.layer.position.x, y: self.bottomContainer.layer.position.y + animateOffset))
} else {
let duration: Double = 0.25
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
}
func update(component: ResizableSheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value
component.animateOut.connect { [weak self] completion in
guard let self else {
return
}
self.animateOut {
completion(Void())
}
}
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = sheetEnvironment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, sheetEnvironment.deviceMetrics.screenSize.width) - sheetEnvironment.safeInsets.left * 2.0
}
let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5)
self.component = component
self.state = state
self.environment = sheetEnvironment
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
let backgroundColor: UIColor
switch component.backgroundColor {
case let .color(color):
backgroundColor = color
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var containerSize: CGSize
if !"".isEmpty, sheetEnvironment.isCentered {
let verticalInset: CGFloat = 44.0
let maxSide = max(availableSize.width, availableSize.height)
let minSide = min(availableSize.width, availableSize.height)
containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0)
if let regularMetricsSize = sheetEnvironment.regularMetricsSize {
containerSize = regularMetricsSize
}
} else {
containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude)
}
var containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0
if component.isFullscreen {
containerInset = 0.0
}
let clippingY: CGFloat
self.contentView.parentState = state
let contentViewSize = self.contentView.update(
transition: transition,
component: component.content,
environment: {
environment[ChildEnvironmentType.self]
},
containerSize: containerSize
)
component.externalState?.contentHeight = contentViewSize.height
if let contentView = self.contentView.view {
if contentView.superview == nil {
self.scrollContentView.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize))
}
let contentHeight = contentViewSize.height
let initialContentHeight = contentHeight
let edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
self.topEdgeEffectView.update(content: backgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
if self.topEdgeEffectView.superview == nil {
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
}
self.topEdgeEffectView.isHidden = !component.hasTopEdgeEffect
if let titleItem = component.titleItem {
let titleItemView: ComponentView<Empty>
if let current = self.titleItemView {
titleItemView = current
} else {
titleItemView = ComponentView<Empty>()
self.titleItemView = titleItemView
}
let titleItemSize = titleItemView.update(
transition: transition,
component: titleItem,
environment: {},
containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0)
)
let titleItemFrame = CGRect(origin: CGPoint(x: rawSideInset + floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(38.0 - titleItemSize.height * 0.5)), size: titleItemSize)
if let view = titleItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
}
transition.setFrame(view: view, frame: titleItemFrame)
}
} else if let titleItemView = self.titleItemView {
self.titleItemView = nil
titleItemView.view?.removeFromSuperview()
}
if let leftItem = component.leftItem {
var leftItemTransition = transition
let leftItemView: ComponentView<Empty>
if let current = self.leftItemView {
leftItemView = current
} else {
leftItemTransition = .immediate
leftItemView = ComponentView<Empty>()
self.leftItemView = leftItemView
}
let leftItemSize = leftItemView.update(
transition: leftItemTransition,
component: leftItem,
environment: {},
containerSize: CGSize(width: 66.0, height: 66.0)
)
let leftItemFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: leftItemSize)
if let view = leftItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
leftItemTransition.setFrame(view: view, frame: leftItemFrame)
}
} else if let leftItemView = self.leftItemView {
self.leftItemView = nil
if !transition.animation.isImmediate {
leftItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
leftItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
leftItemView.view?.removeFromSuperview()
})
} else {
leftItemView.view?.removeFromSuperview()
}
}
if let rightItem = component.rightItem {
var rightItemTransition = transition
let rightItemView: ComponentView<Empty>
if let current = self.rightItemView {
rightItemView = current
} else {
rightItemTransition = .immediate
rightItemView = ComponentView<Empty>()
self.rightItemView = rightItemView
}
let rightItemSize = rightItemView.update(
transition: rightItemTransition,
component: rightItem,
environment: {},
containerSize: CGSize(width: 66.0, height: 66.0)
)
let rightItemFrame = CGRect(origin: CGPoint(x: availableSize.width - rawSideInset - 16.0 - rightItemSize.width, y: 16.0), size: rightItemSize)
if let view = rightItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
rightItemTransition.setFrame(view: view, frame: rightItemFrame)
}
} else if let rightItemView = self.rightItemView {
self.rightItemView = nil
if !transition.animation.isImmediate {
rightItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
rightItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
rightItemView.view?.removeFromSuperview()
})
} else {
rightItemView.view?.removeFromSuperview()
}
}
if let bottomItem = component.bottomItem {
var bottomItemTransition = transition
let bottomItemView: ComponentView<Empty>
if let current = self.bottomItemView {
bottomItemView = current
} else {
bottomItemTransition = .immediate
bottomItemView = ComponentView<Empty>()
self.bottomItemView = bottomItemView
}
let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let bottomItemSize = bottomItemView.update(
transition: bottomItemTransition,
component: bottomItem,
environment: {},
containerSize: CGSize(width: containerSize.width - bottomInsets.left - bottomInsets.right, height: 52.0)
)
let bottomItemFrame = CGRect(origin: CGPoint(x: rawSideInset + floorToScreenPixels((containerSize.width - bottomItemSize.width)) / 2.0, y: availableSize.height - bottomItemSize.height - bottomInsets.bottom), size: bottomItemSize)
if let view = bottomItemView.view {
if view.superview == nil {
self.bottomContainer.addSubview(view)
if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
bottomItemTransition.setFrame(view: view, frame: bottomItemFrame)
}
} else if let bottomItemView = self.bottomItemView {
self.bottomItemView = nil
if !transition.animation.isImmediate {
bottomItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
bottomItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
bottomItemView.view?.removeFromSuperview()
})
} else {
bottomItemView.view?.removeFromSuperview()
}
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - edgeEffectHeight), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
self.bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition)
if self.bottomEdgeEffectView.superview == nil {
self.bottomContainer.insertSubview(self.bottomEdgeEffectView, at: 0)
}
transition.setAlpha(view: self.bottomContainer, alpha: component.bottomItem != nil ? 1.0 : 0.0)
clippingY = availableSize.height
var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
if component.isFullscreen {
topInset = 0.0
}
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 38.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: sheetEnvironment.deviceMetrics.screenCornerRadius, bottomInset: sheetEnvironment.safeInsets.bottom, topInset: topInset, fillingSize: fillingSize, isTablet: sheetEnvironment.metrics.isTablet)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation {
self.animateIn()
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SheetComponent",
module_name = "SheetComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,615 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ViewControllerComponent
import SwiftSignalKit
import DynamicCornerRadiusView
public final class SheetComponentEnvironment: Equatable {
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let isDisplaying: Bool
public let isCentered: Bool
public let hasInputHeight: Bool
public let regularMetricsSize: CGSize?
public let dismiss: (Bool) -> Void
public init(metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isDisplaying: Bool, isCentered: Bool, hasInputHeight: Bool, regularMetricsSize: CGSize?, dismiss: @escaping (Bool) -> Void) {
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.isDisplaying = isDisplaying
self.isCentered = isCentered
self.hasInputHeight = hasInputHeight
self.regularMetricsSize = regularMetricsSize
self.dismiss = dismiss
}
public static func ==(lhs: SheetComponentEnvironment, rhs: SheetComponentEnvironment) -> Bool {
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.isDisplaying != rhs.isDisplaying {
return false
}
if lhs.isCentered != rhs.isCentered {
return false
}
if lhs.hasInputHeight != rhs.hasInputHeight {
return false
}
if lhs.regularMetricsSize != rhs.regularMetricsSize {
return false
}
return true
}
}
public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component {
public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment)
public class ExternalState {
public fileprivate(set) var contentHeight: CGFloat
public init() {
self.contentHeight = 0.0
}
}
public enum BackgroundColor: Equatable {
public enum BlurStyle: Equatable {
case light
case dark
}
case color(UIColor)
case blur(BlurStyle)
}
public enum Style: Equatable {
case glass
case legacy
}
public let content: AnyComponent<ChildEnvironmentType>
public let headerContent: AnyComponent<Empty>?
public let style: Style
public let backgroundColor: BackgroundColor
public let followContentSizeChanges: Bool
public let clipsContent: Bool
public let isScrollEnabled: Bool
public let hasDimView: Bool
public let autoAnimateOut: Bool
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void
public let willDismiss: () -> Void
public init(
content: AnyComponent<ChildEnvironmentType>,
headerContent: AnyComponent<Empty>? = nil,
style: Style = .legacy,
backgroundColor: BackgroundColor,
followContentSizeChanges: Bool = false,
clipsContent: Bool = false,
isScrollEnabled: Bool = true,
hasDimView: Bool = true,
autoAnimateOut: Bool = true,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {},
willDismiss: @escaping () -> Void = {}
) {
self.content = content
self.headerContent = headerContent
self.style = style
self.backgroundColor = backgroundColor
self.followContentSizeChanges = followContentSizeChanges
self.clipsContent = clipsContent
self.isScrollEnabled = isScrollEnabled
self.hasDimView = hasDimView
self.autoAnimateOut = autoAnimateOut
self.externalState = externalState
self.animateOut = animateOut
self.onPan = onPan
self.willDismiss = willDismiss
}
public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.headerContent != rhs.headerContent {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.followContentSizeChanges != rhs.followContentSizeChanges {
return false
}
if lhs.isScrollEnabled != rhs.isScrollEnabled {
return false
}
if lhs.hasDimView != rhs.hasDimView {
return false
}
if lhs.autoAnimateOut != rhs.autoAnimateOut {
return false
}
if lhs.animateOut != rhs.animateOut {
return false
}
return true
}
private class ScrollView: UIScrollView {
var ignoreScroll = false
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
guard !self.ignoreScroll else {
return
}
if animated && abs(contentOffset.y - self.contentOffset.y) > 200.0 {
return
}
super.setContentOffset(contentOffset, animated: animated)
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView {
public final class Tag {
public init() {
}
}
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var component: SheetComponent<ChildEnvironmentType>?
private let dimView: UIView
private let scrollView: ScrollView
private let backgroundView: SheetBackgroundView
private var effectView: UIVisualEffectView?
private let clipView: SheetBackgroundView
private let contentView: ComponentView<ChildEnvironmentType>
private var headerView: ComponentView<Empty>?
private var isAnimatingOut: Bool = false
private var previousIsDisplaying: Bool = false
private var dismiss: ((Bool) -> Void)?
private var keyboardWillShowObserver: AnyObject?
override init(frame: CGRect) {
self.dimView = UIView()
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.scrollView = ScrollView()
self.scrollView.delaysContentTouches = false
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceVertical = true
self.backgroundView = SheetBackgroundView()
self.clipView = SheetBackgroundView()
self.contentView = ComponentView<ChildEnvironmentType>()
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.dimView)
self.scrollView.addSubview(self.backgroundView)
self.addSubview(self.scrollView)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:))))
self.keyboardWillShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil, using: { [weak self] _ in
if let strongSelf = self {
strongSelf.scrollView.ignoreScroll = true
Queue.mainQueue().after(0.1, {
strongSelf.scrollView.ignoreScroll = false
})
}
})
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let keyboardFrameChangeObserver = self.keyboardWillShowObserver {
NotificationCenter.default.removeObserver(keyboardFrameChangeObserver)
}
}
@objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismiss?(true)
}
}
public func dismissAnimated() {
self.dismiss?(true)
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.component?.onPan()
}
private var scrollingOut = false
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0
let dismissalOffset = scrollView.contentSize.height - scrollView.contentInset.top + scrollView.contentSize.height
let delta = dismissalOffset - contentOffset
let initialVelocity = !delta.isZero ? velocity.y / delta : 0.0
let currentContentOffset = scrollView.contentOffset
targetContentOffset.pointee = currentContentOffset
if velocity.y > 300.0 {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false)
})
} else {
if contentOffset < scrollView.contentSize.height * 0.1 {
if contentOffset < 0.0 {
} else {
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true)
}
} else {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false)
})
}
}
}
private var ignoreScrolling: Bool = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !self.ignoreScrolling else {
return
}
let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0
if contentOffset >= scrollView.contentSize.height {
self.dismiss?(false)
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let headerView = self.headerView?.view, headerView.bounds.contains(self.convert(point, to: headerView)) {
return super.hitTest(point, with: event)
}
if !self.backgroundView.bounds.contains(self.convert(point, to: self.backgroundView)) {
return self.dimView
}
return super.hitTest(point, with: event)
}
private func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let targetPosition = self.scrollView.center
var offset: CGFloat = self.scrollView.contentSize.height
if self.isCentered {
offset = self.frame.height + self.scrollView.frame.height * 0.5
}
self.scrollView.center = targetPosition.offsetBy(dx: 0.0, dy: offset)
transition.animateView(allowUserInteraction: true, {
self.scrollView.center = targetPosition
})
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 0.1, to: 0.0, duration: 0.15)
}
}
private func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
if self.isAnimatingOut {
completion()
return
}
self.isAnimatingOut = true
self.isUserInteractionEnabled = false
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
guard let contentView = self.contentView.view else {
return
}
if let initialVelocity = initialVelocity {
let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
let contentOffset = (self.scrollView.contentOffset.y + self.scrollView.contentInset.top - self.scrollView.contentSize.height) * -1.0
let dismissalOffset = self.scrollView.contentSize.height + abs(contentView.frame.minY)
let delta = dismissalOffset - contentOffset
var targetPosition = self.scrollView.center.y + delta + 6.0
if self.isCentered {
targetPosition = self.frame.height + self.scrollView.frame.height * 0.5
}
transition.updatePosition(layer: self.scrollView.layer, position: CGPoint(x: self.scrollView.center.x, y: targetPosition), completion: { _ in
completion()
})
} else {
var targetOffset: CGFloat = self.scrollView.contentSize.height + abs(contentView.frame.minY) + 6.0
if self.currentHasInputHeight {
targetOffset += 330.0
}
if self.isCentered {
targetOffset = self.frame.height + self.scrollView.frame.height * 0.5
}
self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: targetOffset), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
}
}
private var currentHasInputHeight = false
private var isCentered = false
private var currentAvailableSize: CGSize?
func update(component: SheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let previousHasInputHeight = self.currentHasInputHeight
let sheetEnvironment = environment[SheetComponentEnvironment.self].value
component.animateOut.connect { [weak self] completion in
guard let strongSelf = self else {
return
}
strongSelf.animateOut {
completion(Void())
}
}
self.component = component
self.isCentered = sheetEnvironment.isCentered
self.currentHasInputHeight = sheetEnvironment.hasInputHeight
if self.isAnimatingOut {
return availableSize
}
var glassInset: CGFloat = 0.0
var topCornerRadius: CGFloat
var bottomCornerRadius: CGFloat
switch component.style {
case .glass:
topCornerRadius = 38.0
if availableSize.width < availableSize.height {
glassInset = 6.0
}
bottomCornerRadius = sheetEnvironment.deviceMetrics.screenCornerRadius - glassInset
case .legacy:
topCornerRadius = 12.0
bottomCornerRadius = 12.0
}
var backgroundColor: UIColor = .clear
switch component.backgroundColor {
case let .blur(style):
self.backgroundView.isHidden = true
if self.effectView == nil {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light))
effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
effectView.layer.masksToBounds = true
self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView)
self.effectView = effectView
}
case let .color(color):
backgroundColor = color
self.backgroundView.isHidden = false
self.effectView?.removeFromSuperview()
self.effectView = nil
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
var containerSize: CGSize
if sheetEnvironment.isCentered {
let verticalInset: CGFloat = 44.0
let maxSide = max(availableSize.width, availableSize.height)
let minSide = min(availableSize.width, availableSize.height)
containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0)
if let regularMetricsSize = sheetEnvironment.regularMetricsSize {
containerSize = regularMetricsSize
}
} else {
containerSize = CGSize(width: availableSize.width - glassInset * 2.0, height: .greatestFiniteMagnitude)
}
self.contentView.parentState = state
let contentSize = self.contentView.update(
transition: transition,
component: component.content,
environment: {
environment[ChildEnvironmentType.self]
},
containerSize: containerSize
)
component.externalState?.contentHeight = contentSize.height
self.ignoreScrolling = true
if let contentView = self.contentView.view {
if contentView.superview == nil {
self.scrollView.addSubview(self.clipView)
self.clipView.bottomCornersView.addSubview(contentView)
}
contentView.clipsToBounds = component.clipsContent
contentView.layer.cornerRadius = topCornerRadius
if sheetEnvironment.isCentered {
let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0)
let clipFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize)
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame, completion: nil)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
if let effectView = self.effectView {
transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
}
self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition)
} else {
switch component.style {
case .glass:
let clipFrame = CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height))
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius - 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
case .legacy:
let clipFrame = CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0))
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
if let effectView = self.effectView {
transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
}
}
self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition)
}
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
if let headerContent = component.headerContent {
let headerView: ComponentView<Empty>
if let current = self.headerView {
headerView = current
} else {
headerView = ComponentView()
self.headerView = headerView
}
let headerSize = headerView.update(
transition: transition,
component: headerContent,
environment: {},
containerSize: CGSize(width: contentSize.width, height: 44.0)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - headerSize.width) / 2.0), y: self.backgroundView.frame.minY - headerSize.height - 10.0), size: headerSize)
if let headerView = headerView.view {
if headerView.superview == nil {
self.scrollView.addSubview(headerView)
}
transition.setFrame(view: headerView, frame: headerFrame)
}
} else if let headerView = self.headerView {
self.headerView = nil
headerView.view?.removeFromSuperview()
}
let previousContentSize = self.scrollView.contentSize
let updateContentSize = {
self.scrollView.contentSize = contentSize
self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height) + contentSize.height, left: 0.0, bottom: 0.0, right: 0.0)
}
if previousContentSize.height.isZero {
updateContentSize()
}
self.scrollView.isScrollEnabled = component.isScrollEnabled
self.ignoreScrolling = false
if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height {
self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height))
} else if component.followContentSizeChanges, !previousContentSize.height.isZero, previousContentSize != contentSize {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: availableSize), completion: { _ in
updateContentSize()
})
}
if self.currentHasInputHeight != previousHasInputHeight {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: self.scrollView.bounds.size))
}
self.currentAvailableSize = availableSize
if !component.hasDimView {
self.dimView.backgroundColor = .clear
}
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn()
} else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {
if component.autoAnimateOut {
self.animateOut(completion: {})
}
}
if !sheetEnvironment.isDisplaying && !component.autoAnimateOut {
} else {
self.previousIsDisplaying = sheetEnvironment.isDisplaying
}
self.dismiss = sheetEnvironment.dismiss
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class SheetBackgroundView: UIView {
let topCornersView = UIView()
let bottomCornersView = UIView()
public override init(frame: CGRect) {
super.init(frame: frame)
self.topCornersView.clipsToBounds = true
self.topCornersView.layer.cornerCurve = .continuous
self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.bottomCornersView.clipsToBounds = true
self.bottomCornersView.layer.cornerCurve = .continuous
self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.addSubview(self.topCornersView)
self.topCornersView.addSubview(self.bottomCornersView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
public func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius)
transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius)
transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size))
transition.setBackgroundColor(view: self.bottomCornersView, color: color)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SolidRoundedButtonComponent",
module_name = "SolidRoundedButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,201 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import SolidRoundedButtonNode
import AppBundle
public final class SolidRoundedButtonComponent: Component {
public typealias Theme = SolidRoundedButtonTheme
public let title: String?
public let subtitle: String?
public let label: String?
public let badge: String?
public let icon: UIImage?
public let theme: SolidRoundedButtonTheme
public let font: SolidRoundedButtonFont
public let fontSize: CGFloat
public let height: CGFloat
public let cornerRadius: CGFloat
public let gloss: Bool
public let glass: Bool
public let isEnabled: Bool
public let iconName: String?
public let animationName: String?
public let iconPosition: SolidRoundedButtonIconPosition
public let iconSpacing: CGFloat
public let isLoading: Bool
public let action: () -> Void
public init(
title: String? = nil,
subtitle: String? = nil,
label: String? = nil,
badge: String? = nil,
icon: UIImage? = nil,
theme: SolidRoundedButtonTheme,
font: SolidRoundedButtonFont = .bold,
fontSize: CGFloat = 17.0,
height: CGFloat = 48.0,
cornerRadius: CGFloat = 24.0,
gloss: Bool = false,
glass: Bool = false,
isEnabled: Bool = true,
iconName: String? = nil,
animationName: String? = nil,
iconPosition: SolidRoundedButtonIconPosition = .left,
iconSpacing: CGFloat = 8.0,
isLoading: Bool = false,
action: @escaping () -> Void
) {
self.title = title
self.subtitle = subtitle
self.label = label
self.badge = badge
self.icon = icon
self.theme = theme
self.font = font
self.fontSize = fontSize
self.height = height
self.cornerRadius = cornerRadius
self.gloss = gloss
self.glass = glass
self.isEnabled = isEnabled
self.iconName = iconName
self.animationName = animationName
self.iconPosition = iconPosition
self.iconSpacing = iconSpacing
self.isLoading = isLoading
self.action = action
}
public static func ==(lhs: SolidRoundedButtonComponent, rhs: SolidRoundedButtonComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.badge != rhs.badge {
return false
}
if lhs.icon !== rhs.icon {
return false
}
if lhs.theme != rhs.theme {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.height != rhs.height {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.gloss != rhs.gloss {
return false
}
if lhs.glass != rhs.glass {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.animationName != rhs.animationName {
return false
}
if lhs.iconPosition != rhs.iconPosition {
return false
}
if lhs.iconSpacing != rhs.iconSpacing {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
return true
}
public final class View: UIView {
private var component: SolidRoundedButtonComponent?
private var button: SolidRoundedButtonView?
private var currentIsLoading = false
public func update(component: SolidRoundedButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.button == nil {
let button = SolidRoundedButtonView(
title: component.title,
label: component.label,
badge: component.badge,
icon: component.icon,
theme: component.theme,
font: component.font,
fontSize: component.fontSize,
height: component.height,
cornerRadius: component.cornerRadius,
gloss: component.gloss
)
button.progressType = .embedded
self.button = button
self.addSubview(button)
button.pressed = { [weak self] in
self?.component?.action()
}
}
if let button = self.button {
button.title = component.title
button.subtitle = component.subtitle
button.label = component.label
button.badge = component.badge
button.iconPosition = component.iconPosition
button.iconSpacing = component.iconSpacing
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
button.animation = component.animationName
button.gloss = component.gloss
button.isEnabled = component.isEnabled
button.isUserInteractionEnabled = component.isEnabled
button.updateTheme(component.theme)
let height = button.updateLayout(width: availableSize.width, transition: .immediate)
transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil)
if self.currentIsLoading != component.isLoading {
self.currentIsLoading = component.isLoading
if component.isLoading {
button.transitionToProgress()
} else {
button.transitionFromProgress()
}
}
}
self.component = component
return 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, transition: transition)
}
}
@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "UndoPanelComponent",
module_name = "UndoPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,67 @@
import Foundation
import UIKit
import ComponentFlow
public final class UndoPanelComponent: Component {
public let icon: AnyComponent<Empty>?
public let content: AnyComponent<Empty>
public let action: AnyComponent<Empty>?
public init(
icon: AnyComponent<Empty>?,
content: AnyComponent<Empty>,
action: AnyComponent<Empty>?
) {
self.icon = icon
self.content = content
self.action = action
}
public static func ==(lhs: UndoPanelComponent, rhs: UndoPanelComponent) -> Bool {
if lhs.icon != rhs.icon {
return false
}
if lhs.content !== rhs.content {
return false
}
if lhs.action != rhs.action {
return false
}
return true
}
public final class View: UIVisualEffectView {
private var iconView: ComponentHostView<Empty>?
private let centralContentView: ComponentHostView<Empty>
private var actionView: ComponentHostView<Empty>?
init() {
self.centralContentView = ComponentHostView()
super.init(effect: nil)
self.addSubview(self.contentView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: UndoPanelComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.effect = UIBlurEffect(style: .dark)
self.layer.cornerRadius = 10.0
return CGSize(width: availableSize.width, height: 50.0)
}
}
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,83 @@
import Foundation
import UIKit
import ComponentFlow
public final class UndoPanelContainerComponent: Component {
let push: ActionSlot<UndoPanelComponent>
public init(push: ActionSlot<UndoPanelComponent>) {
self.push = push
}
public static func ==(lhs: UndoPanelContainerComponent, rhs: UndoPanelContainerComponent) -> Bool {
if lhs.push != rhs.push {
return false
}
return true
}
public final class View: UIView {
private var topPanel: UndoPanelComponent?
private var topPanelView: ComponentHostView<Empty>?
private var nextPanel: UndoPanelComponent?
public func update(component: UndoPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
component.push.connect { [weak self, weak state] panel in
guard let strongSelf = self, let state = state else {
return
}
strongSelf.nextPanel = panel
state.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)))
}
var animateTopPanelIn = false
var topPanelTransition = transition
if let nextPanel = self.nextPanel {
self.nextPanel = nil
self.topPanel = nextPanel
if let topPanelView = self.topPanelView {
self.topPanelView = nil
transition.withAnimationIfAnimated(.curve(duration: 0.3, curve: .easeInOut)).setAlpha(view: topPanelView, alpha: 0.0, completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
let topPanelView = ComponentHostView<Empty>()
self.topPanelView = topPanelView
self.addSubview(topPanelView)
topPanelTransition = topPanelTransition.withAnimation(.none)
animateTopPanelIn = true
}
if let topPanel = self.topPanel, let topPanelView = self.topPanelView {
let topPanelSize = topPanelView.update(
transition: topPanelTransition,
component: AnyComponent(topPanel),
environment: {},
containerSize: availableSize
)
if animateTopPanelIn {
let _ = transition.withAnimationIfAnimated(.curve(duration: 0.3, curve: .easeInOut))
}
return CGSize(width: availableSize.width, height: topPanelSize.height)
}
return CGSize(width: availableSize.width, height: 0.0)
}
}
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, state: state, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ViewControllerComponent",
module_name = "ViewControllerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,466 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ComponentDisplayAdapters
private func resolveTheme(baseTheme: PresentationTheme, theme: ViewControllerComponentContainer.Theme) -> PresentationTheme {
switch theme {
case .default:
return baseTheme
case let .custom(value):
return value
case .dark:
return customizeDefaultDarkPresentationTheme(theme: defaultDarkPresentationTheme, editing: false, title: nil, accentColor: baseTheme.list.itemAccentColor, backgroundColors: [], bubbleColors: [], animateBubbleColors: false, wallpaper: nil, baseColor: nil)
}
}
open class ViewControllerComponentContainer: ViewController {
public enum NavigationBarAppearance {
case none
case transparent
case `default`
}
public enum StatusBarStyle {
case none
case ignore
case `default`
}
public enum PresentationMode {
case `default`
case modal
}
public enum Theme {
case `default`
case dark
case custom(PresentationTheme)
}
public enum Style {
case glass
case legacy
}
public final class Environment: Equatable {
public let statusBarHeight: CGFloat
public let navigationHeight: CGFloat
public let safeInsets: UIEdgeInsets
public let additionalInsets: UIEdgeInsets
public let inputHeight: CGFloat
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let orientation: UIInterfaceOrientation?
public let isVisible: Bool
public let theme: PresentationTheme
public let strings: PresentationStrings
public let dateTimeFormat: PresentationDateTimeFormat
public let controller: () -> ViewController?
public init(
statusBarHeight: CGFloat,
navigationHeight: CGFloat,
safeInsets: UIEdgeInsets,
additionalInsets: UIEdgeInsets,
inputHeight: CGFloat,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
orientation: UIInterfaceOrientation?,
isVisible: Bool,
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
controller: @escaping () -> ViewController?
) {
self.statusBarHeight = statusBarHeight
self.navigationHeight = navigationHeight
self.safeInsets = safeInsets
self.additionalInsets = additionalInsets
self.inputHeight = inputHeight
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.orientation = orientation
self.isVisible = isVisible
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.controller = controller
}
public static func ==(lhs: Environment, rhs: Environment) -> Bool {
if lhs === rhs {
return true
}
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.navigationHeight != rhs.navigationHeight {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.additionalInsets != rhs.additionalInsets {
return false
}
if lhs.inputHeight != rhs.inputHeight {
return false
}
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.orientation != rhs.orientation {
return false
}
if lhs.isVisible != rhs.isVisible {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
return true
}
}
public final class AnimateInTransition {
public init() {
}
}
public final class AnimateOutTransition {
public init() {
}
}
public final class Node: ViewControllerTracingNode {
fileprivate var presentationData: PresentationData
private weak var controller: ViewControllerComponentContainer?
private var component: AnyComponent<ViewControllerComponentContainer.Environment>
let theme: Theme
var resolvedTheme: PresentationTheme
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private var currentIsVisible: Bool = false
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
init(presentationData: PresentationData, controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>, theme: Theme) {
self.presentationData = presentationData
self.controller = controller
self.component = component
self.theme = theme
self.resolvedTheme = resolveTheme(baseTheme: self.presentationData.theme, theme: theme)
self.hostView = ComponentHostView()
super.init()
self.view.addSubview(self.hostView)
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) {
self.currentLayout = (layout, navigationHeight)
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: navigationHeight,
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: layout.metrics.orientation,
isVisible: self.currentIsVisible,
theme: self.resolvedTheme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let _ = self.hostView.update(
transition: transition,
component: self.component,
environment: {
environment
},
forceUpdate: self.controller?.forceNextUpdate ?? false,
containerSize: layout.size
)
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil)
}
func updateIsVisible(isVisible: Bool, animated: Bool) {
if self.currentIsVisible == isVisible {
return
}
self.currentIsVisible = isVisible
guard let currentLayout = self.currentLayout else {
return
}
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: animated ? ComponentTransition(animation: .none).withUserData(isVisible ? AnimateInTransition() : AnimateOutTransition()) : .immediate)
}
func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) {
self.component = component
guard let currentLayout = self.currentLayout else {
return
}
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = super.hitTest(point, with: event) {
if result === self.view {
return nil
}
return result
}
return nil
}
}
public enum BaseNavigationColors {
case plain
case blocks
}
public var node: Node {
return self.displayNode as! Node
}
private var presentationData: PresentationData
private var theme: Theme
private let baseNavigationColors: BaseNavigationColors
public private(set) var component: AnyComponent<ViewControllerComponentContainer.Environment>
private var presentationDataDisposable: Disposable?
public private(set) var validLayout: ContainerViewLayout?
public var wasDismissed: (() -> Void)?
public var customProceed: (() -> Void)?
public init<C: Component>(
context: AccountContext,
component: C,
navigationBarAppearance: NavigationBarAppearance,
statusBarStyle: StatusBarStyle = .default,
presentationMode: PresentationMode = .default,
theme: Theme = .default,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
baseNavigationColors: BaseNavigationColors = .plain,
) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.component = AnyComponent(component)
self.theme = theme
self.baseNavigationColors = baseNavigationColors
var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (initial: context.sharedContext.currentPresentationData.with { $0 }, signal: context.sharedContext.presentationData)
}
let presentationData = effectiveUpdatedPresentationData.initial
self.presentationData = presentationData
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true, style: .glass)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: false, hideBadge: false, hideSeparator: true, style: .glass, edgeEffectColor: self.baseNavigationColors == .blocks ? self.presentationData.theme.list.itemBlocksBackgroundColor : nil)
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
self._hasGlassStyle = true
self.setupPresentationData(effectiveUpdatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode)
}
public init<C: Component>(
component: C,
navigationBarAppearance: NavigationBarAppearance,
statusBarStyle: StatusBarStyle = .default,
presentationMode: PresentationMode = .default,
theme: Theme = .default,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>),
baseNavigationColors: BaseNavigationColors = .plain
) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.component = AnyComponent(component)
self.theme = theme
self.baseNavigationColors = baseNavigationColors
let presentationData = updatedPresentationData.initial
self.presentationData = presentationData
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true, style: .glass)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: false, hideBadge: false, hideSeparator: true, style: .glass, edgeEffectColor: self.baseNavigationColors == .blocks ? self.presentationData.theme.list.itemBlocksBackgroundColor : nil)
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
self.setupPresentationData(updatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
private func setupPresentationData(_ updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle, presentationMode: PresentationMode) {
self.presentationDataDisposable = (updatedPresentationData.signal
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
var theme = presentationData.theme
var resolvedTheme = resolveTheme(baseTheme: presentationData.theme, theme: strongSelf.theme)
if case .modal = presentationMode {
theme = theme.withModalBlocksBackground()
resolvedTheme = resolvedTheme.withModalBlocksBackground()
}
let presentationData = presentationData.withUpdated(theme: theme)
strongSelf.node.presentationData = presentationData
strongSelf.node.resolvedTheme = resolvedTheme
switch statusBarStyle {
case .none:
strongSelf.statusBar.statusBarStyle = .Hide
case .ignore:
strongSelf.statusBar.statusBarStyle = .Ignore
case .default:
strongSelf.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
}
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true, style: .glass)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: false, hideBadge: false, hideSeparator: true, style: .glass, edgeEffectColor: strongSelf.baseNavigationColors == .blocks ? strongSelf.presentationData.theme.list.itemBlocksBackgroundColor : nil)
}
if let navigationBarPresentationData {
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData, transition: .immediate)
}
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: ContainedViewLayoutTransition.immediate)
}
}
}).strict()
let resolvedTheme = resolveTheme(baseTheme: presentationData.theme, theme: self.theme)
switch statusBarStyle {
case .none:
self.statusBar.statusBarStyle = .Hide
case .ignore:
self.statusBar.statusBarStyle = .Ignore
case .default:
self.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
}
}
override open func loadDisplayNode() {
self.displayNode = Node(presentationData: self.presentationData, controller: self, component: self.component, theme: self.theme)
self.displayNodeDidLoad()
}
private var didDismiss = false
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if !self.didDismiss {
self.didDismiss = true
self.wasDismissed?()
}
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.node.updateIsVisible(isVisible: true, animated: true)
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.node.updateIsVisible(isVisible: false, animated: animated)
}
open override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag, completion: {
completion?()
})
}
fileprivate var forceNextUpdate = false
public func requestLayout(forceUpdate: Bool, transition: ContainedViewLayoutTransition) {
self.forceNextUpdate = forceUpdate
self.requestLayout(transition: transition)
self.forceNextUpdate = false
}
public func requestLayout(forceUpdate: Bool, transition: ComponentTransition) {
self.forceNextUpdate = forceUpdate
if self.isViewLoaded, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
self.forceNextUpdate = false
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.validLayout = layout
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ComponentTransition) {
super.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.validLayout = layout
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
}
public func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) {
self.component = component
self.node.updateComponent(component: component, transition: transition)
}
}