Support fast account switching with new sidebar

This commit is contained in:
Shadowfacts 2024-08-21 14:48:47 -04:00
parent 67e9c1245e
commit cb32c66a59
6 changed files with 148 additions and 11 deletions

View File

@ -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
}
}

View File

@ -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)

View File

@ -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() {

View File

@ -8,7 +8,7 @@
import UIKit
class BaseMainTabBarViewController: UITabBarController {
class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate {
let mastodonController: MastodonController
@ -114,12 +114,15 @@ class BaseMainTabBarViewController: UITabBarController {
fastAccountSwitcher.hide()
}
#endif // !os(visionOS)
}
#if !os(visionOS)
extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
// MARK: FastAccountSwitcherViewControllerDelegate
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
return .iconsTrailing
}
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 }

View File

@ -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 {

View File

@ -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)
}
}