diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift index 0c4fc367..57f3ac51 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -9,6 +9,8 @@ import UIKit protocol FastAccountSwitcherViewControllerDelegate: AnyObject { + 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 } @@ -20,11 +22,13 @@ class FastAccountSwitcherViewController: UIViewController { @IBOutlet weak var blurContentView: UIView! @IBOutlet weak var accountsStack: UIStackView! - private var accountViews: [FastSwitchingAccountView] = [] + private(set) var accountViews: [FastSwitchingAccountView] = [] private var lastSelectedAccountViewIndex: Int? private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator? private var touchBeganFeedbackWorkItem: DispatchWorkItem? + var itemOrientation: ItemOrientation = .iconsTrailing + init() { super.init(nibName: "FastAccountSwitcherViewController", bundle: .main) } @@ -51,6 +55,15 @@ class FastAccountSwitcherViewController: UIViewController { func show() { createAccountViews() + // add after creating account views so that the presenter can align based on them + delegate?.fastAccountSwitcherAddToViewHierarchy(self) + + switch itemOrientation { + case .iconsLeading: + accountsStack.alignment = .leading + case .iconsTrailing: + accountsStack.alignment = .trailing + } view.isHidden = false @@ -87,22 +100,27 @@ class FastAccountSwitcherViewController: UIViewController { } func hide(completion: (() -> Void)? = nil) { + guard view.superview != nil else { + return + } lastSelectedAccountViewIndex = nil selectionChangedFeedbackGenerator = nil UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) { self.view.alpha = 0 } completion: { (_) in + // todo: probably remove these two lines self.view.alpha = 1 self.view.isHidden = true completion?() + self.view.removeFromSuperview() } } private func createAccountViews() { accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } - let addAccountPlaceholder = FastSwitchingAccountView() + let addAccountPlaceholder = FastSwitchingAccountView(orientation: itemOrientation) accountsStack.addArrangedSubview(addAccountPlaceholder) accountViews = [ @@ -110,7 +128,7 @@ class FastAccountSwitcherViewController: UIViewController { ] for account in LocalData.shared.accounts { - let accountView = FastSwitchingAccountView(account: account) + let accountView = FastSwitchingAccountView(account: account, orientation: itemOrientation) accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID accountsStack.addArrangedSubview(accountView) accountViews.append(accountView) @@ -172,10 +190,9 @@ class FastAccountSwitcherViewController: UIViewController { handleGestureMoved(to: location) case .ended: - let location = recognizer.location(in: view) if let index = lastSelectedAccountViewIndex { switchAccount(newIndex: index) - } else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: location) ?? false) { + } else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: recognizer.location(in: recognizer.view)) ?? false) { hide() } @@ -257,9 +274,16 @@ class FastAccountSwitcherViewController: UIViewController { } +extension FastAccountSwitcherViewController { + enum ItemOrientation { + case iconsLeading + case iconsTrailing + } +} + extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let point = gestureRecognizer.location(in: view) + let point = gestureRecognizer.location(in: gestureRecognizer.view) return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false } } diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.xib b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.xib index 83539b80..ad346055 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.xib +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -12,7 +12,6 @@ - @@ -21,10 +20,6 @@ - - - - @@ -38,7 +33,7 @@ - + @@ -48,20 +43,11 @@ - - - - - - - - - diff --git a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift index 48241f37..a939a0e4 100644 --- a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift +++ b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift @@ -35,19 +35,23 @@ class FastSwitchingAccountView: UIView { } } + private let orientation: FastAccountSwitcherViewController.ItemOrientation + private let usernameLabel = UILabel() private let instanceLabel = UILabel() private let avatarImageView = UIImageView() private var avatarRequest: ImageCache.Request? - init(account: LocalData.UserAccountInfo) { + init(account: LocalData.UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) { + self.orientation = orientation super.init(frame: .zero) commonInit() setupAccount(account: account) } - init() { + init(orientation: FastAccountSwitcherViewController.ItemOrientation) { + self.orientation = orientation super.init(frame: .zero) commonInit() setupPlaceholder() @@ -70,7 +74,6 @@ class FastSwitchingAccountView: UIView { ]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical - stackView.alignment = .trailing addSubview(stackView) avatarImageView.translatesAutoresizingMaskIntoConstraints = false @@ -83,15 +86,30 @@ class FastSwitchingAccountView: UIView { NSLayoutConstraint.activate([ avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8), avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), - avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor), avatarImageView.widthAnchor.constraint(equalToConstant: 40), avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), - stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8), stackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), ]) + switch orientation { + case .iconsLeading: + stackView.alignment = .leading + NSLayoutConstraint.activate([ + avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + + case .iconsTrailing: + stackView.alignment = .trailing + NSLayoutConstraint.activate([ + avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8), + ]) + } + updateLabelColors() } diff --git a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift index da49a34c..8eff2cbb 100644 --- a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift +++ b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift @@ -40,6 +40,14 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { config.text = item.title config.image = UIImage(systemName: item.imageName!) self.contentConfiguration = config + if UIDevice.current.userInterfaceIdiom != .mac { + let indicator = FastAccountSwitcherIndicatorView() + // need to explicitly set the frame to get it vertically centered + indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) + accessories = [ + .customView(configuration: .init(customView: indicator, placement: .trailing())) + ] + } let mastodonController = MastodonController.getForAccount(account) guard let account = try? await mastodonController.getOwnAccount(), diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index ae0c25cc..40d79f26 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -330,6 +330,14 @@ class MainSidebarViewController: UIViewController { } } + func myProfileCell() -> UICollectionViewCell? { + guard let indexPath = dataSource.indexPath(for: .tab(.myProfile)), + let item = collectionView.cellForItem(at: indexPath) else { + return nil + } + return item + } + } extension MainSidebarViewController { @@ -514,6 +522,11 @@ extension MainSidebarViewController: UICollectionViewDelegate { let activity = userActivityForItem(item) else { return nil } + if case .tab(.myProfile) = item, + // only disable context menu on long-press, to allow fast account switching + collectionView.contextMenuInteraction?.menuAppearance == .rich { + return nil + } return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in return UIMenu(children: [ UIWindowScene.ActivationAction({ action in @@ -530,6 +543,9 @@ extension MainSidebarViewController: UICollectionViewDragDelegate { let activity = userActivityForItem(item) else { return [] } + if case .tab(.myProfile) = item { + return [] + } let provider = NSItemProvider(object: activity) return [UIDragItem(itemProvider: provider)] diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index fe6b8fdf..86ddf421 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -13,6 +13,7 @@ class MainSplitViewController: UISplitViewController { weak var mastodonController: MastodonController! private var sidebar: MainSidebarViewController! + private var fastAccountSwitcher: FastAccountSwitcherViewController? // Keep track of navigation stacks per-item so that we can only ever use a single navigation controller private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:] @@ -23,14 +24,6 @@ class MainSplitViewController: UISplitViewController { viewController(for: .secondary) as? UINavigationController } - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - if UIDevice.current.userInterfaceIdiom == .phone { - return .portrait - } else { - return .all - } - } - init(mastodonController: MastodonController) { self.mastodonController = mastodonController @@ -60,6 +53,18 @@ class MainSplitViewController: UISplitViewController { select(item: .tab(.timelines)) } + if UIDevice.current.userInterfaceIdiom != .mac { + let switcher = FastAccountSwitcherViewController() + fastAccountSwitcher = switcher + switcher.itemOrientation = .iconsLeading + switcher.view.translatesAutoresizingMaskIntoConstraints = false + switcher.delegate = self + sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture()) + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped)) + tapRecognizer.cancelsTouchesInView = false + sidebar.view.addGestureRecognizer(tapRecognizer) + } + tabBarViewController = MainTabBarViewController(mastodonController: mastodonController) setViewController(tabBarViewController, for: .compact) @@ -100,6 +105,10 @@ class MainSplitViewController: UISplitViewController { sidebar.select(item: item, animated: false) select(item: item) } + + @objc private func sidebarTapped() { + fastAccountSwitcher?.hide() + } } @@ -459,3 +468,27 @@ extension MainSplitViewController: BackgroundableViewController { } } } + +extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate { + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + view.addSubview(fastAccountSwitcher.view) + let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)! + let myProfileCell = sidebar.myProfileCell()! + NSLayoutConstraint.activate([ + currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor), + + fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: sidebar.view.trailingAnchor), + fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), + fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + guard !isCollapsed, + let cell = sidebar.myProfileCell() else { + return false + } + let cellRect = cell.convert(cell.bounds, to: sidebar.view) + return cellRect.contains(point) + } +} diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index a1a004d8..79b658c9 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -60,13 +60,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { fastAccountSwitcher = FastAccountSwitcherViewController() fastAccountSwitcher.delegate = self fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(fastAccountSwitcher.view) - NSLayoutConstraint.activate([ - fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), - fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor), - ]) tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture()) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped)) @@ -77,18 +70,17 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { fastSwitcherIndicator = FastAccountSwitcherIndicatorView() fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false view.addSubview(fastSwitcherIndicator) - NSLayoutConstraint.activate([ - fastSwitcherIndicator.widthAnchor.constraint(equalToConstant: 10), - fastSwitcherIndicator.heightAnchor.constraint(equalToConstant: 12), - ]) } tabBar.isSpringLoaded = true } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + // i hate that we have to do this so often :S + // but doing it only in viewWillAppear makes it not appear initially + // doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed repositionFastSwitcherIndicator() } @@ -201,11 +193,23 @@ extension MainTabBarViewController { } extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate { + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + view.addSubview(fastAccountSwitcher.view) + NSLayoutConstraint.activate([ + fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor), + + fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), + fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor), + ]) + } + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { guard let myProfileButton = findMyProfileTabBarButton() else { return false } - let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view) + let locationInButton = myProfileButton.convert(point, from: tabBar) return myProfileButton.bounds.contains(locationInButton) } } diff --git a/Tusker/Views/FastAccountSwitcherIndicatorView.swift b/Tusker/Views/FastAccountSwitcherIndicatorView.swift index e2100747..cbe6f961 100644 --- a/Tusker/Views/FastAccountSwitcherIndicatorView.swift +++ b/Tusker/Views/FastAccountSwitcherIndicatorView.swift @@ -10,6 +10,10 @@ import UIKit class FastAccountSwitcherIndicatorView: UIView { + override var intrinsicContentSize: CGSize { + CGSize(width: 10, height: 12) + } + override init(frame: CGRect) { super.init(frame: frame)