diff --git a/.github/actions/Auto_close_associate_issue/main.js b/.github/actions/Auto_close_associate_issue/main.js
index 850877d2f..010c4d8a4 100644
--- a/.github/actions/Auto_close_associate_issue/main.js
+++ b/.github/actions/Auto_close_associate_issue/main.js
@@ -8,7 +8,7 @@ let pattern = /Resolves: #\d+/;
let issueNumber;
try {
- issueNumber = body.match(pattern)[0].replace("Resolves: #", "");
+ issueNumber = body.match(pattern)[0].replace("Resolves: #", "").trim();
} catch {
issueNumber = -1;
}
diff --git a/.github/workflows/AutoCloseIssueByPullRequest.yml b/.github/workflows/AutoCloseIssueByPullRequest.yml
index 9408599a8..f2fac78bc 100644
--- a/.github/workflows/AutoCloseIssueByPullRequest.yml
+++ b/.github/workflows/AutoCloseIssueByPullRequest.yml
@@ -2,9 +2,9 @@ name: Auto close issue when PR is merged
on:
pull_request_target:
+ branches-ignore:
+ - "develop"
types: [closed]
- branches:
- - "!develop"
jobs:
close-issue:
diff --git a/Projects/App/Sources/Application/AppComponent+Base.swift b/Projects/App/Sources/Application/AppComponent+Base.swift
index 3410453ac..e1e02f80f 100644
--- a/Projects/App/Sources/Application/AppComponent+Base.swift
+++ b/Projects/App/Sources/Application/AppComponent+Base.swift
@@ -11,10 +11,6 @@ public extension AppComponent {
TextPopupComponent(parent: self)
}
- var togglePopUpFactory: any TogglePopUpFactory {
- TogglePopUpComponent(parent: self)
- }
-
var containSongsFactory: any ContainSongsFactory {
ContainSongsComponent(parent: self)
}
diff --git a/Projects/App/Sources/Application/AppComponent+MyInfo.swift b/Projects/App/Sources/Application/AppComponent+MyInfo.swift
index 442aabc5a..9535416f7 100644
--- a/Projects/App/Sources/Application/AppComponent+MyInfo.swift
+++ b/Projects/App/Sources/Application/AppComponent+MyInfo.swift
@@ -34,4 +34,8 @@ extension AppComponent {
var profilePopupFactory: any ProfilePopupFactory {
ProfilePopupComponent(parent: self)
}
+
+ var playTypeTogglePopupFactory: any PlayTypeTogglePopupFactory {
+ PlayTypeTogglePopupComponent(parent: self)
+ }
}
diff --git a/Projects/App/Sources/Application/AppComponent+Playlist.swift b/Projects/App/Sources/Application/AppComponent+Playlist.swift
index 10c52b256..cf39453fb 100644
--- a/Projects/App/Sources/Application/AppComponent+Playlist.swift
+++ b/Projects/App/Sources/Application/AppComponent+Playlist.swift
@@ -80,9 +80,9 @@ public extension AppComponent {
}
}
- var fetchWmPlaylistDetailUseCase: any FetchWmPlaylistDetailUseCase {
+ var fetchWMPlaylistDetailUseCase: any FetchWMPlaylistDetailUseCase {
shared {
- FetchWmPlaylistDetailUseCaseImpl(playlistRepository: playlistRepository)
+ FetchWMPlaylistDetailUseCaseImpl(playlistRepository: playlistRepository)
}
}
diff --git a/Projects/App/Support/Info.plist b/Projects/App/Support/Info.plist
index 8b9cb9383..691640fe1 100644
--- a/Projects/App/Support/Info.plist
+++ b/Projects/App/Support/Info.plist
@@ -185,5 +185,10 @@
UIUserInterfaceStyle
Light
+ LSApplicationQueriesSchemes
+
+ youtube
+ youtubemusic
+
diff --git a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift
index dbb50ca3d..90d255977 100644
--- a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift
+++ b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistDetailResponseDTO.swift
@@ -29,7 +29,7 @@ public extension ArtistDetailResponseDTO {
let enName: String
private enum CodingKeys: String, CodingKey {
- case krName = "krShort"
+ case krName = "kr"
case enName = "en"
}
}
diff --git a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift
index 98f1ca88a..eec4fdb18 100644
--- a/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift
+++ b/Projects/Domains/ArtistDomain/Sources/ResponseDTO/ArtistListResponseDTO.swift
@@ -29,7 +29,7 @@ public extension ArtistListResponseDTO {
let enName: String
private enum CodingKeys: String, CodingKey {
- case krName = "krShort"
+ case krName = "kr"
case enName = "en"
}
}
diff --git a/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift b/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift
index 86b7d46bc..b4130c423 100644
--- a/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift
+++ b/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift
@@ -6,7 +6,7 @@ import SongsDomainInterface
public protocol RemotePlaylistDataSource {
func fetchRecommendPlaylist() -> Single<[RecommendPlaylistEntity]>
func fetchPlaylistDetail(id: String, type: PlaylistType) -> Single
- func fetchWmPlaylistDetail(id: String) -> Single
+ func fetchWMPlaylistDetail(id: String) -> Single
func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable
func createPlaylist(title: String) -> Single
func fetchPlaylistSongs(key: String) -> Single<[SongEntity]>
diff --git a/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift b/Projects/Domains/PlaylistDomain/Interface/Entity/WMPlaylistDetailEntity.swift
similarity index 89%
rename from Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift
rename to Projects/Domains/PlaylistDomain/Interface/Entity/WMPlaylistDetailEntity.swift
index 7deabaaef..e435b490a 100644
--- a/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift
+++ b/Projects/Domains/PlaylistDomain/Interface/Entity/WMPlaylistDetailEntity.swift
@@ -1,7 +1,7 @@
import Foundation
import SongsDomainInterface
-public struct WmPlaylistDetailEntity: Equatable {
+public struct WMPlaylistDetailEntity: Equatable {
public init(
key: String,
title: String,
diff --git a/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift b/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift
index 4c73b5446..c8d0e9eea 100644
--- a/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift
+++ b/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift
@@ -6,7 +6,7 @@ import SongsDomainInterface
public protocol PlaylistRepository {
func fetchRecommendPlaylist() -> Single<[RecommendPlaylistEntity]>
func fetchPlaylistDetail(id: String, type: PlaylistType) -> Single
- func fetchWmPlaylistDetail(id: String) -> Single
+ func fetchWMPlaylistDetail(id: String) -> Single
func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable
func createPlaylist(title: String) -> Single
func fetchPlaylistSongs(key: String) -> Single<[SongEntity]>
diff --git a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWMPlaylistDetailUseCase.swift b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWMPlaylistDetailUseCase.swift
new file mode 100644
index 000000000..4f3a3ad50
--- /dev/null
+++ b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWMPlaylistDetailUseCase.swift
@@ -0,0 +1,6 @@
+import Foundation
+import RxSwift
+
+public protocol FetchWMPlaylistDetailUseCase {
+ func execute(id: String) -> Single
+}
diff --git a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift
deleted file mode 100644
index bc9dfad58..000000000
--- a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift
+++ /dev/null
@@ -1,6 +0,0 @@
-import Foundation
-import RxSwift
-
-public protocol FetchWmPlaylistDetailUseCase {
- func execute(id: String) -> Single
-}
diff --git a/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift b/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift
index f00ea811f..9262992a1 100644
--- a/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift
+++ b/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift
@@ -8,7 +8,7 @@ import PlaylistDomainInterface
public enum PlaylistAPI {
case fetchPlaylistDetail(id: String, type: PlaylistType) // 플리 상세 불러오기
- case fetchWmPlaylistDetail(id: String) // 왁뮤 플리 상세 불러오기
+ case fetchWMPlaylistDetail(id: String) // 왁뮤 플리 상세 불러오기
case updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) // title and private 업데이트
case createPlaylist(title: String) // 플리 생성
case fetchPlaylistSongs(key: String) // 전체 재생 시 곡 데이터만 가져오기
@@ -36,7 +36,7 @@ extension PlaylistAPI: WMAPI {
case let .fetchPlaylistDetail(id: id, type: type):
return "/\(id)"
- case let .fetchWmPlaylistDetail(id: id):
+ case let .fetchWMPlaylistDetail(id: id):
return "/recommend/\(id)"
case let .updateTitleAndPrivate(key: key, _, _):
@@ -64,7 +64,7 @@ extension PlaylistAPI: WMAPI {
public var method: Moya.Method {
switch self {
- case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWmPlaylistDetail, .fetchPlaylistSongs,
+ case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWMPlaylistDetail, .fetchPlaylistSongs,
.checkSubscription,
.requestPlaylistOwnerID:
return .get
@@ -85,7 +85,7 @@ extension PlaylistAPI: WMAPI {
public var task: Moya.Task {
switch self {
- case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWmPlaylistDetail, .fetchPlaylistSongs,
+ case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWMPlaylistDetail, .fetchPlaylistSongs,
.subscribePlaylist, .checkSubscription,
.requestPlaylistOwnerID:
return .requestPlain
@@ -125,7 +125,7 @@ extension PlaylistAPI: WMAPI {
public var jwtTokenType: JwtTokenType {
switch self {
- case .fetchRecommendPlaylist, .fetchWmPlaylistDetail:
+ case .fetchRecommendPlaylist, .fetchWMPlaylistDetail:
return .none
case let .fetchPlaylistDetail(_, type):
diff --git a/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift b/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift
index a96c06dc0..b53b551a4 100644
--- a/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift
+++ b/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift
@@ -22,9 +22,9 @@ public final class RemotePlaylistDataSourceImpl: BaseRemoteDataSource Single {
- request(.fetchWmPlaylistDetail(id: id))
- .map(WmPlaylistDetailResponseDTO.self)
+ public func fetchWMPlaylistDetail(id: String) -> Single {
+ request(.fetchWMPlaylistDetail(id: id))
+ .map(WMPlaylistDetailResponseDTO.self)
.map { $0.toDomain() }
}
diff --git a/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift b/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift
index 04a6418e8..fd21dbf66 100644
--- a/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift
+++ b/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift
@@ -21,8 +21,8 @@ public final class PlaylistRepositoryImpl: PlaylistRepository {
remotePlaylistDataSource.fetchPlaylistDetail(id: id, type: type)
}
- public func fetchWmPlaylistDetail(id: String) -> Single {
- remotePlaylistDataSource.fetchWmPlaylistDetail(id: id)
+ public func fetchWMPlaylistDetail(id: String) -> Single {
+ remotePlaylistDataSource.fetchWMPlaylistDetail(id: id)
}
public func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable {
diff --git a/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift b/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift
index b5631a1a3..8a81ac91b 100644
--- a/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift
+++ b/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift
@@ -3,7 +3,7 @@ import PlaylistDomainInterface
import SongsDomain
import SongsDomainInterface
-public struct WmPlaylistDetailResponseDTO: Decodable {
+public struct WMPlaylistDetailResponseDTO: Decodable {
public let key: String?
public let title: String
public let songs: [SingleSongResponseDTO]?
@@ -19,9 +19,9 @@ public struct WmPlaylistDetailResponseDTO: Decodable {
}
}
-public extension WmPlaylistDetailResponseDTO {
- func toDomain() -> WmPlaylistDetailEntity {
- WmPlaylistDetailEntity(
+public extension WMPlaylistDetailResponseDTO {
+ func toDomain() -> WMPlaylistDetailEntity {
+ WMPlaylistDetailEntity(
key: key ?? "",
title: title,
songs: (songs ?? []).map { $0.toDomain() },
diff --git a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWMPlaylistDetailUseCaseImpl.swift
similarity index 60%
rename from Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift
rename to Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWMPlaylistDetailUseCaseImpl.swift
index 7c1eb818c..419decf16 100644
--- a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift
+++ b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWMPlaylistDetailUseCaseImpl.swift
@@ -2,7 +2,7 @@ import Foundation
import PlaylistDomainInterface
import RxSwift
-public struct FetchWmPlaylistDetailUseCaseImpl: FetchWmPlaylistDetailUseCase {
+public struct FetchWMPlaylistDetailUseCaseImpl: FetchWMPlaylistDetailUseCase {
private let playlistRepository: any PlaylistRepository
public init(
@@ -11,7 +11,7 @@ public struct FetchWmPlaylistDetailUseCaseImpl: FetchWmPlaylistDetailUseCase {
self.playlistRepository = playlistRepository
}
- public func execute(id: String) -> Single {
- playlistRepository.fetchWmPlaylistDetail(id: id)
+ public func execute(id: String) -> Single {
+ playlistRepository.fetchWMPlaylistDetail(id: id)
}
}
diff --git a/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift b/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift
index 2468db8bf..023dcd975 100644
--- a/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift
+++ b/Projects/Domains/UserDomain/Interface/Entity/PlaylistEntity.swift
@@ -8,11 +8,13 @@ public struct PlaylistEntity: Equatable {
image: String,
songCount: Int,
userId: String,
+ private: Bool,
isSelected: Bool = false
) {
self.key = key
self.title = title
self.image = image
+ self.private = `private`
self.isSelected = isSelected
self.songCount = songCount
self.userId = userId
@@ -20,5 +22,5 @@ public struct PlaylistEntity: Equatable {
public let key, title, image, userId: String
public let songCount: Int
- public var isSelected: Bool
+ public var `private`, isSelected: Bool
}
diff --git a/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift b/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift
index 249d42c2f..4250207fe 100644
--- a/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift
+++ b/Projects/Domains/UserDomain/Sources/ResponseDTO/PlaylistResponseDTO.swift
@@ -16,6 +16,7 @@ public struct PlaylistResponseDTO: Decodable {
public let user: PlaylistResponseDTO.User
public let imageUrl: String
public let songCount: Int
+ public let `private`: Bool
public struct User: Decodable {
public let handle: String
@@ -30,7 +31,8 @@ public extension PlaylistResponseDTO {
title: title,
image: imageUrl,
songCount: songCount,
- userId: user.handle
+ userId: user.handle,
+ private: `private`
)
}
}
diff --git a/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift b/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift
index 530fe9e9f..eb31b2137 100644
--- a/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift
+++ b/Projects/Domains/UserDomain/Testing/FetchPlaylistUseCaseStub.swift
@@ -4,15 +4,36 @@ import UserDomainInterface
public struct FetchPlaylistUseCaseStub: FetchPlaylistUseCase {
let items: [PlaylistEntity] = [
- .init(key: "123", title: "우중충한 장마철 여름에 듣기 좋은 일본 시티팝 플레이리스트", image: "", songCount: 0, userId: ""),
- .init(key: "1234", title: "비내리는 도시, 세련된 무드 감각적인 팝송☔️ 분위기 있는 노래 모음", image: "", songCount: 1, userId: ""),
- .init(key: "1424", title: "[𝐏𝐥𝐚𝐲𝐥𝐢𝐬𝐭] 여름 밤, 퇴근길에 꽂는 플레이리스트🚃", image: "", songCount: 200, userId: ""),
+ .init(
+ key: "123",
+ title: "우중충한 장마철 여름에 듣기 좋은 일본 시티팝 플레이리스트",
+ image: "",
+ songCount: 0,
+ userId: "",
+ private: true
+ ),
+ .init(
+ key: "1234",
+ title: "비내리는 도시, 세련된 무드 감각적인 팝송☔️ 분위기 있는 노래 모음",
+ image: "",
+ songCount: 1,
+ userId: "",
+ private: true
+ ),
+ .init(
+ key: "1424",
+ title: "[𝐏𝐥𝐚𝐲𝐥𝐢𝐬𝐭] 여름 밤, 퇴근길에 꽂는 플레이리스트🚃",
+ image: "",
+ songCount: 200,
+ userId: "",
+ private: false
+ ),
.init(
key: "1324",
title: "𝐏𝐥𝐚𝐲𝐥𝐢𝐬𝐭 벌써 여름이야? 내 방을 청량한 캘리포니아 해변으로 신나는 여름 팝송 𝐒𝐮𝐦𝐦𝐞𝐫 𝐢𝐬 𝐜𝐨𝐦𝐢𝐧𝐠 🌴",
image: "",
songCount: 1000,
- userId: ""
+ userId: "", private: true
)
]
diff --git a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift
index 7346553c5..d67386f5b 100644
--- a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift
+++ b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistDetailHeaderViewController.swift
@@ -51,10 +51,10 @@ class ArtistDetailHeaderViewController: UIViewController, ViewControllerFromStor
extension ArtistDetailHeaderViewController {
func update(model: ArtistEntity) {
self.model = model
- let artistName: String = model.krName
- let artistEngName: String = model.enName.capitalizingFirstLetter
+ let artistKrName: String = model.krName
+ let artistEnName: String = model.enName
let artistNameAttributedString = NSMutableAttributedString(
- string: artistName + " " + artistEngName,
+ string: artistKrName + " " + artistEnName,
attributes: [
.font: DesignSystemFontFamily.Pretendard.bold.font(size: 24),
.foregroundColor: DesignSystemAsset.BlueGrayColor.gray900.color,
@@ -62,8 +62,8 @@ extension ArtistDetailHeaderViewController {
]
)
- let artistNameRange = (artistNameAttributedString.string as NSString).range(of: artistName)
- let artistEngNameRange = (artistNameAttributedString.string as NSString).range(of: artistEngName)
+ let artistKrNameRange = (artistNameAttributedString.string as NSString).range(of: artistKrName)
+ let artistEnNameRange = (artistNameAttributedString.string as NSString).range(of: artistEnName)
artistNameAttributedString.addAttributes(
[
@@ -71,7 +71,7 @@ extension ArtistDetailHeaderViewController {
.foregroundColor: DesignSystemAsset.BlueGrayColor.gray900.color.withAlphaComponent(0.6),
.kern: -0.5
],
- range: artistEngNameRange
+ range: artistEnNameRange
)
let margin: CGFloat = 104.0
@@ -83,7 +83,7 @@ extension ArtistDetailHeaderViewController {
artistNameAttributedString.addAttributes(
[.font: DesignSystemFontFamily.Pretendard.bold.font(size: availableWidth >= artistNameWidth ? 24 : 20)],
- range: artistNameRange
+ range: artistKrNameRange
)
self.artistNameLabelHeight.constant =
diff --git a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift
index f3cf28b5e..bd3646483 100644
--- a/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift
+++ b/Projects/Features/ArtistFeature/Sources/ViewControllers/ArtistMusicContentViewController.swift
@@ -219,7 +219,9 @@ extension ArtistMusicContentViewController: ArtistMusicCellDelegate {
.first(where: { $0.songID == id })
else { return }
PlayState.shared.append(item: .init(id: tappedSong.songID, title: tappedSong.title, artist: tappedSong.artist))
- songDetailPresenter.present(id: id)
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: tappedSong.songID)
}
}
diff --git a/Projects/Features/BaseFeature/Interface/TogglePopUp/TogglePopUpFactory.swift b/Projects/Features/BaseFeature/Interface/TogglePopUp/TogglePopUpFactory.swift
deleted file mode 100644
index 87e64942d..000000000
--- a/Projects/Features/BaseFeature/Interface/TogglePopUp/TogglePopUpFactory.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-import UIKit
-
-public protocol TogglePopUpFactory {
- func makeView(
- titleString: String,
- firstItemString: String,
- secondItemString: String,
- cancelButtonText: String,
- confirmButtonText: String,
- descriptionText: String,
- completion: (() -> Void)?,
- cancelCompletion: (() -> Void)?
- ) -> UIViewController
-}
-
-public extension TogglePopUpFactory {
- func makeView(
- titleString: String,
- firstItemString: String,
- secondItemString: String,
- cancelButtonText: String = "취소",
- confirmButtonText: String = "확인",
- descriptionText: String = "",
- completion: (() -> Void)? = nil,
- cancelCompletion: (() -> Void)? = nil
- ) -> UIViewController {
- self.makeView(
- titleString: titleString,
- firstItemString: firstItemString,
- secondItemString: secondItemString,
- cancelButtonText: cancelButtonText,
- confirmButtonText: confirmButtonText,
- descriptionText: descriptionText,
- completion: completion,
- cancelCompletion: cancelCompletion
- )
- }
-}
diff --git a/Projects/Features/BaseFeature/Sources/Components/TogglePopUpComponent.swift b/Projects/Features/BaseFeature/Sources/Components/TogglePopUpComponent.swift
deleted file mode 100644
index 363e99582..000000000
--- a/Projects/Features/BaseFeature/Sources/Components/TogglePopUpComponent.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-import BaseFeatureInterface
-import NeedleFoundation
-import UIKit
-
-public final class TogglePopUpComponent: Component, TogglePopUpFactory {
- public func makeView(
- titleString: String,
- firstItemString: String,
- secondItemString: String,
- cancelButtonText: String = "취소",
- confirmButtonText: String = "확인",
- descriptionText: String = "",
- completion: (() -> Void)? = nil,
- cancelCompletion: (() -> Void)? = nil
- ) -> UIViewController {
- return TogglePopupViewController(
- titleString: titleString,
- firstItemString: firstItemString,
- secondItemString: secondItemString,
- cancelButtonText: cancelButtonText,
- confirmButtonText: confirmButtonText,
- descriptionText: descriptionText,
- completion: completion,
- cancelCompletion: cancelCompletion
- )
- }
-}
diff --git a/Projects/Features/BaseFeature/Sources/ViewControllers/TogglePopupViewController.swift b/Projects/Features/BaseFeature/Sources/ViewControllers/TogglePopupViewController.swift
deleted file mode 100644
index 1f33b0789..000000000
--- a/Projects/Features/BaseFeature/Sources/ViewControllers/TogglePopupViewController.swift
+++ /dev/null
@@ -1,230 +0,0 @@
-import DesignSystem
-import SnapKit
-import Then
-import UIKit
-import Utility
-
-public final class TogglePopupViewController: UIViewController {
- private let dimmView = UIView().then {
- $0.backgroundColor = .black.withAlphaComponent(0.4)
- }
-
- private let contentView = UIView().then {
- $0.layer.cornerRadius = 24
- $0.backgroundColor = .white
- }
-
- private let titleLabel = WMLabel(
- text: "",
- textColor: DesignSystemAsset.BlueGrayColor.gray900.color,
- font: .t2(weight: .bold),
- alignment: .center,
- lineHeight: UIFont.WMFontSystem.t2().lineHeight,
- kernValue: -0.5
- )
-
- private let firstItemButton = UIButton()
-
- private let secondItemButton = UIButton()
-
- private let dotImageView = UIImageView().then {
- $0.image = DesignSystemAsset.MyInfo.dot.image
- }
-
- private let descriptionLabel = WMLabel(
- text: "",
- textColor: DesignSystemAsset.BlueGrayColor.gray500.color,
- font: .t7(weight: .light),
- alignment: .left,
- lineHeight: UIFont.WMFontSystem.t7().lineHeight,
- kernValue: -0.5
- )
-
- private let stackView = UIStackView().then {
- $0.axis = .horizontal
- $0.spacing = 0
- $0.distribution = .fillEqually
- }
-
- private let cancelButton = UIButton().then {
- let cancleButtonBackgroundColor = DesignSystemAsset.BlueGrayColor.blueGray400.color
- $0.setBackgroundColor(cancleButtonBackgroundColor, for: .normal)
- $0.setTitle("취소", for: .normal)
- $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal)
- $0.titleLabel?.font = .setFont(.t4(weight: .medium))
- $0.titleLabel?.setTextWithAttributes(alignment: .center)
- }
-
- private let confirmButton = UIButton().then {
- let confirmButtonBackgroundColor = DesignSystemAsset.PrimaryColorV2.point.color
- $0.setBackgroundColor(confirmButtonBackgroundColor, for: .normal)
- $0.setTitle("확인", for: .normal)
- $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal)
- $0.titleLabel?.font = .setFont(.t4(weight: .medium))
- $0.titleLabel?.setTextWithAttributes(alignment: .center)
- }
-
- var titleString: String = ""
- var firstItemString: String = ""
- var secondItemString: String = ""
- var cancelButtonText: String = ""
- var confirmButtonText: String = ""
- var descriptionText: String = ""
- var completion: (() -> Void)?
- var cancelCompletion: (() -> Void)?
-
- init(
- titleString: String,
- firstItemString: String,
- secondItemString: String,
- cancelButtonText: String = "취소",
- confirmButtonText: String = "확인",
- descriptionText: String = "",
- completion: (() -> Void)? = nil,
- cancelCompletion: (() -> Void)? = nil
- ) {
- super.init(nibName: nil, bundle: nil)
- self.titleString = titleString
- self.firstItemString = firstItemString
- self.secondItemString = secondItemString
- self.cancelButtonText = cancelButtonText
- self.confirmButtonText = confirmButtonText
- self.descriptionText = descriptionText
- self.completion = completion
- self.cancelCompletion = cancelCompletion
- }
-
- @available(*, unavailable)
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override public func viewDidLoad() {
- super.viewDidLoad()
- addViews()
- setLayout()
- configureUI()
- firstItemButton.addTarget(nil, action: #selector(firstItemDidTap), for: .touchUpInside)
- secondItemButton.addTarget(nil, action: #selector(secondItemDidTap), for: .touchUpInside)
- cancelButton.addTarget(nil, action: #selector(cancelButtonDidTap), for: .touchUpInside)
- confirmButton.addTarget(nil, action: #selector(confirmButtonDidTap), for: .touchUpInside)
- }
-
- override public func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
- contentView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height)
- UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
- self.contentView.transform = CGAffineTransform.identity
- }, completion: nil)
- }
-
- @objc func firstItemDidTap() {
- print("1")
- }
-
- @objc func secondItemDidTap() {
- print("2")
- }
-
- @objc func cancelButtonDidTap() {
- dismiss()
- }
-
- @objc func confirmButtonDidTap() {
- print("confirm")
- }
-}
-
-private extension TogglePopupViewController {
- func addViews() {
- self.view.addSubviews(
- dimmView,
- contentView
- )
- contentView.addSubviews(
- titleLabel,
- firstItemButton,
- secondItemButton,
- dotImageView,
- descriptionLabel,
- stackView
- )
- stackView.addArrangedSubviews(cancelButton, confirmButton)
- }
-
- func setLayout() {
- dimmView.snp.makeConstraints {
- $0.edges.equalToSuperview()
- }
-
- contentView.snp.makeConstraints {
- $0.width.equalTo(335)
- $0.height.equalTo(322)
- $0.center.equalToSuperview()
- }
-
- titleLabel.snp.makeConstraints {
- $0.horizontalEdges.equalToSuperview().inset(20)
- $0.top.equalToSuperview().offset(30)
- }
-
- firstItemButton.snp.makeConstraints {
- $0.height.equalTo(60)
- $0.top.equalTo(titleLabel.snp.bottom).offset(20)
- $0.horizontalEdges.equalToSuperview().inset(20)
- }
-
- secondItemButton.snp.makeConstraints {
- $0.height.equalTo(60)
- $0.top.equalTo(firstItemButton.snp.bottom).offset(8)
- $0.horizontalEdges.equalToSuperview().inset(20)
- }
-
- dotImageView.snp.makeConstraints {
- $0.centerY.equalTo(descriptionLabel.snp.centerY)
- $0.left.equalToSuperview().offset(20)
- }
- descriptionLabel.snp.makeConstraints {
- $0.top.equalTo(secondItemButton.snp.bottom).offset(8)
- $0.left.equalTo(dotImageView.snp.right)
- $0.right.equalToSuperview().inset(20)
- }
-
- stackView.snp.makeConstraints {
- $0.height.equalTo(56)
- $0.horizontalEdges.equalToSuperview()
- $0.bottom.equalToSuperview()
- }
- }
-
- func configureUI() {
- self.view.backgroundColor = .clear
- contentView.clipsToBounds = true
-
- titleLabel.text = self.titleString
- firstItemButton.setTitle(self.firstItemString, for: .normal)
- secondItemButton.setTitle(self.secondItemString, for: .normal)
- descriptionLabel.text = self.descriptionText
- cancelButton.setTitle(self.cancelButtonText, for: .normal)
- confirmButton.setTitle(self.confirmButtonText, for: .normal)
-
- let gesture = UITapGestureRecognizer(target: self, action: #selector(tappedAround(_:)))
- dimmView.addGestureRecognizer(gesture)
- dimmView.isUserInteractionEnabled = true
- }
-
- @objc func tappedAround(_ sender: UITapGestureRecognizer) {
- dismiss()
- }
-
- func dismiss() {
- UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
- self.contentView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height)
- }, completion: nil)
- // 내려가는 애니메이션이 끝난 다음 dismiss 하기 위해 0.3초 딜레이
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
- self.dismiss(animated: false)
- }
- }
-}
diff --git a/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift b/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift
index eebb01120..203be54d2 100644
--- a/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift
+++ b/Projects/Features/ChartFeature/Sources/ViewContrillers/ChartContentViewController.swift
@@ -196,7 +196,9 @@ extension ChartContentViewController: ChartContentTableViewCellDelegate {
.first(where: { $0.id == id })
else { return }
PlayState.shared.append(item: .init(id: tappedSong.id, title: tappedSong.title, artist: tappedSong.artist))
- songDetailPresenter.present(id: id)
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: tappedSong.id)
}
}
diff --git a/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift b/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift
index 416c17b03..6c7849559 100644
--- a/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift
+++ b/Projects/Features/CreditSongListFeature/Demo/Sources/AppDelegate.swift
@@ -1,8 +1,10 @@
+import BaseFeature
import BaseFeatureInterface
import CreditDomainTesting
@testable import CreditSongListFeature
import CreditSongListFeatureInterface
import Inject
+import RxSwift
import SignInFeatureInterface
import UIKit
@@ -65,6 +67,7 @@ final class FakeCreditSongListTabItemFactory: CreditSongListTabItemFactory {
let reactor = CreditSongListTabItemReactor(
workerName: workerName,
creditSortType: sortType,
+ songDetailPresenter: DummySongDetailPresenter(),
fetchCreditSongListUseCase: fetchCreditSongListUseCase
)
return Inject.ViewControllerHost(
@@ -78,6 +81,16 @@ final class FakeCreditSongListTabItemFactory: CreditSongListTabItemFactory {
}
}
+final class DummySongDetailPresenter: SongDetailPresentable {
+ var presentSongDetailObservable: RxSwift.Observable<(ids: [String], selectedID: String)> {
+ .empty()
+ }
+
+ func present(id: String) {}
+
+ func present(ids: [String], selectedID: String) {}
+}
+
final class DummyContainSongsFactory: ContainSongsFactory {
func makeView(songs: [String]) -> UIViewController {
let viewController = UIViewController()
diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift
index 54b79a5a0..b619de867 100644
--- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift
+++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/Component/CreditSongListTabItemComponent.swift
@@ -1,3 +1,4 @@
+import BaseFeature
import BaseFeatureInterface
import CreditDomainInterface
import CreditSongListFeatureInterface
@@ -10,6 +11,7 @@ public protocol CreditSongListTabItemDependency: Dependency {
var containSongsFactory: any ContainSongsFactory { get }
var textPopupFactory: any TextPopupFactory { get }
var signInFactory: any SignInFactory { get }
+ var songDetailPresenter: any SongDetailPresentable { get }
}
public final class CreditSongListTabItemComponent: Component,
@@ -18,6 +20,7 @@ public final class CreditSongListTabItemComponent: Component Void)
case signIn
+ case dismiss(completion: () -> Void)
}
struct State {
@@ -55,16 +57,19 @@ final class CreditSongListTabItemReactor: Reactor {
let initialState: State
private let workerName: String
private let creditSortType: CreditSongSortType
+ private let songDetailPresenter: any SongDetailPresentable
private let fetchCreditSongListUseCase: any FetchCreditSongListUseCase
init(
workerName: String,
creditSortType: CreditSongSortType,
+ songDetailPresenter: any SongDetailPresentable,
fetchCreditSongListUseCase: any FetchCreditSongListUseCase
) {
self.initialState = .init()
self.workerName = workerName
self.creditSortType = creditSortType
+ self.songDetailPresenter = songDetailPresenter
self.fetchCreditSongListUseCase = fetchCreditSongListUseCase
}
@@ -74,6 +79,12 @@ final class CreditSongListTabItemReactor: Reactor {
return viewDidLoad()
case let .songDidTap(id):
return songDidTap(id: id)
+ case let .songThumbnailDidTap(id):
+ return navigateMutation(navigateType: .dismiss(completion: { [songDetailPresenter] in
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: id)
+ }))
case .randomPlayButtonDidTap:
return randomPlayButtonDidTap()
case .allSelectButtonDidTap:
diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift
index 521678215..dfa22de51 100644
--- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift
+++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabItemViewController.swift
@@ -40,6 +40,9 @@ final class CreditSongListTabItemViewController:
> { [reactor] cell, _, model in
let isSelected = reactor?.currentState.selectedSongs.contains(model.id) ?? false
cell.update(model, isSelected: isSelected)
+ cell.setThumbnailTapHandler { [reactor, id = model.id] in
+ reactor?.action.onNext(.songThumbnailDidTap(id: id))
+ }
}
private lazy var creditSongHeaderRegistration = UICollectionView
@@ -159,6 +162,8 @@ final class CreditSongListTabItemViewController:
owner.presentTextPopup(text: text, completion: completion)
case .signIn:
owner.presentSignIn()
+ case let .dismiss(completion):
+ owner.dismiss(completion: completion)
}
}
.disposed(by: disposeBag)
@@ -309,6 +314,10 @@ extension CreditSongListTabItemViewController {
viewController.modalPresentationStyle = .overFullScreen
UIApplication.topVisibleViewController()?.present(viewController, animated: true)
}
+
+ private func dismiss(completion: @escaping () -> Void) {
+ UIApplication.keyRootViewController?.dismiss(animated: true, completion: completion)
+ }
}
private extension CGFloat {
diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift
index df822de88..b5971f7a4 100644
--- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift
+++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/CreditSongListTabViewController.swift
@@ -82,11 +82,12 @@ private extension CreditSongListTabViewController {
button.selectedFont = DesignSystemFontFamily.Pretendard.bold.font(size: 16)
}
- bar.indicator.weight = .custom(value: 3)
+ bar.indicator.weight = .custom(value: 2)
bar.indicator.tintColor = DesignSystemAsset.PrimaryColorV2.point.color
bar.indicator.overscrollBehavior = .compress
addBar(bar, dataSource: self, at: .custom(view: tabContainerView, layout: nil))
+ bar.layer.zPosition = 1
}
}
diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift
index 6fd81b0c6..d3f08f864 100644
--- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift
+++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/CreditSongCollectionViewCell.swift
@@ -1,6 +1,8 @@
import DesignSystem
import Kingfisher
import Lottie
+import RxGesture
+import RxSwift
import SnapKit
import SongsDomainInterface
import Then
@@ -45,10 +47,14 @@ final class CreditSongCollectionViewCell: UICollectionViewCell {
$0.lineBreakMode = .byTruncatingTail
}
+ private let disposeBag = DisposeBag()
+ private var handler: (() -> Void)?
+
override init(frame: CGRect) {
super.init(frame: frame)
addViews()
setLayout()
+ bind()
}
@available(*, unavailable)
@@ -56,6 +62,10 @@ final class CreditSongCollectionViewCell: UICollectionViewCell {
fatalError("init(coder:) has not been implemented")
}
+ func setThumbnailTapHandler(_ handler: @escaping () -> Void) {
+ self.handler = handler
+ }
+
func update(_ model: CreditSongModel, isSelected: Bool) {
self.thumbnailImageView.kf.setImage(
with: URL(string: WMImageAPI.fetchYoutubeThumbnail(id: model.id).toString),
@@ -99,4 +109,13 @@ private extension CreditSongCollectionViewCell {
$0.trailing.equalToSuperview().inset(20)
}
}
+
+ func bind() {
+ thumbnailImageView.rx.tapGesture()
+ .when(.recognized)
+ .bind { [weak self] _ in
+ self?.handler?()
+ }
+ .disposed(by: disposeBag)
+ }
}
diff --git a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift
index e52944b5c..8fe971979 100644
--- a/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift
+++ b/Projects/Features/CreditSongListFeature/Sources/CreditSongListTab/View/RandomPlayButton.swift
@@ -42,7 +42,7 @@ final class RandomPlayButton: UIButton {
randomImageView.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.equalToSuperview().inset(32)
- $0.size.equalTo(24)
+ $0.size.equalTo(32)
}
playLabel.snp.makeConstraints {
diff --git a/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift b/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift
index 00a29a596..92bfdd693 100644
--- a/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift
+++ b/Projects/Features/HomeFeature/Sources/ViewControllers/HomeViewController.swift
@@ -410,7 +410,9 @@ extension HomeViewController: HomeChartCellDelegate {
func thumbnailDidTap(model: ChartRankingEntity) {
LogManager.analytics(HomeAnalyticsLog.clickMusicItem(location: .homeTop100, id: model.id))
PlayState.shared.append(item: .init(id: model.id, title: model.title, artist: model.artist))
- songDetailPresenter.present(id: model.id)
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: model.id)
}
func playButtonDidTap(model: ChartRankingEntity) {
@@ -426,7 +428,9 @@ extension HomeViewController: HomeNewSongCellDelegate {
func thumbnailDidTap(model: NewSongsEntity) {
LogManager.analytics(HomeAnalyticsLog.clickMusicItem(location: .homeRecent, id: model.id))
PlayState.shared.append(item: .init(id: model.id, title: model.title, artist: model.artist))
- songDetailPresenter.present(id: model.id)
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: model.id)
}
func playButtonDidTap(model: NewSongsEntity) {
diff --git a/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift b/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift
index 7be7c8171..f275a9c73 100644
--- a/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift
+++ b/Projects/Features/HomeFeature/Sources/ViewControllers/NewSongsContentViewController.swift
@@ -221,7 +221,9 @@ extension NewSongsContentViewController: NewSongsCellDelegate {
.first(where: { $0.id == id })
else { return }
PlayState.shared.append(item: .init(id: tappedSong.id, title: tappedSong.title, artist: tappedSong.artist))
- songDetailPresenter.present(id: id)
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: id)
}
}
diff --git a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift
index 8c6651f5d..2fe1590a3 100644
--- a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift
+++ b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController+Bind.swift
@@ -8,6 +8,16 @@ extension LyricHighlightingViewController {
func inputBind() {
input.fetchLyric.onNext(())
+ activateButton.rx.tap
+ .do(onNext: { [activateContentView] _ in
+ UIView.animate(withDuration: 0.2) {
+ activateContentView.isHidden = true
+ }
+ })
+ .map { _ in true }
+ .bind(to: input.didTapActivateButton)
+ .disposed(by: disposeBag)
+
collectionView.rx.itemSelected
.bind(to: input.didTapHighlighting)
.disposed(by: disposeBag)
@@ -39,11 +49,11 @@ extension LyricHighlightingViewController {
output.dataSource
.skip(1)
- .do(onNext: { [indicator, warningView, collectionView, writerLabel] model in
- indicator.stopAnimating()
+ .do(onNext: { [activityIndicator, warningView, collectionView, bottomContentStackView] model in
+ activityIndicator.stopAnimating()
warningView.isHidden = !model.isEmpty
- collectionView.isHidden = !warningView.isHidden
- writerLabel.isHidden = !warningView.isHidden
+ collectionView.isHidden = model.isEmpty
+ bottomContentStackView.isHidden = model.isEmpty
})
.bind(to: collectionView.rx.items) { collectionView, index, entity in
guard let cell = collectionView.dequeueReusableCell(
@@ -58,8 +68,11 @@ extension LyricHighlightingViewController {
.disposed(by: disposeBag)
output.isStorable
- .map { !$0 }
- .bind(to: completeButton.rx.isHidden)
+ .bind(with: self) { owner, isStorable in
+ UIView.animate(withDuration: 0.2) {
+ owner.completeButton.alpha = isStorable ? 1 : 0
+ }
+ }
.disposed(by: disposeBag)
output.goDecoratingScene
diff --git a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift
index f3bb05cec..7ee2609d0 100644
--- a/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift
+++ b/Projects/Features/LyricHighlightingFeature/Sources/ViewControllers/LyricHighlightingViewController.swift
@@ -48,6 +48,14 @@ public final class LyricHighlightingViewController: UIViewController {
$0.backgroundColor = .clear
}
+ let bottomContentStackView = UIStackView().then {
+ $0.axis = .vertical
+ $0.distribution = .fill
+ $0.isHidden = true
+ }
+
+ let writerContentView = UIView()
+
let writerLabel = WMLabel(
text: "",
textColor: .white.withAlphaComponent(0.5),
@@ -56,6 +64,19 @@ public final class LyricHighlightingViewController: UIViewController {
kernValue: -0.5
)
+ let activateContentView = UIView()
+
+ let activateTopLineLabel = UILabel().then {
+ $0.backgroundColor = DesignSystemAsset.NewGrayColor.gray700.color
+ }
+
+ let activateButton = UIButton(type: .system).then {
+ $0.setImage(
+ DesignSystemAsset.LyricHighlighting.lyricHighlightSaveOff.image.withRenderingMode(.alwaysOriginal),
+ for: .normal
+ )
+ }
+
let warningView = UIView().then {
$0.isHidden = true
}
@@ -85,10 +106,10 @@ public final class LyricHighlightingViewController: UIViewController {
$0.setTitleColor(DesignSystemAsset.PrimaryColorV2.point.color, for: .normal)
$0.titleLabel?.font = DesignSystemFontFamily.Pretendard.bold.font(size: 12)
$0.titleLabel?.setTextWithAttributes(alignment: .center)
- $0.isHidden = true
+ $0.alpha = 0
}
- let indicator = NVActivityIndicatorView(frame: .zero).then {
+ let activityIndicator = NVActivityIndicatorView(frame: .zero).then {
$0.type = .circleStrokeSpin
$0.color = DesignSystemAsset.PrimaryColorV2.point.color
}
@@ -151,14 +172,19 @@ private extension LyricHighlightingViewController {
thumbnailImageView,
dimmedBackgroundView,
collectionView,
- writerLabel,
+ bottomContentStackView,
warningView,
navigationBarView,
- indicator
+ activityIndicator
)
+
navigationBarView.addSubviews(backButton, navigationTitleStackView, completeButton)
navigationTitleStackView.addArrangedSubview(songLabel)
navigationTitleStackView.addArrangedSubview(artistLabel)
+
+ bottomContentStackView.addArrangedSubviews(writerContentView, activateContentView)
+ writerContentView.addSubview(writerLabel)
+ activateContentView.addSubviews(activateTopLineLabel, activateButton)
warningView.addSubviews(warningImageView, warningLabel)
}
@@ -209,13 +235,36 @@ private extension LyricHighlightingViewController {
$0.horizontalEdges.equalToSuperview()
}
+ bottomContentStackView.snp.makeConstraints {
+ $0.top.equalTo(collectionView.snp.bottom).offset(19)
+ $0.horizontalEdges.equalToSuperview()
+ $0.bottom.equalTo(view.safeAreaLayoutGuide)
+ }
+
+ writerContentView.snp.makeConstraints {
+ $0.height.equalTo(51)
+ }
+
writerLabel.snp.makeConstraints {
- $0.top.equalTo(collectionView.snp.bottom).offset(18)
+ $0.top.equalToSuperview()
$0.horizontalEdges.equalToSuperview().inset(20)
- $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-30)
$0.height.equalTo(22)
}
+ activateContentView.snp.makeConstraints {
+ $0.height.equalTo(56)
+ }
+
+ activateTopLineLabel.snp.makeConstraints {
+ $0.top.equalToSuperview()
+ $0.horizontalEdges.equalToSuperview()
+ $0.height.equalTo(1)
+ }
+
+ activateButton.snp.makeConstraints {
+ $0.edges.equalToSuperview()
+ }
+
warningView.snp.makeConstraints {
$0.top.equalToSuperview().offset(APP_HEIGHT() * ((294.0 - 6.0) / 812.0))
$0.centerX.equalToSuperview()
@@ -233,8 +282,8 @@ private extension LyricHighlightingViewController {
$0.bottom.equalToSuperview()
}
- indicator.snp.makeConstraints {
- $0.center.equalToSuperview()
+ activityIndicator.snp.makeConstraints {
+ $0.center.equalTo(warningView.snp.center)
$0.size.equalTo(30)
}
}
@@ -262,7 +311,7 @@ private extension LyricHighlightingViewController {
.alternativeSources(alternativeSources)
]
)
- indicator.startAnimating()
+ activityIndicator.startAnimating()
}
func createLayout() -> UICollectionViewLayout {
diff --git a/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift b/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift
index 88ed2e459..a23e71a48 100644
--- a/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift
+++ b/Projects/Features/LyricHighlightingFeature/Sources/ViewModels/LyricHighlightingViewModel.swift
@@ -26,6 +26,7 @@ public final class LyricHighlightingViewModel: ViewModelType {
public struct Input {
let fetchLyric: PublishSubject = PublishSubject()
+ let didTapActivateButton: BehaviorRelay = BehaviorRelay(value: false)
let didTapHighlighting: BehaviorRelay = BehaviorRelay(value: .init(row: -1, section: 0))
let didTapSaveButton: PublishSubject = PublishSubject()
}
@@ -61,11 +62,18 @@ public final class LyricHighlightingViewModel: ViewModelType {
.disposed(by: disposeBag)
input.didTapHighlighting
- .map { $0.item }
+ .withLatestFrom(input.didTapActivateButton) { ($0, $1) }
+ .filter { $0.1 }
+ .map { $0.0.item }
.filter { $0 >= 0 }
.withLatestFrom(output.dataSource) { ($0, $1) }
.filter { index, entities in
- guard entities[index].isHighlighting || entities.filter({ $0.isHighlighting }).count < 4 else {
+ let currentTotalLineCount: Int = entities.filter { $0.isHighlighting }
+ .map { $0.text.components(separatedBy: "\n").count }
+ .reduce(0, +)
+ let nowSelectItemLineCount: Int = entities[index].text
+ .components(separatedBy: "\n").count
+ guard entities[index].isHighlighting || (currentTotalLineCount + nowSelectItemLineCount <= 4) else {
output.showToast.onNext("가사는 최대 4줄까지 선택 가능합니다.")
return false
}
diff --git a/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift b/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift
index e8bff1268..beeff0a99 100644
--- a/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift
+++ b/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift
@@ -157,7 +157,9 @@ private extension MainTabBarViewController {
case "songDetail":
let id = params["id"] as? String ?? ""
- songDetailPresenter.present(id: id)
+ let playlistIDs = PlayState.shared.currentPlaylist
+ .map(\.id)
+ songDetailPresenter.present(ids: playlistIDs, selectedID: id)
default:
break
diff --git a/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift b/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift
index ca6209cc0..99b3439b8 100644
--- a/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift
+++ b/Projects/Features/MusicDetailFeature/Sources/MusicDetail/MusicDetailViewController.swift
@@ -60,7 +60,7 @@ final class MusicDetailViewController: BaseReactorViewController Void)?,
+ cancelCompletion: (() -> Void)?
+ ) -> UIViewController
+}
+
+public extension PlayTypeTogglePopupFactory {
+ func makeView(
+ completion: ((_ selectedItemString: String) -> Void)? = nil,
+ cancelCompletion: (() -> Void)? = nil
+ ) -> UIViewController {
+ self.makeView(
+ completion: completion,
+ cancelCompletion: cancelCompletion
+ )
+ }
+}
diff --git a/Projects/Features/MyInfoFeature/Sources/Components/PlayTypeTogglePopupComponent.swift b/Projects/Features/MyInfoFeature/Sources/Components/PlayTypeTogglePopupComponent.swift
new file mode 100644
index 000000000..5866f6004
--- /dev/null
+++ b/Projects/Features/MyInfoFeature/Sources/Components/PlayTypeTogglePopupComponent.swift
@@ -0,0 +1,15 @@
+import MyInfoFeatureInterface
+import NeedleFoundation
+import UIKit
+
+public final class PlayTypeTogglePopupComponent: Component, PlayTypeTogglePopupFactory {
+ public func makeView(
+ completion: ((_ selectedItemString: String) -> Void)? = nil,
+ cancelCompletion: (() -> Void)? = nil
+ ) -> UIViewController {
+ return PlayTypeTogglePopupViewController(
+ completion: completion,
+ cancelCompletion: cancelCompletion
+ )
+ }
+}
diff --git a/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift b/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift
index 85f01520c..a805b41c7 100644
--- a/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift
+++ b/Projects/Features/MyInfoFeature/Sources/Components/SettingComponent.swift
@@ -17,7 +17,7 @@ public protocol SettingDependency: Dependency {
var serviceTermsFactory: any ServiceTermFactory { get }
var privacyFactory: any PrivacyFactory { get }
var openSourceLicenseFactory: any OpenSourceLicenseFactory { get }
- var togglePopUpFactory: any TogglePopUpFactory { get }
+ var playTypeTogglePopupFactory: any PlayTypeTogglePopupFactory { get }
}
public final class SettingComponent: Component, SettingFactory {
@@ -33,7 +33,7 @@ public final class SettingComponent: Component, SettingFactor
serviceTermsFactory: dependency.serviceTermsFactory,
privacyFactory: dependency.privacyFactory,
openSourceLicenseFactory: dependency.openSourceLicenseFactory,
- togglePopUpFactory: dependency.togglePopUpFactory
+ playTypeTogglePopupFactory: dependency.playTypeTogglePopupFactory
)
}
}
diff --git a/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift b/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift
index db37ff0a0..7679b37c2 100644
--- a/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift
+++ b/Projects/Features/MyInfoFeature/Sources/Reactors/SettingReactor.swift
@@ -37,6 +37,7 @@ final class SettingReactor: Reactor {
case updateIsHiddenWithDrawButton(Bool)
case changedNotificationAuthorizationStatus(Bool)
case updateIsShowActivityIndicator(Bool)
+ case reloadTableView
}
enum NavigateType {
@@ -59,6 +60,7 @@ final class SettingReactor: Reactor {
@Pulse var withDrawButtonDidTap: Bool?
@Pulse var confirmRemoveCacheButtonDidTap: Bool?
@Pulse var withDrawResult: BaseEntity?
+ @Pulse var reloadTableView: Void?
}
var initialState: State
@@ -137,6 +139,8 @@ final class SettingReactor: Reactor {
newState.notificationAuthorizationStatus = granted
case let .updateIsShowActivityIndicator(isShow):
newState.isShowActivityIndicator = isShow
+ case .reloadTableView:
+ newState.reloadTableView = ()
}
return newState
}
@@ -161,10 +165,18 @@ final class SettingReactor: Reactor {
return .just(.changedNotificationAuthorizationStatus(granted))
}
+ let updatePlayTypeMutation = PreferenceManager.$playWithYoutubeMusic
+ .distinctUntilChanged()
+ .map { $0 ?? .youtube }
+ .flatMap { playType -> Observable in
+ return .just(.reloadTableView)
+ }
+
return Observable.merge(
mutation,
updateIsLoggedInMutation,
- updatepushNotificationAuthorizationStatusMutation
+ updatepushNotificationAuthorizationStatusMutation,
+ updatePlayTypeMutation
)
}
}
diff --git a/Projects/Features/MyInfoFeature/Sources/ViewControllers/PlayTypeTogglePopup/PlayTypeTogglePopupViewController.swift b/Projects/Features/MyInfoFeature/Sources/ViewControllers/PlayTypeTogglePopup/PlayTypeTogglePopupViewController.swift
new file mode 100644
index 000000000..7cba2d1db
--- /dev/null
+++ b/Projects/Features/MyInfoFeature/Sources/ViewControllers/PlayTypeTogglePopup/PlayTypeTogglePopupViewController.swift
@@ -0,0 +1,282 @@
+import DesignSystem
+import RxCocoa
+import RxSwift
+import SnapKit
+import Then
+import UIKit
+import Utility
+
+public final class PlayTypeTogglePopupViewController: UIViewController {
+ private let dimmView = UIView().then {
+ $0.backgroundColor = .black.withAlphaComponent(0.4)
+ }
+
+ private let contentView = UIView().then {
+ $0.layer.cornerRadius = 24
+ $0.backgroundColor = .white
+ }
+
+ private let titleLabel = WMLabel(
+ text: "어떻게 재생할까요?",
+ textColor: DesignSystemAsset.BlueGrayColor.gray900.color,
+ font: .t2(weight: .bold),
+ alignment: .center,
+ lineHeight: UIFont.WMFontSystem.t2().lineHeight,
+ kernValue: -0.5
+ )
+
+ private let vStackView = UIStackView().then {
+ $0.axis = .vertical
+ $0.spacing = 8
+ $0.distribution = .fillEqually
+ }
+
+ private let firstItemButton = PlayTypeTogglePopupItemButtonView()
+
+ private let secondItemButton = PlayTypeTogglePopupItemButtonView()
+
+ private let firstDotImageView = UIImageView().then {
+ $0.image = DesignSystemAsset.MyInfo.dot.image
+ }
+
+ private let secondDotImageView = UIImageView().then {
+ $0.image = DesignSystemAsset.MyInfo.dot.image
+ }
+
+ private let firstDescriptionLabel = WMLabel(
+ text: "해당 기능을 사용하려면 앱이 설치되어 있어야 합니다.",
+ textColor: DesignSystemAsset.BlueGrayColor.gray500.color,
+ font: .t7(weight: .light),
+ alignment: .left
+ )
+
+ private let secondDescriptionLabel = WMLabel(
+ text: "일부 노래나 쇼츠는 YouTube Music에서 지원되지 않습니다.",
+ textColor: DesignSystemAsset.BlueGrayColor.gray500.color,
+ font: .t7(weight: .light),
+ alignment: .left
+ )
+
+ private let hStackView = UIStackView().then {
+ $0.axis = .horizontal
+ $0.spacing = 0
+ $0.distribution = .fillEqually
+ }
+
+ private let cancelButton = UIButton().then {
+ let cancleButtonBackgroundColor = DesignSystemAsset.BlueGrayColor.blueGray400.color
+ $0.setBackgroundColor(cancleButtonBackgroundColor, for: .normal)
+ $0.setTitle("취소", for: .normal)
+ $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal)
+ $0.titleLabel?.font = .setFont(.t4(weight: .medium))
+ $0.titleLabel?.setTextWithAttributes(alignment: .center)
+ }
+
+ private let confirmButton = UIButton().then {
+ let confirmButtonBackgroundColor = DesignSystemAsset.PrimaryColorV2.point.color
+ $0.setBackgroundColor(confirmButtonBackgroundColor, for: .normal)
+ $0.setTitle("확인", for: .normal)
+ $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray25.color, for: .normal)
+ $0.titleLabel?.font = .setFont(.t4(weight: .medium))
+ $0.titleLabel?.setTextWithAttributes(alignment: .center)
+ }
+
+ private let disposeBag = DisposeBag()
+ private var selectedItemString: String = ""
+ private var completion: ((_ selectedItemString: String) -> Void)?
+ private var cancelCompletion: (() -> Void)?
+
+ init(
+ completion: ((_ selectedItemString: String) -> Void)? = nil,
+ cancelCompletion: (() -> Void)? = nil
+ ) {
+ self.completion = completion
+ self.cancelCompletion = cancelCompletion
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override public func viewDidLoad() {
+ super.viewDidLoad()
+ addViews()
+ setLayout()
+ configureUI()
+ setActions()
+ firstItemButton.setDelegate(self)
+ secondItemButton.setDelegate(self)
+ }
+
+ override public func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ contentView.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height)
+ UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
+ self.contentView.transform = CGAffineTransform.identity
+ }, completion: nil)
+ }
+
+ func setActions() {
+ NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification)
+ .subscribe(onNext: { [weak self] _ in
+ self?.secondItemButton.checkAppIsInstalled()
+ })
+ .disposed(by: disposeBag)
+
+ let cancelAction = UIAction { [weak self] _ in
+ self?.dismiss()
+ }
+
+ let confirmAction = UIAction { [weak self] _ in
+ guard let self else { return }
+ self.completion?(self.selectedItemString)
+ self.dismiss()
+ }
+ cancelButton.addAction(cancelAction, for: .touchUpInside)
+ confirmButton.addAction(confirmAction, for: .touchUpInside)
+ }
+}
+
+private extension PlayTypeTogglePopupViewController {
+ func addViews() {
+ self.view.addSubviews(
+ dimmView,
+ contentView
+ )
+ contentView.addSubviews(
+ titleLabel,
+ firstItemButton,
+ secondItemButton,
+ firstDotImageView,
+ firstDescriptionLabel,
+ secondDotImageView,
+ secondDescriptionLabel,
+ hStackView
+ )
+ hStackView.addArrangedSubviews(cancelButton, confirmButton)
+ }
+
+ func setLayout() {
+ let is320 = APP_WIDTH() <= 320
+
+ dimmView.snp.makeConstraints {
+ $0.edges.equalToSuperview()
+ }
+
+ contentView.snp.makeConstraints {
+ $0.height.equalTo(is320 ? 364 : 344)
+ $0.center.equalToSuperview()
+ $0.horizontalEdges.equalToSuperview().inset(20)
+ }
+
+ titleLabel.snp.makeConstraints {
+ $0.horizontalEdges.equalToSuperview().inset(20)
+ $0.top.equalToSuperview().offset(30)
+ }
+
+ firstItemButton.snp.makeConstraints {
+ $0.height.equalTo(60)
+ $0.top.equalTo(titleLabel.snp.bottom).offset(20)
+ $0.horizontalEdges.equalToSuperview().inset(20)
+ }
+
+ secondItemButton.snp.makeConstraints {
+ $0.height.equalTo(60)
+ $0.top.equalTo(firstItemButton.snp.bottom).offset(8)
+ $0.horizontalEdges.equalToSuperview().inset(20)
+ }
+
+ firstDotImageView.snp.makeConstraints {
+ $0.size.equalTo(16)
+ $0.top.equalTo(secondItemButton.snp.bottom).offset(8)
+ $0.left.equalToSuperview().offset(20)
+ }
+
+ firstDescriptionLabel.snp.makeConstraints {
+ $0.top.equalTo(firstDotImageView)
+ $0.left.equalTo(firstDotImageView.snp.right)
+ $0.right.equalToSuperview().inset(20)
+ }
+
+ secondDotImageView.snp.makeConstraints {
+ $0.size.equalTo(16)
+ $0.top.equalTo(firstDescriptionLabel.snp.bottom).offset(4)
+ $0.left.equalToSuperview().offset(20)
+ }
+
+ secondDescriptionLabel.snp.makeConstraints {
+ $0.top.equalTo(secondDotImageView)
+ $0.left.equalTo(secondDotImageView.snp.right)
+ $0.right.equalToSuperview().inset(20)
+ }
+
+ hStackView.snp.makeConstraints {
+ $0.height.equalTo(56)
+ $0.horizontalEdges.equalToSuperview()
+ $0.bottom.equalToSuperview()
+ }
+ }
+
+ func configureUI() {
+ self.view.backgroundColor = .clear
+ contentView.clipsToBounds = true
+
+ let playType = PreferenceManager.playWithYoutubeMusic ?? .youtube
+ self.selectedItemString = playType.display
+
+ firstItemButton.setTitleWithOption(title: playType.display)
+ secondItemButton.setTitleWithOption(title: playType.display, shouldCheckAppIsInstalled: true)
+
+ firstItemButton.isSelected = playType == .youtube
+ secondItemButton.isSelected = playType == .youtubeMusic
+
+ if APP_WIDTH() <= 320 { // 두줄로 표기하기 위함
+ firstDescriptionLabel.numberOfLines = 0
+ secondDescriptionLabel.numberOfLines = 0
+ }
+
+ let gesture = UITapGestureRecognizer(target: self, action: #selector(tappedAround(_:)))
+ dimmView.addGestureRecognizer(gesture)
+ dimmView.isUserInteractionEnabled = true
+ }
+
+ @objc func tappedAround(_ sender: UITapGestureRecognizer) {
+ dismiss()
+ }
+
+ func dismiss() {
+ UIView.animate(
+ withDuration: 0.3,
+ delay: 0,
+ options: .curveEaseInOut,
+ animations: { [weak self] in
+ guard let self = self else { return }
+ let translationY = self.view.frame.height
+ self.contentView.transform = CGAffineTransform(translationX: 0, y: translationY)
+ },
+ completion: { [weak self] _ in
+ self?.dismiss(animated: false)
+ }
+ )
+ }
+}
+
+extension PlayTypeTogglePopupViewController: PlayTypeTogglePopupItemButtonViewDelegate {
+ func tappedButtonAction(title: String) {
+ switch title {
+ case "YouTube":
+ self.selectedItemString = title
+ firstItemButton.isSelected = true
+ secondItemButton.isSelected = false
+ case "YouTube Music":
+ self.selectedItemString = title
+ firstItemButton.isSelected = false
+ secondItemButton.isSelected = true
+ default:
+ break
+ }
+ }
+}
diff --git a/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift b/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift
index b02d66d42..9d9a9c54d 100644
--- a/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift
+++ b/Projects/Features/MyInfoFeature/Sources/ViewControllers/Setting/SettingViewController.swift
@@ -18,7 +18,7 @@ final class SettingViewController: BaseReactorViewController {
private var serviceTermsFactory: ServiceTermFactory!
private var privacyFactory: PrivacyFactory!
private var openSourceLicenseFactory: OpenSourceLicenseFactory!
- private var togglePopUpFactory: TogglePopUpFactory!
+ private var playTypeTogglePopupFactory: PlayTypeTogglePopupFactory!
let settingView = SettingView()
let settingItemDataSource = SettingItemDataSource()
@@ -45,7 +45,7 @@ final class SettingViewController: BaseReactorViewController {
serviceTermsFactory: ServiceTermFactory,
privacyFactory: PrivacyFactory,
openSourceLicenseFactory: OpenSourceLicenseFactory,
- togglePopUpFactory: TogglePopUpFactory
+ playTypeTogglePopupFactory: PlayTypeTogglePopupFactory
) -> SettingViewController {
let viewController = SettingViewController(reactor: reactor)
viewController.textPopupFactory = textPopupFactory
@@ -53,11 +53,18 @@ final class SettingViewController: BaseReactorViewController {
viewController.serviceTermsFactory = serviceTermsFactory
viewController.privacyFactory = privacyFactory
viewController.openSourceLicenseFactory = openSourceLicenseFactory
- viewController.togglePopUpFactory = togglePopUpFactory
+ viewController.playTypeTogglePopupFactory = playTypeTogglePopupFactory
return viewController
}
override func bindState(reactor: SettingReactor) {
+ reactor.pulse(\.$reloadTableView)
+ .compactMap { $0 }
+ .bind(with: self) { owner, _ in
+ owner.settingView.settingItemTableView.reloadData()
+ }
+ .disposed(by: disposeBag)
+
reactor.state.map(\.isShowActivityIndicator)
.distinctUntilChanged()
.bind(with: self) { owner, isShow in
@@ -222,37 +229,12 @@ extension SettingViewController: UITableViewDelegate {
guard let cell = tableView.cellForRow(at: indexPath) as? SettingItemTableViewCell else { return }
guard let category = cell.category else { return }
- let togglePopUpVC = togglePopUpFactory.makeView(
- titleString: "어떻게 재생할까요?",
- firstItemString: "YouTube",
- secondItemString: "YouTube Music",
- descriptionText: "해당 기능을 사용하려면 앱이 설치되어 있어야 합니다.",
- completion: {},
- cancelCompletion: {}
- )
- togglePopUpVC.modalPresentationStyle = .overFullScreen
-
- let textPopupVC = textPopupFactory.makeView(
- text: "로그아웃 하시겠습니까?",
- cancelButtonIsHidden: false,
- confirmButtonText: "확인",
- cancelButtonText: "취소",
- completion: { [weak self] in
- guard let self else { return }
- let log = SettingAnalyticsLog.completeLogout
- LogManager.analytics(log)
-
- self.reactor?.action.onNext(.confirmLogoutButtonDidTap)
- },
- cancelCompletion: {}
- )
-
switch category {
case .appPush:
LogManager.analytics(SettingAnalyticsLog.clickNotificationButton)
reactor?.action.onNext(.appPushSettingNavigationDidTap)
case .playType:
- self.present(togglePopUpVC, animated: false)
+ showPlayTypeTogglePopup()
case .serviceTerms: LogManager.analytics(SettingAnalyticsLog.clickTermsOfServiceButton)
reactor?.action.onNext(.serviceTermsNavigationDidTap)
case .privacy:
@@ -266,10 +248,46 @@ extension SettingViewController: UITableViewDelegate {
reactor?.action.onNext(.removeCacheButtonDidTap)
case .logout:
LogManager.analytics(SettingAnalyticsLog.clickLogoutButton)
- showBottomSheet(content: textPopupVC, size: .fixed(234))
+ showLogoutTextPopup()
case .versionInfo:
LogManager.analytics(SettingAnalyticsLog.clickVersionButton)
reactor?.action.onNext(.versionInfoButtonDidTap)
}
}
+
+ private func showPlayTypeTogglePopup() {
+ let togglePopupVC = playTypeTogglePopupFactory.makeView(
+ completion: { selectedItemString in
+ switch selectedItemString {
+ case YoutubePlayType.youtube.display:
+ PreferenceManager.playWithYoutubeMusic = .youtube
+ case YoutubePlayType.youtubeMusic.display:
+ PreferenceManager.playWithYoutubeMusic = .youtubeMusic
+ default:
+ break
+ }
+ },
+ cancelCompletion: {}
+ )
+ togglePopupVC.modalPresentationStyle = .overFullScreen
+ self.present(togglePopupVC, animated: false)
+ }
+
+ private func showLogoutTextPopup() {
+ let textPopUpVC = textPopupFactory.makeView(
+ text: "로그아웃 하시겠습니까?",
+ cancelButtonIsHidden: false,
+ confirmButtonText: "확인",
+ cancelButtonText: "취소",
+ completion: { [weak self] in
+ guard let self else { return }
+ let log = SettingAnalyticsLog.completeLogout
+ LogManager.analytics(log)
+
+ self.reactor?.action.onNext(.confirmLogoutButtonDidTap)
+ },
+ cancelCompletion: {}
+ )
+ showBottomSheet(content: textPopUpVC, size: .fixed(234))
+ }
}
diff --git a/Projects/Features/MyInfoFeature/Sources/Views/PlayTypeTogglePopupItemButton.swift b/Projects/Features/MyInfoFeature/Sources/Views/PlayTypeTogglePopupItemButton.swift
new file mode 100644
index 000000000..b046957b0
--- /dev/null
+++ b/Projects/Features/MyInfoFeature/Sources/Views/PlayTypeTogglePopupItemButton.swift
@@ -0,0 +1,172 @@
+import DesignSystem
+import SnapKit
+import Then
+import UIKit
+
+protocol PlayTypeTogglePopupItemButtonViewDelegate: AnyObject {
+ func tappedButtonAction(title: String)
+}
+
+final class PlayTypeTogglePopupItemButtonView: UIView {
+ private let baseView = UIView().then {
+ $0.layer.cornerRadius = 12
+ $0.layer.borderColor = DesignSystemAsset.BlueGrayColor.gray200.color.cgColor
+ $0.layer.borderWidth = 1
+ $0.backgroundColor = .white
+ }
+
+ private let titleLabel = WMLabel(
+ text: "",
+ textColor: DesignSystemAsset.BlueGrayColor.blueGray900.color,
+ font: .t5(weight: .light),
+ alignment: .left
+ )
+
+ private let imageView = UIImageView().then {
+ $0.image = DesignSystemAsset.MyInfo.donut.image
+ $0.contentMode = .scaleAspectFit
+ }
+
+ private let installButton = UIButton().then {
+ $0.layer.cornerRadius = 4
+ $0.layer.borderWidth = 1
+ $0.layer.borderColor = DesignSystemAsset.BlueGrayColor.gray300.color.cgColor
+ $0.setTitle("미설치", for: .normal)
+ $0.setBackgroundColor(.white, for: .normal)
+ $0.setBackgroundColor(.lightGray, for: .highlighted)
+ $0.setTitleColor(DesignSystemAsset.BlueGrayColor.gray400.color, for: .normal)
+ $0.setTitleColor(.white, for: .highlighted)
+ $0.titleLabel?.font = UIFont.WMFontSystem.t7(weight: .bold).font
+ $0.clipsToBounds = true
+ $0.isHidden = true
+ }
+
+ private let button = UIButton()
+
+ private weak var delegate: PlayTypeTogglePopupItemButtonViewDelegate?
+
+ private var shouldCheckAppIsInstalled: Bool = false
+
+ var isSelected: Bool = false {
+ didSet {
+ didChangedSelection(isSelected)
+ }
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ backgroundColor = .white
+ addViews()
+ setLayout()
+ setActions()
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func setDelegate(_ delegate: PlayTypeTogglePopupItemButtonViewDelegate) {
+ self.delegate = delegate
+ }
+
+ func setTitleWithOption(
+ title: String,
+ shouldCheckAppIsInstalled: Bool = false
+ ) {
+ self.titleLabel.text = title
+ self.shouldCheckAppIsInstalled = shouldCheckAppIsInstalled
+ if shouldCheckAppIsInstalled {
+ checkAppIsInstalled()
+ }
+ }
+
+ @discardableResult
+ func checkAppIsInstalled() -> Bool {
+ let isInstalled: Bool
+ if let url = URL(string: "youtubemusic://"), UIApplication.shared.canOpenURL(url) {
+ isInstalled = true
+ } else {
+ isInstalled = false
+ }
+ installButton.isHidden = isInstalled
+ button.isEnabled = isInstalled
+
+ return isInstalled
+ }
+}
+
+private extension PlayTypeTogglePopupItemButtonView {
+ func didChangedSelection(_ isSelected: Bool) {
+ UIView.animate(withDuration: 0.2) { [weak self] in
+ guard let self else { return }
+
+ self.baseView.layer.borderColor = isSelected ?
+ DesignSystemAsset.PrimaryColorV2.point.color.cgColor :
+ DesignSystemAsset.BlueGrayColor.blueGray200.color.cgColor
+
+ self.baseView.layer.borderWidth = isSelected ? 2 : 1
+ }
+
+ self.imageView.image = isSelected ?
+ DesignSystemAsset.MyInfo.donutColor.image :
+ DesignSystemAsset.MyInfo.donut.image
+
+ let font = isSelected ?
+ UIFont.WMFontSystem.t5(weight: .medium) :
+ UIFont.WMFontSystem.t5(weight: .light)
+ self.titleLabel.setFont(font)
+ }
+
+ func setActions() {
+ let buttonAction = UIAction { [weak self] _ in
+ guard let self = self else { return }
+ self.delegate?.tappedButtonAction(title: titleLabel.text ?? "")
+ }
+ button.addAction(buttonAction, for: .touchUpInside)
+
+ installButton.addAction {
+ let youtubeMusicAppStoreURL = "itms-apps://apps.apple.com/app/id1017492454"
+ if let url = URL(string: youtubeMusicAppStoreURL) {
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
+ }
+ }
+ }
+
+ func addViews() {
+ addSubview(baseView)
+ addSubview(titleLabel)
+ addSubview(imageView)
+ addSubview(button)
+ addSubview(installButton)
+ }
+
+ func setLayout() {
+ baseView.snp.makeConstraints {
+ $0.edges.equalToSuperview()
+ }
+
+ titleLabel.snp.makeConstraints {
+ $0.centerY.equalToSuperview()
+ $0.leading.equalToSuperview().inset(16)
+ $0.trailing.equalTo(imageView.snp.leading).offset(-20)
+ }
+
+ imageView.snp.makeConstraints {
+ $0.size.equalTo(24)
+ $0.trailing.equalToSuperview().inset(20)
+ $0.centerY.equalToSuperview()
+ }
+
+ button.snp.makeConstraints {
+ $0.edges.equalTo(baseView)
+ }
+
+ installButton.snp.makeConstraints {
+ $0.width.equalTo(55)
+ $0.height.equalTo(24)
+ $0.trailing.equalToSuperview().inset(20)
+ $0.centerY.equalToSuperview()
+ }
+ }
+}
diff --git a/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift b/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift
index 9d1fad27b..600d4b9ca 100644
--- a/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift
+++ b/Projects/Features/MyInfoFeature/Sources/Views/SettingItemTableViewCell.swift
@@ -72,12 +72,12 @@ private extension SettingItemTableViewCell {
switch type {
case let .navigate(category):
let pushNotificationAuthorizationStatus = PreferenceManager.pushNotificationAuthorizationStatus ?? false
- let playWithYoutubeMusic = PreferenceManager.playWithYoutubeMusic ?? false
+ let playType = PreferenceManager.playWithYoutubeMusic ?? .youtube
switch category {
case .appPush:
self.subTitleLabel.text = pushNotificationAuthorizationStatus ? "켜짐" : "꺼짐"
case .playType:
- self.subTitleLabel.text = playWithYoutubeMusic ? "YouTube Music" : "YouTube"
+ self.subTitleLabel.text = playType.display
default:
self.subTitleLabel.text = ""
}
diff --git a/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift b/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift
index f5d1b47eb..ea6cd31d8 100644
--- a/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift
+++ b/Projects/Features/MyInfoFeature/Testing/SettingComponentStub.swift
@@ -23,7 +23,20 @@ public final class SettingComponentStub: SettingFactory {
signInFactory: SignInComponentStub(),
serviceTermsFactory: ServiceTermComponentStub(),
privacyFactory: PrivacyComponentStub(),
- openSourceLicenseFactory: OpenSourceLicenseComponentStub()
+ openSourceLicenseFactory: OpenSourceLicenseComponentStub(),
+ playTypeTogglePopupFactory: PlayTypeTogglePopupComponentStub()
+ )
+ }
+}
+
+final class PlayTypeTogglePopupComponentStub: PlayTypeTogglePopupFactory {
+ public func makeView(
+ completion: ((_ selectedItemString: String) -> Void)? = nil,
+ cancelCompletion: (() -> Void)? = nil
+ ) -> UIViewController {
+ return PlayTypeTogglePopupViewController(
+ completion: completion,
+ cancelCompletion: cancelCompletion
)
}
}
diff --git a/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift b/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift
index 098e9506d..6e50ad01e 100644
--- a/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift
+++ b/Projects/Features/PlaylistFeature/Interface/PlaylistDetailNavigator.swift
@@ -4,13 +4,13 @@ import UIKit
public protocol PlaylistDetailNavigator {
var playlistDetailFactory: any PlaylistDetailFactory { get }
- func navigateWmPlaylistDetail(key: String)
+ func navigateWMPlaylistDetail(key: String)
func navigatePlaylistDetail(key: String)
}
public extension PlaylistDetailNavigator where Self: UIViewController {
- func navigateWmPlaylistDetail(key: String) {
+ func navigateWMPlaylistDetail(key: String) {
let dest = playlistDetailFactory.makeWmView(key: key)
self.navigationController?.pushViewController(dest, animated: true)
diff --git a/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift b/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift
index a7d6643de..f2ccd621b 100644
--- a/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift
+++ b/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift
@@ -8,7 +8,7 @@ import SignInFeatureInterface
import UIKit
public protocol WakmusicPlaylistDetailDependency: Dependency {
- var fetchWmPlaylistDetailUseCase: any FetchWmPlaylistDetailUseCase { get }
+ var fetchWMPlaylistDetailUseCase: any FetchWMPlaylistDetailUseCase { get }
var containSongsFactory: any ContainSongsFactory { get }
var textPopupFactory: any TextPopupFactory { get }
var songDetailPresenter: any SongDetailPresentable { get }
@@ -21,7 +21,7 @@ public final class WakmusicPlaylistDetailComponent: Component Observable {
return .concat([
.just(.updateLoadingState(true)),
- fetchWmPlaylistDetailUseCase.execute(id: key)
+ fetchWMPlaylistDetailUseCase.execute(id: key)
.asObservable()
.flatMap { data -> Observable in
return .concat([
diff --git a/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift b/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift
index 4260b0980..da51d79fe 100644
--- a/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift
+++ b/Projects/Features/PlaylistFeature/Sources/ViewControllers/MyPlaylistDetailViewController.swift
@@ -53,6 +53,7 @@ final class MyPlaylistDetailViewController: BaseReactorViewController UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
+ let collectionViewWidth = superview?.bounds.width ?? UIScreen.main.bounds.width
+
+ let maxWidth = collectionViewWidth * 0.6
+
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
- newFrame.size = size
+ newFrame.size = .init(
+ width: min(maxWidth, size.width),
+ height: size.height
+ )
layoutAttributes.frame = newFrame
return layoutAttributes
}
diff --git a/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift b/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift
index 0054e8b1e..a65b5b347 100644
--- a/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift
+++ b/Projects/Features/StorageFeature/Sources/ViewControllers/LikeStorageViewController.swift
@@ -216,7 +216,9 @@ final class LikeStorageViewController: BaseReactorViewController