From dff874af76b2722aec8bfcfb04c373adf7795d60 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 31 Mar 2021 20:56:11 +0800 Subject: [PATCH] feature: add SearchRecommendTagsCollectionViewCell --- Mastodon.xcodeproj/project.pbxproj | 16 ++ Mastodon/Extension/UIView+Constraint.swift | 257 ++++++++++++++++++ ...earchRecommendTagsCollectionViewCell.swift | 76 ++++++ .../SearchViewController+recomendView.swift | 26 +- .../Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 36 ++- .../APIService/APIService+Recommend.swift | 4 +- .../API/Mastodon+API+Suggestions.swift | 2 +- .../MastodonSDK/API/Mastodon+API+Trends.swift | 2 +- 9 files changed, 410 insertions(+), 11 deletions(-) create mode 100644 Mastodon/Extension/UIView+Constraint.swift create mode 100644 Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index fb7b643e6..5179c3870 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 2D34D9CB261489930081BFC0 /* SearchViewController+recomendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */; }; 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; }; 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; }; + 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; }; 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; }; 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; }; @@ -99,6 +100,7 @@ 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; }; 2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; }; 2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; }; + 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; }; 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; }; 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; }; @@ -339,6 +341,7 @@ 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+recomendView.swift"; sourceTree = ""; }; 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = ""; }; 2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = ""; }; + 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = ""; }; 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = ""; }; 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = ""; }; @@ -402,6 +405,7 @@ 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = ""; }; 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = ""; }; 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = ""; }; + 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -706,6 +710,14 @@ path = Content; sourceTree = ""; }; + 2D34D9E026149C550081BFC0 /* CollectionViewCell */ = { + isa = PBXGroup; + children = ( + 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */, + ); + path = CollectionViewCell; + sourceTree = ""; + }; 2D364F7025E66D5B00204FDC /* ResendEmail */ = { isa = PBXGroup; children = ( @@ -1352,6 +1364,7 @@ 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, + 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, 2D84350425FF858100EECE90 /* UIScrollView.swift */, @@ -1394,6 +1407,7 @@ DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, + 2D34D9E026149C550081BFC0 /* CollectionViewCell */, ); path = Search; sourceTree = ""; @@ -1986,6 +2000,7 @@ DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, + 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, @@ -1995,6 +2010,7 @@ DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */, DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, + 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */, 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */, 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, diff --git a/Mastodon/Extension/UIView+Constraint.swift b/Mastodon/Extension/UIView+Constraint.swift new file mode 100644 index 000000000..40489629c --- /dev/null +++ b/Mastodon/Extension/UIView+Constraint.swift @@ -0,0 +1,257 @@ +// +// UIView+Constraint.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import UIKit + +enum Dimension { + case width + case height + + var layoutAttribute: NSLayoutConstraint.Attribute { + switch self { + case .width: + return .width + case .height: + return .height + } + } + +} + +extension UIView { + + func constrain(toSuperviewEdges: UIEdgeInsets?) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return} + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + NSLayoutConstraint(item: self, + attribute: .leading, + relatedBy: .equal, + toItem: view, + attribute: .leading, + multiplier: 1.0, + constant: toSuperviewEdges?.left ?? 0.0), + NSLayoutConstraint(item: self, + attribute: .top, + relatedBy: .equal, + toItem: view, + attribute: .top, + multiplier: 1.0, + constant: toSuperviewEdges?.top ?? 0.0), + NSLayoutConstraint(item: self, + attribute: .trailing, + relatedBy: .equal, + toItem: view, + attribute: .trailing, + multiplier: 1.0, + constant: toSuperviewEdges?.right ?? 0.0), + NSLayoutConstraint(item: self, + attribute: .bottom, + relatedBy: .equal, + toItem: view, + attribute: .bottom, + multiplier: 1.0, + constant: toSuperviewEdges?.bottom ?? 0.0) + ]) + } + + func constrain(_ constraints: [NSLayoutConstraint?]) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(constraints.compactMap { $0 }) + } + + func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView, constant: CGFloat?) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil} + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant ?? 0.0) + } + + func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil} + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: 0.0) + } + + func constraint(_ dimension: Dimension, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, + attribute: dimension.layoutAttribute, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: constant) + } + + func constraint(toBottom: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: toBottom, attribute: .bottom, multiplier: 1.0, constant: constant) + } + + func pinToBottom(to: UIView, height: CGFloat) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.width, toView: to), + constraint(toBottom: to, constant: 0.0), + constraint(.height, constant: height) + ]) + } + + func constraint(toTop: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: toTop, attribute: .top, multiplier: 1.0, constant: constant) + } + + func constraint(toTrailing: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: toTrailing, attribute: .trailing, multiplier: 1.0, constant: constant) + } + + func constraint(toLeading: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: toLeading, attribute: .leading, multiplier: 1.0, constant: constant) + } + + func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat, topLayoutGuide: UILayoutSupport) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: topPadding), + constraint(.trailing, toView: view, constant: -sidePadding) + ]) + } + + func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + constraint(.top, toView: view, constant: topPadding), + constraint(.trailing, toView: view, constant: -sidePadding) + ]) + } + + func constrainTopCorners(height: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view), + constraint(.top, toView: view), + constraint(.trailing, toView: view), + constraint(.height, constant: height) + ]) + } + + func constrainBottomCorners(sidePadding: CGFloat, bottomPadding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + constraint(.bottom, toView: view, constant: -bottomPadding), + constraint(.trailing, toView: view, constant: -sidePadding) + ]) + } + + func constrainBottomCorners(height: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view), + constraint(.bottom, toView: view), + constraint(.trailing, toView: view), + constraint(.height, constant: height) + ]) + } + + func constrainLeadingCorners() { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.top, toView: view), + constraint(.leading, toView: view), + constraint(.bottom, toView: view) + ]) + } + + func constrainTrailingCorners() { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.top, toView: view), + constraint(.trailing, toView: view), + constraint(.bottom, toView: view) + ]) + } + + func constrainToCenter() { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.centerX, toView: view), + constraint(.centerY, toView: view) + ]) + } + + func pinTo(viewAbove: UIView, padding: CGFloat = 0.0, height: CGFloat? = nil) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.width, toView: viewAbove), + constraint(toBottom: viewAbove, constant: padding), + self.centerXAnchor.constraint(equalTo: viewAbove.centerXAnchor), + height != nil ? constraint(.height, constant: height!) : nil + ]) + } + + func pin(toSize: CGSize) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + widthAnchor.constraint(equalToConstant: toSize.width), + heightAnchor.constraint(equalToConstant: toSize.height)]) + } + + func pinTopLeft(padding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding), + topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) + } + + func pinTopRight(padding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: padding), + topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) + } + + func pinTopLeft(toView: UIView, topPadding: CGFloat) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + constrain([ + leadingAnchor.constraint(equalTo: toView.leadingAnchor), + topAnchor.constraint(equalTo: toView.bottomAnchor, constant: topPadding)]) + } + + /// Cross-fades between two views by animating their alpha then setting one or the other hidden. + /// - parameters: + /// - lhs: left view + /// - rhs: right view + /// - toRight: fade to the right view if true, fade to the left view if false + /// - duration: animation duration + /// + static func crossfade(_ lhs: UIView, _ rhs: UIView, toRight: Bool, duration: TimeInterval) { + lhs.alpha = toRight ? 1.0 : 0.0 + rhs.alpha = toRight ? 0.0 : 1.0 + lhs.isHidden = false + rhs.isHidden = false + + UIView.animate(withDuration: duration, animations: { + lhs.alpha = toRight ? 0.0 : 1.0 + rhs.alpha = toRight ? 1.0 : 0.0 + }, completion: { _ in + lhs.isHidden = toRight + rhs.isHidden = !toRight + }) + } +} diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift new file mode 100644 index 000000000..108f2b6a2 --- /dev/null +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -0,0 +1,76 @@ +// +// SearchRecommendTagsCollectionViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +import UIKit + +class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { + let backgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + let hashTagTitleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .preferredFont(forTextStyle: .caption1) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let peopleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .preferredFont(forTextStyle: .body) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let flameIconView: UIImageView = { + let imageView = UIImageView() + let image = UIImage(systemName: "flame.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))!.withRenderingMode(.alwaysTemplate) + imageView.image = image + imageView.tintColor = .white + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + override func prepareForReuse() { + super.prepareForReuse() + } + + override init(frame: CGRect) { + super.init(frame: .zero) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +extension SearchRecommendTagsCollectionViewCell { + private func configure() { + contentView.addSubview(backgroundImageView) + backgroundImageView.constrain(toSuperviewEdges: nil) + + contentView.addSubview(hashTagTitleLabel) + hashTagTitleLabel.pinTopLeft(padding: 16) + + contentView.addSubview(peopleLabel) + peopleLabel.constrain([ + peopleLabel.constraint(toTop: contentView, constant: 46), + peopleLabel.constraint(toLeading: contentView, constant: 16) + ]) + + contentView.addSubview(flameIconView) + flameIconView.pinTopRight(padding: 16) + + } +} diff --git a/Mastodon/Scene/Search/SearchViewController+recomendView.swift b/Mastodon/Scene/Search/SearchViewController+recomendView.swift index f3783ddc9..b498aa608 100644 --- a/Mastodon/Scene/Search/SearchViewController+recomendView.swift +++ b/Mastodon/Scene/Search/SearchViewController+recomendView.swift @@ -1,5 +1,5 @@ // -// SearchViewController+recomemndView.swift +// SearchViewController+recommendView.swift // Mastodon // // Created by sxiaojian on 2021/3/31. @@ -10,9 +10,14 @@ import UIKit extension SearchViewController { - func setuprecomemndView() { - recomemndView.dataSource = self - recomemndView.delegate = self + func setuprecommendView() { + recommendView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self)) + recommendView.dataSource = self + recommendView.delegate = self + } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + recommendView.collectionViewLayout.invalidateLayout() } } @@ -21,8 +26,19 @@ extension SearchViewController: UICollectionViewDelegate { } extension SearchViewController: UICollectionViewDataSource { + func numberOfSections(in collectionView: UICollectionView) -> Int { + return (self.viewModel.recommendAccounts.isEmpty ? 0 : 1) + (self.viewModel.recommendHashTags.isEmpty ? 0 : 1) + } + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return 0 + switch section { + case 0: + return viewModel.recommendHashTags.count + case 1: + return viewModel.recommendAccounts.count + default: + return 0 + } } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 5533e2ef0..8d6139e17 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -27,7 +27,7 @@ final class SearchViewController: UIViewController, NeedsDependency { return searchBar }() - let recomemndView: UICollectionView = { + let recommendView: UICollectionView = { let flowLayout = UICollectionViewFlowLayout() flowLayout.scrollDirection = .horizontal let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout) diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index bb2c33ecb..37e4344cd 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -9,6 +9,7 @@ import Foundation import Combine import MastodonSDK import UIKit +import OSLog final class SearchViewModel { @@ -25,6 +26,39 @@ final class SearchViewModel { init(context: AppContext) { self.context = context - + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + context.apiService.recommendTrends(domain: activeMastodonAuthenticationBox.domain, query: Mastodon.API.Trends.Query(limit: 5)) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendTrends fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendTrends success", ((#file as NSString).lastPathComponent), #line, #function) + break + } + + } receiveValue: { [weak self] tags in + guard let self = self else { return } + self.recommendHashTags = tags.value + } + .store(in: &disposeBag) + + context.apiService.recommendAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount success", ((#file as NSString).lastPathComponent), #line, #function) + break + } + } receiveValue: { [weak self] accounts in + guard let self = self else { return } + self.recommendAccounts = accounts.value + } + .store(in: &disposeBag) + } } diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/Mastodon/Service/APIService/APIService+Recommend.swift index dd22ede00..bf6db0179 100644 --- a/Mastodon/Service/APIService/APIService+Recommend.swift +++ b/Mastodon/Service/APIService/APIService+Recommend.swift @@ -13,7 +13,7 @@ extension APIService { func recommendAccount( domain: String, - query: Mastodon.API.Suggestions.Query, + query: Mastodon.API.Suggestions.Query?, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization @@ -23,7 +23,7 @@ extension APIService { func recommendTrends( domain: String, - query: Mastodon.API.Trends.Query + query: Mastodon.API.Trends.Query? ) -> AnyPublisher, Error> { return Mastodon.API.Trends.get(session: session, domain: domain, query: query) } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift index ddb2297d8..5cabe833e 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift @@ -31,7 +31,7 @@ extension Mastodon.API.Suggestions { public static func get( session: URLSession, domain: String, - query: Mastodon.API.Suggestions.Query, + query: Mastodon.API.Suggestions.Query?, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { let request = Mastodon.API.get( diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift index 3c7e8515a..a377439fe 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift @@ -31,7 +31,7 @@ extension Mastodon.API.Trends { public static func get( session: URLSession, domain: String, - query: Mastodon.API.Trends.Query + query: Mastodon.API.Trends.Query? ) -> AnyPublisher, Error> { let request = Mastodon.API.get( url: trendsURL(domain: domain),