diff --git a/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlayState.swift b/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlayState.swift index fd0cf6938..7a844c459 100644 --- a/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlayState.swift +++ b/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlayState.swift @@ -19,7 +19,7 @@ public final class PlayState { private var subscription = Set() public var count: Int { playlist.count } public var isEmpty: Bool { playlist.isEmpty } - public var currentPlaylist: [PlaylistItem] { playlist.list } + public var currentPlaylist: [PlaylistItem] { Array(playlist.list.uniqued()) } public var listChangedPublisher: AnyPublisher<[PlaylistItem], Never> { playlist.subscribeListChanges() } private init() { diff --git a/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlaylistItem.swift b/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlaylistItem.swift index 96e603776..1f96e2df6 100644 --- a/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlaylistItem.swift +++ b/Projects/Features/BaseFeature/Sources/Etc/PlayState/PlaylistItem.swift @@ -1,7 +1,7 @@ import Foundation import SongsDomainInterface -public struct PlaylistItem: Equatable { +public struct PlaylistItem: Equatable, Hashable { public let id: String public let title: String public let artist: String diff --git a/Projects/Features/PlaylistFeature/Sources/Reactors/MyPlaylistDetailReactor.swift b/Projects/Features/PlaylistFeature/Sources/Reactors/MyPlaylistDetailReactor.swift index b20103107..deb8b2603 100644 --- a/Projects/Features/PlaylistFeature/Sources/Reactors/MyPlaylistDetailReactor.swift +++ b/Projects/Features/PlaylistFeature/Sources/Reactors/MyPlaylistDetailReactor.swift @@ -208,6 +208,13 @@ private extension MyPlaylistDetailReactor { fetchPlaylistDetailUseCase.execute(id: key, type: .my) .asObservable() .flatMap { data -> Observable in + let songs = data.songs.map { PlaylistItemModel( + id: $0.id, + title: $0.title, + artist: $0.artist, + isSelected: false + ) } + .uniqued() return .concat([ Observable.just(Mutation.updateHeader( PlaylistDetailHeaderModel( @@ -220,28 +227,10 @@ private extension MyPlaylistDetailReactor { ) )), Observable.just( - Mutation.updatePlaylist( - data.songs.map { - PlaylistItemModel( - id: $0.id, - title: $0.title, - artist: $0.artist, - isSelected: false - ) - } - ) + Mutation.updatePlaylist(Array(songs)) ), Observable.just( - Mutation.updateBackUpPlaylist( - data.songs.map { - PlaylistItemModel( - id: $0.id, - title: $0.title, - artist: $0.artist, - isSelected: false - ) - } - ) + Mutation.updateBackUpPlaylist(Array(songs)) ) ]) } diff --git a/Projects/Features/PlaylistFeature/Sources/Reactors/UnknownPlaylistDetailReactor.swift b/Projects/Features/PlaylistFeature/Sources/Reactors/UnknownPlaylistDetailReactor.swift index 9a071ee81..82e552dc4 100644 --- a/Projects/Features/PlaylistFeature/Sources/Reactors/UnknownPlaylistDetailReactor.swift +++ b/Projects/Features/PlaylistFeature/Sources/Reactors/UnknownPlaylistDetailReactor.swift @@ -170,6 +170,7 @@ private extension UnknownPlaylistDetailReactor { .asObservable() .withUnretained(self) .flatMap { owner, data -> Observable in + let songs = data.songs.uniqued() return .concat([ Observable.just(Mutation.updateHeader( PlaylistDetailHeaderModel( @@ -181,7 +182,9 @@ private extension UnknownPlaylistDetailReactor { songCount: data.songs.count ) )), - Observable.just(Mutation.updateDataSource(data.songs)), + Observable.just( + Mutation.updateDataSource(Array(songs)) + ), PreferenceManager.userInfo == nil ? .just(.updateSubscribeState(false)) : owner .checkSubscription() ]) diff --git a/Projects/Features/SearchFeature/Sources/After/ViewControllers/ListSearchResultViewController.swift b/Projects/Features/SearchFeature/Sources/After/ViewControllers/ListSearchResultViewController.swift index f88933871..42c4e97c1 100644 --- a/Projects/Features/SearchFeature/Sources/After/ViewControllers/ListSearchResultViewController.swift +++ b/Projects/Features/SearchFeature/Sources/After/ViewControllers/ListSearchResultViewController.swift @@ -18,7 +18,7 @@ final class ListSearchResultViewController: BaseReactorViewController, PlaylistDetailNavigator { private let wakmusicRecommendComponent: WakmusicRecommendComponent private let textPopupFactory: TextPopupFactory - private (set) var playlistDetailFactory: any PlaylistDetailFactory + private(set) var playlistDetailFactory: any PlaylistDetailFactory private let tableView: UITableView = UITableView().then { $0.register(RecentRecordTableViewCell.self, forCellReuseIdentifier: "RecentRecordTableViewCell") diff --git a/Projects/Modules/Utility/Sources/Extensions/Extension+Sequence.swift b/Projects/Modules/Utility/Sources/Extensions/Extension+Sequence.swift new file mode 100644 index 000000000..4ee95c90a --- /dev/null +++ b/Projects/Modules/Utility/Sources/Extensions/Extension+Sequence.swift @@ -0,0 +1,33 @@ +import Foundation + +public extension Sequence where Element: Hashable { + @inlinable + func uniqued() -> UniquedSequence { + UniquedSequence(base: self, projection: { $0 }) + } +} + +public extension Sequence { + @inlinable + func uniqued( + on projection: (Element) throws -> Subject + ) rethrows -> [Element] { + var seen: Set = [] + var result: [Element] = [] + for element in self { + if try seen.insert(projection(element)).inserted { + result.append(element) + } + } + return result + } +} + +public extension LazySequenceProtocol { + @inlinable + func uniqued( + on projection: @escaping (Element) -> Subject + ) -> UniquedSequence { + UniquedSequence(base: self, projection: projection) + } +} diff --git a/Projects/Modules/Utility/Sources/Utils/UniquedSequence.swift b/Projects/Modules/Utility/Sources/Utils/UniquedSequence.swift new file mode 100644 index 000000000..1616cc018 --- /dev/null +++ b/Projects/Modules/Utility/Sources/Utils/UniquedSequence.swift @@ -0,0 +1,54 @@ +import Foundation + +public struct UniquedSequence { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let projection: (Base.Element) -> Subject + + @usableFromInline + internal init(base: Base, projection: @escaping (Base.Element) -> Subject) { + self.base = base + self.projection = projection + } +} + +extension UniquedSequence: Sequence { + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var base: Base.Iterator + + @usableFromInline + internal let projection: (Base.Element) -> Subject + + @usableFromInline + internal var seen: Set = [] + + @usableFromInline + internal init( + base: Base.Iterator, + projection: @escaping (Base.Element) -> Subject + ) { + self.base = base + self.projection = projection + } + + @inlinable + public mutating func next() -> Base.Element? { + while let element = base.next() { + if seen.insert(projection(element)).inserted { + return element + } + } + return nil + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(base: base.makeIterator(), projection: projection) + } +} + +extension UniquedSequence: LazySequenceProtocol where Base: LazySequenceProtocol {}