diff --git a/iOS/Layover/Layover.xcodeproj/project.pbxproj b/iOS/Layover/Layover.xcodeproj/project.pbxproj index f18fe35..1903fab 100644 --- a/iOS/Layover/Layover.xcodeproj/project.pbxproj +++ b/iOS/Layover/Layover.xcodeproj/project.pbxproj @@ -248,6 +248,7 @@ FCEE0FF22B036B6000195BBE /* LOButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEE0FF12B036B6000195BBE /* LOButton.swift */; }; FCEE0FF62B03804000195BBE /* LOTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEE0FF52B03804000195BBE /* LOTextField.swift */; }; FCEE0FFA2B03AF8500195BBE /* SignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEE0FF92B03AF8400195BBE /* SignUpViewController.swift */; }; + FCF19BE22B2A4088003002E0 /* AVFileType+.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCF19BE12B2A4088003002E0 /* AVFileType+.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -507,6 +508,7 @@ FCEE0FF12B036B6000195BBE /* LOButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LOButton.swift; sourceTree = ""; }; FCEE0FF52B03804000195BBE /* LOTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LOTextField.swift; sourceTree = ""; }; FCEE0FF92B03AF8400195BBE /* SignUpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SignUpViewController.swift; path = ../SignUpViewController.swift; sourceTree = ""; }; + FCF19BE12B2A4088003002E0 /* AVFileType+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVFileType+.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1165,6 +1167,7 @@ FC767FA42B125F430088CF9B /* UIViewController+.swift */, 1972CCDE2B14C9B000C3C762 /* Notification.Name+.swift */, 19A169482B181AE300DB34C0 /* Sequence+.swift */, + FCF19BE12B2A4088003002E0 /* AVFileType+.swift */, 19AE482D2B2A24C700DD4612 /* URL+.swift */, ); path = Extensions; @@ -1432,6 +1435,8 @@ 1945520D2B0399E500299768 /* MainTabBarViewController.swift in Sources */, FC2511AB2B04EA6B004717BC /* MapConfigurator.swift in Sources */, 1945523B2B05258200299768 /* HomeConfigurator.swift in Sources */, + FCF19BE22B2A4088003002E0 /* AVFileType+.swift in Sources */, + FC5BE11D2B148D160036366D /* EditProfileWorker.swift in Sources */, 19A1693A2B17BCC400DB34C0 /* MemberDTO.swift in Sources */, 194551F62B037F2D00299768 /* LoginViewController.swift in Sources */, FC767FA52B125F430088CF9B /* UIViewController+.swift in Sources */, diff --git a/iOS/Layover/Layover/Extensions/AVFileType+.swift b/iOS/Layover/Layover/Extensions/AVFileType+.swift new file mode 100644 index 0000000..41d700d --- /dev/null +++ b/iOS/Layover/Layover/Extensions/AVFileType+.swift @@ -0,0 +1,25 @@ +// +// AVFileType+.swift +// Layover +// +// Created by kong on 2023/12/14. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import AVFoundation + +extension AVFileType { + static func from(_ url: URL) -> AVFileType? { + let pathExtension = url.pathExtension + switch pathExtension { + case "mp4": + return .mp4 + case "mov": + return .mov + case "m4v": + return .m4v + default: + return nil + } + } +} diff --git a/iOS/Layover/Layover/Extensions/URL+.swift b/iOS/Layover/Layover/Extensions/URL+.swift index 0b6dfd4..cfb7a1c 100644 --- a/iOS/Layover/Layover/Extensions/URL+.swift +++ b/iOS/Layover/Layover/Extensions/URL+.swift @@ -15,11 +15,11 @@ extension URL { return components?.url ?? self } - var customHLS_URL: URL { + var customHLSURL: URL { changeScheme(to: "lhls") } - var originHLS_URL: URL { + var originHLSURL: URL { changeScheme(to: "https") } } diff --git a/iOS/Layover/Layover/Scenes/Home/HomeConfigurator.swift b/iOS/Layover/Layover/Scenes/Home/HomeConfigurator.swift index ba223a7..340d6aa 100644 --- a/iOS/Layover/Layover/Scenes/Home/HomeConfigurator.swift +++ b/iOS/Layover/Layover/Scenes/Home/HomeConfigurator.swift @@ -20,6 +20,7 @@ final class HomeConfigurator: Configurator { let interactor = HomeInteractor() let homeWorker = HomeWorker() let videoFileWorker = VideoFileWorker() + let locationManager = CurrentLocationManager() router.viewController = viewController router.dataStore = interactor @@ -27,6 +28,7 @@ final class HomeConfigurator: Configurator { interactor.presenter = presenter interactor.homeWorker = homeWorker interactor.videoFileWorker = videoFileWorker + interactor.locationManager = locationManager viewController.router = router viewController.interactor = interactor diff --git a/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift b/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift index 58dadc4..aeddd83 100644 --- a/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift +++ b/iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift @@ -6,12 +6,14 @@ // Copyright © 2023 CodeBomber. All rights reserved. // +import CoreLocation import UIKit protocol HomeBusinessLogic { @discardableResult func fetchPosts(with request: HomeModels.FetchPosts.Request) async -> Bool func playPosts(with request: HomeModels.PlayPosts.Request) + func fetchLocationAuthorizationStatus() func selectVideo(with request: HomeModels.SelectVideo.Request) func showTagPlayList(with request: HomeModels.ShowTagPlayList.Request) } @@ -33,6 +35,7 @@ final class HomeInteractor: HomeDataStore { var videoFileWorker: VideoFileWorkerProtocol? var homeWorker: HomeWorkerProtocol? var presenter: HomePresentationLogic? + var locationManager: CurrentLocationManager? // MARK: - DataStore @@ -45,6 +48,7 @@ final class HomeInteractor: HomeDataStore { // MARK: - Use Case extension HomeInteractor: HomeBusinessLogic { + @discardableResult func fetchPosts(with request: Models.FetchPosts.Request) async -> Bool { guard let posts = await homeWorker?.fetchPosts() else { return false } @@ -63,6 +67,20 @@ extension HomeInteractor: HomeBusinessLogic { presenter?.presentPlaybackScene(with: Models.PlayPosts.Response()) } + func fetchLocationAuthorizationStatus() { + guard let authorizationStatus = locationManager?.getAuthorizationStatus() else { return } + switch authorizationStatus { + case .authorizedAlways, .authorizedWhenInUse: + presenter?.presentUploadScene() + case .restricted, .notDetermined: + locationManager?.requestWhenInUseAuthorization() + case .denied: + presenter?.presentSetting() + @unknown default: + return + } + } + func selectVideo(with request: Models.SelectVideo.Request) { selectedVideoURL = videoFileWorker?.copyToNewURL(at: request.videoURL) } diff --git a/iOS/Layover/Layover/Scenes/Home/HomePresenter.swift b/iOS/Layover/Layover/Scenes/Home/HomePresenter.swift index f998e14..5940620 100644 --- a/iOS/Layover/Layover/Scenes/Home/HomePresenter.swift +++ b/iOS/Layover/Layover/Scenes/Home/HomePresenter.swift @@ -12,6 +12,8 @@ protocol HomePresentationLogic { func presentPosts(with response: HomeModels.FetchPosts.Response) func presentPlaybackScene(with response: HomeModels.PlayPosts.Response) func presentTagPlayList(with response: HomeModels.ShowTagPlayList.Response) + func presentUploadScene() + func presentSetting() } final class HomePresenter: HomePresentationLogic { @@ -52,4 +54,12 @@ final class HomePresenter: HomePresentationLogic { func presentTagPlayList(with response: HomeModels.ShowTagPlayList.Response) { viewController?.routeToTagPlayList() } + + func presentUploadScene() { + viewController?.routeToVideoPicker() + } + + func presentSetting() { + viewController?.openSetting() + } } diff --git a/iOS/Layover/Layover/Scenes/Home/HomeViewController.swift b/iOS/Layover/Layover/Scenes/Home/HomeViewController.swift index e4283e2..8387aca 100644 --- a/iOS/Layover/Layover/Scenes/Home/HomeViewController.swift +++ b/iOS/Layover/Layover/Scenes/Home/HomeViewController.swift @@ -13,6 +13,8 @@ protocol HomeDisplayLogic: AnyObject { func displayPosts(with viewModel: HomeModels.FetchPosts.ViewModel) func routeToPlayback() func routeToTagPlayList() + func routeToVideoPicker() + func openSetting() } final class HomeViewController: BaseViewController { @@ -179,7 +181,7 @@ final class HomeViewController: BaseViewController { // MARK: - Actions @objc private func uploadButtonDidTap() { - present(videoPickerManager.phPickerViewController, animated: true) + interactor?.fetchLocationAuthorizationStatus() } @objc private func tagButtonDidTap(_ sender: UIButton) { @@ -231,6 +233,7 @@ extension HomeViewController: VideoPickerDelegate { // MARK: - DisplayLogic extension HomeViewController: HomeDisplayLogic { + func displayPosts(with viewModel: HomeModels.FetchPosts.ViewModel) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([UUID()]) @@ -247,4 +250,16 @@ extension HomeViewController: HomeDisplayLogic { func routeToTagPlayList() { router?.routeToTagPlay() } + + func routeToVideoPicker() { + present(videoPickerManager.phPickerViewController, animated: true) + } + + func openSetting() { + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } diff --git a/iOS/Layover/Layover/Scenes/Map/MapConfigurator.swift b/iOS/Layover/Layover/Scenes/Map/MapConfigurator.swift index 40d83f8..089e4d0 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapConfigurator.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapConfigurator.swift @@ -22,12 +22,14 @@ final class MapConfigurator: Configurator { let router = MapRouter() let worker = MapWorker() let videoFileWorker = VideoFileWorker() + let locationManager = CurrentLocationManager() router.viewController = viewController viewController.interactor = interactor interactor.presenter = presenter interactor.worker = worker interactor.videoFileWorker = videoFileWorker + interactor.locationManager = locationManager presenter.viewController = viewController viewController.router = router router.dataStore = interactor diff --git a/iOS/Layover/Layover/Scenes/Map/MapInteractor.swift b/iOS/Layover/Layover/Scenes/Map/MapInteractor.swift index 90f8a1f..b15ab4c 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapInteractor.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapInteractor.swift @@ -9,16 +9,15 @@ import CoreLocation import Foundation +import OSLog + protocol MapBusinessLogic { - func checkLocationAuthorizationStatus() func playPosts(with: MapModels.PlayPosts.Request) - - @discardableResult - func fetchPosts() -> Task - - @discardableResult - func fetchPost(latitude: Double, longitude: Double) -> Task + func fetchPosts() async + func fetchPost(latitude: Double, longitude: Double) async func selectVideo(with request: MapModels.SelectVideo.Request) + func checkLocationAuthorizationOnEntry() + func checkLocationPermissionOnUpload() } protocol MapDataStore { @@ -35,54 +34,38 @@ final class MapInteractor: NSObject, MapBusinessLogic, MapDataStore { var presenter: MapPresentationLogic? var videoFileWorker: VideoFileWorker? var worker: MapWorkerProtocol? - - private let locationManager = CLLocationManager() + var locationManager: CurrentLocationManager? var postPlayStartIndex: Int? var posts: [Post]? var index: Int? var selectedVideoURL: URL? - override init() { - super.init() - locationManager.delegate = self - } - - func checkLocationAuthorizationStatus() { - checkCurrentLocationAuthorization(for: locationManager.authorizationStatus) - } - func playPosts(with request: MapModels.PlayPosts.Request) { postPlayStartIndex = request.selectedIndex presenter?.presentPlaybackScene() } - func fetchPosts() -> Task { - Task { - locationManager.startUpdatingLocation() - guard let coordinate = locationManager.location?.coordinate else { return false } - let posts = await worker?.fetchPosts(latitude: coordinate.latitude, - longitude: coordinate.longitude) - guard let posts else { return false } - self.posts = posts - let response = Models.FetchPosts.Response(posts: posts) - await MainActor.run { - presenter?.presentFetchedPosts(with: response) - } - return true + func fetchPosts() async { + locationManager?.startUpdatingLocation() + guard let coordinate = locationManager?.getCurrentLocation()?.coordinate else { return } + let posts = await worker?.fetchPosts(latitude: coordinate.latitude, + longitude: coordinate.longitude) + guard let posts else { return } + self.posts = posts + let response = Models.FetchPosts.Response(posts: posts) + await MainActor.run { + presenter?.presentFetchedPosts(with: response) } } - func fetchPost(latitude: Double, longitude: Double) -> Task { - Task { - let posts = await worker?.fetchPosts(latitude: latitude, longitude: longitude) - guard let posts else { return false } - self.posts = posts - let response = Models.FetchPosts.Response(posts: posts) - await MainActor.run { - presenter?.presentFetchedPosts(with: response) - } - return true + func fetchPost(latitude: Double, longitude: Double) async { + let posts = await worker?.fetchPosts(latitude: latitude, longitude: longitude) + guard let posts else { return } + self.posts = posts + let response = Models.FetchPosts.Response(posts: posts) + await MainActor.run { + presenter?.presentFetchedPosts(with: response) } } @@ -90,22 +73,32 @@ final class MapInteractor: NSObject, MapBusinessLogic, MapDataStore { selectedVideoURL = videoFileWorker?.copyToNewURL(at: request.videoURL) } - private func checkCurrentLocationAuthorization(for status: CLAuthorizationStatus) { - switch status { + func checkLocationAuthorizationOnEntry() { + guard let authorizationStatus = locationManager?.getAuthorizationStatus() else { return } + switch authorizationStatus { case .authorizedAlways, .authorizedWhenInUse: presenter?.presentCurrentLocation() case .restricted, .notDetermined: - locationManager.requestWhenInUseAuthorization() + locationManager?.requestWhenInUseAuthorization() case .denied: presenter?.presentDefaultLocation() @unknown default: return } } -} -extension MapInteractor: CLLocationManagerDelegate { - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - checkCurrentLocationAuthorization(for: manager.authorizationStatus) + func checkLocationPermissionOnUpload() { + guard let authorizationStatus = locationManager?.getAuthorizationStatus() else { return } + switch authorizationStatus { + case .authorizedAlways, .authorizedWhenInUse: + presenter?.presentUploadScene() + case .restricted, .notDetermined: + locationManager?.requestWhenInUseAuthorization() + case .denied: + presenter?.presentSetting() + @unknown default: + return + } } + } diff --git a/iOS/Layover/Layover/Scenes/Map/MapModels.swift b/iOS/Layover/Layover/Scenes/Map/MapModels.swift index 55bcf7a..88f08a7 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapModels.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapModels.swift @@ -15,9 +15,10 @@ enum MapModels { struct DisplayedPost: Hashable { let boardID: Int let thumbnailImageData: Data? - let videoURL: URL + let videoURL: URL? let latitude: Double let longitude: Double + let boardStatus: BoardStatus } // MARK: - Fetch Video Use Cases @@ -64,4 +65,11 @@ enum MapModels { } } + + enum CheckLocationAuthorizationOnEntry { + struct ViewModel { + let latitude: Double = 36.350411 + let longitude: Double = 127.384548 + } + } } diff --git a/iOS/Layover/Layover/Scenes/Map/MapPresenter.swift b/iOS/Layover/Layover/Scenes/Map/MapPresenter.swift index ded0094..537ed60 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapPresenter.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapPresenter.swift @@ -13,6 +13,8 @@ protocol MapPresentationLogic { func presentDefaultLocation() func presentFetchedPosts(with response: MapModels.FetchPosts.Response) func presentPlaybackScene() + func presentUploadScene() + func presentSetting() } final class MapPresenter: MapPresentationLogic { @@ -23,25 +25,22 @@ final class MapPresenter: MapPresentationLogic { weak var viewController: MapDisplayLogic? func presentCurrentLocation() { - // TODO: 현재 위치 사용 가능 + viewController?.displayCurrentLocation() } func presentDefaultLocation() { - // TODO: 위치 관련 기능 사용 불가, 디폴트 위치로 이동 + viewController?.displayDefaultLocation(viewModel: MapModels.CheckLocationAuthorizationOnEntry.ViewModel()) } func presentFetchedPosts(with response: MapModels.FetchPosts.Response) { let displayedPost = response.posts .map { post -> Models.DisplayedPost? in - if let videoURL = post.board.videoURL { - return .init(boardID: post.board.identifier, - thumbnailImageData: post.thumbnailImageData, - videoURL: videoURL, - latitude: post.board.latitude, - longitude: post.board.longitude) - } else { - return nil - } + return .init(boardID: post.board.identifier, + thumbnailImageData: post.thumbnailImageData, + videoURL: post.board.videoURL, + latitude: post.board.latitude, + longitude: post.board.longitude, + boardStatus: post.board.status) }.compactMap { $0 } let viewModel = Models.FetchPosts.ViewModel(displayedPosts: displayedPost) @@ -51,4 +50,12 @@ final class MapPresenter: MapPresentationLogic { func presentPlaybackScene() { viewController?.routeToPlayback() } + + func presentUploadScene() { + viewController?.routeToVideoPicker() + } + + func presentSetting() { + viewController?.openSetting() + } } diff --git a/iOS/Layover/Layover/Scenes/Map/MapViewController.swift b/iOS/Layover/Layover/Scenes/Map/MapViewController.swift index 9f98a79..d656e8e 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapViewController.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapViewController.swift @@ -11,7 +11,11 @@ import UIKit protocol MapDisplayLogic: AnyObject { func displayFetchedPosts(viewModel: MapModels.FetchPosts.ViewModel) + func displayCurrentLocation() + func displayDefaultLocation(viewModel: MapModels.CheckLocationAuthorizationOnEntry.ViewModel) func routeToPlayback() + func routeToVideoPicker() + func openSetting() } final class MapViewController: BaseViewController { @@ -21,7 +25,6 @@ final class MapViewController: BaseViewController { private lazy var mapView: MKMapView = { let mapView = MKMapView() mapView.showsUserLocation = true - mapView.setUserTrackingMode(.follow, animated: true) mapView.register(LOAnnotationView.self, forAnnotationViewWithReuseIdentifier: LOAnnotationView.identifier) mapView.delegate = self return mapView @@ -95,12 +98,15 @@ final class MapViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - interactor?.checkLocationAuthorizationStatus() - interactor?.fetchPosts() setCollectionViewDataSource() setDelegation() } + override func viewWillAppear(_ animated: Bool) { + interactor?.checkLocationAuthorizationOnEntry() + fetchPosts() + } + // MARK: - UI + Layout override func setConstraints() { @@ -138,6 +144,12 @@ final class MapViewController: BaseViewController { // MARK: - Methods + private func fetchPosts() { + Task { + await interactor?.fetchPosts() + } + } + private func setDelegation() { carouselCollectionView.delegate = self videoPickerManager.videoPickerDelegate = self @@ -202,17 +214,21 @@ final class MapViewController: BaseViewController { @objc private func searchButtonDidTap() { searchButton.isHidden = true - interactor?.fetchPost(latitude: mapView.centerCoordinate.latitude, - longitude: mapView.centerCoordinate.longitude) + Task { + await interactor?.fetchPost(latitude: mapView.centerCoordinate.latitude, + longitude: mapView.centerCoordinate.longitude) + } } @objc private func currentLocationButtonDidTap() { mapView.setUserTrackingMode(.follow, animated: true) - mapView.removeAnnotations(mapView.annotations) + Task { + await interactor?.fetchPosts() + } } @objc private func uploadButtonDidTap() { - present(videoPickerManager.phPickerViewController, animated: true) + interactor?.checkLocationPermissionOnUpload() } } @@ -262,6 +278,10 @@ extension MapViewController: MKMapViewDelegate { } } + func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + searchButton.isHidden = true + } + } // MARK: - VideoPickerDelegate @@ -315,10 +335,40 @@ extension MapViewController: MapDisplayLogic { router?.routeToPlayback() } + func routeToVideoPicker() { + present(videoPickerManager.phPickerViewController, animated: true) + } + + func openSetting() { + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + + func displayCurrentLocation() { + currentLocationButton.isHidden = false + mapView.setUserTrackingMode(.follow, animated: true) + } + + func displayDefaultLocation(viewModel: MapModels.CheckLocationAuthorizationOnEntry.ViewModel) { + currentLocationButton.isHidden = true + mapView.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: viewModel.latitude, + longitude: viewModel.longitude), + latitudinalMeters: 1000, + longitudinalMeters: 1000) + } + } extension MapViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - interactor?.playPosts(with: Models.PlayPosts.Request(selectedIndex: indexPath.item)) + guard let post = carouselDatasource.itemIdentifier(for: indexPath) else { return } + switch post.boardStatus { + case .complete: + interactor?.playPosts(with: Models.PlayPosts.Request(selectedIndex: indexPath.item)) + default: + Toast.shared.showToast(message: "인코딩 중인 영상입니다. 잠시만 기다려주세요!") + } } } diff --git a/iOS/Layover/Layover/Scenes/Map/MapWorker.swift b/iOS/Layover/Layover/Scenes/Map/MapWorker.swift index 5380b19..e61ea58 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapWorker.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapWorker.swift @@ -20,13 +20,16 @@ final class MapWorker: MapWorkerProtocol { typealias Models = MapModels private let provider: ProviderType + private let authManager: AuthManager private let postEndPointFactory: PostEndPointFactory // MARK: - Methods init(provider: ProviderType = Provider(), + authManager: AuthManager = AuthManager.shared, postEndPointFactory: PostEndPointFactory = DefaultPostEndPointFactory()) { self.provider = provider + self.authManager = authManager self.postEndPointFactory = postEndPointFactory } @@ -35,7 +38,10 @@ final class MapWorker: MapWorkerProtocol { do { let response = try await provider.request(with: endPoint) guard let posts = response.data else { return nil } - return await fetchThumbnailImageData(of: posts) + let filterdPosts = posts.filter { post in + post.member.id == authManager.memberID || post.board.status == .complete + } + return await fetchThumbnailImageData(of: filterdPosts) } catch { os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) return nil diff --git a/iOS/Layover/Layover/Scenes/Map/Views/MapCarouselCollectionViewCell.swift b/iOS/Layover/Layover/Scenes/Map/Views/MapCarouselCollectionViewCell.swift index ed485ac..5fb4243 100644 --- a/iOS/Layover/Layover/Scenes/Map/Views/MapCarouselCollectionViewCell.swift +++ b/iOS/Layover/Layover/Scenes/Map/Views/MapCarouselCollectionViewCell.swift @@ -11,7 +11,7 @@ import AVFoundation final class MapCarouselCollectionViewCell: UICollectionViewCell { - private let loopingPlayerView = LoopingPlayerView() + private let loopingPlayerView: LoopingPlayerView = LoopingPlayerView() private let thumbnailImageView: UIImageView = { let imageView: UIImageView = UIImageView() @@ -19,6 +19,14 @@ final class MapCarouselCollectionViewCell: UICollectionViewCell { return imageView }() + private let spinner: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.backgroundColor = .clear + indicator.hidesWhenStopped = true + indicator.stopAnimating() + return indicator + }() + override init(frame: CGRect) { super.init(frame: frame) setUI() @@ -31,7 +39,13 @@ final class MapCarouselCollectionViewCell: UICollectionViewCell { render() } - func setVideo(url: URL) { + func setVideo(url: URL?) { + guard let url else { + spinner.startAnimating() + loopingPlayerView.disable() + return + } + spinner.stopAnimating() loopingPlayerView.disable() loopingPlayerView.prepareVideo(with: url, timeRange: CMTimeRange(start: .zero, duration: CMTime(value: 1800, timescale: 600))) @@ -41,6 +55,8 @@ final class MapCarouselCollectionViewCell: UICollectionViewCell { func configure(thumbnailImageData: Data?) { if let thumbnailImageData { thumbnailImageView.image = UIImage(data: thumbnailImageData) + } else { + thumbnailImageView.image = nil } } @@ -56,7 +72,7 @@ final class MapCarouselCollectionViewCell: UICollectionViewCell { private func setUI() { backgroundColor = .background - contentView.addSubviews(loopingPlayerView, thumbnailImageView) + contentView.addSubviews(loopingPlayerView, thumbnailImageView, spinner) contentView.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ loopingPlayerView.topAnchor.constraint(equalTo: contentView.topAnchor), @@ -67,7 +83,10 @@ final class MapCarouselCollectionViewCell: UICollectionViewCell { thumbnailImageView.topAnchor.constraint(equalTo: contentView.topAnchor), thumbnailImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), thumbnailImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - thumbnailImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + thumbnailImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + + spinner.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) } diff --git a/iOS/Layover/Layover/Scenes/UIComponents/LoopingPlayerView.swift b/iOS/Layover/Layover/Scenes/UIComponents/LoopingPlayerView.swift index faded42..811a4c1 100644 --- a/iOS/Layover/Layover/Scenes/UIComponents/LoopingPlayerView.swift +++ b/iOS/Layover/Layover/Scenes/UIComponents/LoopingPlayerView.swift @@ -54,7 +54,7 @@ final class LoopingPlayerView: UIView { let asset: AVURLAsset if let assetResourceLoaderDelegate { self.assetResourceLoaderDelegate = assetResourceLoaderDelegate - asset = AVURLAsset(url: url.customHLS_URL) + asset = AVURLAsset(url: url.customHLSURL) asset.resourceLoader.setDelegate(assetResourceLoaderDelegate, queue: DispatchQueue.global(qos: .utility)) Task { diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift index 0a4a956..72650eb 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift @@ -8,6 +8,7 @@ import AVFoundation import CoreLocation +import UniformTypeIdentifiers import UIKit import OSLog @@ -109,10 +110,10 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD let videoURL, let isMuted, let coordinate = locationManager.getCurrentLocation()?.coordinate else { return } - if isMuted { - exportVideoWithoutAudio(at: videoURL) - } Task { + if isMuted { + await exportVideoWithoutAudio(at: videoURL) + } let uploadPostResponse = await worker.uploadPost(with: UploadPost(title: request.title, content: request.content, latitude: coordinate.latitude, @@ -126,33 +127,35 @@ final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostD } } - private func exportVideoWithoutAudio(at url: URL) { + private func exportVideoWithoutAudio(at url: URL) async { let composition = AVMutableComposition() let sourceAsset = AVURLAsset(url: url) guard let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else { return } - Task { - do { - let sourceAssetduration = try await sourceAsset.load(.duration) - let sourceVideoTrack = try await sourceAsset.load(.tracks)[0] - compositionVideoTrack.preferredTransform = try await sourceVideoTrack.load(.preferredTransform) - - let timeRange: CMTimeRange = CMTimeRangeMake(start: .zero, duration: sourceAssetduration) - try compositionVideoTrack.insertTimeRange(timeRange, - of: sourceVideoTrack, - at: .zero) + do { + let sourceAssetduration = try await sourceAsset.load(.duration) + let sourceVideoTrack = try await sourceAsset.load(.tracks)[0] + compositionVideoTrack.preferredTransform = try await sourceVideoTrack.load(.preferredTransform) - if fileManager.fileExists(atPath: url.path()) { - try fileManager.removeItem(at: url) - } + let timeRange: CMTimeRange = CMTimeRangeMake(start: .zero, duration: sourceAssetduration) + try compositionVideoTrack.insertTimeRange(timeRange, + of: sourceVideoTrack, + at: .zero) - let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) + if fileManager.fileExists(atPath: url.path()) { + try fileManager.removeItem(at: url) + } + guard let videoURL else { return } + if let outputFileType = AVFileType.from(videoURL) { + let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetPassthrough) exporter?.outputURL = videoURL - exporter?.outputFileType = AVFileType.mov + exporter?.outputFileType = .from(videoURL) await exporter?.export() - } catch { - os_log(.error, log: .data, "Failed to extract Video Without Audio with error: %@", error.localizedDescription) + } else { + presenter?.presentUnsupportedFormatAlert() } + } catch { + os_log(.error, log: .data, "Failed to extract Video Without Audio with error: %@", error.localizedDescription) } } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift index 7d4510b..d8c249f 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift @@ -13,7 +13,7 @@ protocol UploadPostPresentationLogic { func presentThumbnailImage(with response: UploadPostModels.FetchThumbnail.Response) func presentCurrentAddress(with response: UploadPostModels.FetchCurrentAddress.Response) func presentUploadButton(with response: UploadPostModels.CanUploadPost.Response) -// func presentUploadProgress(with response: UploadPostModels.UploadPost.Response) + func presentUnsupportedFormatAlert() } final class UploadPostPresenter: UploadPostPresentationLogic { @@ -56,4 +56,7 @@ final class UploadPostPresenter: UploadPostPresentationLogic { viewController?.displayUploadButton(viewModel: viewModel) } + func presentUnsupportedFormatAlert() { + viewController?.displayUnsupportedFormatAlert() + } } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift index 917387e..3836130 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift @@ -13,6 +13,7 @@ protocol UploadPostDisplayLogic: AnyObject { func displayThumbnail(viewModel: UploadPostModels.FetchThumbnail.ViewModel) func displayCurrentAddress(viewModel: UploadPostModels.FetchCurrentAddress.ViewModel) func displayUploadButton(viewModel: UploadPostModels.CanUploadPost.ViewModel) + func displayUnsupportedFormatAlert() } final class UploadPostViewController: BaseViewController { @@ -315,4 +316,9 @@ extension UploadPostViewController: UploadPostDisplayLogic { uploadButton.isEnabled = viewModel.canUpload } + func displayUnsupportedFormatAlert() { + router?.routeToBack() + Toast.shared.showToast(message: "지원하지 않는 파일 형식이에요 😢") + } + } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/VideoPickerManager.swift b/iOS/Layover/Layover/Scenes/UploadPost/VideoPickerManager.swift index 4044a00..187d3ea 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/VideoPickerManager.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/VideoPickerManager.swift @@ -45,7 +45,7 @@ final class VideoPickerManager: NSObject, PHPickerViewControllerDelegate { if error != nil { Task { await MainActor.run { - Toast.shared.showToast(message: "지원하지 않는 동영상 형식입니다 T.T") + Toast.shared.showToast(message: "지원하지 않는 파일 형식이에요 😢") } } } diff --git a/iOS/Layover/Layover/Services/HLSResourceLoader/HLSAssetResourceLoaderDelegate.swift b/iOS/Layover/Layover/Services/HLSResourceLoader/HLSAssetResourceLoaderDelegate.swift index e31c53e..08dc1ce 100644 --- a/iOS/Layover/Layover/Services/HLSResourceLoader/HLSAssetResourceLoaderDelegate.swift +++ b/iOS/Layover/Layover/Services/HLSResourceLoader/HLSAssetResourceLoaderDelegate.swift @@ -37,7 +37,7 @@ class HLSAssetResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate { // 공통으로 처리 func loadRequestedResource(_ loadingRequest: AVAssetResourceLoadingRequest) -> Bool { - guard let url = loadingRequest.request.url?.originHLS_URL else { return false } + guard let url = loadingRequest.request.url?.originHLSURL else { return false } if url.pathExtension.contains("ts") { // ts 파일은 리디렉션 시킨다. loadingRequest.redirect = URLRequest(url: url) diff --git a/iOS/Layover/Layover/Services/HLSResourceLoader/HLSSliceResourceLoader.swift b/iOS/Layover/Layover/Services/HLSResourceLoader/HLSSliceResourceLoader.swift index bc91ce1..70aadf1 100644 --- a/iOS/Layover/Layover/Services/HLSResourceLoader/HLSSliceResourceLoader.swift +++ b/iOS/Layover/Layover/Services/HLSResourceLoader/HLSSliceResourceLoader.swift @@ -36,7 +36,7 @@ final class HLSSliceResourceLoader: ResourceLoader { // MARK: - ResourceLoader func loadResource(from url: URL) async -> Data? { - let urlRequest = URLRequest(url: url.originHLS_URL) // 원래 url scheme 으로 변경 + let urlRequest = URLRequest(url: url.originHLSURL) // 원래 url scheme 으로 변경 guard let (data, response) = try? await session.data(for: urlRequest), let httpResponse = response as? HTTPURLResponse, diff --git a/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift b/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift index 7649be4..c2e77cc 100644 --- a/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift +++ b/iOS/Layover/Layover/Services/Location/CurrentLocationManager.swift @@ -14,7 +14,6 @@ final class CurrentLocationManager: NSObject { private var locationFetcher: LocationFetcher var currentLocationCompletion: LocationCompletion? - var authrizationCompletion: ((CLAuthorizationStatus) -> Void)? init(locationFetcher: LocationFetcher = CLLocationManager()) { self.locationFetcher = locationFetcher @@ -29,10 +28,18 @@ final class CurrentLocationManager: NSObject { return CLLocation(latitude: space.latitude, longitude: space.longitude) } + func getAuthorizationStatus() -> CLAuthorizationStatus { + return locationFetcher.authorizationStatus + } + func startUpdatingLocation() { self.locationFetcher.startUpdatingLocation() } + func requestWhenInUseAuthorization() { + self.locationFetcher.requestWhenInUseAuthorization() + } + } extension CurrentLocationManager: LocationFetcherDelegate { @@ -41,19 +48,10 @@ extension CurrentLocationManager: LocationFetcherDelegate { self.currentLocationCompletion?(location) self.currentLocationCompletion = nil } - - func locationFetcher(_ fetcher: LocationFetcher, didChangeAuthorization authorization: CLAuthorizationStatus) { - self.authrizationCompletion?(authorization) - self.authrizationCompletion = nil - } } extension CurrentLocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { self.locationFetcher(manager, didUpdateLocations: locations) } - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - self.locationFetcher(manager, didChangeAuthorization: manager.authorizationStatus) - } } diff --git a/iOS/Layover/Layover/Services/Location/LocationFetcher.swift b/iOS/Layover/Layover/Services/Location/LocationFetcher.swift index a76535e..7a45054 100644 --- a/iOS/Layover/Layover/Services/Location/LocationFetcher.swift +++ b/iOS/Layover/Layover/Services/Location/LocationFetcher.swift @@ -10,18 +10,17 @@ import CoreLocation protocol LocationFetcherDelegate: AnyObject { func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locations: [CLLocation]) - func locationFetcher(_ fetcher: LocationFetcher, didChangeAuthorization authorization: CLAuthorizationStatus) } protocol LocationFetcher { var location: CLLocation? { get } var locationFetcherDelegate: LocationFetcherDelegate? { get set } + var authorizationStatus: CLAuthorizationStatus { get } var desiredAccuracy: CLLocationAccuracy { get set } func requestLocation() func startUpdatingLocation() func requestWhenInUseAuthorization() - func requestAlwaysAuthorization() } extension CLLocationManager: LocationFetcher { diff --git a/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift b/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift index 98481c0..7992152 100644 --- a/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift +++ b/iOS/Layover/LayoverTests/Mocks/LocationFetcher/MockLocationFetcher.swift @@ -11,9 +11,11 @@ import CoreLocation @testable import Layover final class MockLocationFetcher: LocationFetcher { + var location: CLLocation? var locationFetcherDelegate: Layover.LocationFetcherDelegate? var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest + var authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse func requestLocation() { } diff --git a/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift b/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift index 7d6a36e..94c6dbf 100644 --- a/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift +++ b/iOS/Layover/LayoverTests/Scenes/Home/HomeInteractorTests.swift @@ -35,7 +35,15 @@ final class HomeInteractorTests: XCTestCase { // MARK: - Test doubles - final class HomePresentationLogicSpy: HomePresentationLogic { // 호출 테스트를 위한 Spy + final class HomePresentationLogicSpy: HomePresentationLogic { + func presentUploadScene() { + + } + + func presentSetting() { + + } + // 호출 테스트를 위한 Spy var presentPostsCalled = false var presentPostsReceivedResponse: Models.FetchPosts.Response! var presentPlaybackSceneCalled = false diff --git a/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift b/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift index 6560aa6..35bfd4f 100644 --- a/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift +++ b/iOS/Layover/LayoverTests/Scenes/Home/HomePresenterTests.swift @@ -39,6 +39,7 @@ final class HomePresenterTests: XCTestCase { // MARK: - Test doubles final class HomeDisplayLogicSpy: HomeDisplayLogic { + var displayPostsCalled = false var displayPostsReceivedViewModel: Models.FetchPosts.ViewModel! var displayThumbnailImageCalled = false @@ -58,6 +59,15 @@ final class HomePresenterTests: XCTestCase { func routeToTagPlayList() { routeToTagPlayListCalled = true } + + func routeToVideoPicker() { + + } + + func openSetting() { + + } + } // MARK: - Tests diff --git a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift index d508455..f334d86 100644 --- a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift +++ b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostInteractorTests.swift @@ -69,6 +69,10 @@ class UploadPostInteractorTests: XCTestCase { presentUploadButtonResponse = response } + func presentUnsupportedFormatAlert() { + + } + } // MARK: - Tests diff --git a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift index 99cef62..f137e30 100644 --- a/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift +++ b/iOS/Layover/LayoverTests/Scenes/UploadPost/UploadPostPresenterTests.swift @@ -69,6 +69,10 @@ class UploadPostPresenterTests: XCTestCase { displayUploadButtonViewModel = viewModel } + func displayUnsupportedFormatAlert() { + + } + } // MARK: - Tests