Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Audio] 페이지에 오디오 플레이어 연결 #122

Merged
merged 38 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0d5bbd1
WIP on feature/audioplayer
yuncheol-AHN Dec 2, 2024
59f72eb
Merge branch 'develop' into feature/audioplayer
yuncheol-AHN Dec 2, 2024
32890b8
feat: add action to push audiocontroller when audio button tapped
yuncheol-AHN Dec 2, 2024
b87ac6c
chore: file system access setting
yuncheol-AHN Dec 2, 2024
ba4e4dc
feat: send uuid (audio vc -> editbook vc)
yuncheol-AHN Dec 2, 2024
52ab72f
feat: add media view factory
yuncheol-AHN Dec 2, 2024
f9a7975
feat: uuid to url
yuncheol-AHN Dec 2, 2024
7be6699
feat: audio player logic
yuncheol-AHN Dec 2, 2024
25ee31b
feat: process view design
yuncheol-AHN Dec 2, 2024
df027f8
fix: autolayout error
yuncheol-AHN Dec 2, 2024
1620cbe
feat: player style changed
yuncheol-AHN Dec 3, 2024
19393ba
feat: uuid -> url
yuncheol-AHN Dec 3, 2024
dda2954
refactor: 파일 이름 클래스와 동기화
iceHood Dec 3, 2024
dbc0862
refactor: 마이크 권한 버전별 분기
iceHood Dec 3, 2024
77e3b9a
refactor: public제거, 함수 위치 조정
iceHood Dec 3, 2024
9b0d6a9
refactor: 프로퍼티 정리
iceHood Dec 3, 2024
e4bb229
feat: viewLoad됐을 때 로직 작성
iceHood Dec 3, 2024
e45f320
refactor: mainActor로 권한 묻기
iceHood Dec 3, 2024
8e4ca0a
refactor: bookID 열고 핸들러 호출
iceHood Dec 3, 2024
a532d6f
refactor: 뷰모델 연결관계 설정
iceHood Dec 3, 2024
2972f41
Merge branch 'develop' into feature/audioplayer
yuncheol-AHN Dec 3, 2024
f3fc468
WIP on feature/audioplayer
yuncheol-AHN Dec 3, 2024
67670f0
feat: 의존성 추가
iceHood Dec 3, 2024
7d8105d
feat: editbookVM에 함수 추가
iceHood Dec 3, 2024
004f6e3
feat: audioViewModelFactory사용
iceHood Dec 3, 2024
3200219
feat: audioViewModelFactory 만듦
iceHood Dec 3, 2024
ebba5de
feat: createAudio 구조 반영
iceHood Dec 3, 2024
2f5c8cc
feat: wip
yuncheol-AHN Dec 3, 2024
9b334e1
Merge branch 'develop' into feature/audioplayer
yuncheol-AHN Dec 3, 2024
844e0f8
feat: read audio
yuncheol-AHN Dec 3, 2024
047311f
chore: apply review
yuncheol-AHN Dec 3, 2024
a01cc47
chore: apply review
yuncheol-AHN Dec 3, 2024
3fa7a1a
chore: apply review
yuncheol-AHN Dec 3, 2024
ee798d1
feat: fix light mode
yuncheol-AHN Dec 3, 2024
aec03b9
chore: apply review
yuncheol-AHN Dec 3, 2024
3ee5c61
chore: apply review
yuncheol-AHN Dec 3, 2024
74d5165
chore: delete unused code
yuncheol-AHN Dec 4, 2024
9239faf
chore: fix portrait
yuncheol-AHN Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -5,7 +5,7 @@
import MHFoundation
import MHPresentation

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {

Check warning on line 8 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Type Body Length Violation: Type body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines (type_body_length)

Check warning on line 8 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Type Body Length Violation: Type body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines (type_body_length)
var window: UIWindow?

func scene(
Expand Down Expand Up @@ -141,7 +141,7 @@
)
}

private func registerUseCaseDependency() throws {

Check warning on line 144 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function Body Length Violation: Function body should span 50 lines or less excluding comments and whitespace: currently spans 86 lines (function_body_length)

Check warning on line 144 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function Body Length Violation: Function body should span 50 lines or less excluding comments and whitespace: currently spans 86 lines (function_body_length)
// MARK: MemorialHouse UseCase
let memorialHouseNameRepository = try DIContainer.shared.resolve(MemorialHouseNameRepository.self)
DIContainer.shared.register(
Expand Down Expand Up @@ -233,9 +233,15 @@
DeleteMediaUseCase.self,
object: DefaultDeleteMediaUseCase(repository: mediaRepository)
)

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

private func registerViewModelFactoryDependency() throws {

Check warning on line 244 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function Body Length Violation: Function body should span 50 lines or less excluding comments and whitespace: currently spans 85 lines (function_body_length)

Check warning on line 244 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function Body Length Violation: Function body should span 50 lines or less excluding comments and whitespace: currently spans 85 lines (function_body_length)
// MARK: Register ViewModel
let createMemorialHouseNameUseCase = try DIContainer.shared.resolve(CreateMemorialHouseNameUseCase.self)
DIContainer.shared.register(
Expand Down Expand Up @@ -329,5 +335,14 @@
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 @@

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 @@
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 @@
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 All @@ -107,7 +111,7 @@
let snapshotData = try await storage.read(at: path, fileName: snapshotFileName).get()
let mediaSet = Set<String>(try JSONDecoder().decode([String].self, from: snapshotData))
// snapshot 파일은 제외
let currentFiles = Set<String>(try await storage.getFileNames(at: path).get()).subtracting([snapshotFileName])

Check warning on line 114 in MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line Length Violation: Line should be 120 characters or less; currently it has 122 characters (line_length)

Check warning on line 114 in MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line Length Violation: Line should be 120 characters or less; currently it has 122 characters (line_length)
let shouldDelete = currentFiles.subtracting(mediaSet)
for fileName in shouldDelete {
_ = try await storage.delete(at: path, fileName: fileName).get()
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
Loading