forked from shadowfacts/Tusker
Add Duckable package, make Compose screen duckable
This commit is contained in:
parent
7600954f4b
commit
01124b76a3
|
@ -0,0 +1,9 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/config/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
|
@ -0,0 +1,31 @@
|
||||||
|
// swift-tools-version: 5.7
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "Duckable",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v15),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "Duckable",
|
||||||
|
targets: ["Duckable"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
// Dependencies declare other packages that this package depends on.
|
||||||
|
// .package(url: /* package url */, from: "1.0.0"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
|
.target(
|
||||||
|
name: "Duckable",
|
||||||
|
dependencies: []),
|
||||||
|
.testTarget(
|
||||||
|
name: "DuckableTests",
|
||||||
|
dependencies: ["Duckable"]),
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Duckable
|
||||||
|
|
||||||
|
A package that allows modally-presented view controllers to be 'ducked' to make the content behind them accessible (à la Mail.app).
|
|
@ -0,0 +1,44 @@
|
||||||
|
//
|
||||||
|
// API.swift
|
||||||
|
// Duckable
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol DuckableViewController: UIViewController {
|
||||||
|
var duckableDelegate: DuckableViewControllerDelegate? { get set }
|
||||||
|
|
||||||
|
func duckableViewControllerMayAttemptToDuck()
|
||||||
|
|
||||||
|
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat)
|
||||||
|
|
||||||
|
func duckableViewControllerDidFinishAnimatingDuck()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DuckableViewController {
|
||||||
|
public func duckableViewControllerMayAttemptToDuck() {}
|
||||||
|
public func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {}
|
||||||
|
public func duckableViewControllerDidFinishAnimatingDuck() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol DuckableViewControllerDelegate: AnyObject {
|
||||||
|
func duckableViewControllerWillDismiss(animated: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
public func presentDuckable(_ viewController: DuckableViewController) -> Bool {
|
||||||
|
var cur: UIViewController? = self
|
||||||
|
while let vc = cur {
|
||||||
|
if let container = vc as? DuckableContainerViewController {
|
||||||
|
container.presentDuckable(viewController, animated: true, completion: nil)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
cur = vc.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// DetentIdentifier.swift
|
||||||
|
// Duckable
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UISheetPresentationController.Detent.Identifier {
|
||||||
|
static let bottom = Self("\(Bundle.main.bundleIdentifier!).bottom")
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// DuckAnimationController.swift
|
||||||
|
// Duckable
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
class DuckAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
let owner: DuckableContainerViewController
|
||||||
|
let needsShrinkAnimation: Bool
|
||||||
|
|
||||||
|
init(owner: DuckableContainerViewController, needsShrinkAnimation: Bool) {
|
||||||
|
self.owner = owner
|
||||||
|
self.needsShrinkAnimation = needsShrinkAnimation
|
||||||
|
}
|
||||||
|
|
||||||
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
guard case .ducked(let duckable, placeholder: let placeholder) = owner.state,
|
||||||
|
let presented = transitionContext.viewController(forKey: .from) else {
|
||||||
|
transitionContext.completeTransition(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard transitionContext.isAnimated else {
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = transitionContext.containerView
|
||||||
|
|
||||||
|
|
||||||
|
if needsShrinkAnimation {
|
||||||
|
|
||||||
|
duckable.duckableViewControllerWillAnimateDuck(withDuration: 0.2, afterDelay: 0.2)
|
||||||
|
|
||||||
|
let presentedFrameInContainer = container.convert(presented.view.bounds, from: presented.view)
|
||||||
|
let heightToSlide = container.bounds.height - container.safeAreaInsets.bottom - detentHeight - presentedFrameInContainer.minY
|
||||||
|
|
||||||
|
let slideAnimator = UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1)
|
||||||
|
slideAnimator.addAnimations {
|
||||||
|
container.transform = CGAffineTransform(translationX: 0, y: heightToSlide + 10)
|
||||||
|
}
|
||||||
|
slideAnimator.addCompletion { _ in
|
||||||
|
duckable.duckableViewControllerDidFinishAnimatingDuck()
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
}
|
||||||
|
slideAnimator.startAnimation()
|
||||||
|
|
||||||
|
placeholder.view.transform = CGAffineTransform(translationX: 0, y: 10)
|
||||||
|
let bounceAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
|
||||||
|
placeholder.view.transform = .identity
|
||||||
|
container.transform = CGAffineTransform(translationX: 0, y: heightToSlide)
|
||||||
|
}
|
||||||
|
bounceAnimator.startAnimation(afterDelay: 0.3)
|
||||||
|
|
||||||
|
let fadeAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
|
||||||
|
presented.view.layer.opacity = 0
|
||||||
|
}
|
||||||
|
fadeAnimator.startAnimation(afterDelay: 0.3)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
duckable.duckableViewControllerWillAnimateDuck(withDuration: 0.2, afterDelay: 0)
|
||||||
|
|
||||||
|
placeholder.view.transform = CGAffineTransform(translationX: 0, y: 10)
|
||||||
|
let bounceAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
|
||||||
|
placeholder.view.transform = .identity
|
||||||
|
container.transform = CGAffineTransform(translationX: 0, y: -10)
|
||||||
|
}
|
||||||
|
bounceAnimator.startAnimation(afterDelay: 0.2)
|
||||||
|
|
||||||
|
let fadeAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
|
||||||
|
presented.view.layer.opacity = 0
|
||||||
|
}
|
||||||
|
fadeAnimator.addCompletion { _ in
|
||||||
|
duckable.duckableViewControllerDidFinishAnimatingDuck()
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
}
|
||||||
|
fadeAnimator.startAnimation(afterDelay: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
//
|
||||||
|
// DuckableContainerViewController.swift
|
||||||
|
// Duckable
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
let duckedCornerRadius: CGFloat = 10
|
||||||
|
let detentHeight: CGFloat = 44
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
public class DuckableContainerViewController: UIViewController, DuckableViewControllerDelegate {
|
||||||
|
|
||||||
|
public let child: UIViewController
|
||||||
|
private var bottomConstraint: NSLayoutConstraint!
|
||||||
|
private(set) var state = State.idle
|
||||||
|
|
||||||
|
public init(child: UIViewController) {
|
||||||
|
self.child = child
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
swizzleSheetController()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .black
|
||||||
|
|
||||||
|
child.beginAppearanceTransition(true, animated: false)
|
||||||
|
addChild(child)
|
||||||
|
child.didMove(toParent: self)
|
||||||
|
child.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(child.view)
|
||||||
|
child.endAppearanceTransition()
|
||||||
|
|
||||||
|
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
child.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
bottomConstraint,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
guard case .idle = state else {
|
||||||
|
if animated,
|
||||||
|
case .ducked(_, placeholder: let placeholder) = state {
|
||||||
|
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||||
|
let origConstant = placeholder.topConstraint.constant
|
||||||
|
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
||||||
|
placeholder.topConstraint.constant = origConstant - 20
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
|
||||||
|
placeholder.topConstraint.constant = origConstant
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .presentingDucked(viewController, isFirstPresentation: true)
|
||||||
|
doPresentDuckable(viewController, animated: animated, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func doPresentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
viewController.duckableDelegate = self
|
||||||
|
let nav = UINavigationController(rootViewController: viewController)
|
||||||
|
nav.modalPresentationStyle = .custom
|
||||||
|
nav.transitioningDelegate = self
|
||||||
|
present(nav, animated: animated) {
|
||||||
|
self.bottomConstraint.isActive = false
|
||||||
|
self.bottomConstraint = self.child.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight - 4)
|
||||||
|
self.bottomConstraint.isActive = true
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duckableViewControllerWillDismiss(animated: Bool) {
|
||||||
|
state = .idle
|
||||||
|
bottomConstraint.isActive = false
|
||||||
|
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
|
bottomConstraint.isActive = true
|
||||||
|
child.view.layer.cornerRadius = 0
|
||||||
|
setOverrideTraitCollection(nil, forChild: child)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPlaceholderForDuckedViewController(_ viewController: DuckableViewController) -> DuckedPlaceholderViewController {
|
||||||
|
let placeholder = DuckedPlaceholderViewController(for: viewController, owner: self)
|
||||||
|
placeholder.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
placeholder.beginAppearanceTransition(true, animated: false)
|
||||||
|
self.addChild(placeholder)
|
||||||
|
placeholder.didMove(toParent: self)
|
||||||
|
self.view.addSubview(placeholder.view)
|
||||||
|
placeholder.endAppearanceTransition()
|
||||||
|
|
||||||
|
let placeholderTopConstraint = placeholder.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight)
|
||||||
|
placeholder.topConstraint = placeholderTopConstraint
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
placeholder.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
|
||||||
|
placeholder.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
|
||||||
|
placeholder.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
|
||||||
|
placeholderTopConstraint
|
||||||
|
])
|
||||||
|
|
||||||
|
// otherwise the layout changes get lumped in with the system animation
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
func duckViewController() {
|
||||||
|
guard case .presentingDucked(let viewController, isFirstPresentation: _) = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let placeholder = createPlaceholderForDuckedViewController(viewController)
|
||||||
|
state = .ducked(viewController, placeholder: placeholder)
|
||||||
|
child.view.layer.cornerRadius = duckedCornerRadius
|
||||||
|
child.view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
child.view.layer.masksToBounds = true
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func unduckViewController() {
|
||||||
|
guard case .ducked(let viewController, placeholder: let placeholder) = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .presentingDucked(viewController, isFirstPresentation: false)
|
||||||
|
doPresentDuckable(viewController, animated: true) {
|
||||||
|
placeholder.view.removeFromSuperview()
|
||||||
|
placeholder.willMove(toParent: nil)
|
||||||
|
placeholder.removeFromParent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sheetOffsetDidChange() {
|
||||||
|
if case .presentingDucked(let duckable, isFirstPresentation: _) = state {
|
||||||
|
duckable.duckableViewControllerMayAttemptToDuck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
case idle
|
||||||
|
case presentingDucked(DuckableViewController, isFirstPresentation: Bool)
|
||||||
|
case ducked(DuckableViewController, placeholder: DuckedPlaceholderViewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
extension DuckableContainerViewController: UIViewControllerTransitioningDelegate {
|
||||||
|
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
|
||||||
|
let controller = UISheetPresentationController(presentedViewController: presented, presenting: presenting)
|
||||||
|
controller.delegate = self
|
||||||
|
controller.prefersGrabberVisible = true
|
||||||
|
controller.selectedDetentIdentifier = .large
|
||||||
|
controller.largestUndimmedDetentIdentifier = .bottom
|
||||||
|
controller.detents = [
|
||||||
|
.custom(identifier: .bottom, resolver: { context in
|
||||||
|
return detentHeight
|
||||||
|
}),
|
||||||
|
.large(),
|
||||||
|
]
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
|
if case .ducked(_, placeholder: _) = state {
|
||||||
|
return DuckAnimationController(
|
||||||
|
owner: self,
|
||||||
|
needsShrinkAnimation: isDetentChangingDueToGrabberAction
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
extension DuckableContainerViewController: UISheetPresentationControllerDelegate {
|
||||||
|
public func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
|
||||||
|
let snapshot = child.view.snapshotView(afterScreenUpdates: false)!
|
||||||
|
snapshot.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.view.addSubview(snapshot)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
snapshot.leadingAnchor.constraint(equalTo: child.view.leadingAnchor),
|
||||||
|
snapshot.trailingAnchor.constraint(equalTo: child.view.trailingAnchor),
|
||||||
|
snapshot.topAnchor.constraint(equalTo: child.view.topAnchor),
|
||||||
|
snapshot.bottomAnchor.constraint(equalTo: child.view.bottomAnchor),
|
||||||
|
])
|
||||||
|
setOverrideTraitCollection(UITraitCollection(userInterfaceLevel: .elevated), forChild: child)
|
||||||
|
transitionCoordinator!.animate { context in
|
||||||
|
snapshot.layer.opacity = 0
|
||||||
|
} completion: { _ in
|
||||||
|
snapshot.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
|
||||||
|
if sheetPresentationController.selectedDetentIdentifier == .bottom {
|
||||||
|
duckViewController()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// DuckedPlaceholderView.swift
|
||||||
|
// Duckable
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
class DuckedPlaceholderViewController: UIViewController {
|
||||||
|
private unowned let owner: DuckableContainerViewController
|
||||||
|
private let navBar = UINavigationBar()
|
||||||
|
|
||||||
|
var topConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
init(for duckableViewController: DuckableViewController, owner: DuckableContainerViewController) {
|
||||||
|
self.owner = owner
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
let item = UINavigationItem()
|
||||||
|
item.title = duckableViewController.navigationItem.title
|
||||||
|
item.titleView = duckableViewController.navigationItem.titleView
|
||||||
|
navBar.setItems([item], animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
setBackgroundColor()
|
||||||
|
view.layer.cornerRadius = duckedCornerRadius
|
||||||
|
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
view.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
view.layer.shadowOpacity = 0.05
|
||||||
|
|
||||||
|
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(placeholderTapped)))
|
||||||
|
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithTransparentBackground()
|
||||||
|
navBar.standardAppearance = appearance
|
||||||
|
navBar.isUserInteractionEnabled = false
|
||||||
|
navBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(navBar)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
navBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
navBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
navBar.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
setBackgroundColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setBackgroundColor() {
|
||||||
|
// when just using .systemBackground and setting the override trait collection for the placeholder VC,
|
||||||
|
// the color doesn't change until after the dismiss animation occurs (but only when tapping the grabber to duck, not when swiping)
|
||||||
|
view.backgroundColor = .systemBackground.resolvedColor(with: UITraitCollection(traitsFrom: [traitCollection, UITraitCollection(userInterfaceLevel: .elevated)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func placeholderTapped() {
|
||||||
|
owner.unduckViewController()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// Swizzler.swift
|
||||||
|
// Duckable
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
private var hasInitialized = false
|
||||||
|
var isDetentChangingDueToGrabberAction = false
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
func swizzleSheetController() {
|
||||||
|
guard !hasInitialized else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasInitialized = true
|
||||||
|
|
||||||
|
var originalIMP: IMP?
|
||||||
|
let imp = imp_implementationWithBlock({ (self: UISheetPresentationController, param: AnyObject) in
|
||||||
|
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UISheetPresentationController, AnyObject) -> Void).self)
|
||||||
|
isDetentChangingDueToGrabberAction = true
|
||||||
|
original(self, param)
|
||||||
|
isDetentChangingDueToGrabberAction = false
|
||||||
|
} as @convention(block) (UISheetPresentationController, AnyObject) -> Void)
|
||||||
|
let sel = [":", "PrimaryAction", "GrabberDidTrigger", "dropShadowView", "_"].reversed().joined()
|
||||||
|
originalIMP = class_replaceMethod(UISheetPresentationController.self, Selector(sel), imp, "v@:@")
|
||||||
|
if originalIMP == nil {
|
||||||
|
os_log(.fault, log: .default, "Unable to initialize Duckable grabber tap hook")
|
||||||
|
}
|
||||||
|
}
|
|
@ -255,6 +255,8 @@
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */; };
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */; };
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
||||||
|
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; };
|
||||||
|
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; };
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||||
|
@ -611,6 +613,8 @@
|
||||||
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsMode.swift; sourceTree = "<group>"; };
|
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsMode.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = "<group>"; };
|
||||||
|
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; };
|
||||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
||||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -689,6 +693,7 @@
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
||||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
||||||
|
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -935,6 +940,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
|
||||||
|
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||||
|
@ -1362,6 +1368,7 @@
|
||||||
children = (
|
children = (
|
||||||
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */,
|
D674A50727F910F300BA03AC /* Pachyderm */,
|
||||||
|
D6BEA243291A0C83002F4D01 /* Duckable */,
|
||||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||||
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
||||||
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
||||||
|
@ -1528,6 +1535,7 @@
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||||
D6552366289870790048A653 /* ScreenCorners */,
|
D6552366289870790048A653 /* ScreenCorners */,
|
||||||
D63CC701290EC0B8000E19DE /* Sentry */,
|
D63CC701290EC0B8000E19DE /* Sentry */,
|
||||||
|
D6BEA244291A0EDE002F4D01 /* Duckable */,
|
||||||
);
|
);
|
||||||
productName = Tusker;
|
productName = Tusker;
|
||||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||||
|
@ -1990,6 +1998,7 @@
|
||||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
||||||
|
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||||
|
@ -2693,6 +2702,10 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
};
|
};
|
||||||
|
D6BEA244291A0EDE002F4D01 /* Duckable */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Duckable;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import MessageUI
|
import MessageUI
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import Duckable
|
||||||
|
|
||||||
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
|
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
|
||||||
|
|
||||||
|
@ -31,10 +32,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
|
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
showAppOrOnboardingUI(session: session)
|
showAppOrOnboardingUI(session: session)
|
||||||
if connectionOptions.urlContexts.count > 0 {
|
if connectionOptions.urlContexts.count > 0 {
|
||||||
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||||
}
|
}
|
||||||
|
|
||||||
window!.makeKeyAndVisible()
|
window!.makeKeyAndVisible()
|
||||||
|
|
||||||
|
@ -205,7 +206,14 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
mastodonController.getOwnAccount()
|
mastodonController.getOwnAccount()
|
||||||
mastodonController.getOwnInstance()
|
mastodonController.getOwnInstance()
|
||||||
|
|
||||||
return MainSplitViewController(mastodonController: mastodonController)
|
let split = MainSplitViewController(mastodonController: mastodonController)
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone,
|
||||||
|
#available(iOS 16.0, *) {
|
||||||
|
// TODO: maybe the duckable container should be outside the account switching container
|
||||||
|
return DuckableContainerViewController(child: split)
|
||||||
|
} else {
|
||||||
|
return split
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createOnboardingUI() -> UIViewController {
|
func createOnboardingUI() -> UIViewController {
|
||||||
|
|
|
@ -10,14 +10,16 @@ import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import PencilKit
|
import PencilKit
|
||||||
|
import Duckable
|
||||||
|
|
||||||
protocol ComposeHostingControllerDelegate: AnyObject {
|
protocol ComposeHostingControllerDelegate: AnyObject {
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
class ComposeHostingController: UIHostingController<ComposeContainerView>, DuckableViewController {
|
||||||
|
|
||||||
weak var delegate: ComposeHostingControllerDelegate?
|
weak var delegate: ComposeHostingControllerDelegate?
|
||||||
|
weak var duckableDelegate: DuckableViewControllerDelegate?
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
@ -103,16 +105,15 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
override func willMove(toParent parent: UIViewController?) {
|
||||||
super.didMove(toParent: parent)
|
super.willMove(toParent: parent)
|
||||||
|
|
||||||
if let parent = parent {
|
if let parent = parent {
|
||||||
parent.view.addSubview(mainToolbar)
|
parent.view.addSubview(mainToolbar)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
mainToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
mainToolbar.leadingAnchor.constraint(equalTo: parent.view.leadingAnchor),
|
||||||
mainToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
mainToolbar.trailingAnchor.constraint(equalTo: parent.view.trailingAnchor),
|
||||||
// use the top anchor of the toolbar so our additionalSafeAreaInsets (which has the bottom as the toolbar height) don't affect it
|
mainToolbar.bottomAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.bottomAnchor),
|
||||||
mainToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,6 +302,19 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Duckable
|
||||||
|
|
||||||
|
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {
|
||||||
|
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
|
||||||
|
self.mainToolbar.layer.opacity = 0
|
||||||
|
}
|
||||||
|
animator.startAnimation(afterDelay: delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func duckableViewControllerDidFinishAnimatingDuck() {
|
||||||
|
mainToolbar.layer.opacity = 1
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
// MARK: Interaction
|
||||||
|
|
||||||
@objc func cwButtonPressed() {
|
@objc func cwButtonPressed() {
|
||||||
|
@ -335,6 +349,7 @@ extension ComposeHostingController: ComposeUIStateDelegate {
|
||||||
let dismissed = delegate?.dismissCompose(mode: mode) ?? false
|
let dismissed = delegate?.dismissCompose(mode: mode) ?? false
|
||||||
if !dismissed {
|
if !dismissed {
|
||||||
self.dismiss(animated: true)
|
self.dismiss(animated: true)
|
||||||
|
self.duckableDelegate?.duckableViewControllerWillDismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -424,6 +439,8 @@ extension ComposeHostingController: DraftsTableViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// superseded by duckable stuff
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
extension ComposeHostingController: UIAdaptivePresentationControllerDelegate {
|
extension ComposeHostingController: UIAdaptivePresentationControllerDelegate {
|
||||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||||
return Preferences.shared.automaticallySaveDrafts || !draft.hasContent
|
return Preferences.shared.automaticallySaveDrafts || !draft.hasContent
|
||||||
|
|
|
@ -97,7 +97,7 @@ struct ComposeView: View {
|
||||||
globalFrameOutsideList = frame
|
globalFrameOutsideList = frame
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.navigationBarTitle("Compose")
|
.navigationTitle(navTitle)
|
||||||
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
||||||
.alert(isPresented: $isShowingPostErrorAlert) {
|
.alert(isPresented: $isShowingPostErrorAlert) {
|
||||||
Alert(
|
Alert(
|
||||||
|
@ -180,6 +180,15 @@ struct ComposeView: View {
|
||||||
}.frame(height: 50)
|
}.frame(height: 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var navTitle: Text {
|
||||||
|
if let id = draft.inReplyToID,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: id) {
|
||||||
|
return Text("Reply to @\(status.account.acct)")
|
||||||
|
} else {
|
||||||
|
return Text("New Post")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var cancelButton: some View {
|
private var cancelButton: some View {
|
||||||
Button(action: self.cancel) {
|
Button(action: self.cancel) {
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// Duckable+Root.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Duckable
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
extension DuckableContainerViewController: TuskerRootViewController {
|
||||||
|
func presentCompose() {
|
||||||
|
(child as? TuskerRootViewController)?.presentCompose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
|
(child as? TuskerRootViewController)?.select(tab: tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||||
|
return (child as? TuskerRootViewController)?.getTabController(tab: tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func performSearch(query: String) {
|
||||||
|
(child as? TuskerRootViewController)?.performSearch(query: query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentPreferences(completion: (() -> Void)?) {
|
||||||
|
(child as? TuskerRootViewController)?.presentPreferences(completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
|
(child as? TuskerRootViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
|
||||||
|
}
|
||||||
|
}
|
|
@ -373,19 +373,13 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MainSplitViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: TuskerRootViewController {
|
extension MainSplitViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
self.compose()
|
||||||
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
|
||||||
options.preferredPresentationStyle = .prominent
|
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
|
||||||
} else {
|
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
|
||||||
nav.presentationController?.delegate = vc
|
|
||||||
present(nav, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(tab: MainTabBarViewController.Tab) {
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
|
|
|
@ -228,19 +228,13 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MainTabBarViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: TuskerRootViewController {
|
extension MainTabBarViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
compose()
|
||||||
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
|
||||||
options.preferredPresentationStyle = .prominent
|
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
|
||||||
} else {
|
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
|
||||||
nav.presentationController?.delegate = vc
|
|
||||||
present(nav, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(tab: Tab) {
|
func select(tab: Tab) {
|
||||||
|
|
|
@ -96,9 +96,15 @@ extension TuskerNavigationDelegate {
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
||||||
} else {
|
} else {
|
||||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||||
let nav = UINavigationController(rootViewController: compose)
|
if #available(iOS 16.0, *),
|
||||||
nav.presentationController?.delegate = compose
|
presentDuckable(compose) {
|
||||||
present(nav, animated: true)
|
return
|
||||||
|
} else {
|
||||||
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||||
|
let nav = UINavigationController(rootViewController: compose)
|
||||||
|
nav.presentationController?.delegate = compose
|
||||||
|
present(nav, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue