Support fast account switching with new sidebar
This commit is contained in:
parent
67e9c1245e
commit
cb32c66a59
|
@ -9,10 +9,14 @@
|
|||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
class Box<Value> {
|
||||
final class Box<Value> {
|
||||
var wrappedValue: Value
|
||||
|
||||
init(wrappedValue: Value) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
var projectedValue: Box<Value> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import UserAccounts
|
|||
|
||||
@MainActor
|
||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation
|
||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
||||
/// - 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
|
||||
|
@ -31,7 +32,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
#endif
|
||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||
|
||||
var itemOrientation: ItemOrientation = .iconsTrailing
|
||||
private var itemOrientation: ItemOrientation = .iconsTrailing
|
||||
|
||||
init() {
|
||||
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
||||
|
@ -60,6 +61,9 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
}
|
||||
|
||||
func show() {
|
||||
if let delegate {
|
||||
itemOrientation = delegate.fastAccountSwitcherItemOrientation(self)
|
||||
}
|
||||
createAccountViews()
|
||||
// add after creating account views so that the presenter can align based on them
|
||||
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
||||
|
|
|
@ -39,7 +39,16 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
override func 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() {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class BaseMainTabBarViewController: UITabBarController {
|
||||
class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate {
|
||||
|
||||
let mastodonController: MastodonController
|
||||
|
||||
|
@ -115,11 +115,14 @@ class BaseMainTabBarViewController: UITabBarController {
|
|||
}
|
||||
#endif // !os(visionOS)
|
||||
|
||||
}
|
||||
// MARK: FastAccountSwitcherViewControllerDelegate
|
||||
|
||||
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||
return .iconsTrailing
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||
#if !os(visionOS)
|
||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(fastAccountSwitcher.view)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -134,17 +137,21 @@ extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegat
|
|||
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
#endif // !os(visionOS)
|
||||
}
|
||||
|
||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||
#if !os(visionOS)
|
||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||
return false
|
||||
}
|
||||
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
||||
return myProfileButton.bounds.contains(locationInButton)
|
||||
#else
|
||||
return false
|
||||
#endif // !os(visionOS)
|
||||
}
|
||||
}
|
||||
#endif // !os(visionOS)
|
||||
|
||||
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
|
|
|
@ -93,7 +93,6 @@ class MainSplitViewController: UISplitViewController {
|
|||
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||
let switcher = FastAccountSwitcherViewController()
|
||||
fastAccountSwitcher = switcher
|
||||
switcher.itemOrientation = .iconsLeading
|
||||
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
switcher.delegate = self
|
||||
// accessing .view unconditionally loads the view, which we don't want to happen
|
||||
|
@ -664,6 +663,10 @@ extension MainSplitViewController: BackgroundableViewController {
|
|||
}
|
||||
|
||||
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
||||
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
|
||||
return .iconsLeading
|
||||
}
|
||||
|
||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||
view.addSubview(fastAccountSwitcher.view)
|
||||
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
||||
|
@ -677,6 +680,7 @@ extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
|||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||
guard !isCollapsed,
|
||||
let cell = sidebar.myProfileCell() else {
|
||||
|
|
|
@ -28,6 +28,8 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
|
||||
private var navigationStacks = [String: [UIViewController]]()
|
||||
private var isCompact: Bool?
|
||||
@Box fileprivate var myProfileCell: UIView?
|
||||
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -202,6 +204,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
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]) {
|
||||
listsGroup.children = lists.map { list 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)
|
||||
}
|
||||
|
||||
@objc private func sidebarTapped() {
|
||||
fastAccountSwitcher?.hide()
|
||||
}
|
||||
|
||||
fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) {
|
||||
guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else {
|
||||
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.
|
||||
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, *)
|
||||
|
@ -303,6 +371,24 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
|||
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, *)
|
||||
|
@ -393,3 +479,26 @@ extension NewMainTabBarViewController: AccountSwitchableViewController {
|
|||
#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