Skip to content

Commit

Permalink
Merge pull request #122 from boostcampwm-2024/feature/audioplayer
Browse files Browse the repository at this point in the history
[Audio] 페이지에 오디오 플레이어 연결
  • Loading branch information
yuncheol-AHN authored Dec 4, 2024
2 parents 7a4eefd + 9239faf commit 89f9cd0
Show file tree
Hide file tree
Showing 20 changed files with 559 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,9 @@
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "기록소는 사진 권한을 필요로 합니다. 허용 안 함 시 일부 기능이 동작하지 않을 수 있습니다.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down Expand Up @@ -272,8 +273,9 @@
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "기록소는 사진 권한을 필요로 합니다. 허용 안 함 시 일부 기능이 동작하지 않을 수 있습니다.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down
2 changes: 2 additions & 0 deletions MemorialHouse/MHApplication/MHApplication/Resource/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
DeleteMediaUseCase.self,
object: DefaultDeleteMediaUseCase(repository: mediaRepository)
)

// MARK: - TemporaryStoreMedia UseCase
DIContainer.shared.register(
TemporaryStoreMediaUseCase.self,
object: DefaultTemporaryStoreMediaUseCase(repository: mediaRepository)
)
}

private func registerViewModelFactoryDependency() throws {
Expand Down Expand Up @@ -329,5 +335,14 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
ReadPageViewModelFactory.self,
object: ReadPageViewModelFactory(fetchMediaUseCase: fetchMediaUseCase)
)

// MARK: - CreateMediaViewModel
let temporaryStoreMediaUseCase = try DIContainer.shared.resolve(TemporaryStoreMediaUseCase.self)
DIContainer.shared.register(
CreateAudioViewModelFactory.self,
object: CreateAudioViewModelFactory(
temporaryStoreMediaUseCase: temporaryStoreMediaUseCase
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,22 @@ extension MHFileManager: FileStorage {
return .success(originDataPath)
}

public func makeDirectory(through path: String) async -> Result<Void, MHDataError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
).first?.appending(path: path)
else { return .failure(.directorySettingFailure) }
guard (
try? fileManager.createDirectory(
at: originDirectory,
withIntermediateDirectories: true
)
) != nil else { return .failure(.directorySettingFailure)}

return .success(())
}

public func getFileNames(at path: String) async -> Result<[String], MHDataError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
Expand Down
2 changes: 2 additions & 0 deletions MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public protocol FileStorage: Sendable {
/// - Returns: 파일 URL을 반환합니다.
func getURL(at path: String, fileName name: String) async -> Result<URL, MHDataError>

func makeDirectory(through path: String) async -> Result<Void, MHDataError>

/// 지정된 경로의 파일 목록을 반환합니다.
/// Documents폴더를 기준으로 파일 이름 목록을 반환합니다.
/// path는 디렉토리여야 합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import AVFoundation

public struct LocalMediaRepository: MediaRepository, Sendable {
private let storage: FileStorage
private let temporaryPath = "temp" // TODO: - 지워질 것임!
private let temporaryPath = "temporary"
private let snapshotFileName = ".snapshot"

public init(storage: FileStorage) {
Expand Down Expand Up @@ -68,9 +68,9 @@ public struct LocalMediaRepository: MediaRepository, Sendable {
to bookID: UUID
) async -> Result<Void, MHDataError> {
let path = bookID.uuidString
let fileName = mediaDescription.id.uuidString
let fileName = fileName(of: mediaDescription)

return await storage.move(at: "temp", fileName: fileName, to: path)
return await storage.move(at: temporaryPath, fileName: fileName, to: path)
}

public func getURL(
Expand All @@ -85,6 +85,10 @@ public struct LocalMediaRepository: MediaRepository, Sendable {
return await storage.getURL(at: path, fileName: fileName)
}

public func makeTemporaryDirectory() async -> Result<Void, MHDataError> {
return await storage.makeDirectory(through: temporaryPath)
}

public func moveAllTemporaryMedia(to bookID: UUID) async -> Result<Void, MHDataError> {
let path = bookID.uuidString

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public protocol MediaRepository: Sendable {
func create(media mediaDescription: MediaDescription, from: URL, to bookID: UUID?) async -> Result<Void, MHDataError>
func fetch(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result<Data, MHDataError>
func getURL(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result<URL, MHDataError>
func makeTemporaryDirectory() async -> Result<Void, MHDataError>
func delete(media mediaDescription: MediaDescription, at bookID: UUID?) async -> Result<Void, MHDataError>
func moveTemporaryMedia(_ mediaDescription: MediaDescription, to bookID: UUID) async -> Result<Void, MHDataError>
func moveAllTemporaryMedia(to bookID: UUID) async -> Result<Void, MHDataError>
Expand Down
22 changes: 22 additions & 0 deletions MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public struct DefaultDeleteMediaUseCase: DeleteMediaUseCase {
}

public struct DefaultPersistentlyStoreMediaUseCase: PersistentlyStoreMediaUseCase {

// MARK: - Property
let repository: MediaRepository

Expand All @@ -76,4 +77,25 @@ public struct DefaultPersistentlyStoreMediaUseCase: PersistentlyStoreMediaUseCas

try await repository.deleteMediaBySnapshot(for: bookID).get()
}

public func excute(media: MediaDescription, to bookID: UUID) async throws {
try await repository.moveTemporaryMedia(media, to: bookID).get()
}

}

public struct DefaultTemporaryStoreMediaUseCase: TemporaryStoreMediaUseCase {
// MARK: - Property
let repository: MediaRepository

// MARK: - Initializer
public init(repository: MediaRepository) {
self.repository = repository
}

// MARK: - Method
public func execute(media: MediaDescription) async throws -> URL {
try await repository.makeTemporaryDirectory().get()
return try await repository.getURL(media: media, from: nil).get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ public protocol PersistentlyStoreMediaUseCase: Sendable {
/// mediaList가 없을 경우 현재 디렉토리의 스냅샷 기준으로 저장합니다.
/// mediaList가 있을 경우 해당 목록을 기준으로 저장합니다.
func execute(to bookID: UUID, mediaList: [MediaDescription]?) async throws

func excute(media: MediaDescription, to bookID: UUID) async throws
}

public protocol TemporaryStoreMediaUseCase: Sendable {
func execute(media: MediaDescription) async throws -> URL
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ final class CreateAudioViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
// auido
private var audioRecorder: AVAudioRecorder?
private var isRecording = false
// auido metering
private var upBarLayers: [CALayer] = []
private var downBarLayers: [CALayer] = []
Expand All @@ -29,8 +28,6 @@ final class CreateAudioViewController: UIViewController {
AVNumberOfChannelsKey: 2,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
// UUID
private let identifier: UUID = UUID()

// MARK: - UI Component
// title and buttons
Expand Down Expand Up @@ -101,7 +98,8 @@ final class CreateAudioViewController: UIViewController {
}

required init?(coder: NSCoder) {
self.viewModel = CreateAudioViewModel()
guard let viewModelFactory = try? DIContainer.shared.resolve(CreateAudioViewModelFactory.self) else { return nil }
self.viewModel = viewModelFactory.make { _ in }
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -111,17 +109,13 @@ final class CreateAudioViewController: UIViewController {

setup()
bind()
configureAudioSession()
configureAddSubviews()
configureConstraints()
configureAddActions()
input.send(.viewDidLoad)
}

override func viewDidDisappear(_ animated: Bool) {
self.input.send(.viewDidDisappear)
}

// MARK: - setup
// MARK: - Setup
private func setup() {
view.backgroundColor = .white
setupBars()
Expand Down Expand Up @@ -157,54 +151,29 @@ final class CreateAudioViewController: UIViewController {
}
}

private func requestMicrophonePermission() {
AVAudioSession.sharedInstance().requestRecordPermission { @Sendable granted in
if !granted {
Task { @MainActor in
let alert = UIAlertController(
title: "마이크 권한 필요",
message: "설정에서 마이크 권한을 허용해주세요.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true, completion: nil)
}
}
}
}

// MARK: - bind
private func bind() {
let output = viewModel?.transform(input: input.eraseToAnyPublisher())
output?.sink(receiveValue: { [weak self] event in
switch event {
case .updatedAudioFileURL:
// TODO: - update audio file url
MHLogger.debug("updated audio file URL")
case .savedAudioFile:
// TODO: - show audio player
MHLogger.debug("saved audio file")
case .deleteTemporaryAudioFile:
// TODO: - delete temporary audio file
MHLogger.debug("delete temporary audio file")
case .audioStart:
self?.startRecording()
case .audioStop:
self?.stopRecording()
}
}).store(in: &cancellables)
output?.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] event in
switch event {
case let .audioFileURL(url):
self?.configureAudioSession(for: url)
case .audioStart:
self?.startRecording()
case .audioStop:
self?.stopRecording()
case .recordCompleted:
self?.dismiss(animated: true)
}
}).store(in: &cancellables)
}

// MARK: - configure

private func configureAudioSession() {
let fileName = "\(identifier).m4a"
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let audioFileURL = documentDirectory.appendingPathComponent(fileName)

// MARK: - Configuration
private func configureAudioSession(for url: URL) {
try? audioSession.setCategory(.record, mode: .default)

audioRecorder = try? AVAudioRecorder(url: audioFileURL, settings: audioRecordersettings)
audioRecorder = try? AVAudioRecorder(url: url, settings: audioRecordersettings)
audioRecorder?.isMeteringEnabled = true
}

Expand Down Expand Up @@ -263,6 +232,57 @@ final class CreateAudioViewController: UIViewController {
timeTextLabel.setWidthAndHeight(width: 60, height: 16)
}

private func configureAddActions() {
addTappedEventToAudioButton()
addTappedEventToCancelButton()
addTappedEventToSaveButton()
}

private func addTappedEventToAudioButton() {
audioButton.addAction(
UIAction { [weak self] _ in
self?.input.send(.audioButtonTapped)
}, for: .touchUpInside
)
}

private func addTappedEventToCancelButton() {
cancelButton.addAction(
UIAction { [weak self] _ in
self?.input.send(.recordCancelled)
}, for: .touchUpInside
)
}

private func addTappedEventToSaveButton() {
saveButton.addAction(
UIAction { [weak self] _ in
self?.input.send(.saveButtonTapped)
}, for: .touchUpInside
)
}

// MARK: - Helper
private func requestMicrophonePermission() {
let alert = UIAlertController(
title: "마이크 권한 필요",
message: "설정에서 마이크 권한을 허용해주세요.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
self?.dismiss(animated: true)
})
Task {
AVAudioSession.sharedInstance().requestRecordPermission { @Sendable granted in
Task { @MainActor in
if !granted {
self.present(alert, animated: true, completion: nil)
}
}
}
}
}

private func startRecording() {
try? audioSession.setActive(true)

Expand Down Expand Up @@ -356,29 +376,4 @@ final class CreateAudioViewController: UIViewController {
let seconds = recordingSeconds % 60
timeTextLabel.text = String(format: "%02d:%02d", minutes, seconds)
}

private func configureAddActions() {
addTappedEventToAudioButton()
addTappedEventToCancelButton()
addTappedEventToSaveButton()
}

private func addTappedEventToAudioButton() {
audioButton.addAction(UIAction { [weak self] _ in
self?.input.send(.audioButtonTapped)
}, for: .touchUpInside)
}
private func addTappedEventToCancelButton() {
cancelButton.addAction(
UIAction { [weak self]_ in
self?.dismiss(animated: true)
},
for: .touchUpInside)
}
private func addTappedEventToSaveButton() {
saveButton.addAction(UIAction { _ in
self.input.send(.saveButtonTapped)
self.dismiss(animated: true)
}, for: .touchUpInside)
}
}
Loading

0 comments on commit 89f9cd0

Please sign in to comment.