Tusker/Tusker/Screens/Timeline/TimelineJumpButton.swift

185 lines
5.8 KiB
Swift

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