mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Various improvements
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "NavigationStackComponent",
|
||||
module_name = "NavigationStackComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,297 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AppBundle
|
||||
import BundleIconComponent
|
||||
|
||||
private final class NavigationContainer: UIView, UIGestureRecognizerDelegate {
|
||||
var requestUpdate: ((ComponentTransition) -> Void)?
|
||||
var requestPop: (() -> Void)?
|
||||
var transitionFraction: CGFloat = 0.0
|
||||
|
||||
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
||||
|
||||
var isNavigationEnabled: Bool = false {
|
||||
didSet {
|
||||
self.panRecognizer?.isEnabled = self.isNavigationEnabled
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
|
||||
guard let strongSelf = self else {
|
||||
return []
|
||||
}
|
||||
let _ = strongSelf
|
||||
return [.right]
|
||||
})
|
||||
panRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panRecognizer)
|
||||
self.panRecognizer = panRecognizer
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
|
||||
return false
|
||||
}
|
||||
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
self.transitionFraction = 0.0
|
||||
case .changed:
|
||||
let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width
|
||||
let transitionFraction = max(0.0, min(1.0, distanceFactor))
|
||||
if self.transitionFraction != transitionFraction {
|
||||
self.transitionFraction = transitionFraction
|
||||
self.requestUpdate?(.immediate)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width
|
||||
let transitionFraction = max(0.0, min(1.0, distanceFactor))
|
||||
if transitionFraction > 0.2 {
|
||||
self.transitionFraction = 0.0
|
||||
self.requestPop?()
|
||||
} else {
|
||||
self.transitionFraction = 0.0
|
||||
self.requestUpdate?(.spring(duration: 0.45))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class NavigationStackComponent<ChildEnvironment: Equatable>: Component {
|
||||
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
||||
public let requestPop: () -> Void
|
||||
|
||||
public init(
|
||||
items: [AnyComponentWithIdentity<ChildEnvironment>],
|
||||
requestPop: @escaping () -> Void
|
||||
) {
|
||||
self.items = items
|
||||
self.requestPop = requestPop
|
||||
}
|
||||
|
||||
public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ItemView: UIView {
|
||||
let contents = ComponentView<ChildEnvironment>()
|
||||
let dimView = UIView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.dimView.alpha = 0.0
|
||||
self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
||||
self.dimView.isUserInteractionEnabled = false
|
||||
self.addSubview(self.dimView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReadyItem {
|
||||
var index: Int
|
||||
var itemId: AnyHashable
|
||||
var itemView: ItemView
|
||||
var itemTransition: ComponentTransition
|
||||
var itemSize: CGSize
|
||||
|
||||
init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) {
|
||||
self.index = index
|
||||
self.itemId = itemId
|
||||
self.itemView = itemView
|
||||
self.itemTransition = itemTransition
|
||||
self.itemSize = itemSize
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var itemViews: [AnyHashable: ItemView] = [:]
|
||||
private let navigationContainer = NavigationContainer()
|
||||
|
||||
private var component: NavigationStackComponent?
|
||||
private var state: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.navigationContainer)
|
||||
|
||||
self.navigationContainer.requestUpdate = { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
|
||||
self.navigationContainer.requestPop = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.component?.requestPop()
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let navigationTransitionFraction = self.navigationContainer.transitionFraction
|
||||
self.navigationContainer.isNavigationEnabled = component.items.count > 1
|
||||
|
||||
var validItemIds: [AnyHashable] = []
|
||||
|
||||
var readyItems: [ReadyItem] = []
|
||||
for i in 0 ..< component.items.count {
|
||||
let item = component.items[i]
|
||||
let itemId = item.id
|
||||
validItemIds.append(itemId)
|
||||
|
||||
let itemView: ItemView
|
||||
var itemTransition = transition
|
||||
if let current = self.itemViews[itemId] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ItemView()
|
||||
self.itemViews[itemId] = itemView
|
||||
itemView.contents.parentState = state
|
||||
}
|
||||
|
||||
let itemSize = itemView.contents.update(
|
||||
transition: itemTransition,
|
||||
component: item.component,
|
||||
environment: { environment[ChildEnvironment.self] },
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
)
|
||||
|
||||
readyItems.append(ReadyItem(
|
||||
index: i,
|
||||
itemId: itemId,
|
||||
itemView: itemView,
|
||||
itemTransition: itemTransition,
|
||||
itemSize: itemSize
|
||||
))
|
||||
}
|
||||
|
||||
let sortedItems = readyItems.sorted(by: { $0.index < $1.index })
|
||||
for readyItem in sortedItems {
|
||||
let transitionFraction: CGFloat
|
||||
let alphaTransitionFraction: CGFloat
|
||||
if readyItem.index == readyItems.count - 1 {
|
||||
transitionFraction = navigationTransitionFraction
|
||||
alphaTransitionFraction = 1.0
|
||||
} else if readyItem.index == readyItems.count - 2 {
|
||||
transitionFraction = navigationTransitionFraction - 1.0
|
||||
alphaTransitionFraction = navigationTransitionFraction
|
||||
} else {
|
||||
transitionFraction = 0.0
|
||||
alphaTransitionFraction = 0.0
|
||||
}
|
||||
|
||||
let transitionOffset: CGFloat
|
||||
if readyItem.index == readyItems.count - 1 {
|
||||
transitionOffset = readyItem.itemSize.width * transitionFraction
|
||||
} else {
|
||||
transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize)
|
||||
|
||||
let itemBounds = CGRect(origin: .zero, size: itemFrame.size)
|
||||
if let itemComponentView = readyItem.itemView.contents.view {
|
||||
var isAdded = false
|
||||
if itemComponentView.superview == nil {
|
||||
isAdded = true
|
||||
|
||||
readyItem.itemView.insertSubview(itemComponentView, at: 0)
|
||||
self.navigationContainer.addSubview(readyItem.itemView)
|
||||
}
|
||||
readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame)
|
||||
readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds)
|
||||
readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction)
|
||||
|
||||
if readyItem.index > 0 && isAdded {
|
||||
transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastHeight = sortedItems.last?.itemSize.height ?? 0.0
|
||||
let previousHeight: CGFloat
|
||||
if sortedItems.count > 1 {
|
||||
previousHeight = sortedItems[sortedItems.count - 2].itemSize.height
|
||||
} else {
|
||||
previousHeight = lastHeight
|
||||
}
|
||||
let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction
|
||||
|
||||
var removedItemIds: [AnyHashable] = []
|
||||
for (id, _) in self.itemViews {
|
||||
if !validItemIds.contains(id) {
|
||||
removedItemIds.append(id)
|
||||
}
|
||||
}
|
||||
for id in removedItemIds {
|
||||
guard let itemView = self.itemViews[id] else {
|
||||
continue
|
||||
}
|
||||
if let itemComponeentView = itemView.contents.view {
|
||||
var position = itemComponeentView.center
|
||||
position.x += itemComponeentView.bounds.width
|
||||
transition.setPosition(view: itemComponeentView, position: position, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
self.itemViews.removeValue(forKey: id)
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
self.itemViews.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize)
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user