Skip to content

Commit

Permalink
✨ :: [#118] MusicFeature / 유튜브 바로가기 ActionSheet
Browse files Browse the repository at this point in the history
  • Loading branch information
baekteun committed Jul 25, 2023
1 parent 9f7e29e commit b39c140
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 15 deletions.
9 changes: 6 additions & 3 deletions Projects/Feature/MusicFeature/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ let project = Project.module(
targets: [
.implements(module: .feature(.MusicFeature), dependencies: [
.feature(target: .BaseFeature),
.domain(target: .MusicDomain, type: .interface)
.domain(target: .MusicDomain, type: .interface),
.domain(target: .UserDomain, type:. interface)
]),
.tests(module: .feature(.MusicFeature), dependencies: [
.feature(target: .MusicFeature),
.domain(target: .MusicDomain, type: .testing)
.domain(target: .MusicDomain, type: .testing),
.domain(target: .UserDomain, type:. testing)
]),
.demo(module: .feature(.MusicFeature), dependencies: [
.feature(target: .MusicFeature),
.domain(target: .MusicDomain, type: .testing)
.domain(target: .MusicDomain, type: .testing),
.domain(target: .UserDomain, type:. testing)
])
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ final class MusicMoordinator: Moordinator {
case .music:
return coordinateToMusic()

case let .alert(title, message, style, actions):
return presentToAlert(title: title, message: message, style: style, actions: actions)

default:
return .none
}
Expand All @@ -37,4 +40,20 @@ private extension MusicMoordinator {
)
)
}

func presentToAlert(
title: String?,
message: String?,
style: UIAlertController.Style,
actions: [UIAlertAction]
) -> MoordinatorContributors {
let alert = UIAlertController(title: title, message: message, preferredStyle: style)
if !actions.isEmpty {
actions.forEach(alert.addAction(_:))
} else {
alert.addAction(.init(title: "확인", style: .default))
}
self.rootVC.topViewController?.present(alert, animated: true)
return .none
}
}
43 changes: 43 additions & 0 deletions Projects/Feature/MusicFeature/Sources/Scene/MusicStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Foundation
import Moordinator
import MusicDomainInterface
import Store
import UIKit
import UserDomainInterface

final class MusicStore: BaseStore {
Expand Down Expand Up @@ -34,6 +35,7 @@ final class MusicStore: BaseStore {
enum Action {
case viewDidLoad
case refresh
case cellMeatballDidTap(music: MusicModel)
}
enum Mutation {
case updateMusicList([MusicModel])
Expand All @@ -47,6 +49,9 @@ extension MusicStore {
switch action {
case .viewDidLoad, .refresh:
return self.fetchMusicList()

case let .cellMeatballDidTap(music):
return self.cellMeatballDidTap(music: music)
}
}
}
Expand Down Expand Up @@ -84,6 +89,12 @@ private extension MusicStore {
)
}

func cellMeatballDidTap(music: MusicModel) -> SideEffect<Mutation, Never> {
let alertActions: [UIAlertAction] = self.cellMeatballAction(music: music)
route.send(DotoriRoutePath.alert(style: .actionSheet, actions: alertActions))
return .none
}

func fetchMusicList() -> SideEffect<Mutation, Never> {
guard !currentState.isRefreshing else { return .none }
let musicListEffect = SideEffect<[MusicModel], Error>
Expand All @@ -95,6 +106,29 @@ private extension MusicStore {
.catchToNever()
return self.makeRefreshingSideEffect(musicListEffect)
}

func cellMeatballAction(music: MusicModel) -> [UIAlertAction] {
let youtubeID = self.parseYoutubeID(url: music.url)
var actions: [UIAlertAction] = [
.init(title: "바로가기", style: .default, handler: { _ in
guard let youtubeID,
let url = URL(string: "youtube://\(youtubeID)")
else { return }
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
} else if let webURL = URL(string: "https://youtube.com/watch?v=\(youtubeID)") {
UIApplication.shared.open(webURL)
}
})
]
if currentState.currentUserRole != .member {
actions.append(.init(title: "기상음악 삭제", style: .destructive, handler: { _ in

}))
}
actions.append(.init(title: "취소", style: .cancel))
return actions
}
}

// MARK: - Reusable
Expand All @@ -111,4 +145,13 @@ private extension MusicStore {
.append(endLoadingPublisher)
.eraseToSideEffect()
}

func parseYoutubeID(url: String) -> String? {
guard let url = URL(string: url) else { return nil }
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
if let idValue = queryItems?.first(where: { $0.name == "v" })?.value {
return idValue
}
return url.lastPathComponent.isEmpty ? nil : url.lastPathComponent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ final class MusicViewController: BaseStoredViewController<MusicStore> {
private let musicRefreshControl = UIRefreshControl()
private lazy var musicTableAdapter = TableViewAdapter<GenericSectionModel<MusicModel>>(
tableView: musicTableView
) { tableView, indexPath, item in
) { [weak self] tableView, indexPath, item in
let cell: MusicCell = tableView.dequeueReusableCell(for: indexPath)
cell.adapt(model: item)
cell.delegate = self
return cell
}
private let emptyMusicStackView = VStackView(spacing: 8) {
Expand Down Expand Up @@ -103,9 +104,15 @@ final class MusicViewController: BaseStoredViewController<MusicStore> {
.map(\.musicList)
.map(\.isEmpty)
.removeDuplicates()
.sink(with: emptyMusicStackView) { emptyView, musicListIsEmpty in
.sink(with: emptyMusicStackView, receiveValue: { emptyView, musicListIsEmpty in
emptyView.isHidden = !musicListIsEmpty
}
})
.store(in: &subscription)
}
}

extension MusicViewController: MusicCellDelegate {
func cellMeatballDidTap(model: MusicModel) {
store.send(.cellMeatballDidTap(music: model))
}
}
19 changes: 19 additions & 0 deletions Projects/Feature/MusicFeature/Sources/Scene/View/MusicCell.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import BaseFeature
import Combine
import CombineUtility
import Configure
import DateUtility
import DesignSystem
Expand All @@ -8,7 +10,12 @@ import MusicDomainInterface
import UIKit
import UIKitUtil

protocol MusicCellDelegate: AnyObject {
func cellMeatballDidTap(model: MusicModel)
}

final class MusicCell: BaseTableViewCell<MusicModel> {
weak var delegate: (any MusicCellDelegate)?
private let containerView = UIView()
.set(\.backgroundColor, .dotori(.background(.card)))
private let thumbnailView = UIImageView(
Expand All @@ -24,6 +31,12 @@ final class MusicCell: BaseTableViewCell<MusicModel> {
private let meatballButton = DotoriIconButton(
image: .Dotori.meatBall.tintColor(color: .dotori(.neutral(.n30)))
)
private var subscription = Set<AnyCancellable>()

override func prepareForReuse() {
super.prepareForReuse()
subscription.removeAll()
}

override func addView() {
contentView.addSubviews {
Expand Down Expand Up @@ -69,5 +82,11 @@ final class MusicCell: BaseTableViewCell<MusicModel> {
authorLabel.text = "\(model.stuNum) \(model.username)\(timeString)"
titleLabel.text = model.title
thumbnailView.image = model.thumbnailUIImage

meatballButton.tapPublisher
.sink(with: self, receiveValue: { owner, _ in
owner.delegate?.cellMeatballDidTap(model: model)
})
.store(in: &subscription)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,6 @@ extension SelfStudyStore {
}
}

extension SelfStudyStore: SelfStudyCellDelegate {
func selfStudyCheckBoxDidTap(id: Int, isChecked: Bool) {
self.send(.selfStudyCheckButtonDidTap(id: id, isChecked: isChecked))
}
}

// MARK: - Mutate
private extension SelfStudyStore {
func fetchSelfStudyRank() -> SideEffect<Mutation, Never> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ final class SelfStudyViewController: BaseStoredViewController<SelfStudyStore> {
private let selfStudyRefreshContorol = UIRefreshControl()
private lazy var selfStudyTableAdapter = TableViewAdapter<GenericSectionModel<SelfStudyRankModel>>(
tableView: selfStudyTableView
) { [store] tableView, indexPath, item in
) { [weak self] tableView, indexPath, item in
guard let self else { return .init() }
let cell: SelfStudyCell = tableView.dequeueReusableCell(for: indexPath)
cell.adapt(model: item)
cell.setUserRole(userRole: store.currentState.currentUserRole)
cell.delegate = store
cell.setUserRole(userRole: self.store.currentState.currentUserRole)
cell.delegate = self
return cell
}
private let emptySelfStudyStackView = VStackView(spacing: 8) {
Expand Down Expand Up @@ -112,3 +113,9 @@ final class SelfStudyViewController: BaseStoredViewController<SelfStudyStore> {
.store(in: &subscription)
}
}

extension SelfStudyViewController: SelfStudyCellDelegate {
func selfStudyCheckBoxDidTap(id: Int, isChecked: Bool) {
store.send(.selfStudyCheckButtonDidTap(id: id, isChecked: isChecked))
}
}

0 comments on commit b39c140

Please sign in to comment.