2024-07-13 18:13:58 +04:00

298 lines
12 KiB
Swift

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