feat: make search result works as statuses list
This commit is contained in:
parent
10c2b57b79
commit
ae1a153536
@ -275,7 +275,7 @@
|
||||
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */; };
|
||||
DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */; };
|
||||
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; };
|
||||
DB4FFC2C269EC39600D62E92 /* SearchDetailTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchDetailTransitionController.swift */; };
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; };
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
|
||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||
@ -922,7 +922,7 @@
|
||||
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = "<group>"; };
|
||||
DB4FFC2A269EC39600D62E92 /* SearchDetailTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchDetailTransitionController.swift; sourceTree = "<group>"; };
|
||||
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = "<group>"; };
|
||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
||||
@ -2019,13 +2019,13 @@
|
||||
path = SearchResult;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4FFC2D269EC39C00D62E92 /* SearchDetail */ = {
|
||||
DB4FFC2D269EC39C00D62E92 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB4FFC2A269EC39600D62E92 /* SearchDetailTransitionController.swift */,
|
||||
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */,
|
||||
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */,
|
||||
);
|
||||
path = SearchDetail;
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB5086CB25CC0DB400C2C187 /* Preference */ = {
|
||||
@ -2081,7 +2081,7 @@
|
||||
children = (
|
||||
DB6180E226391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift */,
|
||||
DB6180E726391B580018D199 /* MediaPreview */,
|
||||
DB4FFC2D269EC39C00D62E92 /* SearchDetail */,
|
||||
DB4FFC2D269EC39C00D62E92 /* Search */,
|
||||
);
|
||||
path = Transition;
|
||||
sourceTree = "<group>";
|
||||
@ -3519,7 +3519,7 @@
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchDetailTransitionController.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||
|
@ -12,12 +12,12 @@
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>21</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
@ -37,7 +37,7 @@
|
||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>19</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
@ -15,6 +15,7 @@ import GameController
|
||||
// - HashtagTimelineViewController: 2021/4/30
|
||||
// - UserTimelineViewController: 2021/4/30
|
||||
// - ThreadViewController: 2021/4/30
|
||||
// - SearchResultViewController: 2021/7/15
|
||||
// * StatusTableViewControllerAspect: 2021/7/15
|
||||
|
||||
// (Fake) Aspect protocol to group common protocol extension implementations
|
||||
@ -45,6 +46,7 @@ extension StatusTableViewControllerAspect {
|
||||
}
|
||||
}
|
||||
|
||||
// [A2] aspectViewDidDisappear(_:)
|
||||
extension StatusTableViewControllerAspect where Self: NeedsDependency {
|
||||
/// [Media] hook to notify video service
|
||||
func aspectViewDidDisappear(_ animated: Bool) {
|
||||
|
@ -931,7 +931,7 @@ extension ComposeViewController: UICollectionViewDelegate {
|
||||
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
return .fullScreen
|
||||
return .overFullScreen
|
||||
//return traitCollection.userInterfaceIdiom == .pad ? .formSheet : .automatic
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var searchDetailTransitionController = SearchDetailTransitionController()
|
||||
var searchTransitionController = SearchTransitionController()
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator)
|
||||
@ -165,7 +165,7 @@ extension SearchViewController: UISearchBarDelegate {
|
||||
// push to search detail
|
||||
let searchDetailViewModel = SearchDetailViewModel()
|
||||
searchDetailViewModel.needsBecomeFirstResponder = true
|
||||
self.navigationController?.delegate = self.searchDetailTransitionController
|
||||
self.navigationController?.delegate = self.searchTransitionController
|
||||
self.coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .customPush)
|
||||
return false
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||
let logger = Logger(subsystem: "SearchDetail", category: "UI")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
@ -22,10 +23,24 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency {
|
||||
var viewModel: SearchDetailViewModel!
|
||||
var viewControllers: [SearchResultViewController]!
|
||||
|
||||
let navigationBarBackgroundView = UIView()
|
||||
let navigationBar: UINavigationBar = {
|
||||
let navigationItem = UINavigationItem()
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
|
||||
let navigationBar = UINavigationBar()
|
||||
navigationBar.setItems([navigationItem], animated: false)
|
||||
return navigationBar
|
||||
}()
|
||||
let searchBar: UISearchBar = {
|
||||
let searchBar = UISearchBar()
|
||||
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
|
||||
searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle }
|
||||
searchBar.scopeBarBackgroundImage = UIImage()
|
||||
return searchBar
|
||||
}()
|
||||
}
|
||||
@ -35,11 +50,39 @@ extension SearchDetailViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.setHidesBackButton(true, animated: false)
|
||||
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] theme in
|
||||
guard let self = self else { return }
|
||||
self.setupBackgroundColor(theme: theme)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
navigationBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationBar)
|
||||
NSLayoutConstraint.activate([
|
||||
navigationBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
setupSearchBar()
|
||||
navigationBar.layer.observe(\.bounds, options: [.new]) { [weak self] navigationBar, _ in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.navigationBarFrame.value = navigationBar.frame
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
navigationBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.insertSubview(navigationBarBackgroundView, belowSubview: navigationBar)
|
||||
NSLayoutConstraint.activate([
|
||||
navigationBarBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
navigationBarBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationBarBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
navigationBarBackgroundView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
|
||||
])
|
||||
|
||||
transition = Transition(style: .fade, duration: 0.1)
|
||||
// transition = nil
|
||||
isScrollEnabled = false
|
||||
|
||||
viewControllers = viewModel.searchScopes.map { scope in
|
||||
@ -47,10 +90,17 @@ extension SearchDetailViewController {
|
||||
searchResultViewController.context = context
|
||||
searchResultViewController.coordinator = coordinator
|
||||
searchResultViewController.viewModel = SearchResultViewModel(context: context, searchScope: scope)
|
||||
|
||||
// bind searchText
|
||||
viewModel.searchText
|
||||
.assign(to: \.value, on: searchResultViewController.viewModel.searchText)
|
||||
.store(in: &searchResultViewController.disposeBag)
|
||||
|
||||
// bind navigationBarFrame
|
||||
viewModel.navigationBarFrame
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: searchResultViewController.viewModel.navigationBarFrame)
|
||||
.store(in: &searchResultViewController.disposeBag)
|
||||
return searchResultViewController
|
||||
}
|
||||
|
||||
@ -107,8 +157,8 @@ extension SearchDetailViewController {
|
||||
|
||||
// bind search trigger
|
||||
viewModel.searchText
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||
.removeDuplicates()
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { [weak self] searchText in
|
||||
guard let self = self else { return }
|
||||
guard let searchResultViewController = self.currentViewController as? SearchResultViewController else {
|
||||
@ -120,6 +170,18 @@ extension SearchDetailViewController {
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
@ -131,12 +193,19 @@ extension SearchDetailViewController {
|
||||
|
||||
extension SearchDetailViewController {
|
||||
private func setupSearchBar() {
|
||||
navigationItem.titleView = searchBar
|
||||
searchBar.setShowsScope(true, animated: false)
|
||||
searchBar.sizeToFit()
|
||||
|
||||
navigationBar.topItem?.titleView = searchBar
|
||||
navigationBar.sizeToFit()
|
||||
|
||||
searchBar.delegate = self
|
||||
}
|
||||
|
||||
private func setupBackgroundColor(theme: Theme) {
|
||||
navigationBarBackgroundView.backgroundColor = theme.navigationBarBackgroundColor
|
||||
navigationBar.tintColor = Asset.Colors.brandBlue.color
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
@ -15,6 +16,7 @@ final class SearchDetailViewModel {
|
||||
// input
|
||||
var needsBecomeFirstResponder = false
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
|
||||
// output
|
||||
let searchScopes = SearchScope.allCases
|
||||
|
@ -8,6 +8,7 @@
|
||||
import UIKit
|
||||
import Combine
|
||||
import AVKit
|
||||
import GameplayKit
|
||||
|
||||
final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
@ -25,6 +26,8 @@ final class SearchResultViewController: UIViewController, NeedsDependency, Media
|
||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.separatorStyle = .none
|
||||
tableView.tableFooterView = UIView()
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
}()
|
||||
|
||||
@ -54,6 +57,7 @@ extension SearchResultViewController {
|
||||
])
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.prefetchDataSource = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView,
|
||||
dependency: self,
|
||||
@ -96,6 +100,29 @@ extension SearchResultViewController {
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// works for already onscreen page
|
||||
viewModel.navigationBarFrame
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] frame in
|
||||
guard let self = self else { return }
|
||||
guard self.viewModel.viewDidAppear.value else { return }
|
||||
self.tableView.contentInset.top = frame.height
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// works for appearing page
|
||||
if !viewModel.viewDidAppear.value {
|
||||
tableView.contentInset.top = viewModel.navigationBarFrame.value.height
|
||||
tableView.contentOffset.y = -viewModel.navigationBarFrame.value.height
|
||||
}
|
||||
|
||||
aspectViewWillAppear(animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
@ -104,47 +131,121 @@ extension SearchResultViewController {
|
||||
viewModel.viewDidAppear.value = true
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
aspectViewDidDisappear(animated)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchResultViewController {
|
||||
private func setupBackgroundColor(theme: Theme) {
|
||||
view.backgroundColor = theme.systemGroupedBackgroundColor
|
||||
tableView.backgroundColor = theme.systemBackgroundColor
|
||||
// tableView.backgroundColor = theme.systemBackgroundColor
|
||||
// searchHeader.backgroundColor = theme.systemGroupedBackgroundColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
extension SearchResultViewController: AVPlayerViewControllerDelegate {
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
extension SearchResultViewController: StatusTableViewCellDelegate {
|
||||
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||
func parent() -> UIViewController { return self }
|
||||
}
|
||||
|
||||
//extension SearchResultViewController: LoadMoreConfigurableTableViewContainer {
|
||||
// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
// typealias LoadingState = SearchViewModel.LoadOldestState.Loading
|
||||
// var loadMoreConfigurableTableView: UITableView { searchingTableView }
|
||||
// var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||
//}
|
||||
|
||||
// MARK: - StatusTableViewControllerAspect
|
||||
extension SearchResultViewController: StatusTableViewControllerAspect { }
|
||||
|
||||
// MARK: - LoadMoreConfigurableTableViewContainer
|
||||
extension SearchResultViewController: LoadMoreConfigurableTableViewContainer {
|
||||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
typealias LoadingState = SearchResultViewModel.State.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.stateMachine }
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension SearchResultViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
aspectScrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TableViewCellHeightCacheableContainer
|
||||
extension SearchResultViewController: TableViewCellHeightCacheableContainer {
|
||||
var cellFrameCache: NSCache<NSNumber, NSValue> {
|
||||
viewModel.cellFrameCache
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension SearchResultViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||
switch item {
|
||||
case .account(let account):
|
||||
let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id)
|
||||
coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show)
|
||||
case .hashtag(let hashtag):
|
||||
let hashtagViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name)
|
||||
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: self, transition: .show)
|
||||
case .status:
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
extension SearchResultViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
extension SearchResultViewController: AVPlayerViewControllerDelegate {
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ final class SearchResultViewModel {
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
var navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
|
||||
// output
|
||||
private(set) lazy var stateMachine: GKStateMachine = {
|
||||
|
@ -10,31 +10,7 @@ import UIKit
|
||||
// Make status bar style adaptive for child view controller
|
||||
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
|
||||
final class AdaptiveStatusBarStyleNavigationController: UINavigationController {
|
||||
var viewControllersHiddenNavigationBar: [UIViewController.Type]
|
||||
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
visibleViewController
|
||||
}
|
||||
|
||||
override init(rootViewController: UIViewController) {
|
||||
self.viewControllersHiddenNavigationBar = [SearchViewController.self]
|
||||
super.init(rootViewController: rootViewController)
|
||||
// self.delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
//extension AdaptiveStatusBarStyleNavigationController: UINavigationControllerDelegate {
|
||||
// func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
||||
// let isContain = self.viewControllersHiddenNavigationBar.contains { type(of: viewController) == $0 }
|
||||
// if isContain {
|
||||
// self.setNavigationBarHidden(true, animated: animated)
|
||||
// } else {
|
||||
// self.setNavigationBarHidden(false, animated: animated)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// SearchDetailTransitionController.swift
|
||||
// SearchTransitionController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-13.
|
||||
@ -7,12 +7,12 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
final class SearchDetailTransitionController: NSObject {
|
||||
final class SearchTransitionController: NSObject {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UINavigationControllerDelegate
|
||||
extension SearchDetailTransitionController: UINavigationControllerDelegate {
|
||||
extension SearchTransitionController: UINavigationControllerDelegate {
|
||||
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
switch operation {
|
||||
case .push where fromVC is SearchViewController && toVC is SearchDetailViewController:
|
@ -102,7 +102,7 @@ extension APIService.CoreData {
|
||||
let metaData = attachment.meta.flatMap { meta in
|
||||
try? encoder.encode(meta)
|
||||
}
|
||||
let property = Attachment.Property(domain: domain, index: index, id: attachment.id, typeRaw: attachment.type.rawValue, url: attachment.url, previewURL: attachment.previewURL, remoteURL: attachment.remoteURL, metaData: metaData, textURL: attachment.textURL, descriptionString: attachment.description, blurhash: attachment.blurhash, networkDate: networkDate)
|
||||
let property = Attachment.Property(domain: domain, index: index, id: attachment.id, typeRaw: attachment.type.rawValue, url: attachment.url ?? "", previewURL: attachment.previewURL, remoteURL: attachment.remoteURL, metaData: metaData, textURL: attachment.textURL, descriptionString: attachment.description, blurhash: attachment.blurhash, networkDate: networkDate)
|
||||
attachments.append(Attachment.insert(into: managedObjectContext, property: property))
|
||||
}
|
||||
guard !attachments.isEmpty else { return nil }
|
||||
|
@ -0,0 +1,57 @@
|
||||
//
|
||||
// Mastodon+API+V2+Media.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.V2.Media {
|
||||
static func uploadMediaEndpointURL(domain: String) -> URL {
|
||||
Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("media")
|
||||
}
|
||||
|
||||
/// Upload media as attachment
|
||||
///
|
||||
/// Creates an attachment to be used with a new status.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.4.1
|
||||
/// # Last Update
|
||||
/// 2021/7/15
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/media/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `UploadMediaQuery`
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Attachment` nested in the response
|
||||
public static func uploadMedia(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: Mastodon.API.Media.UploadMediaQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
var request = Mastodon.API.post(
|
||||
url: uploadMediaEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment
|
||||
let serialStream = query.serialStream
|
||||
request.httpBodyStream = serialStream.boundStreams.input
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.handleEvents(receiveCancel: {
|
||||
// retain and handle cancel task
|
||||
serialStream.boundStreams.output.close()
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
@ -123,6 +123,7 @@ extension Mastodon.API {
|
||||
extension Mastodon.API.V2 {
|
||||
public enum Search { }
|
||||
public enum Suggestions { }
|
||||
public enum Media { }
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
|
@ -22,8 +22,8 @@ extension Mastodon.Entity {
|
||||
|
||||
public let id: ID
|
||||
public let type: Type
|
||||
public let url: String
|
||||
public let previewURL: String? // could be nil when attachement is audio
|
||||
public let url: String? // media v2 may return null url
|
||||
public let previewURL: String? // could be nil when attachment is audio
|
||||
|
||||
public let remoteURL: String?
|
||||
public let textURL: String?
|
||||
|
Loading…
x
Reference in New Issue
Block a user