From fc888b168cb1f402f5f9d7f235158f848d335df5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Nov 2020 19:39:42 -0500 Subject: [PATCH] Add fast account switching on iPhone --- Tusker.xcodeproj/project.pbxproj | 20 ++ Tusker/Extensions/UIFont+Traits.swift | 2 +- Tusker/LocalData.swift | 6 +- .../FastAccountSwitcherViewController.swift | 244 ++++++++++++++++++ .../FastAccountSwitcherViewController.xib | 68 +++++ .../FastSwitchingAccountView.swift | 116 +++++++++ .../Main/MainTabBarViewController.swift | 35 +++ Tusker/Views/ContentTextView.swift | 4 +- 8 files changed, 489 insertions(+), 6 deletions(-) create mode 100644 Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift create mode 100644 Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.xib create mode 100644 Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index f530a29f..69806303 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -211,6 +211,9 @@ D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; }; D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; }; D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */; }; + D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; }; + D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; }; + D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; }; D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */; }; D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; @@ -556,6 +559,9 @@ D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = ""; }; D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = ""; }; D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListTableViewController.swift; sourceTree = ""; }; + D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherViewController.swift; sourceTree = ""; }; + D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FastAccountSwitcherViewController.xib; sourceTree = ""; }; + D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastSwitchingAccountView.swift; sourceTree = ""; }; D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIMocks.swift; sourceTree = ""; }; D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = ""; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = ""; }; @@ -932,6 +938,7 @@ D6F2E960249E772F005846BB /* Crash Reporter */, D641C782213DD7F0004B4513 /* Main */, D641C783213DD7FE004B4513 /* Onboarding */, + D6A4DCC92553666600D9DE31 /* Fast Account Switcher */, D641C781213DD7DD004B4513 /* Timeline */, D641C784213DD819004B4513 /* Profile */, D641C785213DD83B004B4513 /* Conversation */, @@ -1227,6 +1234,16 @@ path = "Status Action Account List"; sourceTree = ""; }; + D6A4DCC92553666600D9DE31 /* Fast Account Switcher */ = { + isa = PBXGroup; + children = ( + D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */, + D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */, + D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */, + ); + path = "Fast Account Switcher"; + sourceTree = ""; + }; D6A5BB2623BAC88E003BF21D /* Preferences */ = { isa = PBXGroup; children = ( @@ -1691,6 +1708,7 @@ D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */, + D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1830,7 +1848,9 @@ D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, 0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */, + D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */, D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */, + D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */, D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */, D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, diff --git a/Tusker/Extensions/UIFont+Traits.swift b/Tusker/Extensions/UIFont+Traits.swift index 36d40b2f..c10827d7 100644 --- a/Tusker/Extensions/UIFont+Traits.swift +++ b/Tusker/Extensions/UIFont+Traits.swift @@ -10,7 +10,7 @@ import UIKit extension UIFont { - func addingTraits(_ traits: UIFontDescriptor.SymbolicTraits, size: CGFloat? = nil) -> UIFont? { + func withTraits(_ traits: UIFontDescriptor.SymbolicTraits, size: CGFloat? = nil) -> UIFont? { let descriptor = self.fontDescriptor guard let newDescriptor = descriptor.withSymbolicTraits([descriptor.symbolicTraits, traits]) else { return nil diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index 9d985161..2c70a68a 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -87,7 +87,7 @@ class LocalData: ObservableObject { } private let mostRecentAccountKey = "mostRecentAccount" - private var mostRecentAccount: String? { + private(set) var mostRecentAccountID: String? { get { return defaults.string(forKey: mostRecentAccountKey) } @@ -131,7 +131,7 @@ class LocalData: ObservableObject { func getMostRecentAccount() -> UserAccountInfo? { guard onboardingComplete else { return nil } let mostRecent: UserAccountInfo? - if let id = mostRecentAccount { + if let id = mostRecentAccountID { mostRecent = accounts.first { $0.id == id } } else { mostRecent = nil @@ -140,7 +140,7 @@ class LocalData: ObservableObject { } func setMostRecentAccount(_ account: UserAccountInfo?) { - mostRecentAccount = account?.id + mostRecentAccountID = account?.id } } diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift new file mode 100644 index 00000000..1eae776f --- /dev/null +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -0,0 +1,244 @@ +// +// FastAccountSwitcherViewController.swift +// Tusker +// +// Created by Shadowfacts on 11/4/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +protocol FastAccountSwitcherViewControllerDelegate: class { + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool +} + +class FastAccountSwitcherViewController: UIViewController { + + weak var delegate: FastAccountSwitcherViewControllerDelegate? + + @IBOutlet weak var dimmingView: UIView! + @IBOutlet weak var blurContentView: UIView! + @IBOutlet weak var accountsStack: UIStackView! + + private var accountViews: [FastSwitchingAccountView] = [] + private var lastSelectedAccountViewIndex: Int? + private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator? + private var touchBeganFeedbackWorkItem: DispatchWorkItem? + + init() { + super.init(nibName: "FastAccountSwitcherViewController", bundle: .main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.isHidden = true + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))) + accountsStack.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) + } + + func createSwitcherGesture() -> UIGestureRecognizer { + let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) + recognizer.delegate = self + recognizer.minimumPressDuration = 0.25 + return recognizer + } + + func show() { + createAccountViews() + + view.isHidden = false + + if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat { + view.alpha = 0 + UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) { + self.view.alpha = 1 + } + } else { + let totalDuration: TimeInterval = 0.5 + UIView.animateKeyframes(withDuration: totalDuration, delay: 0, options: .allowUserInteraction) { + self.view.alpha = 0 + UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) { + self.view.alpha = 1 + } + + for (index, accountView) in self.accountViews.reversed().enumerated() { + let relStart = 0.5 * Double(index) / Double(self.accountsStack.arrangedSubviews.count) + let relDuration = 0.5 * 2 / Double(self.accountsStack.arrangedSubviews.count) + + accountView.alpha = 0 + accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) + UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration) { + accountView.alpha = 1 + accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + } + + UIView.addKeyframe(withRelativeStartTime: relStart + relDuration, relativeDuration: relDuration) { + accountView.transform = .identity + } + } + } + } + } + + func hide() { + lastSelectedAccountViewIndex = nil + selectionChangedFeedbackGenerator = nil + + UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) { + self.view.alpha = 0 + } completion: { (_) in + self.view.alpha = 1 + self.view.isHidden = true + } + } + + private func createAccountViews() { + accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } + accountViews = [] + + for account in LocalData.shared.accounts { + let accountView = FastSwitchingAccountView(account: account) + accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID + accountsStack.addArrangedSubview(accountView) + accountViews.append(accountView) + } + } + + private func accountViewIndex(at point: CGPoint) -> Int? { + for (index, accountView) in accountViews.enumerated() { + let pointInAccountView = accountView.convert(point, from: view) + if accountView.bounds.contains(pointInAccountView) { + return index + } + } + return nil + } + + private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) { + let account = LocalData.shared.accounts[newIndex] + + if account.id != LocalData.shared.mostRecentAccountID { + if hapticFeedback { + selectionChangedFeedbackGenerator?.impactOccurred() + } + selectionChangedFeedbackGenerator = nil + (view.window!.windowScene!.delegate as! SceneDelegate).activateAccount(account) + } + } + + // MARK: - Interaction + + @objc private func handleLongPress(_ recognizer: UIGestureRecognizer) { + switch recognizer.state { + case .began: + selectionChangedFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + selectionChangedFeedbackGenerator?.impactOccurred() + selectionChangedFeedbackGenerator?.prepare() + + show() + + case .changed: + let location = recognizer.location(in: view) + + handleGestureMoved(to: location) + + case .ended: + let location = recognizer.location(in: view) + if let index = lastSelectedAccountViewIndex { + switchAccount(newIndex: index) + hide() + } else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: location) ?? false) { + hide() + } + + default: + break + } + } + + @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + let location = recognizer.location(in: view) + + handleGestureMoved(to: location) + + case .ended: + if let index = lastSelectedAccountViewIndex { + switchAccount(newIndex: index) + hide() + } + + default: + break + } + } + + private func handleGestureMoved(to location: CGPoint, hapticFeedback: Bool = true) { + let selectedAccountViewIndex = accountViewIndex(at: location) + + if selectedAccountViewIndex != lastSelectedAccountViewIndex { + if let lastSelected = lastSelectedAccountViewIndex { + accountViews[lastSelected].isSelected = false + } + if let newSelected = selectedAccountViewIndex { + accountViews[newSelected].isSelected = true + } + lastSelectedAccountViewIndex = selectedAccountViewIndex + + if hapticFeedback { + selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5) + selectionChangedFeedbackGenerator?.prepare() + } + } + } + + @objc private func handleTap(_ recognizer: UITapGestureRecognizer) { + if let tappedIndex = accountViewIndex(at: recognizer.location(in: view)) { + // cancel the selection-changed feedback initiated in touchesBegan so that, + // upon switching, we don't trigger a double feedback + touchBeganFeedbackWorkItem?.cancel() + touchBeganFeedbackWorkItem = nil + + switchAccount(newIndex: tappedIndex) + } + + hide() + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if touches.count == 1, + let touch = touches.first, + accountsStack.bounds.contains(touch.location(in: accountsStack)) { + handleGestureMoved(to: touch.location(in: view), hapticFeedback: false) + + // don't trigger the haptic feedback immedaitely + // if the user is merely tapping, not initiating a pan, we don't want to trigger a double-impact + // if the tap ends very quickly, this will be cancelled + touchBeganFeedbackWorkItem = DispatchWorkItem { + self.selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5) + self.selectionChangedFeedbackGenerator?.prepare() + self.touchBeganFeedbackWorkItem = nil + } + // 100ms determined experimentally to be fast enough that there's not a hugely-perceivable delay when beginning a pan gesture + // and slow enough that it's longer than most reasonable-speed taps + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: touchBeganFeedbackWorkItem!) + } + + super.touchesBegan(touches, with: event) + } + +} + +extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let point = gestureRecognizer.location(in: 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 new file mode 100644 index 00000000..83539b80 --- /dev/null +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.xib @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift new file mode 100644 index 00000000..5a047842 --- /dev/null +++ b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift @@ -0,0 +1,116 @@ +// +// FastSwitchingAccountView.swift +// Tusker +// +// Created by Shadowfacts on 11/4/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class FastSwitchingAccountView: UIView { + + let account: LocalData.UserAccountInfo + + private static let selectedColor = UIColor { (traits) in + if traits.userInterfaceStyle == .dark { + return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 100 / 100, alpha: 1) + } else { + return UIColor(hue: 211 / 360, saturation: 70 / 100, brightness: 100 / 100, alpha: 1) + } + } + private static let currentColor = UIColor { (traits) in + if traits.userInterfaceStyle == .dark { + return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 85 / 100, alpha: 1) + } else { + return UIColor(hue: 211 / 360, saturation: 50 / 100, brightness: 100 / 100, alpha: 1) + } + } + var isSelected = false { + didSet { + updateLabelColors() + } + } + var isCurrent = false { + didSet { + updateLabelColors() + } + } + + private let usernameLabel = UILabel() + private let instanceLabel = UILabel() + + private var avatarRequest: ImageCache.Request? + + init(account: LocalData.UserAccountInfo) { + self.account = account + + super.init(frame: .zero) + + usernameLabel.textColor = .white + usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0) + usernameLabel.text = account.username + + instanceLabel.textColor = .white + instanceLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0) + instanceLabel.text = account.instanceURL.host! + + let stackView = UIStackView(arrangedSubviews: [ + usernameLabel, + instanceLabel + ]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .trailing + addSubview(stackView) + + let avatarImageView = UIImageView() + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + avatarImageView.layer.masksToBounds = true + avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 40 + avatarImageView.image = UIImage(systemName: Preferences.shared.avatarStyle == .circle ? "person.crop.circle" : "person.crop.square") + avatarImageView.contentMode = .scaleAspectFit + addSubview(avatarImageView) + + 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), + ]) + + let controller = MastodonController.getForAccount(account) + controller.getOwnAccount { [weak self] (result) in + guard let self = self, case let .success(account) = result else { return } + self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (data) in + guard let avatarImageView = avatarImageView, let data = data, let image = UIImage(data: data) else { return } + DispatchQueue.main.async { + avatarImageView.image = image + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateLabelColors() { + let color: UIColor + if isSelected { + color = FastSwitchingAccountView.selectedColor + } else if isCurrent { + color = FastSwitchingAccountView.currentColor + } else { + color = .white + } + usernameLabel.textColor = color + instanceLabel.textColor = color + } + +} diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 58ae420c..390dcc5c 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -14,6 +14,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { weak var mastodonController: MastodonController! private var composePlaceholder: UIViewController! + private var fastAccountSwitcher: FastAccountSwitcherViewController! var selectedTab: Tab { return Tab(rawValue: selectedIndex)! @@ -53,6 +54,26 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { embedInNavigationController(Tab.explore.createViewController(mastodonController)), embedInNavigationController(Tab.myProfile.createViewController(mastodonController)), ] + + 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)) + tapRecognizer.cancelsTouchesInView = false + tabBar.addGestureRecognizer(tapRecognizer) + } + + @objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) { + fastAccountSwitcher.hide() } func embedInNavigationController(_ vc: UIViewController) -> UINavigationController { @@ -120,6 +141,20 @@ extension MainTabBarViewController { } } +extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate { + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") } + // sanity check that there is 1 button per VC + guard tabBarButtons.count == viewControllers!.count, + let myProfileButton = tabBarButtons.last else { + return false + } + + let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view) + return myProfileButton.bounds.contains(locationInButton) + } +} + extension MainTabBarViewController: TuskerRootViewController { func presentCompose() { let vc = ComposeHostingController(mastodonController: mastodonController) diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index f568e405..42e83cab 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -137,10 +137,10 @@ class ContentTextView: LinkTextView { attributed.append(NSAttributedString(string: "\n\n")) case "em", "i": let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font! - attributed.addAttribute(.font, value: currentFont.addingTraits(.traitItalic)!, range: attributed.fullRange) + attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange) case "strong", "b": let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font! - attributed.addAttribute(.font, value: currentFont.addingTraits(.traitBold)!, range: attributed.fullRange) + attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange) case "del": attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange) case "code":