185 lines
5.8 KiB
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
|
|
}
|
|
}
|