Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

๐Ÿ”€ :: (#525) AfterSearch VC๋ฅผ ReactorKit์œผ๋กœ ๋ฆฌํŒฉํ•ฉ๋‹ˆ๋‹ค. #526

Merged
merged 16 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class AfterSearchComponent: Component<AfterSearchDependency> {
return AfterSearchViewController.viewController(
afterSearchContentComponent: dependency.afterSearchContentComponent,
containSongsFactory: dependency.containSongsFactory,
viewModel: .init(fetchSearchSongUseCase: dependency.fetchSearchSongUseCase)
reactor: .init(fetchSearchSongUseCase: dependency.fetchSearchSongUseCase)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Foundation
import LogManager
import ReactorKit
import RxSwift
import SongsDomainInterface

public final class AfterSearchReactor: Reactor {
var disposeBag: DisposeBag = DisposeBag()
private let fetchSearchSongUseCase: FetchSearchSongUseCase

public enum Action {
case fetchData(String)
}

public enum Mutation {
case fetchData([[SearchSectionModel]])
}

public struct State {
var dataSource: [[SearchSectionModel]]
var text: String
}

public var initialState: State

init(fetchSearchSongUseCase: FetchSearchSongUseCase) {
self.fetchSearchSongUseCase = fetchSearchSongUseCase
self.initialState = State(
dataSource: [],
text: ""
)
}

deinit {
LogManager.printDebug("\(Self.self)")
}

public func reduce(state: State, mutation: Mutation) -> State {
var newState = state

switch mutation {
case let .fetchData(data):
newState.dataSource = data
}

return newState
}

public func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .fetchData(text):
return fetchData(text)
}
}
}

private extension AfterSearchReactor {
func fetchData(_ text: String) -> Observable<Mutation> {
return fetchSearchSongUseCase
.execute(keyword: text)
.asObservable()
.map { res in

let r1 = res.song
let r2 = res.artist
let r3 = res.remix

let limitCount: Int = 3

let all: [SearchSectionModel] = [
SearchSectionModel(
model: (.song, r1.count),
items: r1.count > limitCount ? Array(r1[0 ... limitCount - 1]) : r1
),
SearchSectionModel(
model: (.artist, r2.count),
items: r2.count > limitCount ? Array(r2[0 ... limitCount - 1]) : r2
),
SearchSectionModel(
model: (.remix, r3.count),
items: r3.count > limitCount ? Array(r3[0 ... limitCount - 1]) : r3
)
]

var results: [[SearchSectionModel]] = []
results.append(all)
results.append([SearchSectionModel(model: (.song, r1.count), items: r1)])
results.append([SearchSectionModel(model: (.artist, r2.count), items: r2)])
results.append([SearchSectionModel(model: (.remix, r3.count), items: r3)])

return results
}
.map { Mutation.fetchData($0) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,29 @@ extension AfterSearchContentViewController {
return
}
self.input.deSelectedAllSongs.accept(())
parent.output.songEntityOfSelectedSongs.accept([])
// parent.output.songEntityOfSelectedSongs.accept([])
}).disposed(by: disposeBag)
}

func requestFromParent() {
guard let parent = self.parent?.parent as? AfterSearchViewController else {
return
}
let entities = parent.output.songEntityOfSelectedSongs.value
let models = output.dataSource.value

let indexPaths = entities.map { entity -> IndexPath? in
var indexPath: IndexPath?

models.enumerated().forEach { section, model in
if let row = model.items.firstIndex(where: { $0 == entity }) {
indexPath = IndexPath(row: row, section: section)
}
}
return indexPath
}.compactMap { $0 }

input.mandatoryLoadIndexPath.accept(indexPaths)
// let entities = parent.output.songEntityOfSelectedSongs.value
// let models = output.dataSource.value
//
// let indexPaths = entities.map { entity -> IndexPath? in
// var indexPath: IndexPath?
//
// models.enumerated().forEach { section, model in
// if let row = model.items.firstIndex(where: { $0 == entity }) {
// indexPath = IndexPath(row: row, section: section)
// }
// }
// return indexPath
// }.compactMap { $0 }

// input.mandatoryLoadIndexPath.accept(indexPaths)
yongbeomkwak marked this conversation as resolved.
Show resolved Hide resolved
}

private func configureUI() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ import BaseFeatureInterface
import DesignSystem
import NVActivityIndicatorView
import Pageboy
import ReactorKit
import RxSwift
import SongsDomainInterface
import Tabman
import UIKit
import Utility

public final class AfterSearchViewController: TabmanViewController, ViewControllerFromStoryBoard, SongCartViewType {
public final class AfterSearchViewController: TabmanViewController, ViewControllerFromStoryBoard, StoryboardView,
SongCartViewType {
@IBOutlet weak var tabBarView: UIView!
@IBOutlet weak var fakeView: UIView!
@IBOutlet weak var indicator: NVActivityIndicatorView!

var viewModel: AfterSearchViewModel!
var afterSearchContentComponent: AfterSearchContentComponent!
var containSongsFactory: ContainSongsFactory!
let disposeBag = DisposeBag()
public var disposeBag = DisposeBag()

private var viewControllers: [UIViewController] = [
UIViewController(),
UIViewController(),
UIViewController(),
UIViewController()
]
lazy var input = AfterSearchViewModel.Input()
lazy var output = viewModel.transform(from: input)

public var songCartView: SongCartView!
public var bottomSheetView: BottomSheetView!
Expand All @@ -35,7 +34,6 @@ public final class AfterSearchViewController: TabmanViewController, ViewControll
override public func viewDidLoad() {
super.viewDidLoad()
configureUI()
bindRx()
}

override public func viewWillAppear(_ animated: Bool) {
Expand All @@ -46,21 +44,53 @@ public final class AfterSearchViewController: TabmanViewController, ViewControll
public static func viewController(
afterSearchContentComponent: AfterSearchContentComponent,
containSongsFactory: ContainSongsFactory,
viewModel: AfterSearchViewModel
reactor: AfterSearchReactor
) -> AfterSearchViewController {
let viewController = AfterSearchViewController.viewController(storyBoardName: "Search", bundle: Bundle.module)
viewController.viewModel = viewModel
viewController.afterSearchContentComponent = afterSearchContentComponent
viewController.containSongsFactory = containSongsFactory
viewController.reactor = reactor
return viewController
}

deinit {
DEBUG_LOG("โŒ \(Self.self)")
}

public func bind(reactor: AfterSearchReactor) {
bindState(reacotr: reactor)
bindAction(reactor: reactor)
}
}

extension AfterSearchViewController {
func bindState(reacotr: AfterSearchReactor) {
let currentState = reacotr.state.share(replay: 2)

// TODO: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ™”๋ฉด ๋‚˜์˜ฌ ๋•Œ , Content์ชฝ tableView hidden์ฒ˜๋ฆฌ ๋ฐ indicator start ์‹œ์  ๊ณ ๋ ค
currentState.map(\.dataSource)
.filter { !$0.isEmpty }
.withUnretained(self)
.bind { owner, dataSource in

guard let comp = owner.afterSearchContentComponent else {
return
}

owner.viewControllers = [
comp.makeView(type: .all, dataSource: dataSource[0]),
comp.makeView(type: .song, dataSource: dataSource[1]),
comp.makeView(type: .artist, dataSource: dataSource[2]),
comp.makeView(type: .remix, dataSource: dataSource[3])
]
owner.indicator.stopAnimating()
owner.reloadData()
}
.disposed(by: disposeBag)
}

func bindAction(reactor: AfterSearchReactor) {}

private func configureUI() {
self.fakeView.backgroundColor = DesignSystemAsset.GrayColor.gray100.color
self.indicator.type = .circleStrokeSpin
Expand Down Expand Up @@ -96,62 +126,44 @@ extension AfterSearchViewController {
)
}

private func bindRx() {
output.dataSource
.skip(1)
.subscribe(onNext: { [weak self] result in
guard let self = self else {
return
}
guard let comp = self.afterSearchContentComponent else {
return
}
self.viewControllers = [
comp.makeView(type: .all, dataSource: result[0]),
comp.makeView(type: .song, dataSource: result[1]),
comp.makeView(type: .artist, dataSource: result[2]),
comp.makeView(type: .remix, dataSource: result[3])
]
self.indicator.stopAnimating()
self.reloadData()
})
.disposed(by: disposeBag)

output.isFetchStart
.subscribe(onNext: { [weak self] _ in
guard let self = self else {
return
}
self.indicator.startAnimating()
guard let child = self.viewControllers.first as? AfterSearchContentViewController else {
return
}
child.tableView.isHidden = true // ๊ฒ€์ƒ‰ ์‹œ์ž‘ ์‹œ ํ…Œ์ด๋ธ” ๋ทฐ ์ˆจ๊น€
})
.disposed(by: disposeBag)

output.songEntityOfSelectedSongs
.skip(1)
.subscribe(onNext: { [weak self] (songs: [SongEntity]) in
guard let self = self else { return }
if !songs.isEmpty {
self.showSongCart(
in: self.view,
type: .searchSong,
selectedSongCount: songs.count,
totalSongCount: 100,
useBottomSpace: false
)
self.songCartView.delegate = self
} else {
self.hideSongCart()
}
})
.disposed(by: disposeBag)
}
// TODO: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ™”๋ฉด ๋‚˜์˜ค๋ฉด ์ด์–ด์„œ ์ž‘์—…
// private func bindRx() {
//
// output.isFetchStart
// .subscribe(onNext: { [weak self] _ in
// guard let self = self else {
// return
// }
// self.indicator.startAnimating()
// guard let child = self.viewControllers.first as? AfterSearchContentViewController else {
// return
// }
// child.tableView.isHidden = true // ๊ฒ€์ƒ‰ ์‹œ์ž‘ ์‹œ ํ…Œ์ด๋ธ” ๋ทฐ ์ˆจ๊น€
// })
// .disposed(by: disposeBag)
//
// output.songEntityOfSelectedSongs
// .skip(1)
// .subscribe(onNext: { [weak self] (songs: [SongEntity]) in
// guard let self = self else { return }
// if !songs.isEmpty {
// self.showSongCart(
// in: self.view,
// type: .searchSong,
// selectedSongCount: songs.count,
// totalSongCount: 100,
// useBottomSpace: false
// )
// self.songCartView.delegate = self
// } else {
// self.hideSongCart()
// }
// })
// .disposed(by: disposeBag)
// }

func clearSongCart() {
self.output.songEntityOfSelectedSongs.accept([])
// self.output.songEntityOfSelectedSongs.accept([])
self.viewControllers.forEach { vc in
guard let afterContentVc = vc as? AfterSearchContentViewController else {
return
Expand Down Expand Up @@ -202,23 +214,25 @@ extension AfterSearchViewController: SongCartViewDelegate {
return

case .addSong:
let songs: [String] = output.songEntityOfSelectedSongs.value.map { $0.id }
let viewController = containSongsFactory.makeView(songs: songs)
viewController.modalPresentationStyle = .overFullScreen
self.present(viewController, animated: true) { [weak self] in
guard let self = self else { return }
self.clearSongCart()
}
// let songs: [String] = output.songEntityOfSelectedSongs.value.map { $0.id }
// let viewController = containSongsFactory.makeView(songs: songs)
// viewController.modalPresentationStyle = .overFullScreen
// self.present(viewController, animated: true) { [weak self] in
// guard let self = self else { return }
// self.clearSongCart()
// }
break

case .addPlayList:
let songs = output.songEntityOfSelectedSongs.value
playState.appendSongsToPlaylist(songs)
self.clearSongCart()

// let songs = output.songEntityOfSelectedSongs.value
// playState.appendSongsToPlaylist(songs)
// self.clearSongCart()
break
case .play:
let songs = output.songEntityOfSelectedSongs.value
playState.loadAndAppendSongsToPlaylist(songs)
self.clearSongCart()
// let songs = output.songEntityOfSelectedSongs.value
// playState.loadAndAppendSongsToPlaylist(songs)
// self.clearSongCart()
break

case .remove:
return
Expand Down
Loading
Loading