mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-28 00:39:56 +00:00
1014 lines
45 KiB
Swift
1014 lines
45 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import ComponentFlow
|
|
import ComponentDisplayAdapters
|
|
import GlassBackgroundComponent
|
|
import MultilineTextComponent
|
|
import LottieComponent
|
|
import UIKitRuntimeUtils
|
|
import BundleIconComponent
|
|
import TextBadgeComponent
|
|
import LiquidLens
|
|
import AppBundle
|
|
import SearchBarNode
|
|
|
|
private final class TabSelectionRecognizer: UIGestureRecognizer {
|
|
private var initialLocation: CGPoint?
|
|
private var currentLocation: CGPoint?
|
|
|
|
override init(target: Any?, action: Selector?) {
|
|
super.init(target: target, action: action)
|
|
|
|
self.delaysTouchesBegan = false
|
|
self.delaysTouchesEnded = false
|
|
}
|
|
|
|
override func reset() {
|
|
super.reset()
|
|
|
|
self.initialLocation = nil
|
|
}
|
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
if self.initialLocation == nil {
|
|
self.initialLocation = touches.first?.location(in: self.view)
|
|
}
|
|
self.currentLocation = self.initialLocation
|
|
|
|
self.state = .began
|
|
}
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
self.state = .ended
|
|
}
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
self.state = .cancelled
|
|
}
|
|
|
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesMoved(touches, with: event)
|
|
|
|
self.currentLocation = touches.first?.location(in: self.view)
|
|
|
|
self.state = .changed
|
|
}
|
|
|
|
func translation(in: UIView?) -> CGPoint {
|
|
if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation {
|
|
return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y)
|
|
}
|
|
return CGPoint()
|
|
}
|
|
}
|
|
|
|
public final class NavigationSearchView: UIView {
|
|
private struct Params: Equatable {
|
|
let size: CGSize
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let isActive: Bool
|
|
|
|
init(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool) {
|
|
self.size = size
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.isActive = isActive
|
|
}
|
|
|
|
static func ==(lhs: Params, rhs: Params) -> Bool {
|
|
if lhs.size != rhs.size {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.isActive != rhs.isActive {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private let action: () -> Void
|
|
|
|
private let backgroundView: GlassBackgroundView
|
|
private let iconView: UIImageView
|
|
private(set) var searchBarNode: SearchBarNode?
|
|
|
|
private var params: Params?
|
|
|
|
public init(action: @escaping () -> Void) {
|
|
self.action = action
|
|
|
|
self.backgroundView = GlassBackgroundView()
|
|
self.backgroundView.contentView.clipsToBounds = true
|
|
self.iconView = UIImageView()
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.addSubview(self.backgroundView)
|
|
self.backgroundView.contentView.addSubview(self.iconView)
|
|
|
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
|
}
|
|
|
|
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.action()
|
|
}
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func update(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool, transition: ComponentTransition) {
|
|
let params = Params(size: size, theme: theme, strings: strings, isActive: isActive)
|
|
if self.params == params {
|
|
return
|
|
}
|
|
self.params = params
|
|
self.update(params: params, transition: transition)
|
|
}
|
|
|
|
private func update(params: Params, transition: ComponentTransition) {
|
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
|
|
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
|
|
|
self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
|
|
|
|
if self.iconView.image == nil {
|
|
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
|
|
}
|
|
transition.setTintColor(view: self.iconView, color: params.isActive ? params.theme.rootController.navigationSearchBar.inputIconColor : params.theme.chat.inputPanel.panelControlColor)
|
|
|
|
if let image = self.iconView.image {
|
|
let imageSize: CGSize
|
|
let iconFrame: CGRect
|
|
if params.isActive {
|
|
let iconFraction: CGFloat = 0.8
|
|
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
|
|
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((params.size.height - imageSize.height) * 0.5)), size: imageSize)
|
|
} else {
|
|
iconFrame = CGRect(origin: CGPoint(x: floor((params.size.width - image.size.width) * 0.5), y: floor((params.size.height - image.size.height) * 0.5)), size: image.size)
|
|
}
|
|
transition.setPosition(view: self.iconView, position: iconFrame.center)
|
|
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
|
}
|
|
|
|
if params.isActive {
|
|
let searchBarNode: SearchBarNode
|
|
var searchBarNodeTransition = transition
|
|
if let current = self.searchBarNode {
|
|
searchBarNode = current
|
|
} else {
|
|
searchBarNodeTransition = searchBarNodeTransition.withAnimation(.none)
|
|
searchBarNode = SearchBarNode(
|
|
theme: SearchBarNodeTheme(
|
|
background: .clear,
|
|
separator: .clear,
|
|
inputFill: .clear,
|
|
primaryText: params.theme.chat.inputPanel.panelControlColor,
|
|
placeholder: params.theme.chat.inputPanel.inputPlaceholderColor,
|
|
inputIcon: params.theme.chat.inputPanel.inputControlColor,
|
|
inputClear: params.theme.chat.inputPanel.inputControlColor,
|
|
accent: params.theme.chat.inputPanel.panelControlAccentColor,
|
|
keyboard: params.theme.rootController.keyboardColor
|
|
),
|
|
strings: params.strings,
|
|
fieldStyle: .inlineNavigation,
|
|
icon: .loupe,
|
|
forceSeparator: false,
|
|
displayBackground: false,
|
|
cancelText: nil
|
|
)
|
|
searchBarNode.placeholderString = NSAttributedString(string: params.strings.Common_Search, font: Font.regular(17.0), textColor: params.theme.chat.inputPanel.inputPlaceholderColor)
|
|
self.searchBarNode = searchBarNode
|
|
self.backgroundView.contentView.addSubview(searchBarNode.view)
|
|
searchBarNode.view.alpha = 0.0
|
|
}
|
|
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: params.size.width - 36.0 - 4.0, height: params.size.height))
|
|
transition.setFrame(view: searchBarNode.view, frame: searchBarFrame)
|
|
searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
|
|
alphaTransition.setAlpha(view: searchBarNode.view, alpha: 1.0)
|
|
} else {
|
|
if let searchBarNode = self.searchBarNode {
|
|
self.searchBarNode = nil
|
|
let searchBarNodeView = searchBarNode.view
|
|
alphaTransition.setAlpha(view: searchBarNode.view, alpha: 0.0, completion: { [weak searchBarNodeView] _ in
|
|
searchBarNodeView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class TabBarComponent: Component {
|
|
public final class Item: Equatable {
|
|
public let item: UITabBarItem
|
|
public let action: (Bool) -> Void
|
|
public let contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?
|
|
|
|
fileprivate var id: AnyHashable {
|
|
return AnyHashable(ObjectIdentifier(self.item))
|
|
}
|
|
|
|
public init(item: UITabBarItem, action: @escaping (Bool) -> Void, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) {
|
|
self.item = item
|
|
self.action = action
|
|
self.contextAction = contextAction
|
|
}
|
|
|
|
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
|
if lhs === rhs {
|
|
return true
|
|
}
|
|
if lhs.item !== rhs.item {
|
|
return false
|
|
}
|
|
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class Search: Equatable {
|
|
public let isActive: Bool
|
|
public let activate: () -> Void
|
|
public let deactivate: () -> Void
|
|
|
|
public init(isActive: Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) {
|
|
self.isActive = isActive
|
|
self.activate = activate
|
|
self.deactivate = deactivate
|
|
}
|
|
|
|
public static func ==(lhs: Search, rhs: Search) -> Bool {
|
|
if lhs.isActive != rhs.isActive {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public let theme: PresentationTheme
|
|
public let strings: PresentationStrings
|
|
public let items: [Item]
|
|
public let search: Search?
|
|
public let selectedId: AnyHashable?
|
|
public let isTablet: Bool
|
|
|
|
public init(
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
items: [Item],
|
|
search: Search?,
|
|
selectedId: AnyHashable?,
|
|
isTablet: Bool
|
|
) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.items = items
|
|
self.search = search
|
|
self.selectedId = selectedId
|
|
self.isTablet = isTablet
|
|
}
|
|
|
|
public static func ==(lhs: TabBarComponent, rhs: TabBarComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.items != rhs.items {
|
|
return false
|
|
}
|
|
if lhs.search != rhs.search {
|
|
return false
|
|
}
|
|
if lhs.selectedId != rhs.selectedId {
|
|
return false
|
|
}
|
|
if lhs.isTablet != rhs.isTablet {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate {
|
|
private let backgroundContainer: GlassBackgroundContainerView
|
|
private let liquidLensView: LiquidLensView
|
|
private let contextGestureContainerView: ContextControllerSourceView
|
|
|
|
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
|
private var selectedItemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
|
|
|
private var searchView: NavigationSearchView?
|
|
|
|
private var tabSelectionRecognizer: TabSelectionRecognizer?
|
|
private var itemWithActiveContextGesture: AnyHashable?
|
|
|
|
private var component: TabBarComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)?
|
|
private var overrideSelectedItemId: AnyHashable?
|
|
|
|
public var currentSearchNode: ASDisplayNode? {
|
|
return self.searchView?.searchBarNode
|
|
}
|
|
|
|
public override init(frame: CGRect) {
|
|
self.backgroundContainer = GlassBackgroundContainerView()
|
|
self.liquidLensView = LiquidLensView(useBackgroundContainer: false)
|
|
|
|
self.contextGestureContainerView = ContextControllerSourceView()
|
|
self.contextGestureContainerView.isGestureEnabled = true
|
|
|
|
super.init(frame: frame)
|
|
|
|
if #available(iOS 17.0, *) {
|
|
self.traitOverrides.verticalSizeClass = .compact
|
|
self.traitOverrides.horizontalSizeClass = .compact
|
|
}
|
|
|
|
self.addSubview(self.backgroundContainer)
|
|
self.backgroundContainer.contentView.addSubview(self.contextGestureContainerView)
|
|
|
|
self.contextGestureContainerView.addSubview(self.liquidLensView)
|
|
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
|
|
self.tabSelectionRecognizer = tabSelectionRecognizer
|
|
self.contextGestureContainerView.addGestureRecognizer(tabSelectionRecognizer)
|
|
|
|
self.contextGestureContainerView.shouldBegin = { [weak self] point in
|
|
guard let self, let component = self.component else {
|
|
return false
|
|
}
|
|
|
|
if let itemId = self.item(at: point) {
|
|
guard let item = component.items.first(where: { $0.id == itemId }) else {
|
|
return false
|
|
}
|
|
if item.contextAction == nil {
|
|
return false
|
|
}
|
|
|
|
self.itemWithActiveContextGesture = itemId
|
|
|
|
let startPoint = point
|
|
self.contextGestureContainerView.contextGesture?.externalUpdated = { [weak self] _, point in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let dist = sqrt(pow(startPoint.x - point.x, 2.0) + pow(startPoint.y - point.y, 2.0))
|
|
if dist > 10.0 {
|
|
self.contextGestureContainerView.contextGesture?.cancel()
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
self.contextGestureContainerView.customActivationProgress = { _, _ in
|
|
}
|
|
self.contextGestureContainerView.activated = { [weak self] gesture, _ in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
guard let itemWithActiveContextGesture = self.itemWithActiveContextGesture else {
|
|
return
|
|
}
|
|
|
|
var itemView: ItemComponent.View?
|
|
itemView = self.itemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View
|
|
|
|
guard let itemView else {
|
|
return
|
|
}
|
|
|
|
if let tabSelectionRecognizer = self.tabSelectionRecognizer {
|
|
tabSelectionRecognizer.state = .cancelled
|
|
}
|
|
|
|
guard let item = component.items.first(where: { $0.id == itemWithActiveContextGesture }) else {
|
|
return
|
|
}
|
|
item.contextAction?(gesture, itemView.contextContainerView)
|
|
}
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
if let index = tabBar.items?.firstIndex(where: { $0 === item }) {
|
|
if index < component.items.count {
|
|
component.items[index].action(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return true
|
|
}
|
|
|
|
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
switch recognizer.state {
|
|
case .began:
|
|
if let search = component.search, search.isActive {
|
|
} else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view {
|
|
let startX = itemView.frame.minX - 4.0
|
|
self.selectionGestureState = (startX, startX, itemId)
|
|
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
|
}
|
|
case .changed:
|
|
if let search = component.search, search.isActive {
|
|
} else if var selectionGestureState = self.selectionGestureState {
|
|
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
|
|
if let itemId = self.item(at: recognizer.location(in: self)) {
|
|
selectionGestureState.itemId = itemId
|
|
}
|
|
self.selectionGestureState = selectionGestureState
|
|
self.state?.updated(transition: .immediate, isLocal: true)
|
|
}
|
|
case .ended, .cancelled:
|
|
if let search = component.search, search.isActive {
|
|
search.deactivate()
|
|
} else if let selectionGestureState = self.selectionGestureState {
|
|
self.selectionGestureState = nil
|
|
if case .ended = recognizer.state, let component = self.component {
|
|
guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else {
|
|
return
|
|
}
|
|
self.overrideSelectedItemId = selectionGestureState.itemId
|
|
item.action(false)
|
|
}
|
|
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
public func frameForItem(at index: Int) -> CGRect? {
|
|
guard let component = self.component else {
|
|
return nil
|
|
}
|
|
if index < 0 || index >= component.items.count {
|
|
return nil
|
|
}
|
|
guard let itemView = self.itemViews[component.items[index].id]?.view else {
|
|
return nil
|
|
}
|
|
return self.convert(itemView.bounds, from: itemView)
|
|
}
|
|
|
|
private func item(at point: CGPoint) -> AnyHashable? {
|
|
var closestItem: (AnyHashable, CGFloat)?
|
|
for (id, itemView) in self.itemViews {
|
|
guard let itemView = itemView.view else {
|
|
continue
|
|
}
|
|
if itemView.frame.contains(point) {
|
|
return id
|
|
} else {
|
|
let distance = abs(point.x - itemView.center.x)
|
|
if let closestItemValue = closestItem {
|
|
if closestItemValue.1 > distance {
|
|
closestItem = (id, distance)
|
|
}
|
|
} else {
|
|
closestItem = (id, distance)
|
|
}
|
|
}
|
|
}
|
|
return closestItem?.0
|
|
}
|
|
|
|
public override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
|
|
self.state?.updated()
|
|
}
|
|
|
|
func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
|
let _ = alphaTransition
|
|
|
|
let innerInset: CGFloat = 4.0
|
|
let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height)
|
|
|
|
let previousComponent = self.component
|
|
self.component = component
|
|
self.state = state
|
|
|
|
self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light
|
|
|
|
let barHeight: CGFloat = 56.0 + innerInset * 2.0
|
|
|
|
var availableItemsWidth: CGFloat = availableSize.width - innerInset * 2.0
|
|
if component.search != nil {
|
|
availableItemsWidth -= barHeight + 8.0
|
|
}
|
|
|
|
let itemSize = CGSize(width: floor(availableItemsWidth / CGFloat(component.items.count)), height: 56.0)
|
|
let contentWidth: CGFloat = innerInset * 2.0 + CGFloat(component.items.count) * itemSize.width
|
|
let tabsSize = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0)
|
|
|
|
var validIds: [AnyHashable] = []
|
|
var selectionFrame: CGRect?
|
|
for index in 0 ..< component.items.count {
|
|
let item = component.items[index]
|
|
validIds.append(item.id)
|
|
|
|
let itemView: ComponentView<Empty>
|
|
var itemTransition = transition
|
|
|
|
if let current = self.itemViews[item.id] {
|
|
itemView = current
|
|
} else {
|
|
itemTransition = itemTransition.withAnimation(.none)
|
|
itemView = ComponentView()
|
|
self.itemViews[item.id] = itemView
|
|
}
|
|
|
|
let selectedItemView: ComponentView<Empty>
|
|
if let current = self.selectedItemViews[item.id] {
|
|
selectedItemView = current
|
|
} else {
|
|
selectedItemView = ComponentView()
|
|
self.selectedItemViews[item.id] = selectedItemView
|
|
}
|
|
|
|
let isItemSelected: Bool
|
|
if let overrideSelectedItemId = self.overrideSelectedItemId {
|
|
isItemSelected = overrideSelectedItemId == item.id
|
|
} else {
|
|
isItemSelected = component.selectedId == item.id
|
|
}
|
|
|
|
let _ = itemView.update(
|
|
transition: itemTransition,
|
|
component: AnyComponent(ItemComponent(
|
|
item: item,
|
|
theme: component.theme,
|
|
isCompact: component.search?.isActive == true,
|
|
isSelected: false
|
|
)),
|
|
environment: {},
|
|
containerSize: itemSize
|
|
)
|
|
let _ = selectedItemView.update(
|
|
transition: itemTransition,
|
|
component: AnyComponent(ItemComponent(
|
|
item: item,
|
|
theme: component.theme,
|
|
isCompact: component.search?.isActive == true,
|
|
isSelected: true
|
|
)),
|
|
environment: {},
|
|
containerSize: itemSize
|
|
)
|
|
|
|
var itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((tabsSize.height - itemSize.height) * 0.5)), size: itemSize)
|
|
if isItemSelected {
|
|
selectionFrame = itemFrame
|
|
}
|
|
|
|
if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View {
|
|
let itemAlphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
|
|
|
if itemComponentView.superview == nil {
|
|
itemComponentView.isUserInteractionEnabled = false
|
|
selectedItemComponentView.isUserInteractionEnabled = false
|
|
|
|
self.liquidLensView.contentView.addSubview(itemComponentView)
|
|
self.liquidLensView.selectedContentView.addSubview(selectedItemComponentView)
|
|
}
|
|
|
|
if let search = component.search, search.isActive {
|
|
if isItemSelected {
|
|
itemFrame.origin.x = floor((48.0 - itemSize.width) * 0.5)
|
|
itemTransition.setAlpha(view: itemComponentView, alpha: 1.0)
|
|
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0)
|
|
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0)
|
|
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0)
|
|
} else {
|
|
itemTransition.setAlpha(view: itemComponentView, alpha: 0.0)
|
|
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 10.0)
|
|
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 0.0)
|
|
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 10.0)
|
|
}
|
|
} else {
|
|
itemTransition.setAlpha(view: itemComponentView, alpha: 1.0)
|
|
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0)
|
|
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0)
|
|
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0)
|
|
}
|
|
|
|
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
|
itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center)
|
|
itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
|
itemTransition.setScale(view: selectedItemComponentView, scale: self.selectionGestureState != nil ? 1.15 : 1.0)
|
|
|
|
if let previousComponent, previousComponent.selectedId != item.id, isItemSelected {
|
|
itemComponentView.playSelectionAnimation()
|
|
selectedItemComponentView.playSelectionAnimation()
|
|
}
|
|
}
|
|
}
|
|
|
|
var removeIds: [AnyHashable] = []
|
|
for (id, itemView) in self.itemViews {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
itemView.view?.removeFromSuperview()
|
|
self.selectedItemViews[id]?.view?.removeFromSuperview()
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.itemViews.removeValue(forKey: id)
|
|
self.selectedItemViews.removeValue(forKey: id)
|
|
}
|
|
|
|
var tabsFrame = CGRect(origin: CGPoint(), size: tabsSize)
|
|
if let search = component.search, search.isActive {
|
|
tabsFrame.size = CGSize(width: 48.0, height: 48.0)
|
|
tabsFrame.origin.y = tabsSize.height - 48.0
|
|
}
|
|
|
|
transition.setFrame(view: self.contextGestureContainerView, frame: tabsFrame)
|
|
transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: tabsSize))
|
|
|
|
var lensSelection: (x: CGFloat, width: CGFloat)
|
|
if let selectionGestureState = self.selectionGestureState {
|
|
lensSelection = (selectionGestureState.currentX, itemSize.width + innerInset * 2.0)
|
|
} else if let selectionFrame {
|
|
lensSelection = (selectionFrame.minX - innerInset, itemSize.width + innerInset * 2.0)
|
|
} else {
|
|
lensSelection = (0.0, itemSize.width)
|
|
}
|
|
|
|
var lensSize: CGSize = tabsSize
|
|
var isLensCollapsed = false
|
|
if let search = component.search, search.isActive {
|
|
isLensCollapsed = true
|
|
lensSize = CGSize(width: 48.0, height: 48.0)
|
|
lensSelection = (0.0, 48.0)
|
|
}
|
|
self.liquidLensView.update(size: lensSize, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition)
|
|
|
|
var size = tabsSize
|
|
|
|
if let search = component.search {
|
|
let searchSize: CGSize
|
|
let searchFrame: CGRect
|
|
if search.isActive {
|
|
size.width = availableSize.width
|
|
searchSize = CGSize(width: availableSize.width - 48.0 - 8.0, height: 48.0)
|
|
searchFrame = CGRect(origin: CGPoint(x: 48.0 + 8.0, y: size.height - searchSize.height), size: searchSize)
|
|
} else {
|
|
searchSize = CGSize(width: barHeight, height: barHeight)
|
|
size.width += barHeight + 8.0
|
|
searchFrame = CGRect(origin: CGPoint(x: availableSize.width - searchSize.width, y: 0.0), size: searchSize)
|
|
}
|
|
|
|
let searchView: NavigationSearchView
|
|
var searchViewTransition = transition
|
|
if let current = self.searchView {
|
|
searchView = current
|
|
} else {
|
|
searchViewTransition = searchViewTransition.withAnimation(.none)
|
|
searchView = NavigationSearchView(action: { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
component.search?.activate()
|
|
})
|
|
self.searchView = searchView
|
|
self.backgroundContainer.contentView.addSubview(searchView)
|
|
searchView.frame = CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchSize)
|
|
}
|
|
searchView.update(size: searchSize, theme: component.theme, strings: component.strings, isActive: search.isActive, transition: searchViewTransition)
|
|
transition.setFrame(view: searchView, frame: searchFrame)
|
|
} else {
|
|
if let searchView = self.searchView {
|
|
self.searchView = nil
|
|
transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchView.bounds.size), completion: { [weak searchView] completed in
|
|
guard let searchView, completed else {
|
|
return
|
|
}
|
|
searchView.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
|
|
self.backgroundContainer.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private final class ItemComponent: Component {
|
|
let item: TabBarComponent.Item
|
|
let theme: PresentationTheme
|
|
let isCompact: Bool
|
|
let isSelected: Bool
|
|
|
|
init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool) {
|
|
self.item = item
|
|
self.theme = theme
|
|
self.isCompact = isCompact
|
|
self.isSelected = isSelected
|
|
}
|
|
|
|
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
|
if lhs.item != rhs.item {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.isCompact != rhs.isCompact {
|
|
return false
|
|
}
|
|
if lhs.isSelected != rhs.isSelected {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
let contextContainerView: ContextExtractedContentContainingView
|
|
|
|
private var imageIcon: ComponentView<Empty>?
|
|
private var animationIcon: ComponentView<Empty>?
|
|
private let title = ComponentView<Empty>()
|
|
private var badge: ComponentView<Empty>?
|
|
|
|
private var component: ItemComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
private var setImageListener: Int?
|
|
private var setSelectedImageListener: Int?
|
|
private var setBadgeListener: Int?
|
|
|
|
override init(frame: CGRect) {
|
|
self.contextContainerView = ContextExtractedContentContainingView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.contextContainerView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
if let component = self.component {
|
|
if let setImageListener = self.setImageListener {
|
|
component.item.item.removeSetImageListener(setImageListener)
|
|
}
|
|
if let setSelectedImageListener = self.setSelectedImageListener {
|
|
component.item.item.removeSetSelectedImageListener(setSelectedImageListener)
|
|
}
|
|
if let setBadgeListener = self.setBadgeListener {
|
|
component.item.item.removeSetBadgeListener(setBadgeListener)
|
|
}
|
|
}
|
|
}
|
|
|
|
func playSelectionAnimation() {
|
|
if let animationIconView = self.animationIcon?.view as? LottieComponent.View {
|
|
animationIconView.playOnce()
|
|
}
|
|
}
|
|
|
|
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
|
|
|
|
let previousComponent = self.component
|
|
|
|
if previousComponent?.item.item !== component.item.item {
|
|
if let setImageListener = self.setImageListener {
|
|
self.component?.item.item.removeSetImageListener(setImageListener)
|
|
}
|
|
if let setSelectedImageListener = self.setSelectedImageListener {
|
|
self.component?.item.item.removeSetSelectedImageListener(setSelectedImageListener)
|
|
}
|
|
if let setBadgeListener = self.setBadgeListener {
|
|
self.component?.item.item.removeSetBadgeListener(setBadgeListener)
|
|
}
|
|
self.setImageListener = component.item.item.addSetImageListener { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.state?.updated(transition: .immediate, isLocal: true)
|
|
}
|
|
self.setSelectedImageListener = component.item.item.addSetSelectedImageListener { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.state?.updated(transition: .immediate, isLocal: true)
|
|
}
|
|
self.setBadgeListener = UITabBarItem_addSetBadgeListener(component.item.item) { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.state?.updated(transition: .immediate, isLocal: true)
|
|
}
|
|
}
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
if let animationName = component.item.item.animationName {
|
|
if let imageIcon = self.imageIcon {
|
|
self.imageIcon = nil
|
|
imageIcon.view?.removeFromSuperview()
|
|
}
|
|
|
|
let animationIcon: ComponentView<Empty>
|
|
var iconTransition = transition
|
|
if let current = self.animationIcon {
|
|
animationIcon = current
|
|
} else {
|
|
iconTransition = iconTransition.withAnimation(.none)
|
|
animationIcon = ComponentView()
|
|
self.animationIcon = animationIcon
|
|
}
|
|
|
|
let iconSize = animationIcon.update(
|
|
transition: iconTransition,
|
|
component: AnyComponent(LottieComponent(
|
|
content: LottieComponent.AppBundleContent(
|
|
name: animationName
|
|
),
|
|
color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor,
|
|
placeholderColor: nil,
|
|
startingPosition: .end,
|
|
size: CGSize(width: 48.0, height: 48.0),
|
|
loop: false
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 48.0, height: 48.0)
|
|
)
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: -4.0), size: iconSize).offsetBy(dx: component.item.item.animationOffset.x, dy: component.item.item.animationOffset.y)
|
|
if let animationIconView = animationIcon.view {
|
|
if animationIconView.superview == nil {
|
|
if let badgeView = self.badge?.view {
|
|
self.contextContainerView.contentView.insertSubview(animationIconView, belowSubview: badgeView)
|
|
} else {
|
|
self.contextContainerView.contentView.addSubview(animationIconView)
|
|
}
|
|
}
|
|
iconTransition.setFrame(view: animationIconView, frame: iconFrame)
|
|
}
|
|
} else {
|
|
if let animationIcon = self.animationIcon {
|
|
self.animationIcon = nil
|
|
animationIcon.view?.removeFromSuperview()
|
|
}
|
|
|
|
let imageIcon: ComponentView<Empty>
|
|
var iconTransition = transition
|
|
if let current = self.imageIcon {
|
|
imageIcon = current
|
|
} else {
|
|
iconTransition = iconTransition.withAnimation(.none)
|
|
imageIcon = ComponentView()
|
|
self.imageIcon = imageIcon
|
|
}
|
|
|
|
let iconSize = imageIcon.update(
|
|
transition: iconTransition,
|
|
component: AnyComponent(Image(
|
|
image: component.isSelected ? component.item.item.selectedImage : component.item.item.image,
|
|
tintColor: nil,
|
|
contentMode: .center
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 3.0), size: iconSize)
|
|
if let imageIconView = imageIcon.view {
|
|
if imageIconView.superview == nil {
|
|
if let badgeView = self.badge?.view {
|
|
self.contextContainerView.contentView.insertSubview(imageIconView, belowSubview: badgeView)
|
|
} else {
|
|
self.contextContainerView.contentView.addSubview(imageIconView)
|
|
}
|
|
}
|
|
iconTransition.setFrame(view: imageIconView, frame: iconFrame)
|
|
}
|
|
}
|
|
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.item.item.title ?? " ", font: Font.semibold(10.0), textColor: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: availableSize.height - 8.0 - titleSize.height), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.contextContainerView.contentView.addSubview(titleView)
|
|
}
|
|
titleView.frame = titleFrame
|
|
alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0)
|
|
}
|
|
|
|
if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty {
|
|
let badge: ComponentView<Empty>
|
|
var badgeTransition = transition
|
|
if let current = self.badge {
|
|
badge = current
|
|
} else {
|
|
badgeTransition = badgeTransition.withAnimation(.none)
|
|
badge = ComponentView()
|
|
self.badge = badge
|
|
}
|
|
let badgeSize = badge.update(
|
|
transition: badgeTransition,
|
|
component: AnyComponent(TextBadgeComponent(
|
|
text: badgeText,
|
|
font: Font.regular(13.0),
|
|
background: component.theme.rootController.tabBar.badgeBackgroundColor,
|
|
foreground: component.theme.rootController.tabBar.badgeTextColor,
|
|
insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 1.0, right: 6.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
let contentWidth: CGFloat = 25.0
|
|
let badgeFrame = CGRect(origin: CGPoint(x: floor(availableSize.width / 2.0) + contentWidth - badgeSize.width - 1.0, y: 5.0), size: badgeSize)
|
|
if let badgeView = badge.view {
|
|
if badgeView.superview == nil {
|
|
self.contextContainerView.contentView.addSubview(badgeView)
|
|
}
|
|
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
|
|
alphaTransition.setAlpha(view: badgeView, alpha: component.isCompact ? 0.0 : 1.0)
|
|
}
|
|
} else if let badge = self.badge {
|
|
self.badge = nil
|
|
badge.view?.removeFromSuperview()
|
|
}
|
|
|
|
transition.setFrame(view: self.contextContainerView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
self.contextContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|