Add stretchy menus
This commit is contained in:
parent
8e6bf219c8
commit
503d35f301
|
@ -48,6 +48,7 @@
|
||||||
D6E24371278BE1250005E546 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24370278BE1250005E546 /* HTMLEntities */; };
|
D6E24371278BE1250005E546 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24370278BE1250005E546 /* HTMLEntities */; };
|
||||||
D6E24373278BE2B80005E546 /* read.css in Resources */ = {isa = PBXBuildFile; fileRef = D6E24372278BE2B80005E546 /* read.css */; };
|
D6E24373278BE2B80005E546 /* read.css in Resources */ = {isa = PBXBuildFile; fileRef = D6E24372278BE2B80005E546 /* read.css */; };
|
||||||
D6EB531D278C89C300AD2E61 /* AppNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB531C278C89C300AD2E61 /* AppNavigationController.swift */; };
|
D6EB531D278C89C300AD2E61 /* AppNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB531C278C89C300AD2E61 /* AppNavigationController.swift */; };
|
||||||
|
D6EB531F278E4A7500AD2E61 /* StretchyMenuInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB531E278E4A7500AD2E61 /* StretchyMenuInteraction.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -131,6 +132,7 @@
|
||||||
D6E2436D278BD8160005E546 /* ReadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadViewController.swift; sourceTree = "<group>"; };
|
D6E2436D278BD8160005E546 /* ReadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadViewController.swift; sourceTree = "<group>"; };
|
||||||
D6E24372278BE2B80005E546 /* read.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = read.css; sourceTree = "<group>"; };
|
D6E24372278BE2B80005E546 /* read.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = read.css; sourceTree = "<group>"; };
|
||||||
D6EB531C278C89C300AD2E61 /* AppNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationController.swift; sourceTree = "<group>"; };
|
D6EB531C278C89C300AD2E61 /* AppNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
D6EB531E278E4A7500AD2E61 /* StretchyMenuInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StretchyMenuInteraction.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -242,6 +244,7 @@
|
||||||
D65B18B527504920004A9448 /* FervorController.swift */,
|
D65B18B527504920004A9448 /* FervorController.swift */,
|
||||||
D65B18BD275051A1004A9448 /* LocalData.swift */,
|
D65B18BD275051A1004A9448 /* LocalData.swift */,
|
||||||
D6E24368278BABB40005E546 /* UIColor+App.swift */,
|
D6E24368278BABB40005E546 /* UIColor+App.swift */,
|
||||||
|
D6EB531E278E4A7500AD2E61 /* StretchyMenuInteraction.swift */,
|
||||||
D6A8A33527766E9300CCEC72 /* CoreData */,
|
D6A8A33527766E9300CCEC72 /* CoreData */,
|
||||||
D65B18AF2750468B004A9448 /* Screens */,
|
D65B18AF2750468B004A9448 /* Screens */,
|
||||||
D6C687F7272CD27700874C10 /* Assets.xcassets */,
|
D6C687F7272CD27700874C10 /* Assets.xcassets */,
|
||||||
|
@ -499,6 +502,7 @@
|
||||||
D6E24360278B97240005E546 /* Group+CoreDataProperties.swift in Sources */,
|
D6E24360278B97240005E546 /* Group+CoreDataProperties.swift in Sources */,
|
||||||
D6E2434C278B456A0005E546 /* ItemsViewController.swift in Sources */,
|
D6E2434C278B456A0005E546 /* ItemsViewController.swift in Sources */,
|
||||||
D6E2435E278B97240005E546 /* Item+CoreDataProperties.swift in Sources */,
|
D6E2435E278B97240005E546 /* Item+CoreDataProperties.swift in Sources */,
|
||||||
|
D6EB531F278E4A7500AD2E61 /* StretchyMenuInteraction.swift in Sources */,
|
||||||
D6E24358278B96E40005E546 /* Feed+CoreDataProperties.swift in Sources */,
|
D6E24358278B96E40005E546 /* Feed+CoreDataProperties.swift in Sources */,
|
||||||
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */,
|
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */,
|
||||||
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
|
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
|
||||||
|
|
|
@ -11,6 +11,8 @@ class AppNavigationController: UINavigationController, UINavigationControllerDel
|
||||||
|
|
||||||
private var statusBarBlockingView: UIView!
|
private var statusBarBlockingView: UIView!
|
||||||
|
|
||||||
|
static let panRecognizerName = "AppNavPanRecognizer"
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -21,6 +23,7 @@ class AppNavigationController: UINavigationController, UINavigationControllerDel
|
||||||
interactivePopGestureRecognizer?.isEnabled = false
|
interactivePopGestureRecognizer?.isEnabled = false
|
||||||
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized))
|
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized))
|
||||||
recognizer.allowedScrollTypesMask = .continuous
|
recognizer.allowedScrollTypesMask = .continuous
|
||||||
|
recognizer.name = AppNavigationController.panRecognizerName
|
||||||
view.addGestureRecognizer(recognizer)
|
view.addGestureRecognizer(recognizer)
|
||||||
|
|
||||||
isNavigationBarHidden = true
|
isNavigationBarHidden = true
|
||||||
|
|
|
@ -33,6 +33,10 @@ class HomeViewController: UIViewController {
|
||||||
// todo: account info
|
// todo: account info
|
||||||
title = "Reader"
|
title = "Reader"
|
||||||
|
|
||||||
|
view.addInteraction(StretchyMenuInteraction(delegate: self))
|
||||||
|
|
||||||
|
view.backgroundColor = .appBackground
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
config.headerMode = .supplementary
|
config.headerMode = .supplementary
|
||||||
config.backgroundColor = .appBackground
|
config.backgroundColor = .appBackground
|
||||||
|
@ -236,3 +240,19 @@ extension HomeViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension HomeViewController: StretchyMenuInteractionDelegate {
|
||||||
|
func stretchyMenuTitle() -> String? {
|
||||||
|
return "Switch Accounts"
|
||||||
|
}
|
||||||
|
func stretchyMenuItems() -> [StretchyMenuItem] {
|
||||||
|
return [
|
||||||
|
StretchyMenuItem(title: "foo", subtitle: "bar", action: {
|
||||||
|
print("foo")
|
||||||
|
}),
|
||||||
|
StretchyMenuItem(title: "baz", subtitle: "qux", action: {
|
||||||
|
print("baz")
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ class ReadViewController: UIViewController {
|
||||||
navigationItem.largeTitleDisplayMode = .never
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
|
||||||
view.backgroundColor = .appBackground
|
view.backgroundColor = .appBackground
|
||||||
|
view.addInteraction(StretchyMenuInteraction(delegate: self))
|
||||||
|
|
||||||
let webView = WKWebView()
|
let webView = WKWebView()
|
||||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -167,3 +168,26 @@ extension ReadViewController: WKUIDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ReadViewController: StretchyMenuInteractionDelegate {
|
||||||
|
func stretchyMenuTitle() -> String? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stretchyMenuItems() -> [StretchyMenuItem] {
|
||||||
|
guard let url = item.url else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
StretchyMenuItem(title: "Open in Safari", subtitle: nil, action: { [unowned self] in
|
||||||
|
self.present(createSafariVC(url: url), animated: true)
|
||||||
|
}),
|
||||||
|
StretchyMenuItem(title: "Share", subtitle: nil, action: { [unowned self] in
|
||||||
|
self.present(UIActivityViewController(activityItems: [url], applicationActivities: nil), animated: true)
|
||||||
|
}),
|
||||||
|
StretchyMenuItem(title: item.read ? "Mark as Unread" : "Mark as Read", subtitle: nil, action: { [unowned self] in
|
||||||
|
item.read = !item.read
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,405 @@
|
||||||
|
//
|
||||||
|
// StretchyMenuInteraction.swift
|
||||||
|
// Reader
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/11/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct StretchyMenuItem {
|
||||||
|
let title: String
|
||||||
|
let subtitle: String?
|
||||||
|
let action: () -> Void
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol StretchyMenuInteractionDelegate: AnyObject {
|
||||||
|
func stretchyMenuTitle() -> String?
|
||||||
|
func stretchyMenuItems() -> [StretchyMenuItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
class StretchyMenuInteraction: NSObject, UIInteraction {
|
||||||
|
|
||||||
|
weak var delegate: StretchyMenuInteractionDelegate?
|
||||||
|
|
||||||
|
private(set) weak var view: UIView? = nil
|
||||||
|
|
||||||
|
private let menuHintView = MenuHintView()
|
||||||
|
fileprivate let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
private var snapshot: UIView?
|
||||||
|
private var menuView: MenuView?
|
||||||
|
|
||||||
|
private let menuOpenThreshold: CGFloat = 100
|
||||||
|
|
||||||
|
fileprivate var isShowingMenu = false
|
||||||
|
|
||||||
|
init(delegate: StretchyMenuInteractionDelegate) {
|
||||||
|
self.delegate = delegate
|
||||||
|
|
||||||
|
menuHintView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func willMove(to view: UIView?) {
|
||||||
|
if self.view != nil {
|
||||||
|
fatalError("removing StretchyMenuInteraction from view is unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didMove(to view: UIView?) {
|
||||||
|
self.view = view
|
||||||
|
|
||||||
|
guard let view = view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized))
|
||||||
|
panRecognizer.delegate = self
|
||||||
|
view.addGestureRecognizer(panRecognizer)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var prevTranslation: CGFloat = 0
|
||||||
|
|
||||||
|
@objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
guard let view = view,
|
||||||
|
!isShowingMenu,
|
||||||
|
let delegate = delegate else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevTranslation = self.prevTranslation
|
||||||
|
let translation = recognizer.translation(in: view)
|
||||||
|
self.prevTranslation = translation.x
|
||||||
|
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
snapshot = view.snapshotView(afterScreenUpdates: false)
|
||||||
|
let effectiveTranslation = 0.5 * max(0, -translation.x)
|
||||||
|
snapshot!.transform = CGAffineTransform(translationX: -effectiveTranslation, y: 0)
|
||||||
|
view.addSubview(snapshot!)
|
||||||
|
view.insertSubview(menuHintView, belowSubview: snapshot!)
|
||||||
|
menuHintView.backgroundColor = view.backgroundColor
|
||||||
|
menuHintView.frame = CGRect(x: view.bounds.width - effectiveTranslation, y: 0, width: effectiveTranslation, height: view.bounds.height)
|
||||||
|
menuHintView.updateForProgress(0, animate: false)
|
||||||
|
feedbackGenerator.prepare()
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
let effectiveTranslation = 0.5 * max(0, -translation.x)
|
||||||
|
snapshot!.transform = CGAffineTransform(translationX: -effectiveTranslation, y: 0)
|
||||||
|
if -prevTranslation < menuOpenThreshold && -translation.x >= menuOpenThreshold {
|
||||||
|
feedbackGenerator.impactOccurred()
|
||||||
|
}
|
||||||
|
menuHintView.frame = CGRect(x: view.bounds.width - effectiveTranslation, y: 0, width: effectiveTranslation, height: view.bounds.height)
|
||||||
|
menuHintView.updateForProgress(-translation.x / menuOpenThreshold, animate: true)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.snapshot!.transform = .identity
|
||||||
|
} completion: { _ in
|
||||||
|
self.snapshot!.removeFromSuperview()
|
||||||
|
self.menuHintView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if -translation.x > menuOpenThreshold {
|
||||||
|
guard let rootView = view.window?.rootViewController?.view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let menuView = MenuView(title: delegate.stretchyMenuTitle(), items: delegate.stretchyMenuItems(), owner: self)
|
||||||
|
self.menuView = menuView
|
||||||
|
menuView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
menuView.layer.zPosition = 102
|
||||||
|
rootView.addSubview(menuView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
menuView.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
|
||||||
|
menuView.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
|
||||||
|
menuView.topAnchor.constraint(equalTo: rootView.topAnchor),
|
||||||
|
menuView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
|
||||||
|
])
|
||||||
|
rootView.layoutIfNeeded()
|
||||||
|
menuView.animateIn()
|
||||||
|
|
||||||
|
isShowingMenu = true
|
||||||
|
feedbackGenerator.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideMenu(completion: (() -> Void)? = nil) {
|
||||||
|
guard let menuView = menuView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
menuView.animateOut() {
|
||||||
|
menuView.removeFromSuperview()
|
||||||
|
self.isShowingMenu = false
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StretchyMenuInteraction: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
return otherGestureRecognizer.name == AppNavigationController.panRecognizerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MenuHintView: UIView {
|
||||||
|
private let pill = UIView()
|
||||||
|
private var progress: CGFloat = 0
|
||||||
|
|
||||||
|
private var animator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
pill.frame = CGRect(x: 0, y: 0, width: 5, height: 50)
|
||||||
|
pill.backgroundColor = .systemGray
|
||||||
|
pill.layer.cornerRadius = 2.5
|
||||||
|
addSubview(pill)
|
||||||
|
|
||||||
|
pill.backgroundColor = .systemGray
|
||||||
|
pill.frame.size.height = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
let width: CGFloat = 5
|
||||||
|
pill.frame.origin.x = bounds.midX - width / 2
|
||||||
|
pill.frame.origin.y = bounds.midY - pill.frame.height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateForProgress(_ progress: CGFloat, animate: Bool) {
|
||||||
|
let oldCompleted = self.progress >= 1
|
||||||
|
let completed = progress >= 1
|
||||||
|
|
||||||
|
self.progress = progress
|
||||||
|
|
||||||
|
if oldCompleted != completed {
|
||||||
|
func updatePill() {
|
||||||
|
pill.backgroundColor = completed ? .tintColor : .systemGray
|
||||||
|
let height: CGFloat = completed ? 75 : 50
|
||||||
|
pill.frame.origin.y = bounds.midY - height / 2
|
||||||
|
pill.frame.size.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
animator?.stopAnimation(true)
|
||||||
|
if animate {
|
||||||
|
animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.5, animations: updatePill)
|
||||||
|
animator!.startAnimation()
|
||||||
|
} else {
|
||||||
|
updatePill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MenuView: UIView {
|
||||||
|
|
||||||
|
weak var owner: StretchyMenuInteraction?
|
||||||
|
|
||||||
|
private let blurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark)
|
||||||
|
private var blurView: UIView!
|
||||||
|
private let optionsStack = UIStackView()
|
||||||
|
|
||||||
|
private let items: [StretchyMenuItem]
|
||||||
|
|
||||||
|
init(title: String?, items: [StretchyMenuItem], owner: StretchyMenuInteraction) {
|
||||||
|
self.items = items
|
||||||
|
self.owner = owner
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
blurView = UIVisualEffectView(effect: blurEffect)
|
||||||
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(blurView)
|
||||||
|
|
||||||
|
optionsStack.axis = .vertical
|
||||||
|
optionsStack.alignment = .fill
|
||||||
|
optionsStack.spacing = 2
|
||||||
|
optionsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
for item in items {
|
||||||
|
optionsStack.addArrangedSubview(MenuItemView(item: item, owner: owner))
|
||||||
|
}
|
||||||
|
addSubview(optionsStack)
|
||||||
|
|
||||||
|
if let title = title {
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
titleLabel.text = title
|
||||||
|
titleLabel.textAlignment = .right
|
||||||
|
titleLabel.font = .preferredFont(forTextStyle: .title1)
|
||||||
|
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect, style: .label))
|
||||||
|
vibrancyView.contentView.addSubview(titleLabel)
|
||||||
|
optionsStack.insertArrangedSubview(vibrancyView, at: 0)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
titleLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: vibrancyView.contentView.leadingAnchor, multiplier: 1),
|
||||||
|
vibrancyView.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: titleLabel.trailingAnchor, multiplier: 1),
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: vibrancyView.contentView.topAnchor),
|
||||||
|
titleLabel.bottomAnchor.constraint(equalTo: vibrancyView.contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
|
||||||
|
optionsStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
optionsStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
optionsStack.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
blurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(blurTapped)))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
blurView.layer.opacity = 0
|
||||||
|
let count = optionsStack.arrangedSubviews.count
|
||||||
|
for (index, item) in optionsStack.arrangedSubviews.enumerated() {
|
||||||
|
let multiplier = (1 + CGFloat(index) * 1 / CGFloat(count - 1))
|
||||||
|
item.transform = CGAffineTransform(translationX: bounds.width * multiplier, y: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.blurView.layer.opacity = 1
|
||||||
|
for item in self.optionsStack.arrangedSubviews {
|
||||||
|
item.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
|
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.blurView.layer.opacity = 0
|
||||||
|
let count = self.optionsStack.arrangedSubviews.count
|
||||||
|
for (index, item) in self.optionsStack.arrangedSubviews.enumerated() {
|
||||||
|
let multiplier = (1 + CGFloat(index) * 1 / CGFloat(count - 1))
|
||||||
|
item.transform = CGAffineTransform(translationX: self.bounds.width * -multiplier, y: 0)
|
||||||
|
}
|
||||||
|
} completion: { _ in
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func blurTapped() {
|
||||||
|
owner?.hideMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MenuItemView: UIView {
|
||||||
|
weak var owner: StretchyMenuInteraction?
|
||||||
|
|
||||||
|
private let item: StretchyMenuItem
|
||||||
|
|
||||||
|
init(item: StretchyMenuItem, owner: StretchyMenuInteraction) {
|
||||||
|
self.item = item
|
||||||
|
self.owner = owner
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
backgroundColor = .appBackground
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
titleLabel.text = item.title
|
||||||
|
titleLabel.textColor = .tintColor
|
||||||
|
titleLabel.textAlignment = .right
|
||||||
|
titleLabel.font = .preferredFont(forTextStyle: .title2)
|
||||||
|
addSubview(titleLabel)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||||
|
titleLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
|
||||||
|
trailingAnchor.constraint(equalToSystemSpacingAfter: titleLabel.trailingAnchor, multiplier: 1),
|
||||||
|
|
||||||
|
heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
|
||||||
|
])
|
||||||
|
|
||||||
|
if let subtitle = item.subtitle {
|
||||||
|
let subtitleLabel = UILabel()
|
||||||
|
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
subtitleLabel.text = subtitle
|
||||||
|
subtitleLabel.textColor = .secondaryLabel
|
||||||
|
subtitleLabel.textAlignment = .right
|
||||||
|
addSubview(subtitleLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
subtitleLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
|
||||||
|
trailingAnchor.constraint(equalToSystemSpacingAfter: subtitleLabel.trailingAnchor, multiplier: 1),
|
||||||
|
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 4),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(itemTapped)))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func itemTapped() {
|
||||||
|
owner?.feedbackGenerator.impactOccurred()
|
||||||
|
|
||||||
|
UIView.animateKeyframes(withDuration: 0.15, delay: 0, options: []) {
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
||||||
|
self.backgroundColor = .appSecondaryBackground
|
||||||
|
}
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
|
||||||
|
self.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
} completion: { _ in
|
||||||
|
self.owner?.hideMenu() {
|
||||||
|
self.item.action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addBorders(top: Bool, bottom: Bool) {
|
||||||
|
if top {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.backgroundColor = .systemGray
|
||||||
|
addSubview(view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.heightAnchor.constraint(equalToConstant: 1),
|
||||||
|
view.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if bottom {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.backgroundColor = .systemGray
|
||||||
|
addSubview(view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.heightAnchor.constraint(equalToConstant: 1),
|
||||||
|
view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,6 +20,17 @@ extension UIColor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let appSecondaryBackground = UIColor { traitCollection in
|
||||||
|
switch traitCollection.userInterfaceStyle {
|
||||||
|
case .dark:
|
||||||
|
return UIColor(white: 0.2, alpha: 1)
|
||||||
|
case .unspecified, .light:
|
||||||
|
fallthrough
|
||||||
|
@unknown default:
|
||||||
|
return UIColor(white: 0.8, alpha: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static let appCellHighlightBackground = UIColor { traitCollection in
|
static let appCellHighlightBackground = UIColor { traitCollection in
|
||||||
switch traitCollection.userInterfaceStyle {
|
switch traitCollection.userInterfaceStyle {
|
||||||
case .dark:
|
case .dark:
|
||||||
|
|
Loading…
Reference in New Issue