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
This commit is contained in:
Nathan Mattes 2024-04-05 09:55:46 +02:00 committed by GitHub
parent cc9faf5aea
commit eace1ea815
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 82 additions and 158 deletions

View File

@ -7,13 +7,12 @@
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
@ -99,21 +39,9 @@ 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)
}
}

View File

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

View File

@ -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<String, Never>()
@ -46,11 +39,34 @@ final class SearchViewController: UIViewController, NeedsDependency {
coordinator: coordinator,
authContext: authContext
)
viewController.delegate = self
return viewController
}()
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)
}
extension SearchViewController {
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
}
}

View File

@ -21,7 +21,6 @@ final class SearchViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext?
let viewDidAppeared = PassthroughSubject<Void, Never>()
// output
var diffableDataSource: UICollectionViewDiffableDataSource<SearchSection, SearchItem>?