Support fast account switching with new sidebar
This commit is contained in:
parent
67e9c1245e
commit
cb32c66a59
|
@ -9,10 +9,14 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
class Box<Value> {
|
final class Box<Value> {
|
||||||
var wrappedValue: Value
|
var wrappedValue: Value
|
||||||
|
|
||||||
init(wrappedValue: Value) {
|
init(wrappedValue: Value) {
|
||||||
self.wrappedValue = wrappedValue
|
self.wrappedValue = wrappedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var projectedValue: Box<Value> {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import UserAccounts
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||||
|
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
||||||
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||||
|
@ -31,7 +32,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
#endif
|
#endif
|
||||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
var itemOrientation: ItemOrientation = .iconsTrailing
|
private var itemOrientation: ItemOrientation = .iconsTrailing
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
||||||
|
@ -60,6 +61,9 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
|
if let delegate {
|
||||||
|
itemOrientation = delegate.fastAccountSwitcherItemOrientation(self)
|
||||||
|
}
|
||||||
createAccountViews()
|
createAccountViews()
|
||||||
// add after creating account views so that the presenter can align based on them
|
// add after creating account views so that the presenter can align based on them
|
||||||
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
||||||
|
|
|
@ -39,7 +39,16 @@ class AccountSwitchingContainerViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
embedChild(root)
|
addChild(root)
|
||||||
|
root.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(root.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
root.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
root.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
root.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
root.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
root.didMove(toParent: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didReceiveMemoryWarning() {
|
override func didReceiveMemoryWarning() {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class BaseMainTabBarViewController: UITabBarController {
|
class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate {
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
@ -114,12 +114,15 @@ class BaseMainTabBarViewController: UITabBarController {
|
||||||
fastAccountSwitcher.hide()
|
fastAccountSwitcher.hide()
|
||||||
}
|
}
|
||||||
#endif // !os(visionOS)
|
#endif // !os(visionOS)
|
||||||
|
|
||||||
}
|
// MARK: FastAccountSwitcherViewControllerDelegate
|
||||||
|
|
||||||
#if !os(visionOS)
|
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||||
extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
return .iconsTrailing
|
||||||
|
}
|
||||||
|
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
#if !os(visionOS)
|
||||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -134,17 +137,21 @@ extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegat
|
||||||
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
])
|
])
|
||||||
|
#endif // !os(visionOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
|
#if !os(visionOS)
|
||||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
||||||
return myProfileButton.bounds.contains(locationInButton)
|
return myProfileButton.bounds.contains(locationInButton)
|
||||||
|
#else
|
||||||
|
return false
|
||||||
|
#endif // !os(visionOS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif // !os(visionOS)
|
|
||||||
|
|
||||||
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController! { mastodonController }
|
||||||
|
|
|
@ -93,7 +93,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
if UIDevice.current.userInterfaceIdiom != .mac {
|
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||||
let switcher = FastAccountSwitcherViewController()
|
let switcher = FastAccountSwitcherViewController()
|
||||||
fastAccountSwitcher = switcher
|
fastAccountSwitcher = switcher
|
||||||
switcher.itemOrientation = .iconsLeading
|
|
||||||
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
switcher.delegate = self
|
switcher.delegate = self
|
||||||
// accessing .view unconditionally loads the view, which we don't want to happen
|
// accessing .view unconditionally loads the view, which we don't want to happen
|
||||||
|
@ -664,6 +663,10 @@ extension MainSplitViewController: BackgroundableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||||
|
return .iconsLeading
|
||||||
|
}
|
||||||
|
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
||||||
|
@ -677,6 +680,7 @@ extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
guard !isCollapsed,
|
guard !isCollapsed,
|
||||||
let cell = sidebar.myProfileCell() else {
|
let cell = sidebar.myProfileCell() else {
|
||||||
|
|
|
@ -28,6 +28,8 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||||
|
|
||||||
private var navigationStacks = [String: [UIViewController]]()
|
private var navigationStacks = [String: [UIViewController]]()
|
||||||
private var isCompact: Bool?
|
private var isCompact: Bool?
|
||||||
|
@Box fileprivate var myProfileCell: UIView?
|
||||||
|
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
@ -202,6 +204,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||||
return nav
|
return nav
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if sidebarTapRecognizer == nil,
|
||||||
|
let sidebarView = findSidebarView() {
|
||||||
|
sidebarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
|
||||||
|
sidebarTapRecognizer!.cancelsTouchesInView = false
|
||||||
|
sidebarView.addGestureRecognizer(sidebarTapRecognizer!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func reloadLists(_ lists: [List]) {
|
private func reloadLists(_ lists: [List]) {
|
||||||
listsGroup.children = lists.map { list in
|
listsGroup.children = lists.map { list in
|
||||||
UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in
|
UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in
|
||||||
|
@ -214,6 +227,10 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||||
compose(editing: nil)
|
compose(editing: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func sidebarTapped() {
|
||||||
|
fastAccountSwitcher?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) {
|
fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) {
|
||||||
guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else {
|
guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else {
|
||||||
return
|
return
|
||||||
|
@ -222,6 +239,57 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||||
// The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size.
|
// The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size.
|
||||||
vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0)
|
vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func findSidebarView() -> UIView? {
|
||||||
|
var next = myProfileCell
|
||||||
|
while let cur = next {
|
||||||
|
if cur.superview?.superview === self.view {
|
||||||
|
return cur
|
||||||
|
} else {
|
||||||
|
next = cur.superview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
override func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||||
|
guard !sidebar.isHidden,
|
||||||
|
myProfileCell != nil else {
|
||||||
|
return super.fastAccountSwitcherItemOrientation(fastAccountSwitcher)
|
||||||
|
}
|
||||||
|
return .iconsLeading
|
||||||
|
}
|
||||||
|
|
||||||
|
override func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
guard !sidebar.isHidden,
|
||||||
|
let myProfileCell else {
|
||||||
|
super.fastAccountSwitcherAddToViewHierarchy(fastAccountSwitcher)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
|
||||||
|
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor),
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: selectedTab!.viewController!.view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
override func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
|
guard !sidebar.isHidden,
|
||||||
|
myProfileCell != nil else {
|
||||||
|
return super.fastAccountSwitcher(fastAccountSwitcher, triggerZoneContains: point)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 18.0, *)
|
@available(iOS 18.0, *)
|
||||||
|
@ -303,6 +371,24 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
||||||
vc.view.layoutIfNeeded()
|
vc.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem {
|
||||||
|
let item = UITabSidebarItem(request: request)
|
||||||
|
if case .tab(let tab) = request.content,
|
||||||
|
UIDevice.current.userInterfaceIdiom != .mac,
|
||||||
|
tab.identifier == Tab.myProfile.rawValue {
|
||||||
|
let indicator = FastAccountSwitcherIndicatorView()
|
||||||
|
// need to explicitly set the frame to get it vertically centered
|
||||||
|
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
||||||
|
item.accessories = [
|
||||||
|
.customView(configuration: .init(customView: indicator, placement: .trailing()))
|
||||||
|
]
|
||||||
|
item.contentConfiguration = MyProfileContentConfiguration(wrapped: item.contentConfiguration, view: $myProfileCell) { [unowned self] in
|
||||||
|
$0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 18.0, *)
|
@available(iOS 18.0, *)
|
||||||
|
@ -393,3 +479,26 @@ extension NewMainTabBarViewController: AccountSwitchableViewController {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct MyProfileContentConfiguration: UIContentConfiguration {
|
||||||
|
let wrapped: any UIContentConfiguration
|
||||||
|
@Box var view: UIView?
|
||||||
|
let configureView: (UIView) -> Void
|
||||||
|
|
||||||
|
init(wrapped: any UIContentConfiguration, view: Box<UIView?>, configureView: @escaping (UIView) -> Void) {
|
||||||
|
self.wrapped = wrapped
|
||||||
|
self._view = view
|
||||||
|
self.configureView = configureView
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeContentView() -> any UIView & UIContentView {
|
||||||
|
let view = wrapped.makeContentView()
|
||||||
|
self.view = view
|
||||||
|
configureView(view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: any UIConfigurationState) -> Self {
|
||||||
|
return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue