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:
parent
cc9faf5aea
commit
eace1ea815
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>?
|
||||||
|
|
Loading…
Reference in New Issue