forked from zelo72/mastodon-ios
fix: search bar active with re-layout animation on iPad device issue
This commit is contained in:
parent
f5aaf2737f
commit
8a33ed9f9f
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue