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 UIKit
import Combine import Combine
import Tabman
import Pageboy import Pageboy
import MastodonAsset import MastodonAsset
import MastodonCore import MastodonCore
import MastodonUI import MastodonUI
public class DiscoveryViewController: TabmanViewController, NeedsDependency { public class DiscoveryViewController: PageboyViewController, NeedsDependency {
public static let containerViewMarginForRegularHorizontalSizeClass: CGFloat = 64 public static let containerViewMarginForRegularHorizontalSizeClass: CGFloat = 64
public static let containerViewMarginForCompactHorizontalSizeClass: CGFloat = 16 public static let containerViewMarginForCompactHorizontalSizeClass: CGFloat = 16
@ -25,71 +24,12 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency {
var viewModel: DiscoveryViewModel! 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() { public override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupAppearance() setupAppearance()
dataSource = viewModel dataSource = viewModel
addBar(
buttonBar,
dataSource: viewModel,
at: .top
)
customizeButtonBarAppearance()
viewModel.$viewControllers viewModel.$viewControllers
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
@ -99,21 +39,9 @@ extension DiscoveryViewController {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
customizeButtonBarAppearance()
}
}
extension DiscoveryViewController {
private func setupAppearance() { private func setupAppearance() {
view.backgroundColor = .secondarySystemBackground view.backgroundColor = .secondarySystemBackground
buttonBarBackgroundView.backgroundColor = .systemBackground
} }
} }
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
@ -148,5 +76,4 @@ extension DiscoveryViewController: PageboyNavigateable {
@objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { @objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
pageboyNavigateKeyCommandHandler(sender) pageboyNavigateKeyCommandHandler(sender)
} }
} }

View File

@ -7,7 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import Tabman
import Pageboy import Pageboy
import MastodonCore import MastodonCore
import MastodonLocalization import MastodonLocalization
@ -24,7 +23,7 @@ final class DiscoveryViewModel {
let discoveryNewsViewController: DiscoveryNewsViewController let discoveryNewsViewController: DiscoveryNewsViewController
let discoveryForYouViewController: DiscoveryForYouViewController let discoveryForYouViewController: DiscoveryForYouViewController
@Published var viewControllers: [ScrollViewContainer & PageViewController] @Published var viewControllers: [ScrollViewContainer]
@MainActor @MainActor
init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) { 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 Combine
import GameplayKit
import MastodonSDK import MastodonSDK
import UIKit import UIKit
import MastodonAsset import MastodonAsset
import MastodonCore import MastodonCore
import MastodonLocalization import MastodonLocalization
import Pageboy
final class HeightFixedSearchBar: UISearchBar {
override var intrinsicContentSize: CGSize {
return CGSize(width: CGFloat.greatestFiniteMagnitude, height: 36)
}
}
final class SearchViewController: UIViewController, NeedsDependency { final class SearchViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } 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 // use AutoLayout could set search bar margin automatically to
// layout alongside with split mode button (on iPad) // layout alongside with split mode button (on iPad)
let titleViewContainer = UIView() let searchBar = UISearchBar()
let searchBar = HeightFixedSearchBar()
// value is the initial search text to set // value is the initial search text to set
let searchBarTapPublisher = PassthroughSubject<String, Never>() let searchBarTapPublisher = PassthroughSubject<String, Never>()
@ -46,11 +39,34 @@ final class SearchViewController: UIViewController, NeedsDependency {
coordinator: coordinator, coordinator: coordinator,
authContext: authContext authContext: authContext
) )
viewController.delegate = self
return viewController 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() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -59,26 +75,40 @@ extension SearchViewController {
title = L10n.Scene.Search.title title = L10n.Scene.Search.title
setupSearchBar() setupSearchBar()
guard let discoveryViewController = self.discoveryViewController else { return } guard let discoveryViewController else { return }
segmentedControlBackground.addSubview(segmentedControl)
view.addSubview(segmentedControlBackground)
addChild(discoveryViewController) addChild(discoveryViewController)
discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(discoveryViewController.view) 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) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.viewDidAppear(animated) searchBar.scopeBarBackgroundImage = .placeholder(color: .systemBackground)
viewModel?.viewDidAppeared.send()
// note:
// need set alpha because (maybe) SDK forget set alpha back
titleViewContainer.alpha = 1
}
} }
extension SearchViewController {
private func setupAppearance() { private func setupAppearance() {
view.backgroundColor = .systemGroupedBackground view.backgroundColor = .systemGroupedBackground
@ -97,13 +127,8 @@ extension SearchViewController {
private func setupSearchBar() { private func setupSearchBar() {
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
searchBar.delegate = self searchBar.delegate = self
searchBar.translatesAutoresizingMaskIntoConstraints = false searchBar.sizeToFit()
titleViewContainer.addSubview(searchBar) navigationItem.titleView = searchBar
searchBar.pinToParent()
searchBar.setContentHuggingPriority(.required, for: .horizontal)
searchBar.setContentHuggingPriority(.required, for: .vertical)
navigationItem.titleView = titleViewContainer
// navigationItem.titleView = searchBar
searchBarTapPublisher searchBarTapPublisher
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false)
@ -122,6 +147,11 @@ extension SearchViewController {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
@objc
private func segmentedControlValueChanged(_ sender: UISegmentedControl) {
discoveryViewController?.scrollToPage(.at(index: sender.selectedSegmentIndex), animated: true)
}
} }
// MARK: - UISearchBarDelegate // MARK: - UISearchBarDelegate
@ -152,3 +182,22 @@ extension SearchViewController: ScrollViewContainer {
discoveryViewController?.scrollToTop(animated: animated) 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 // input
let context: AppContext let context: AppContext
let authContext: AuthContext? let authContext: AuthContext?
let viewDidAppeared = PassthroughSubject<Void, Never>()
// output // output
var diffableDataSource: UICollectionViewDiffableDataSource<SearchSection, SearchItem>? var diffableDataSource: UICollectionViewDiffableDataSource<SearchSection, SearchItem>?