Skip to content

Commit

Permalink
feat: Social 검색 뷰 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Sonny-Kor committed Jan 21, 2025
1 parent b7e0df6 commit 0d2028f
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 83 deletions.
11 changes: 6 additions & 5 deletions Projects/TDData/Sources/Repository/PostRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ public final class PostRepositoryImpl: PostRepository {
.filter { $0.contentText.contains(keyword) && $0.category?.contains(category) ?? false }
}

public func togglePostLike(postID: Post.ID) async throws -> Bool {
guard var post = dummyPost.filter({ $0.id == postID }).first else {
return false
public func togglePostLike(postID: Post.ID) async throws -> Result<Post, Error> {
if let index = dummyPost.firstIndex(where: { $0.id == postID }) {
dummyPost[index].toggleLike()
return .success(dummyPost[index])
}
post.likeCount = (post.likeCount ?? 0) + 1
return true;
//TODO: 실제 리소스에 반영 후 적절한 Error 처리 필요
return .failure(NSError(domain: "PostRepositoryImpl", code: 0, userInfo: nil))
}

public func bringUserRoutine(routine: Routine) async throws -> Routine {
Expand Down
13 changes: 11 additions & 2 deletions Projects/TDDomain/Sources/Entity/Post.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public struct Post: Identifiable {
public let imageList: [String]?
public let timestamp: Date

public var likeCount: Int?
public var likeCount: Int
public var isLike: Bool
public let commentCount: Int?
public let shareCount: Int?
Expand All @@ -22,7 +22,7 @@ public struct Post: Identifiable {
contentText: String,
imageList: [String]?,
timestamp: Date,
likeCount: Int?,
likeCount: Int,
isLike: Bool,
commentCount: Int?,
shareCount: Int?,
Expand All @@ -41,6 +41,15 @@ public struct Post: Identifiable {
self.routine = routine
self.category = category
}

public mutating func toggleLike(){
if isLike && likeCount > 0 {
likeCount -= 1
} else {
likeCount += 1
}
isLike.toggle()
}
}

public extension Post {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
public protocol PostRepository {
func fetchPostList(category: PostCategory?) async throws -> [Post]
func searchPost(keyword: String, category: PostCategory?) async throws -> [Post]?
func togglePostLike(postID: Post.ID) async throws -> Bool
func togglePostLike(postID: Post.ID) async throws -> Result<Post, Error>
func bringUserRoutine(routine: Routine) async throws -> Routine

// MARK: - Post CRUD
Expand Down
6 changes: 3 additions & 3 deletions Projects/TDDomain/Sources/UseCase/TogglePostLikeUseCase.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

public protocol TogglePostLikeUseCase {
func execute(postID: Post.ID) async throws -> Bool
func execute(postID: Post.ID) async throws -> Post
}

public final class TogglePostLikeUseCaseImpl: TogglePostLikeUseCase {
Expand All @@ -11,7 +11,7 @@ public final class TogglePostLikeUseCaseImpl: TogglePostLikeUseCase {
self.repository = repository
}

public func execute(postID: Post.ID) async throws -> Bool {
try await repository.togglePostLike(postID: postID)
public func execute(postID: Post.ID) async throws -> Post {
try await repository.togglePostLike(postID: postID).get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ protocol SocialListDelegate: AnyObject {
func didTapCreateButton()
func didTapReport(id: Post.ID)
func didTapUserProfile(id: User.ID)
func didTapSearch()
}

final class SocialListCoordinator: Coordinator, SocialSearchDelegate {
final class SocialListCoordinator: Coordinator {
var navigationController: UINavigationController
var childCoordinators = [Coordinator]()
var finishDelegate: CoordinatorFinishDelegate?
Expand All @@ -28,10 +27,12 @@ final class SocialListCoordinator: Coordinator, SocialSearchDelegate {
let fetchPostUseCase = injector.resolve(FetchPostUseCase.self)
let togglePostLikeUseCase = injector.resolve(TogglePostLikeUseCase.self)
let blockUserUseCase = injector.resolve(BlockUserUseCase.self)
let searchPostUseCase = injector.resolve(SearchPostUseCase.self)
let socialViewModel = SocialListViewModel(
fetchPostUseCase: fetchPostUseCase,
togglePostLikeUseCase: togglePostLikeUseCase,
blockUserUseCase: blockUserUseCase
blockUserUseCase: blockUserUseCase,
searchPostUseCase: searchPostUseCase
)
let socialViewController = SocialListViewController(viewModel: socialViewModel)
socialViewController.coordinator = self
Expand Down Expand Up @@ -90,17 +91,6 @@ extension SocialListCoordinator: SocialListDelegate {
childCoordinators.append(createCoordinator)
createCoordinator.start()
}

func didTapSearch() {
let searchCoordinator = SocialSearchCoordinator(
navigationController: navigationController,
injector: injector
)
searchCoordinator.finishDelegate = self
searchCoordinator.delegate = self
childCoordinators.append(searchCoordinator)
searchCoordinator.start()
}
}

// MARK: - Navigation Delegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ final class SocialListView: BaseView {
$0.setImage(TDImage.searchMedium, for: .normal)
}

private(set) var searchView = SocialSearchView().then {
$0.isHidden = true
}

private let chipType: TDChipType = .init(
backgroundColor: .init(
activeColor: TDColor.Primary.primary500,
Expand Down Expand Up @@ -113,6 +117,10 @@ final class SocialListView: BaseView {
loadingView.snp.makeConstraints { make in
make.edges.equalTo(socialFeedCollectionView)
}

searchView.snp.makeConstraints { make in
make.edges.equalTo(safeAreaLayoutGuide)
}
}

override func configure() {
Expand All @@ -121,7 +129,7 @@ final class SocialListView: BaseView {
}

override func addview() {
[segmentedControl, dropDownHoverView, chipCollectionView, socialFeedCollectionView, addPostButton, loadingView].forEach {
[segmentedControl, dropDownHoverView, chipCollectionView, socialFeedCollectionView, addPostButton, loadingView, searchView].forEach {
addSubview($0)
}
}
Expand Down Expand Up @@ -151,6 +159,19 @@ extension SocialListView {
}
}

func showSearchView() {
searchView.isHidden = false
}

func hideSearchView() {
searchView.isHidden = true
searchView.hideKeyboard()
}

func clearSearchText() {
searchView.searchBar.text = nil
}

func showEmptyView() {}

func showErrorView() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ final class SocialListViewController: BaseViewController<SocialListView>, TDPopu
private let input = PassthroughSubject<SocialListViewModel.Input, Never>()
private var cancellables = Set<AnyCancellable>()
private var datasource: UICollectionViewDiffableDataSource<Int, Post.ID>?
private var keywordDataSource: UICollectionViewDiffableDataSource<SocialSearchView.KeywordSection, SocialListViewModel.Keyword>!

init(viewModel: SocialListViewModel) {
self.viewModel = viewModel
Expand All @@ -23,19 +24,43 @@ final class SocialListViewController: BaseViewController<SocialListView>, TDPopu
override func viewDidLoad() {
super.viewDidLoad()
input.send(.fetchPosts)
setupDefaultNavigationBar()
}

private func setupDefaultNavigationBar() {
setupNavigationBar(style: .social, navigationDelegate: coordinator!)
navigationItem.titleView = nil
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: layoutView.searchButton)
}

private func setupNavigationSearchBar() {
navigationItem.leftBarButtonItems = nil
navigationItem.titleView = layoutView.searchView.searchBar
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: layoutView.searchView.cancleButton)
}

override func configure() {
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: layoutView.searchButton)
layoutView.searchButton.addAction(UIAction { [weak self] _ in
self?.coordinator?.didTapSearch()
self?.layoutView.showSearchView()
self?.setupNavigationSearchBar()
self?.input.send(.loadKeywords)
}, for: .touchUpInside)

layoutView.searchView.cancleButton.addAction(UIAction {
[weak self] _ in
self?.layoutView.hideSearchView()
self?.layoutView.clearSearchText()
self?.setupDefaultNavigationBar()
self?.input.send(.clearSearch)
}, for: .touchUpInside)

layoutView.searchView.searchBar.delegate = self
layoutView.searchView.keywordCollectionView.delegate = self
layoutView.searchView.keywordCollectionView.dataSource = keywordDataSource
layoutView.socialFeedCollectionView.delegate = self
layoutView.socialFeedCollectionView.refreshControl = layoutView.refreshControl
layoutView.chipCollectionView.chipDelegate = self
layoutView.chipCollectionView.setChips(viewModel.chips)
layoutView.chipCollectionView.setChips(PostCategory.allCases.map { TDChipItem(title: $0.rawValue)})
setupDataSource()
layoutView.dropDownHoverView.delegate = self
layoutView.addPostButton.addTarget(self, action: #selector(didTapCreateButton), for: .touchUpInside)
Expand All @@ -50,14 +75,21 @@ final class SocialListViewController: BaseViewController<SocialListView>, TDPopu
.receive(on: DispatchQueue.main)
.sink { [weak self] output in
switch output {
case .fetchPosts:
case .fetchPosts(let posts):
self?.layoutView.showFinishView()
self?.applySnapshot(self?.viewModel.posts ?? [])
self?.applySnapshot(posts)
case .likePost(let post):
self?.updateSnapshot(post)
case .failure(let message):
// TODO: Error Alert
self?.layoutView.showErrorView()
case .searchPosts(let posts):
// TODO: Search
self?.layoutView.showFinishView()
self?.layoutView.hideSearchView()
self?.applySnapshot(posts)
case .updateKeywords:
self?.applyKeywordSnapshot()
}
}.store(in: &cancellables)
}
Expand All @@ -79,13 +111,82 @@ extension SocialListViewController: UICollectionViewDelegate {

return cell
})

keywordDataSource = UICollectionViewDiffableDataSource<SocialSearchView.KeywordSection, SocialListViewModel.Keyword>(
collectionView: layoutView.searchView.keywordCollectionView
) { [weak self] collectionView, indexPath, keyword in
guard let section = SocialSearchView.KeywordSection(rawValue: indexPath.section) else { return UICollectionViewCell() }

switch section {
case .recent:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: CancleTagCell.identifier,
for: indexPath
) as? CancleTagCell else {
return UICollectionViewCell()
}
cell.configure(tag: keyword.word)
cell.delegate = self
return cell

case .popular:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TagCell.identifier,
for: indexPath
) as? TagCell else {
return UICollectionViewCell()
}
cell.configure(tag: keyword.word)
return cell
}
}

keywordDataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
guard kind == UICollectionView.elementKindSectionHeader,
let section = SocialSearchView.KeywordSection(rawValue: indexPath.section),
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: KeywordSectionHeaderView.identifier,
for: indexPath
) as? KeywordSectionHeaderView
else {
return UICollectionReusableView()
}
header.configure(section: section)

header.delegate = self
return header
}
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
viewModel.posts.count
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == layoutView.socialFeedCollectionView {
didTapPost(at: indexPath)
} else if collectionView == layoutView.searchView.keywordCollectionView {
didTapKeyword(at: indexPath)
}
}

private func didTapKeyword(at indexPath: IndexPath) {
guard let section = SocialSearchView.KeywordSection(rawValue: indexPath.section) else { return }
switch section {
case .recent:
let keyword = viewModel.recentKeywords[indexPath.item].word
input.send(.search(term: keyword))
layoutView.searchView.searchBar.text = keyword
case .popular:
let keyword = viewModel.popularKeywords[indexPath.item].word
input.send(.search(term: keyword))
layoutView.searchView.searchBar.text = keyword
}
layoutView.hideSearchView()
}

private func didTapPost(at indexPath: IndexPath) {
let postId = viewModel.posts[indexPath.item].id
coordinator?.didTapPost(id: postId)
}
Expand Down Expand Up @@ -152,11 +253,42 @@ extension SocialListViewController: SocialFeedCollectionViewCellDelegate, TDDrop
}
}

// MARK: - 검색 및 삭제버튼 처리

extension SocialListViewController: UISearchBarDelegate, CancleTagCellDelegate, KeywordHeaderCellDelegate {
func didTapAllDeleteButton(cell: KeywordSectionHeaderView) {
input.send(.deleteRecentAllKeywords)
}

func didTapCancleButton(cell: CancleTagCell) {
guard let indexPath = layoutView.searchView.keywordCollectionView.indexPath(for: cell),
let section = SocialSearchView.KeywordSection(rawValue: indexPath.section)
else { return }

if section == .recent {
input.send(.deleteRecentKeyword(index: indexPath.item))
}
}

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let searchText = searchBar.text, !searchText.isEmpty else { return }
layoutView.hideSearchView()
input.send(.search(term: searchText))
}

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
input.send(.loadKeywords)
}
}
}

// MARK: Collection View Datasource Apply

extension SocialListViewController {
private func applySnapshot(_ posts: [Post]) {
var snapshot = NSDiffableDataSourceSnapshot<Int, Post.ID>()
snapshot.deleteAllItems()
snapshot.appendSections([0])
snapshot.appendItems(posts.map(\.id))
datasource?.apply(snapshot, animatingDifferences: false)
Expand All @@ -167,4 +299,13 @@ extension SocialListViewController {
snapshot?.reloadItems([post.id])
datasource?.apply(snapshot!, animatingDifferences: false)
}

private func applyKeywordSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<SocialSearchView.KeywordSection, SocialListViewModel.Keyword>()
snapshot.appendSections(SocialSearchView.KeywordSection.allCases)
snapshot.appendItems(viewModel.recentKeywords, toSection: .recent)
snapshot.appendItems(viewModel.popularKeywords, toSection: .popular)

keywordDataSource.apply(snapshot, animatingDifferences: false)
}
}
Loading

0 comments on commit 0d2028f

Please sign in to comment.