fix: search bar active with re-layout animation on iPad device issue

This commit is contained in:
CMK 2022-04-15 17:20:41 +08:00
parent f5aaf2737f
commit 8a33ed9f9f
6 changed files with 144 additions and 119 deletions

View File

@ -144,7 +144,7 @@ extension SceneCoordinator {
case popover(sourceView: UIView) case popover(sourceView: UIView)
case panModal case panModal
case custom(transitioningDelegate: UIViewControllerTransitioningDelegate) case custom(transitioningDelegate: UIViewControllerTransitioningDelegate)
case customPush case customPush(animated: Bool)
case safariPresent(animated: Bool, completion: (() -> Void)? = nil) case safariPresent(animated: Bool, completion: (() -> Void)? = nil)
case alertController(animated: Bool, completion: (() -> Void)? = nil) case alertController(animated: Bool, completion: (() -> Void)? = nil)
case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil) case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil)
@ -339,10 +339,10 @@ extension SceneCoordinator {
viewController.transitioningDelegate = transitioningDelegate viewController.transitioningDelegate = transitioningDelegate
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
case .customPush: case .customPush(let animated):
// set delegate in view controller // set delegate in view controller
assert(sender?.navigationController?.delegate != nil) assert(sender?.navigationController?.delegate != nil)
sender?.navigationController?.pushViewController(viewController, animated: true) sender?.navigationController?.pushViewController(viewController, animated: animated)
case .safariPresent(let animated, let completion): case .safariPresent(let animated, let completion):
if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene { if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene {

View File

@ -15,7 +15,7 @@ import MastodonLocalization
final class HeightFixedSearchBar: UISearchBar { final class HeightFixedSearchBar: UISearchBar {
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
return CGSize(width: CGFloat.greatestFiniteMagnitude, height: 44) return CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
} }
} }
@ -35,20 +35,20 @@ final class SearchViewController: UIViewController, NeedsDependency {
// layout alongside with split mode button (on iPad) // layout alongside with split mode button (on iPad)
let titleViewContainer = UIView() let titleViewContainer = UIView()
let searchBar = HeightFixedSearchBar() let searchBar = HeightFixedSearchBar()
let collectionView: UICollectionView = { // let collectionView: UICollectionView = {
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) // var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.backgroundColor = .clear // configuration.backgroundColor = .clear
configuration.headerMode = .supplementary // configuration.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: configuration) // let layout = UICollectionViewCompositionalLayout.list(using: configuration)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) // let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .clear // collectionView.backgroundColor = .clear
return collectionView // return collectionView
}() // }()
let searchBarTapPublisher = PassthroughSubject<Void, Never>() let searchBarTapPublisher = PassthroughSubject<Void, Never>()
private(set) lazy var trendViewController: DiscoveryViewController = { private(set) lazy var discoveryViewController: DiscoveryViewController = {
let viewController = DiscoveryViewController() let viewController = DiscoveryViewController()
viewController.context = context viewController.context = context
viewController.coordinator = coordinator viewController.coordinator = coordinator
@ -78,29 +78,31 @@ extension SearchViewController {
setupSearchBar() setupSearchBar()
collectionView.translatesAutoresizingMaskIntoConstraints = false // collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) // view.addSubview(collectionView)
// NSLayoutConstraint.activate([
// collectionView.topAnchor.constraint(equalTo: view.topAnchor),
// collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// ])
//
// collectionView.delegate = self
// viewModel.setupDiffableDataSource(
// collectionView: collectionView
// )
addChild(discoveryViewController)
discoveryViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(discoveryViewController.view)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor), discoveryViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), discoveryViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), discoveryViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), discoveryViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])
collectionView.delegate = self // discoveryViewController.view.isHidden = true
viewModel.setupDiffableDataSource(
collectionView: collectionView
)
addChild(trendViewController)
trendViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(trendViewController.view)
NSLayoutConstraint.activate([
trendViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
trendViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
trendViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
trendViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -130,7 +132,10 @@ extension SearchViewController {
searchBar.trailingAnchor.constraint(equalTo: titleViewContainer.trailingAnchor), searchBar.trailingAnchor.constraint(equalTo: titleViewContainer.trailingAnchor),
searchBar.bottomAnchor.constraint(equalTo: titleViewContainer.bottomAnchor), searchBar.bottomAnchor.constraint(equalTo: titleViewContainer.bottomAnchor),
]) ])
searchBar.setContentHuggingPriority(.required, for: .horizontal)
searchBar.setContentHuggingPriority(.required, for: .vertical)
navigationItem.titleView = titleViewContainer 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)
@ -140,7 +145,10 @@ extension SearchViewController {
let searchDetailViewModel = SearchDetailViewModel() let searchDetailViewModel = SearchDetailViewModel()
searchDetailViewModel.needsBecomeFirstResponder = true searchDetailViewModel.needsBecomeFirstResponder = true
self.navigationController?.delegate = self.searchTransitionController self.navigationController?.delegate = self.searchTransitionController
self.coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .customPush) // FIXME:
// use `.customPush(animated: false)` false to disable navigation bar animation for searchBar layout
// but that should be a fade transition whe fixed size searchBar
self.coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .customPush(animated: false))
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
@ -168,21 +176,21 @@ extension SearchViewController: UISearchControllerDelegate {
} }
// MARK: - UICollectionViewDelegate // MARK: - UICollectionViewDelegate
extension SearchViewController: UICollectionViewDelegate { //extension SearchViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select item at: \(indexPath.debugDescription)") // logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select item at: \(indexPath.debugDescription)")
//
defer { // defer {
collectionView.deselectItem(at: indexPath, animated: true) // collectionView.deselectItem(at: indexPath, animated: true)
} // }
//
guard let diffableDataSource = viewModel.diffableDataSource else { return } // guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } // guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
//
switch item { // switch item {
case .trend(let hashtag): // case .trend(let hashtag):
let viewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name) // let viewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name)
coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: self, transition: .show) // coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: self, transition: .show)
} // }
} // }
} //}

View File

@ -8,35 +8,35 @@
import UIKit import UIKit
import MastodonSDK import MastodonSDK
extension SearchViewModel { //extension SearchViewModel {
//
func setupDiffableDataSource( // func setupDiffableDataSource(
collectionView: UICollectionView // collectionView: UICollectionView
) { // ) {
diffableDataSource = SearchSection.diffableDataSource( // diffableDataSource = SearchSection.diffableDataSource(
collectionView: collectionView, // collectionView: collectionView,
context: context // context: context
) // )
//
var snapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>() // var snapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>()
snapshot.appendSections([.trend]) // snapshot.appendSections([.trend])
diffableDataSource?.apply(snapshot) // diffableDataSource?.apply(snapshot)
//
$hashtags // $hashtags
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] hashtags in // .sink { [weak self] hashtags in
guard let self = self else { return } // guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return } // guard let diffableDataSource = self.diffableDataSource else { return }
//
var snapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>() // var snapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>()
snapshot.appendSections([.trend]) // snapshot.appendSections([.trend])
//
let trendItems = hashtags.map { SearchItem.trend($0) } // let trendItems = hashtags.map { SearchItem.trend($0) }
snapshot.appendItems(trendItems, toSection: .trend) // snapshot.appendItems(trendItems, toSection: .trend)
//
diffableDataSource.apply(snapshot) // diffableDataSource.apply(snapshot)
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
} // }
//
} //}

View File

@ -29,31 +29,31 @@ final class SearchViewModel: NSObject {
self.context = context self.context = context
super.init() super.init()
Publishers.CombineLatest( // Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox, // context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared // viewDidAppeared
) // )
.compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in // .compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in
return authenticationBox // return authenticationBox
} // }
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true) // .throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
.asyncMap { authenticationBox in // .asyncMap { authenticationBox in
try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil) // try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
} // }
.retry(3) // .retry(3)
.map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } } // .map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) } // .catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [weak self] result in // .sink { [weak self] result in
guard let self = self else { return } // guard let self = self else { return }
switch result { // switch result {
case .success(let response): // case .success(let response):
self.hashtags = response.value // self.hashtags = response.value
case .failure: // case .failure:
break // break
} // }
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
} }
} }

View File

@ -12,6 +12,14 @@ import Pageboy
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
final class CustomSearchController: UISearchController {
let customSearchBar = UISearchBar(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
override var searchBar: UISearchBar { customSearchBar }
}
// Fake search bar not works on iPad with UISplitViewController // Fake search bar not works on iPad with UISplitViewController
// check device and fallback to standard UISearchController // check device and fallback to standard UISearchController
final class SearchDetailViewController: PageboyViewController, NeedsDependency { final class SearchDetailViewController: PageboyViewController, NeedsDependency {
@ -48,8 +56,8 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
return navigationBar return navigationBar
}() }()
let searchController: UISearchController = { let searchController: CustomSearchController = {
let searchController = UISearchController() let searchController = CustomSearchController()
searchController.automaticallyShowsScopeBar = false searchController.automaticallyShowsScopeBar = false
searchController.dimsBackgroundDuringPresentation = false searchController.dimsBackgroundDuringPresentation = false
return searchController return searchController
@ -235,11 +243,17 @@ extension SearchDetailViewController {
if isPhoneDevice { if isPhoneDevice {
searchBar.setShowsCancelButton(true, animated: animated) searchBar.setShowsCancelButton(true, animated: animated)
searchBar.becomeFirstResponder() UIView.performWithoutAnimation {
self.searchBar.becomeFirstResponder()
}
} else { } else {
searchController.isActive = true searchController.searchBar.setShowsCancelButton(true, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.33) { searchController.searchBar.setShowsScope(true, animated: false)
self.searchController.searchBar.becomeFirstResponder() UIView.performWithoutAnimation {
self.searchController.isActive = true
}
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
} }
} }
} }

View File

@ -46,14 +46,17 @@ extension SearchToSearchDetailViewControllerAnimatedTransitioning {
let toViewEndFrame = transitionContext.finalFrame(for: toVC) let toViewEndFrame = transitionContext.finalFrame(for: toVC)
transitionContext.containerView.addSubview(toView) transitionContext.containerView.addSubview(toView)
toView.frame = toViewEndFrame toView.frame = toViewEndFrame
toView.setNeedsLayout()
toView.layoutIfNeeded()
toVC.searchBar.setNeedsLayout()
toVC.searchBar.layoutIfNeeded()
toView.alpha = 0 toView.alpha = 0
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve) let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve)
animator.addAnimations { animator.addAnimations {
toView.alpha = 1
} }
animator.addCompletion { position in animator.addCompletion { position in
toView.alpha = 1
transitionContext.completeTransition(true) transitionContext.completeTransition(true)
} }
return animator return animator