// // TimelineJumpButton.swift // Tusker // // Created by Shadowfacts on 2/6/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit class TimelineJumpButton: UIView { var action: ((Mode) async -> Void)? override var intrinsicContentSize: CGSize { #if os(visionOS) CGSize(width: 44, height: 44) #else CGSize(width: UIView.noIntrinsicMetric, height: 44) #endif } private let button: UIButton = { #if os(visionOS) var config = UIButton.Configuration.borderedProminent() #else var config = UIButton.Configuration.plain() // We don't want a background for this button, even when accessibility button shapes are enabled, because it's in the navbar. config.background.backgroundColor = .clear #endif config.image = UIImage(systemName: "arrow.up") config.contentInsets = .zero return UIButton(configuration: config) }() private(set) var mode = Mode.jump var offscreen = false { didSet { updateOffscreenTransform() } } private(set) var isSyncing = false init() { super.init(frame: .zero) layer.masksToBounds = true button.addAction(UIAction(handler: { [unowned self] _ in Task { switch self.mode { case .jump: await self.jumpAction() case .sync: await self.syncAction() } } }), for: .touchUpInside) button.translatesAutoresizingMaskIntoConstraints = false addSubview(button) NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: leadingAnchor), button.trailingAnchor.constraint(equalTo: trailingAnchor), button.topAnchor.constraint(equalTo: topAnchor), button.bottomAnchor.constraint(equalTo: bottomAnchor), ]) let jumpToPresentAction = UIAction(title: "Jump to Present", image: UIImage(systemName: "arrow.up")) { [unowned self] _ in Task { self.setMode(.jump, animated: false) await self.jumpAction() } } jumpToPresentAction.accessibilityAttributedLabel = TimelinesPageViewController.jumpToPresentTitle button.menu = UIMenu(children: [ jumpToPresentAction, UIAction(title: "Sync Position", image: UIImage(systemName: "arrow.triangle.2.circlepath"), handler: { [unowned self] _ in Task { self.setMode(.sync, animated: false) await self.syncAction() } }) ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() updateOffscreenTransform() } private func updateOffscreenTransform() { if offscreen { button.transform = CGAffineTransform(translationX: 0, y: button.bounds.height) } else { button.transform = .identity } } func setMode(_ mode: Mode, animated: Bool) { guard self.mode != mode else { return } self.mode = mode var config = button.configuration! switch mode { case .jump: config.image = UIImage(systemName: "arrow.up") case .sync: config.image = UIImage(systemName: "arrow.triangle.2.circlepath") } if animated, let snapshot = button.snapshotView(afterScreenUpdates: false) { snapshot.translatesAutoresizingMaskIntoConstraints = false addSubview(snapshot) NSLayoutConstraint.activate([ snapshot.centerXAnchor.constraint(equalTo: centerXAnchor), snapshot.centerYAnchor.constraint(equalTo: centerYAnchor), ]) button.configuration = config button.layer.opacity = 0 UIView.animate(withDuration: 0.5, delay: 0) { self.button.layer.opacity = 1 snapshot.layer.opacity = 0 } completion: { _ in snapshot.removeFromSuperview() } } else { button.configuration = config } } private func jumpAction() async { button.isUserInteractionEnabled = false var config = button.configuration! config.showsActivityIndicator = true button.configuration = config await action?(.jump) config.showsActivityIndicator = false button.configuration = config button.isUserInteractionEnabled = true } private func syncAction() async { isSyncing = true button.isUserInteractionEnabled = false UIView.animateKeyframes(withDuration: 1, delay: 0, options: .repeat) { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) { self.button.imageView!.transform = CGAffineTransform(rotationAngle: 0.5 * .pi) } UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) { // the translation is because the symbol isn't perfectly centered self.button.imageView!.transform = CGAffineTransform(translationX: -0.5, y: 0).rotated(by: .pi) } } completion: { _ in } await action?(.sync) button.imageView!.layer.removeAllAnimations() button.imageView!.transform = .identity button.isUserInteractionEnabled = true isSyncing = false setMode(.jump, animated: true) } } extension TimelineJumpButton { enum Mode { case jump case sync } }