Skip to content

Commit

Permalink
Merge pull request #282 from boostcampwm2023/iOS/feat#174
Browse files Browse the repository at this point in the history
�feat: 재생화면 페이징 구현
  • Loading branch information
chopmozzi authored Dec 11, 2023
2 parents 7606181 + c82d0af commit da0f8e9
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 29 deletions.
2 changes: 1 addition & 1 deletion iOS/Layover/Layover/Scenes/Map/MapRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ final class MapRouter: MapRoutingLogic, MapDataPassing {
private func passDataToPlayback(source: MapDataStore, destination: inout PlaybackDataStore) {
destination.posts = source.posts
destination.index = source.postPlayStartIndex
destination.parentView = .other
destination.parentView = .map
}
}
84 changes: 60 additions & 24 deletions iOS/Layover/Layover/Scenes/Playback/PlaybackInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ protocol PlaybackBusinessLogic {
func resumeVideo()
func moveToProfile(with request: PlaybackModels.MoveToRelativeView.Request)
func moveToTagPlay(with request: PlaybackModels.MoveToRelativeView.Request)
@discardableResult
func fetchPosts() -> Task<Bool, Never>
}

protocol PlaybackDataStore: AnyObject {
Expand Down Expand Up @@ -67,14 +69,18 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore {

var selectedTag: String?

private var isFetchReqeust: Bool = false

private var currentPage: Int = 1

// MARK: - UseCase Load Video List

func displayVideoList() -> Task<Bool, Never> {
Task {
guard let parentView: Models.ParentView,
var posts: [Post],
let worker: PlaybackWorkerProtocol else { return false }
if parentView == .other {
if parentView == .map {
posts = worker.makeInfiniteScroll(posts: posts)
self.posts = posts
}
Expand All @@ -91,24 +97,17 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore {
guard let parentView,
let index
else { return }
switch parentView {
case .home, .myProfile:
presenter?.presentMoveInitialPlaybackCell(with: Models.SetInitialPlaybackCell.Response(indexPathRow: index))
case .other:
presenter?.presentSetCellIfInfinite(with: Models.SetInitialPlaybackCell.Response(indexPathRow: index + 1))
}
let willMoveIndex: Int
willMoveIndex = parentView == .map ? index + 1 : index
presenter?.presentMoveInitialPlaybackCell(with: Models.SetInitialPlaybackCell.Response(indexPathRow: willMoveIndex))
}

func setInitialPlaybackCell() {
guard let parentView,
let index else { return }
let response: Models.SetInitialPlaybackCell.Response
switch parentView {
case .home, .myProfile:
response = Models.SetInitialPlaybackCell.Response(indexPathRow: index)
case .other:
response = Models.SetInitialPlaybackCell.Response(indexPathRow: index + 1)
}
let willMoveIndex: Int
willMoveIndex = parentView == .map ? index + 1 : index
let response: Models.SetInitialPlaybackCell.Response = Models.SetInitialPlaybackCell.Response(indexPathRow: willMoveIndex)
presenter?.presentSetInitialPlaybackCell(with: response)
}

Expand All @@ -129,8 +128,8 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore {
isTeleport = false
return
}
// Home이 아닌 다른 뷰에서 왔을 경우(로드한 목록 무한 반복)
if parentView == .other {
// map에서 왔을 경우(로드한 목록 무한 반복)
if parentView == .map {
if request.indexPathRow == (posts.count - 1) {
response = Models.DisplayPlaybackVideo.Response(indexPathRow: 1, previousCell: previousCell, currentCell: nil)
} else if request.indexPathRow == 0 {
Expand All @@ -146,7 +145,7 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore {
presenter?.presentTeleportCell(with: response)
return
}
// Home이면 다음 셀로 이동(추가적인 비디오 로드)
// map이 아니면 다음 셀로 이동(추가적인 비디오 로드)
isTeleport = false
response = Models.DisplayPlaybackVideo.Response(previousCell: previousCell, currentCell: request.currentCell)
previousCell = request.currentCell
Expand Down Expand Up @@ -205,13 +204,9 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore {
func configurePlaybackCell() {
guard let posts,
let parentView else { return }
let response: Models.ConfigurePlaybackCell.Response
switch parentView {
case .home, .myProfile:
response = Models.ConfigurePlaybackCell.Response(teleportIndex: nil)
case .other:
response = Models.ConfigurePlaybackCell.Response(teleportIndex: posts.count + 1)
}
let willMoveTeleportIndex: Int?
willMoveTeleportIndex = parentView == .map ? posts.count + 1 : nil
let response: Models.ConfigurePlaybackCell.Response = Models.ConfigurePlaybackCell.Response(teleportIndex: willMoveTeleportIndex)
presenter?.presentConfigureCell(with: response)
}

Expand Down Expand Up @@ -289,4 +284,45 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore {
self.selectedTag = selectedTag
presenter?.presentTagPlay()
}

func fetchPosts() -> Task<Bool, Never> {
Task {
if !isFetchReqeust {
isFetchReqeust = true
var page: Int = 0
if parentView != .home {
guard let posts else { return false }
page = posts.count / 15 + 1
if page == currentPage {
return false
}
}
currentPage = page
var newPosts: [Post]?
switch parentView {
case .home:
newPosts = await worker?.fetchHomePosts()
case .map:
return false
case .myProfile, .otherProfile:
newPosts = await worker?.fetchProfilePosts(profileID: memberID, page: page)
case .tag:
guard let selectedTag else { return false }
newPosts = await worker?.fetchTagPosts(selectedTag: selectedTag, page: page)
default:
return false
}
guard let newPosts else { return false }
self.posts?.append(contentsOf: newPosts)
let videos: [Models.PlaybackVideo] = await transPostToVideo(newPosts)
let response: Models.LoadPlaybackVideoList.Response = Models.LoadPlaybackVideoList.Response(videos: videos)
await MainActor.run {
presenter?.presentLoadFetchVideos(with: response)
isFetchReqeust = false
}
return true
}
return false
}
}
}
4 changes: 3 additions & 1 deletion iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ enum PlaybackModels {
enum ParentView {
case home
case myProfile
case other
case otherProfile
case tag
case map
}

struct DisplayedPost: Hashable {
Expand Down
6 changes: 6 additions & 0 deletions iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation

protocol PlaybackPresentationLogic {
func presentVideoList(with response: PlaybackModels.LoadPlaybackVideoList.Response)
func presentLoadFetchVideos(with response: PlaybackModels.LoadPlaybackVideoList.Response)
func presentSetCellIfInfinite(with response: PlaybackModels.SetInitialPlaybackCell.Response)
func presentMoveCellNext(with response: PlaybackModels.DisplayPlaybackVideo.Response)
func presentSetInitialPlaybackCell(with response: PlaybackModels.SetInitialPlaybackCell.Response)
Expand Down Expand Up @@ -42,6 +43,11 @@ final class PlaybackPresenter: PlaybackPresentationLogic {
viewController?.displayVideoList(viewModel: viewModel)
}

func presentLoadFetchVideos(with response: PlaybackModels.LoadPlaybackVideoList.Response) {
let viewModel: Models.LoadPlaybackVideoList.ViewModel = Models.LoadPlaybackVideoList.ViewModel(videos: response.videos)
viewController?.loadFetchVideos(viewModel: viewModel)
}

func presentSetCellIfInfinite(with response: PlaybackModels.SetInitialPlaybackCell.Response) {
viewController?.displayMoveCellIfinfinite(viewModel: Models.SetInitialPlaybackCell.ViewModel(indexPathRow: response.indexPathRow))
}
Expand Down
15 changes: 15 additions & 0 deletions iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ protocol PlaybackViewControllerDelegate: AnyObject {

protocol PlaybackDisplayLogic: AnyObject {
func displayVideoList(viewModel: PlaybackModels.LoadPlaybackVideoList.ViewModel)
func loadFetchVideos(viewModel: PlaybackModels.LoadPlaybackVideoList.ViewModel)
func displayMoveCellIfinfinite(viewModel: PlaybackModels.SetInitialPlaybackCell.ViewModel)
func stopPrevPlayerAndPlayCurPlayer(viewModel: PlaybackModels.DisplayPlaybackVideo.ViewModel)
func setInitialPlaybackCell(viewModel: PlaybackModels.SetInitialPlaybackCell.ViewModel)
Expand Down Expand Up @@ -188,6 +189,12 @@ extension PlaybackViewController: PlaybackDisplayLogic {
dataSource?.apply(snapshot, animatingDifferences: false)
}

func loadFetchVideos(viewModel: PlaybackModels.LoadPlaybackVideoList.ViewModel) {
guard var currentSnapshot = dataSource?.snapshot() else { return }
currentSnapshot.appendItems(viewModel.videos)
dataSource?.apply(currentSnapshot, animatingDifferences: true)
}

func displayMoveCellIfinfinite(viewModel: Models.SetInitialPlaybackCell.ViewModel) {
playbackCollectionView.setContentOffset(.init(x: playbackCollectionView.contentOffset.x, y: playbackCollectionView.bounds.height * CGFloat(viewModel.indexPathRow)), animated: false)
}
Expand Down Expand Up @@ -334,6 +341,14 @@ extension PlaybackViewController: UICollectionViewDelegate {
interactor?.playTeleportVideo(with: request)
interactor?.careVideoLoading(with: request)
}

func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maximumOffset < currentOffset {
interactor?.fetchPosts()
}
}
}

extension PlaybackViewController: PlaybackViewControllerDelegate {
Expand Down
42 changes: 41 additions & 1 deletion iOS/Layover/Layover/Scenes/Playback/PlaybackWorker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ protocol PlaybackWorkerProtocol {
func makeInfiniteScroll(posts: [Post]) -> [Post]
func transLocation(latitude: Double, longitude: Double) async -> String?
func fetchImageData(with url: URL?) async -> Data?
func fetchHomePosts() async -> [Post]?
func fetchProfilePosts(profileID: Int?, page: Int) async -> [Post]?
func fetchTagPosts(selectedTag: String, page: Int) async -> [Post]?
}

final class PlaybackWorker: PlaybackWorkerProtocol {
Expand All @@ -26,12 +29,16 @@ final class PlaybackWorker: PlaybackWorkerProtocol {

private let provider: ProviderType
private let defaultPostManagerEndPointFactory: PostManagerEndPointFactory
private let defaultPostEndPointFactory: PostEndPointFactory
private let defaultUserEndPointFactory: UserEndPointFactory

// MARK: - Methods

init(provider: ProviderType = Provider(), defaultPostManagerEndPointFactory: PostManagerEndPointFactory = DefaultPostManagerEndPointFactory()) {
init(provider: ProviderType = Provider(), defaultPostManagerEndPointFactory: PostManagerEndPointFactory = DefaultPostManagerEndPointFactory(), defaultPostEndPointFactory: PostEndPointFactory = DefaultPostEndPointFactory(), defaultUserEndPointFactory: UserEndPointFactory = DefaultUserEndPointFactory()) {
self.provider = provider
self.defaultPostManagerEndPointFactory = defaultPostManagerEndPointFactory
self.defaultPostEndPointFactory = defaultPostEndPointFactory
self.defaultUserEndPointFactory = defaultUserEndPointFactory
}

func makeInfiniteScroll(posts: [Post]) -> [Post] {
Expand Down Expand Up @@ -78,4 +85,37 @@ final class PlaybackWorker: PlaybackWorkerProtocol {
return nil
}
}

func fetchHomePosts() async -> [Post]? {
let endPoint = defaultPostEndPointFactory.makeHomePostListEndPoint()
do {
let response = try await provider.request(with: endPoint)
return response.data?.map { $0.toDomain() }
} catch {
os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription)
return nil
}
}

func fetchProfilePosts(profileID: Int?, page: Int) async -> [Post]? {
let endPoint = defaultUserEndPointFactory.makeUserPostsEndPoint(at: page, of: profileID)
do {
let response = try await provider.request(with: endPoint)
return response.data?.map { $0.toDomain() }
} catch {
os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription)
return nil
}
}

func fetchTagPosts(selectedTag: String, page: Int) async -> [Post]? {
let endPoint = defaultPostEndPointFactory.makeTagSearchPostListEndPoint(of: selectedTag, at: page)
do {
let response = try await provider.request(with: endPoint)
return response.data?.map { $0.toDomain() }
} catch {
os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription)
return nil
}
}
}
2 changes: 1 addition & 1 deletion iOS/Layover/Layover/Scenes/Profile/ProfileRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@ final class ProfileRouter: ProfileRoutingLogic, ProfileDataPassing {
private func passDataToPlayback(source: ProfileDataStore, destination: inout PlaybackDataStore) {
destination.posts = source.posts
destination.index = source.playbackStartIndex
destination.parentView = .myProfile
destination.parentView = source.profileId == nil ? .myProfile : .otherProfile
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class TagPlayListRouter: TagPlayListRoutingLogic, TagPlayListDataPassing {
}

private func passDataToPlayback(source: TagPlayListDataStore, destination: inout PlaybackDataStore) {
destination.parentView = .other
destination.parentView = .tag
destination.index = source.postPlayStartIndex
destination.posts = source.posts
}
Expand Down
76 changes: 76 additions & 0 deletions iOS/Layover/Layover/Workers/Mocks/MockPlaybackWorker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,80 @@ final class MockPlaybackWorker: PlaybackWorkerProtocol {
return nil
}
}

func fetchHomePosts() async -> [Post]? {
guard let fileLocation = Bundle.main.url(forResource: "PostList",
withExtension: "json") else { return nil }

do {
let mockData = try Data(contentsOf: fileLocation)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
return (response, mockData, nil)
}
let endPoint: EndPoint = EndPoint<Response<[PostDTO]>>(path: "/board/home",
method: .GET)
let response = try await provider.request(with: endPoint)
guard let data = response.data else { return nil }
return data.map { $0.toDomain() }
} catch {
os_log(.error, log: .data, "%@", error.localizedDescription)
return nil
}
}

func fetchProfilePosts(profileID: Int?, page: Int) async -> [Post]? {
let resourceFileName = switch page { case 1: "PostList" case 2: "PostListMore" default: "PostListEnd" }
guard let fileLocation = Bundle.main.url(forResource: resourceFileName, withExtension: "json") else { return nil }
do {
let mockData = try Data(contentsOf: fileLocation)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
return (response, mockData, nil)
}
let endPoint = EndPoint<Response<[PostDTO]>>(path: "/member/posts",
method: .GET,
queryParameters: ["page": page])
let response = try await provider.request(with: endPoint)
return response.data?.map { $0.toDomain() }
} catch {
os_log(.error, log: .data, "%@", error.localizedDescription)
return nil
}
}

func fetchTagPosts(selectedTag: String, page: Int) async -> [Post]? {
let resourceFileName = switch page { case 1: "PostList" case 2: "PostListMore" default: "PostListEnd" }
guard let fileLocation = Bundle.main.url(forResource: resourceFileName, withExtension: "json") else {
return nil
}

do {
let mockData = try? Data(contentsOf: fileLocation)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
return (response, mockData, nil)
}

let endPoint = EndPoint<Response<[PostDTO]>>(path: "/board/tag",
method: .GET,
queryParameters: ["tag": selectedTag])

let response = try await provider.request(with: endPoint)
return response.data?.map { $0.toDomain() }
} catch {
os_log(.error, log: .data, "%@", error.localizedDescription)
return nil
}
}

}

0 comments on commit da0f8e9

Please sign in to comment.