feat: make search result works as statuses list

This commit is contained in:
CMK 2021-07-15 15:49:30 +08:00
parent 10c2b57b79
commit ae1a153536
16 changed files with 279 additions and 69 deletions

View File

@ -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 */,

View File

@ -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>

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 = {

View File

@ -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)
// }
// }
//}

View File

@ -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:

View File

@ -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 }

View File

@ -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()
}
}

View File

@ -123,6 +123,7 @@ extension Mastodon.API {
extension Mastodon.API.V2 {
public enum Search { }
public enum Suggestions { }
public enum Media { }
}
extension Mastodon.API {

View File

@ -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?