From eace1ea815149fa182c916639c688ef183a9ef86 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Apr 2024 09:55:46 +0200 Subject: [PATCH] Explore Tab: Use a segmented picker under the search bar (IOS-237) (#1261) * Remove custom tab-bar from explore-tab (IOS-237) * Add scope-bar to SearchBar on discovery-screen and attach scrolling etc. (IOS-237) * Replace searchbar-scopes with proper segmented control (IOS-237) The reason for this is that scopes didn't work on iPad in DetailView of the UISplitViewController. I should blog about this. * kill some whitespace --- .../Discovery/DiscoveryViewController.swift | 77 +------------ .../Scene/Discovery/DiscoveryViewModel.swift | 53 +-------- .../Search/Search/SearchViewController.swift | 109 +++++++++++++----- .../Scene/Search/Search/SearchViewModel.swift | 1 - 4 files changed, 82 insertions(+), 158 deletions(-) diff --git a/Mastodon/Scene/Discovery/DiscoveryViewController.swift b/Mastodon/Scene/Discovery/DiscoveryViewController.swift index 9e9707d7d..b74cc250b 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewController.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewController.swift @@ -7,14 +7,13 @@ import UIKit import Combine -import Tabman import Pageboy import MastodonAsset import MastodonCore import MastodonUI -public class DiscoveryViewController: TabmanViewController, NeedsDependency { - +public class DiscoveryViewController: PageboyViewController, NeedsDependency { + public static let containerViewMarginForRegularHorizontalSizeClass: CGFloat = 64 public static let containerViewMarginForCompactHorizontalSizeClass: CGFloat = 16 @@ -25,71 +24,12 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency { var viewModel: DiscoveryViewModel! - private(set) lazy var buttonBar: TMBar.ButtonBar = { - let buttonBar = TMBar.ButtonBar() - buttonBar.backgroundView.style = .custom(view: buttonBarBackgroundView) - buttonBar.layout.interButtonSpacing = 0 - buttonBar.layout.contentInset = .zero - buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color - buttonBar.indicator.weight = .custom(value: 2) - return buttonBar - }() - - let buttonBarBackgroundView: UIView = { - let view = UIView() - let barBottomLine = UIView.separatorLine - barBottomLine.backgroundColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.5) - barBottomLine.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(barBottomLine) - NSLayoutConstraint.activate([ - barBottomLine.leadingAnchor.constraint(equalTo: view.leadingAnchor), - barBottomLine.trailingAnchor.constraint(equalTo: view.trailingAnchor), - barBottomLine.bottomAnchor.constraint(equalTo: view.bottomAnchor), - barBottomLine.heightAnchor.constraint(equalToConstant: 2).priority(.required - 1), - ]) - return view - }() - - func customizeButtonBarAppearance() { - // The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors - // Needs trigger update when `userInterfaceStyle` chagnes - let userInterfaceStyle = traitCollection.userInterfaceStyle - buttonBar.buttons.customize { button in - switch userInterfaceStyle { - case .dark: - // Asset.Colors.Label.primary.color - button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0) - // Asset.Colors.Label.secondary.color - button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0) - default: - // Asset.Colors.Label.primary.color - button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0) - // Asset.Colors.Label.secondary.color - button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6) - } - - button.backgroundColor = .clear - button.contentInset = UIEdgeInsets(top: 12, left: 26, bottom: 12, right: 26) - } - } - -} - -extension DiscoveryViewController { - public override func viewDidLoad() { super.viewDidLoad() setupAppearance() dataSource = viewModel - addBar( - buttonBar, - dataSource: viewModel, - at: .top - ) - customizeButtonBarAppearance() - viewModel.$viewControllers .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -98,22 +38,10 @@ extension DiscoveryViewController { } .store(in: &disposeBag) } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - customizeButtonBarAppearance() - } - -} - -extension DiscoveryViewController { - private func setupAppearance() { view.backgroundColor = .secondarySystemBackground - buttonBarBackgroundView.backgroundColor = .systemBackground } - } // MARK: - ScrollViewContainer @@ -148,5 +76,4 @@ extension DiscoveryViewController: PageboyNavigateable { @objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { pageboyNavigateKeyCommandHandler(sender) } - } diff --git a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift index 630848b27..b6c56e379 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import Tabman import Pageboy import MastodonCore import MastodonLocalization @@ -24,7 +23,7 @@ final class DiscoveryViewModel { let discoveryNewsViewController: DiscoveryNewsViewController let discoveryForYouViewController: DiscoveryForYouViewController - @Published var viewControllers: [ScrollViewContainer & PageViewController] + @Published var viewControllers: [ScrollViewContainer] @MainActor init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) { @@ -110,53 +109,3 @@ extension DiscoveryViewModel: PageboyViewControllerDataSource { } } - -// MARK: - TMBarDataSource -extension DiscoveryViewModel: TMBarDataSource { - func barItem(for bar: TMBar, at index: Int) -> TMBarItemable { - guard !viewControllers.isEmpty, index < viewControllers.count else { - assertionFailure() - return TMBarItem(title: "") - } - return viewControllers[index].tabItem - } -} - -protocol PageViewController: UIViewController { - var tabItemTitle: String { get } - var tabItem: TMBarItemable { get } -} - -// MARK: - PageViewController -extension DiscoveryPostsViewController: PageViewController { - var tabItemTitle: String { L10n.Scene.Discovery.Tabs.posts } - var tabItem: TMBarItemable { - return TMBarItem(title: tabItemTitle) - } -} - - -// MARK: - PageViewController -extension DiscoveryHashtagsViewController: PageViewController { - var tabItemTitle: String { L10n.Scene.Discovery.Tabs.hashtags } - var tabItem: TMBarItemable { - - return TMBarItem(title: tabItemTitle) - } -} - -// MARK: - PageViewController -extension DiscoveryNewsViewController: PageViewController { - var tabItemTitle: String { L10n.Scene.Discovery.Tabs.news } - var tabItem: TMBarItemable { - return TMBarItem(title: tabItemTitle) - } -} - -// MARK: - PageViewController -extension DiscoveryForYouViewController: PageViewController { - var tabItemTitle: String { L10n.Scene.Discovery.Tabs.forYou } - var tabItem: TMBarItemable { - return TMBarItem(title: tabItemTitle) - } -} diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 9af36edd1..b9722ba1c 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -6,18 +6,12 @@ // import Combine -import GameplayKit import MastodonSDK import UIKit import MastodonAsset import MastodonCore import MastodonLocalization - -final class HeightFixedSearchBar: UISearchBar { - override var intrinsicContentSize: CGSize { - return CGSize(width: CGFloat.greatestFiniteMagnitude, height: 36) - } -} +import Pageboy final class SearchViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -30,8 +24,7 @@ final class SearchViewController: UIViewController, NeedsDependency { // use AutoLayout could set search bar margin automatically to // layout alongside with split mode button (on iPad) - let titleViewContainer = UIView() - let searchBar = HeightFixedSearchBar() + let searchBar = UISearchBar() // value is the initial search text to set let searchBarTapPublisher = PassthroughSubject() @@ -46,11 +39,34 @@ final class SearchViewController: UIViewController, NeedsDependency { coordinator: coordinator, authContext: authContext ) + viewController.delegate = self return viewController }() -} -extension SearchViewController { + let segmentedControl: UISegmentedControl + let segmentedControlBackground: UIView + + init() { + segmentedControl = UISegmentedControl(items: [ + L10n.Scene.Discovery.Tabs.posts, + L10n.Scene.Discovery.Tabs.hashtags, + L10n.Scene.Discovery.Tabs.news, + L10n.Scene.Discovery.Tabs.forYou + ]) + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + segmentedControl.selectedSegmentIndex = 0 + + segmentedControlBackground = UIView() + segmentedControlBackground.translatesAutoresizingMaskIntoConstraints = false + segmentedControlBackground.backgroundColor = .systemBackground + + super.init(nibName: nil, bundle: nil) + + segmentedControl.addTarget(self, action: #selector(SearchViewController.segmentedControlValueChanged(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func viewDidLoad() { super.viewDidLoad() @@ -59,26 +75,40 @@ extension SearchViewController { title = L10n.Scene.Search.title setupSearchBar() - guard let discoveryViewController = self.discoveryViewController else { return } + guard let discoveryViewController else { return } + + segmentedControlBackground.addSubview(segmentedControl) + view.addSubview(segmentedControlBackground) + addChild(discoveryViewController) discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(discoveryViewController.view) - discoveryViewController.view.pinToParent() + discoveryViewController.didMove(toParent: self) + + let constraints = [ + segmentedControl.topAnchor.constraint(equalTo: segmentedControlBackground.topAnchor, constant: 8), + segmentedControl.leadingAnchor.constraint(equalTo: segmentedControlBackground.leadingAnchor, constant: 8), + segmentedControlBackground.trailingAnchor.constraint(equalTo: segmentedControl.trailingAnchor, constant: 8), + segmentedControlBackground.bottomAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 8), + + segmentedControlBackground.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + segmentedControlBackground.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: segmentedControlBackground.trailingAnchor), + + discoveryViewController.view.topAnchor.constraint(equalTo: segmentedControlBackground.bottomAnchor), + discoveryViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: discoveryViewController.view.trailingAnchor), + view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: discoveryViewController.view.bottomAnchor), + ] + + NSLayoutConstraint.activate(constraints) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - viewModel?.viewDidAppeared.send() - - // note: - // need set alpha because (maybe) SDK forget set alpha back - titleViewContainer.alpha = 1 + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + searchBar.scopeBarBackgroundImage = .placeholder(color: .systemBackground) } -} -extension SearchViewController { private func setupAppearance() { view.backgroundColor = .systemGroupedBackground @@ -97,13 +127,8 @@ extension SearchViewController { private func setupSearchBar() { searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder searchBar.delegate = self - searchBar.translatesAutoresizingMaskIntoConstraints = false - titleViewContainer.addSubview(searchBar) - searchBar.pinToParent() - searchBar.setContentHuggingPriority(.required, for: .horizontal) - searchBar.setContentHuggingPriority(.required, for: .vertical) - navigationItem.titleView = titleViewContainer -// navigationItem.titleView = searchBar + searchBar.sizeToFit() + navigationItem.titleView = searchBar searchBarTapPublisher .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) @@ -122,6 +147,11 @@ extension SearchViewController { .store(in: &disposeBag) } + @objc + private func segmentedControlValueChanged(_ sender: UISegmentedControl) { + discoveryViewController?.scrollToPage(.at(index: sender.selectedSegmentIndex), animated: true) + } + } // MARK: - UISearchBarDelegate @@ -152,3 +182,22 @@ extension SearchViewController: ScrollViewContainer { discoveryViewController?.scrollToTop(animated: animated) } } + +//MARK: - PageboyViewControllerDelegate +extension SearchViewController: PageboyViewControllerDelegate { + func pageboyViewController(_ pageboyViewController: Pageboy.PageboyViewController, didReloadWith currentViewController: UIViewController, currentPageIndex: Pageboy.PageboyViewController.PageIndex) { + // do nothing + } + + func pageboyViewController(_ pageboyViewController: Pageboy.PageboyViewController, didScrollTo position: CGPoint, direction: Pageboy.PageboyViewController.NavigationDirection, animated: Bool) { + // do nothing + } + + func pageboyViewController(_ pageboyViewController: PageboyViewController, willScrollToPageAt index: PageboyViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { + // do nothing + } + + func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: PageboyViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { + segmentedControl.selectedSegmentIndex = index + } +} diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index 620099bfc..2e00576c2 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -21,7 +21,6 @@ final class SearchViewModel: NSObject { // input let context: AppContext let authContext: AuthContext? - let viewDidAppeared = PassthroughSubject() // output var diffableDataSource: UICollectionViewDiffableDataSource?