๋น๋์ค ๋ นํ, ์ ์ฅ ๋ฐ ์ฌ์ํ๋ ์ ํ๋ฆฌ์ผ์ด์
ํ๋ก์ ํธ ๊ธฐ๊ฐ: 2023.06.05 - 2023.06.11
๋ฆฌ์ง |
---|
![]() |
Github Profile |
Untitled-4.mp4
- ์นด๋ฉ๋ผ๊ตฌํ์ผ๋ก ์์ ๋ นํ, ํ๋ฉด ์ ํ ๊ฐ๋ฅ
- ์์ ๋ นํ์๊ฐ ๊ธฐ๋ก
- ์์ ์ ๋ชฉ ์ ์ฅ
- ์ธ๋ค์ผ ์ถ์ถ, ์์๊ธธ์ด ํ์
- ๋ นํํ ์์ ํด๋ฆญ์ ์ฌ์ํ๋ฉด์ผ๋ก ์ด๋ ๋ฐ ์ฌ์
- ๋ นํํ ์์ LocalDB ๋ฐ RemoteDB์ ์ ์ฅ/๋ฐฑ์
๋ฉ์ธ ํ๋ฉด | ์ญ์ | ๋ นํ ํ๋ฉด | ์์์ ๋ชฉ ์ ์ฅ | ํ๋ ์ด |
---|---|---|---|---|
LocalDB | RemoteDB |
---|---|
UI | Local DB | Remote DB | Reactive | Architecture | Dependency |
---|---|---|---|---|---|
UIKit | CoreData | FirebaseStorage | Combine | MVVM | SPM |
- UIKit์ ์ฌ์ฉํ์ฌ ์ฝ๋๋ฒ ์ด์ค๋ก UI๋ฅผ ๊ตฌ์ฑํ์์ต๋๋ค.
- ์ด 3๊ฐ์ ํ๋ฉด์ผ๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
- VideoList ํ๋ฉด
- ์์ ๋ นํ ํ๋ฉด
- ๋ นํ๋ ์์ ํ๋ ์ด๋๋ ํ๋ฉด
VideoList
ํ๋ฉด์UICollectionView
๋ฅผ ํ์ฉํ์๊ณ ,UICollectionCompositionalLayout
์List
๋ชจ๋๋ฅผ ์ ์ฉํ์์ต๋๋ค.- ๋ฐ์ดํฐ ๊ตฌ์ฑ์
DiffableDataSource
,NSDiffableDataSourceSnapshot
๋ฅผ ์ฌ์ฉํ์์ต๋๋ค. - ์์ ๋
นํ ํ๋ฉด์
AVFoundation
ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ง์ ์ปค์คํ ํ์์ผ๋ฉฐ ๋ค๋ฅธ UI์์๋ ์ปค์คํ ํ์ฌ ํ๋ฉด์ ๋์ ์ต๋๋ค. - ๋
นํ๋ ์์์
AVPlayerViewController
๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- ๋ นํ๊ฐ ์ข ๋ฃ๋ ์ง์ ์์ LocalDB์ RemoteDB์ ์ ์ฅ๋๋๋ก ๊ตฌํํ์์ต๋๋ค.
- LocalDB๋ Apple์์ ์ ๊ณตํ๋ CoreData ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- RemoteDB๋ FirebaseStorage๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- ์ ์ฅ๋ ๋ฐ์ดํฐ๋ ์ฑ์์ ์ ์ฅ, ์ญ์ ์ ๋๊ธฐํ๋๋๋ก ๊ตฌํํ์์ต๋๋ค.
- ์์กด์ฑ ๊ด๋ฆฌ๋๊ตฌ๋ก Swift Package Manager๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- ViewController์ ์ญํ ์ ๋ถ๋ฆฌํ๊ณ ์ MVVM ํจํด์ ์ฌ์ฉํ์์ต๋๋ค.
- View - ViewModel๊ฐ ๋ฐ์ธ๋ฉ์ Apple์์ ์ ๊ณตํ๋ Combine ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
โ AVFoundation
Framework Video Record ํ๋ฉด ๊ตฌํํ๊ธฐ
AVFoundation์ Apple ํ๋ซํผ์์ ์์ฒญ๊ฐ ๋ฏธ๋์ด๋ฅผ ๊ฒ์ฌ, ์ฌ์, ์บก์ฒ ๋ฐ ์ฒ๋ฆฌํ๊ธฐ ์ํ ๊ด๋ฒ์ํ ์์ ์ ํฌํจํ๋ ๋ช ๊ฐ์ง ์ฃผ์ ๊ธฐ์ ์์ญ์ ๊ฒฐํฉํ๋ค.
์ฆ, Apple ํ๋ซํผ์ ์์ฒญ๊ฐ ๊ด๋ จํ ํ๋์จ์ด๋ฅผ ์ปจํธ๋กคํ ์ ์๊ฒ ํด์ฃผ๋ ํ๋ ์์ํฌ์ด๋ค.
- STEP
- CaptureSession ์์ฑ
- CaptureDevice ์์ฑ
- CaptureDeivceInput ์์ฑ
- Video UI์ ์ถ๋ ฅ
- Recording
API Collection media capture๋ฅผ ์ํด ๋ด์ฅ ์นด๋ฉ๋ผ๋ ๋ง์ดํฌ ๊ทธ๋ฆฌ๊ณ ์ธ๋ถ ๋๋ฐ์ด์ค๋ฅผ ๊ตฌ์ฑํด์ผํ๋ค.
- ์ฌ์ฉ์ ์ง์ ์นด๋ฉ๋ผ UI ๊ตฌํ
- ์ฌ์ฉ์๊ฐ ์ด์ , ๋ ธ์ถ ๋ฐ ์์ ํ ์ต์ ๊ณผ ๊ฐ์ ์ฌ์ง ๋ฐ ๋น๋์ค ์บก์ฒ๋ฅผ ์ง์ ์ ์ดํ ์ ์๋๋ก ๊ตฌํ
- RAW ํ์ ์ฌ์ง, ๊น์ด ์ง๋ ๋๋ ์ฌ์ฉ์ ์ง์ ์๊ฐ ๋ฉํ๋ฐ์ดํฐ๊ฐ ์๋ ๋น๋์ค์ ๊ฐ์ ์์คํ ์นด๋ฉ๋ผ UI์ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํ๋ค.
- ์บก์ฒ ์ฅ์น์์ ์ง์ ํฝ์ ๋๋ ์ค๋์ค ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ์ ์ค์๊ฐ์ผ๋ก ์ก์ธ์คํ ์ ์๋ค.
์บก์ณ ์ํคํ
์ณ์ ๋ฉ์ธํํธ๋ sessions
, inputs
, output
3๊ฐ์ง ์ด๋ค.
CaptureSession
: ํ๋ ์ด์์input
๊ณผoutput
์ ์ฐ๊ฒฐํ๋ค.Inputs
: iOS๋ Mac์ ๋นํธ์ธ๋ ์นด๋ฉ๋ผ๋ ๋ง์ดํฌ์ ๊ฐ์ ๋๋ฐ์ด์ค๋ฅผ ํฌํจํ media์ ์์ค๋ฅผ ๋ปํ๋ค. ๋๋ฐ์ด์ค๋ก ์ฐ์ ์ฌ์ง์ด๋ ๋์์์ ๋งํ๋ค.Outputs
: ์ฌ์ฉ๊ฐ๋ฅํ ๋ฐ์ดํฐ๋ฅผ ๋ง๋ค์ด ๋ธ ๊ฒฐ๊ณผ๋ฌผCatureDevice
: ๋๋ฐ์ด์ค, ๋ด ์์ดํฐ ์นด๋ฉ๋ผ
Input
๊ณผ Output
์ ์ฐ๊ฒฐํด์ฃผ์ด ๋ฐ์ดํฐ ํ๋ฆ์ ์ ์ดํ๋ค.
let captureSession = AVCaptureSession()
captureSession.sessionPreset = .high
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.global(qos: .background).async { [weak self] in
self?.captureSession.startRunning()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
DispatchQueue.global(qos: .background).async { [weak self] in
self?.captureSession.stopRunning()
}
}
sessionPreset
: ๋ นํ ํ์ง์ ์ค์ ํ ์ ์๋ค. ๋๊ฒํ ์๋ก ๋ฐฐํฐ๋ฆฌ ์๋น๋์ด ๋์ด๋๋ค.startRunning()
: ์ค์ง์ ์ ํ๋ก์ฐ๊ฐ ์์๋๋ค. ์ด๋ UI๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฉ์ธ์ค๋ ๋์ ๋ค๋ฅธ ์ค๋ ๋์์ ์ฒ๋ฆฌํด์ค์ผํ๋ค.stopRunning()
: ์ธ์ ์ ์ผ์ด ๋๋ฌ์ ๋ ํธ์ถํ๋ค.
๋๋ view๊ฐ ๋ํ๋ ๋ ์์์ํด์ฃผ๊ณ view๊ฐ ์ฌ๋ผ์ง๋ ์ข ๋ฃ๋ฅผ ์ค์ ํด์ฃผ์๋ค. ๋๋ค ๋ฉ์ธ์ค๋ ๋๊ฐ ์๋ global()์์ ๋ฃ์ด์ฃผ์๋ค.
์ฌ์ฉํ๋ ค๋ ์ฅ์น๋ฅผ ์ ์ํด์ค๋ค.
// audioDevice
let audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
// cameraDevice
private func selectedCamera(in position: AVCaptureDevice.Position) -> AVCaptureDevice? {
let deviceTypes: [AVCaptureDevice.DeviceType] = [.builtInTrueDepthCamera, .builtInDualCamera, .builtInWideAngleCamera]
let discoverySession = AVCaptureDevice.DiscoverySession(
deviceTypes: deviceTypes,
mediaType: .video,
position: .unspecified
)
let devices = discoverySession.devices
guard !devices.isEmpty,
let device = devices.first(where: { device in device.position == position }) else { return nil }
return device
}
captureDevice
๋ฅผ ์ด์ฉํด์ session
์ captureDeviceInput
์ ์ถ๊ฐํด์ค๋ค.
private func setUpSession() {
guard let audioDevice = AVCaptureDevice.default(for: AVMediaType.audio) else { return }
captureSession.sessionPreset = .high
do {
// 1
captureSession.beginConfiguration()
// 2
videoDevice = selectedCamera(in: .back)
guard let videoDevice else { return }
videoInput = try AVCaptureDeviceInput(device: videoDevice)
guard let videoInput else { return }
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
}
// 3
let audioInput = try AVCaptureDeviceInput(device: audioDevice)
if captureSession.canAddInput(audioInput) {
captureSession.addInput(audioInput)
}
// 4
if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput)
}
// 5
captureSession.commitConfiguration()
self.view.layer.addSublayer(videoPreViewLayer)
// 6
setUpCloseButton()
setUpRecordStackView()
videoPreViewLayer.session = captureSession
} catch let error as NSError {
print(error.localizedDescription)
}
}
- ์ธ์ ๊ตฌ์ฑ์ ์์์ ๋ํ๋ธ๋ค.
- ๋น๋์ค ๋๋ฐ์ด์ค์ ๋ํ ์ ๋ ฅ์ ๋ง๋ค์ด ์ธ์ ์ ์ถ๊ฐํ๋ค.
- ์ค๋์ค ๋๋ฐ์ด์ค์ ๋ํ ์ ๋ ฅ์ ๋ง๋ค์ด ์ธ์ ์ ์ถ๊ฐํ๋ค.
- ๋น๋์ค, ์ค๋์ค๋ฅผ ํ์ผ๋ก ์ถ๋ ฅํ๊ธฐ ์ํ output์ ๋ง๋ค์ด ์ธ์ ์ ์ถ๊ฐํ๋ค.
- ์ธ์ ๊ตฌ์ฑ์ ์๋ฃ๋ฅผ ๋ํ๋ธ๋ค.
- videoPreViewLayer ์์ ์ถ๊ฐํ๋ UI์์๋ฅผ ๋ฃ์ด์ค๋ค.
ํ๋ฉด์ ๋น๋์ค๋ ์ฌ์ง ์ดฌ์์ ๋ณด์ฌ์ง๋ UI๋ฅผ ๊ตฌํํ๋ค.
private lazy var videoPreViewLayer: AVCaptureVideoPreviewLayer = {
let previewLayer = AVCaptureVideoPreviewLayer()
previewLayer.frame = self.view.frame
previewLayer.videoGravity = .resizeAspectFill
return previewLayer
}()
- frame ์ view์ frame์ ๋ง์ถ๊ณ videoGravity์ ์ํ๋ ๊ฐ์ ๋ฃ์ด์ฃผ์๋ค.
- session์ ๊ตฌ์ฑํ ๋ view์ layer์ ์ถ๊ฐํด์ฃผ๊ณ ๊ทธ ์๋ ๋ค๋ฅธ ์์๋ฅผ ์ถ๊ฐํด์ผ ์ ์์ ์ผ๋ก ํ๋ฉด์ ๋ํ๋ฌ๋ค.
- ์ฐ์ Layer๋ฅผ ์ถ๊ฐํ๊ณ ๋ค๋ฅธ view๋ button๋ฑ UI๋ ๊ทธ ๋ค์์ ์ถ๊ฐํด์ผ ํ๋ฉด์ ๋ธ!
- ๋ํ ์นด๋ฉ๋ผ ์ฌ์ฉ์ ๊ธฐ๋ณธ info.plist ์ค์ ๋ ํด์ผํ๋ค.
์ด์ view์ ๋ํ๋ ํ๋ฉด์ ๋
นํํด์ผํ๋๋ฐ ์ด๋ AVCaptureMovieFileOutput
์ ์ด์ฉํ๋ฉด ๋๋ค.
let videoOutput = AVCaptureMovieFileOutput()
// ๋
นํ์์
private func startRecording() {
startTimer()
outputURL = viewModel.createVideoURL()
guard let outputURL else { return }
videoOutput.startRecording(to: outputURL, recordingDelegate: self)
}
// ๋
นํ์ข
๋ฃ
private func stopRecording() {
if videoOutput.isRecording == true {
stopTimer()
videoOutput.stopRecording()
}
}
๋
นํ๊ฐ ์ข
๋ฃ๋ ํ์ ์์
์ AVCaptureFileOutputRecordingDelegate
๋ฅผ ์ค์ํ์ฌ ์ํ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
extension RecordVideoViewController: AVCaptureFileOutputRecordingDelegate {
func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
recordStackView.changeCameraModeButton.isEnabled = false
}
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
recordStackView.changeCameraModeButton.isEnabled = true
if (error != nil) {
print(error?.localizedDescription as Any)
} else {
guard let videoRecordedURL = outputURL,
let videoData = try? Data(contentsOf: videoRecordedURL) else { return }
let title = "์์์ ์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์."
let save = "์ ์ฅ"
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
let saveAction = UIAlertAction(title: save, style: .default) { [weak self] _ in
guard let videoTitle = alert.textFields?[0].text else { return }
self?.fetchThumbnail(from: videoRecordedURL, videoData: videoData, title: videoTitle)
}
alert.addTextField()
alert.addAction(saveAction)
self.present(alert, animated: true)
}
}
- ์ฒซ ๋ฒ์งธ ๋ฉ์๋๋ ๋ นํ๊ฐ ์์๋๋ฉด ํธ์ถ๋๋ ๋ฉ์๋๋ก ๋๋ ๋ นํ๊ฐ ์์๋์์ ๋, ์นด๋ฉ๋ผ ๋ชจ๋๋ฅผ ๋ฐ๊พธ๋ ๋ฒํผ์ด ์๋ํ์ง ์๋๋ก ๊ตฌํํ๋ค.
- ๋ ๋ฒ์งธ ๋ฉ์๋๋ ๋ นํ๊ฐ ์ข ๋ฃ๋๋ฉด ํธ์ถ๋๋ ๋ฉ์๋๋ก ์ข ๋ฃ๊ฐ ๋๋ฉด ์ ๋ชฉ์ ์ ๋ ฅํ๋ Alert์ฐฝ์ด ๋จ๊ณ ์ ๋ ฅ๋ ์ ๋ชฉ๊ณผ ๋๋จธ์ง ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํด ๋ก์ปฌ DB์ ์ ์ฅํ๋๋ก ๊ตฌํํ๋ค.
โ AVAssetImageGenerator
class video asset์์ image๋ฅผ ๋ง๋ค์ด๋ด๋ ๊ฐ์ฒด
.mp4 ํฌ๋งท ์์์ ์ธ๋ค์ผ์ ๊ตฌํ๊ธฐ ์ํด AVAssetImageGenerator
๋ฅผ ์ฌ์ฉํ์๋ค.
์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ธฐ ์ํด Asset
์ด ํ์ํ๊ณ Asset
์ ๊ตฌํ๊ธฐ ์ํด URL
์ด ํ์ํ๋ค.
๋๋ ๋
นํ๊ฐ ์๋ฃ๋ ๋ฐ์ดํฐ(์์)์ CoreData
์ ์ ์ฅํ์๋๋ฐ ์ด๋ outPutURL(์์ URL)
์ ๋ด์ ํ Data
๋ก ๋ฝ์์ Data
ํ์
์ผ๋ก ์ ์ฅ์ ํ์๋ค.
๊ทธ๋์ CoreData
์ ์ ์ฅํ model ํ์
์ ๋ง๋ค ๋ URL
์ด ์ด์ฉ๋๊ณ ์ด๋ ์ธ๋ค์ผ์ ๋ฝ์์ ๊ฐ์ด CoreData
์ ๋ฃ๋ ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํ๋ค.
URL
์ ๋ฐ์์์ Asset
์ ๋ง๋ค๊ณ AVAssetImageGenerator
๋ฅผ ์ด์ฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ๋ฝ์๋ด๋ ๋ฉ์๋๋ฅผ ๊ตฌํ
private func generateThumbnail(from url: URL) -> Future<UIImage?, RecordingError> {
return Future<UIImage?, RecordingError> { promise in
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
let time = CMTime(seconds: 1, preferredTimescale: 1)
guard let cgImage = try? imageGenerator.copyCGImage(at: time, actualTime: nil) else {
promise(.failure(.thumbnail))
return
}
let thumbnailImage = UIImage(cgImage: cgImage)
promise(.success(thumbnailImage))
}
}
}
- ๋๋ Combine์ ์ฌ์ฉํ์ฌ
Future
๋ฅผ ์ด์ฉํ๋ค. (์ด๋ ๊ฒ ์ฐ๋๊ฒ ๋ง๋..?) - UI๋ฅผ ๋ฐฉํดํ๋ฉด ์๋๊ธฐ ๋๋ฌธ์
DispatchQueue.global().async
๋ธ๋ก ์์ ๋ฃ์ด์ฃผ์๋ค. CMTime
: 1์ด๋ก ์ ์ ํ๋ค.copyCGImage(at, actualTime:)
: cgImage๋ฅผ ๊ตฌํ๋ค. ์ด ํ๋กํผํฐ๋ iOS16๋ถํฐ ์ฌ์ฉํ ์ ์์ง๋ง ์ด ์ฑ์ ํ๊ฒ์ ์ต์ iOS14๋ฒ์ ์ด๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ์๋ค.- ๋ง์ง๋ง์ UIImage๋ก ๋ณ๊ฒฝํ์ฌ success์ ๋ฃ์ด์ฃผ์๋ค.
private func fetchThumbnail(from videoRecordedURL: URL, videoData: Data, title: String) {
generateThumbnail(from: videoRecordedURL)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
} receiveValue: { [weak self] image in
if let image {
guard let imageData = image.pngData(),
let playTime = self?.fetchPlayTime(videoRecordedURL.absoluteString) else { return }
let video = Video(title: "\(title).mp4", date: Date(), savedVideo: videoData, thumbnailImage: imageData, playTime: playTime)
self?.viewModel.create(video)
}
}
.store(in: &cancellables)
}
์ ๋ชฉ ๊ทธ๋๋ก Future
ํ์
์ ๋ฐํํ๋ ๋ฉ์๋๋ฅผ ๊ตฌ๋
ํ์ฌ ์คํจ์ ์ฑ๊ณต ์ผ์ด์ค๋ก ๋๋ ์ฑ๊ณตํ์ ๋ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์์ CoreData
์ ๋ฃ์ model
ํ์
์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ด ์ ์ฅํด์ฃผ์๋ค.
๊ทธ๋ฆฌ๊ณ viewModel์ create
๋ฅผ ์คํํด ์ค์ ๋ฐ์ดํฐ๋ฅผ ๋ก์ปฌ DB์ ์ ์ฅํ๋ค.
AVFoundation์๋ AVCaptureFileOutputRecordingDelegate
๊ฐ ์๊ณ ์ฌ๊ธฐ์ ๋
นํ๊ฐ ์ข
๋ฃ๋๋ฉด ์คํ๋๋ ๋ฉ์๋๊ฐ ์๋ค. ๋๋ ๋
นํ๊ฐ ์ข
๋ฃ๋๋ฉด ์ธ๋ค์ผ์ ๊ฐ์ ธ์ค๊ณ ๊ทธ๊ฑธ ๋ก์ปฌDB์ ์ ์ฅํ๋๋ก ํ๋ค.
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
recordStackView.changeCameraModeButton.isEnabled = true
if (error != nil) {
print(error?.localizedDescription as Any)
} else {
guard let videoRecordedURL = outputURL,
let videoData = try? Data(contentsOf: videoRecordedURL) else { return }
let title = "์์์ ์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์."
let save = "์ ์ฅ"
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
let saveAction = UIAlertAction(title: save, style: .default) { [weak self] _ in
guard let videoTitle = alert.textFields?[0].text else { return }
self?.fetchThumbnail(from: videoRecordedURL, videoData: videoData, title: videoTitle)
}
alert.addTextField()
alert.addAction(saveAction)
self.present(alert, animated: true)
}
}
- ์ถ๊ฐ์ ์ผ๋ก ๋น๋์ค์ ์ด๋ฆ์ ์ ์ฅ์ด ๋๋๋ฉด Alert์ฐฝ์ด ๋จ๊ณ ๊ฑฐ๊ธฐ TextField์ ์ ๋ ฅํ์ฌ ์ป์ text๋ฅผ ์ฌ์ฉํ๋ค. Alert์ textField๊ฐ ํฌํจ๋์ด์์ด ์ฝ๊ฒ ๊ตฌํํ ์ ์์๋ค.
โ AVPlayerViewController
Class playback ์ปจํธ๋กค์ ์ํ ์ ์ ์ธํฐํ์ด์ค๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ํ๋ ์ด์ด๋ก ๋ถํฐ ์ปจํ ์ธ ๋ฅผ ๋ณด์ฌ์ฃผ๋ viewController
AVKitํ๋ ์์ํฌ๋ AVPlayerViewController subclassing์ ์ง์ํ์ง ์๋๋ค.
- Airplay ์ง์
- Picture in Picture(PiP) ์ง์ PiP ์ฌ์์ ํตํด ์ฌ์ฉ์๋ ๋น๋์ค ํ๋ ์ด์ด๋ฅผ ์์ ํ๋กํ ์ฐฝ์ผ๋ก ์ต์ํํ์ฌ ๊ธฐ๋ณธ ์ฑ์ด๋ ๋ค๋ฅธ ์ฑ์์ ๋ค๋ฅธ ํ๋์ ์ํํ ์ ์๋ค.
- tvOS Playback ๊ฒฝํ ์ปค์คํ ์ง์
video๋ฅผ ํ๋ ์ดํ๋ ค๋ฉด videoURL์ด ํ์ํ๋ค. ์ด URL(filepath)์ AVPlayer(url:)์ ๋ฃ์ด์ฃผ์ด ํ๋ ์ดํ๋๋ก ํด์ผํ๋๋ฐ ์ฌ๊ธฐ์ ํธ๋ฌ๋ธ์ํ ์ด ์์๋ค.
- ๐ ์ ์ฅํ๋ Model ํ์ ์ URL์ ํตํด ์ธ๋ค์ผ์ ์ ์ฅํ๋ URL์ ๊ฐ์ด ์ ์ฅํด์ ์ด URL์ ์ฌ์ฉํด๋ณด์
์ฒ์ ์ด๋ ๊ฒ ์ ๊ทผํ๋๋ฐ, ์ฑ์ ์ข ๋ฃํ๊ณ ๋ค์ ์คํํ๋ ๋์์ ์ฌ์์ด ์ ๋๋ก ๋์ง ์์๋ค. ์ด์ ๋ฅผ ์๊ฐํด๋ณด๋ ์์๋ก URL์ ๋ง๋ค์ด์ ์ด URL์ด ๋ณ๊ฒฝ๋์๋? ์ ํํ์ง ์์๊ฐ ์์ฌํ๋ค.
func createVideoURL() -> URL? {
let directory = NSTemporaryDirectory() as NSString
if directory != "" {
let path = directory.appendingPathComponent(NSUUID().uuidString + ".mp4")
return URL(fileURLWithPath: path)
}
return nil
}
์ด๋ ๊ฒ ์์Directory๋ฅผ ๋ง๋ค๊ณ Filepath๋ฅผ ์ ์ฅํ ๊ฒ์ด ๋ฌธ์ ์ธ๊ฐ ์ถ์ด์ FileManager๋ฅผ ์ด์ฉํด์ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ผ๋ก ๋ณ๊ฒฝํ๋ค. ๊ทธ๋ฌ๋ ์ด๋ง์ ๋ ํด๊ฒฐ๋์ง ์์๋ค. URL์ ์ฑ์ ๋ค์ ์ผฐ์ ๋์ ์ฒ์๊ณผ ๋์ผํ๋ค. ๊ฒฐ๊ตญ ๋ฌธ์ ๋ URL์ ๋ง์ง๋ง ๊ทธ ์์ ํ์ผ์ด ์๋ค๋๊ฒ ๋ฌธ์ ์๋ค. ์๊ฐํด๋ณด๋ CoreData์ ์์์ ์ ์ฅํ๋๋ฐ filePath๋ CoreData ์์น์ ๋ง์ง ์์๋ค. ๊ทธ๋ฅ filePath๋ง ์ผ์นํ์ ๋ฟ...๐ฅฒ
์ ์ฅ๋ ๋ฐ์ดํฐ์ url์ ์ฐพ์์ผํ๋๋ฐ CoreData์ ์ฅ ์์น๋ฅผ ์ผ์ผํ ์์๋ด๋ ๊ฒ์ ํ๋ค์๋ค. ์ ์ฅ๋๊ณ ๋์ ์๋๊ฑด ๋์ง๋ง ๊ณ์ํด์ ์ถ์ ํ๊ธฐ๋ ๋ถ๊ฐ๋ฅํ๋ค.
๋ฐ๋ผ์ Dataํ์
์ url๋ก ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ์ ๊ณ์ํด์ ์ฐพ์๋ณด์๊ณ write(to:options)
๋ฉ์๋๋ฅผ ๋ฐ๊ฒฌํ๋ค. ์ด๋ ๋ฐ์ดํฐ๋ฅผ ๋ด์์ค url์ ๋ณ์๊ณ ๊ฐ์ง๊ณ ์๋ค.
- ์๋ก์ด ์์ url์ ๋ง๋ ๋ค.
- ํ๋ฉด์ ๋์ธ data๋ฅผ url์ ์ ์ฅํ๋ค.
- ๊ทธ url์ player์ ๋ฃ์ด์ฃผ์ด ํ๋ฉด์ ๋์ด๋ค.
์ด๋ ๊ฒ ํ๋๊น ์ฑ์ ์ข ๋ฃํ๊ณ ๋ค์์ผ๋ ์ ์ ์๋ํ๋ ๊ฒ์ ํ์ธํ๋ค.
import AVKit
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let videoEntity = viewModel.read(at: indexPath),
let video = videoEntity.savedVideo,
let videoURL = viewModel.createVideoURL() else { return }
do {
try video.write(to: videoURL)
let playerController = AVPlayerViewController()
let player = AVPlayer(url: videoURL)
playerController.player = player
playerController.entersFullScreenWhenPlaybackBegins = true
self.present(playerController, animated: true) {
player.play()
}
} catch let error {
print(error.localizedDescription)
}
}
โ Future - Combine
Class single value๋ฅผ ์์ฐํ๊ณ finishํ๊ฑฐ๋ failํ๋ publisher
Future๋ ๋น๋๊ธฐ์ ์ผ๋ก single element๋ฅผ publishํ ๋ ์ฌ์ฉํ๋ค. ํด๋ก์ ๋ promise๋ฅผ ํธ์ถํ์ฌ ์ฑ๊ณต์ธ์ง ์คํจ์ธ์ง ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฌํ๋ค. ์ฑ๊ณต์ธ ๊ฒฝ์ฐ future์ ๋ค์ด์คํธ๋ฆผ ๊ตฌ๋ ์๊ฐ ๊ทธ ์์๋ฅผ ๋ฐ๊ณ error์ธ ๊ฒฝ์ฐ์ publishing์ ์ข ๋ฃํ๋ค.
func generateAsyncRandomNumberFromFuture() -> Future <Int, Never> {
return Future() { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
promise(Result.success(number))
}
}
}
Published value๋ฅผ ์ ๋ฌ๋ฐ์ ๋ subscriber๋ฅผ ์ด์ฉํ์ฌ ๋ฐ๋๋ค.
cancellable = generateAsyncRandomNumberFromFuture()
.sink { number in print("Got random number \(number).") }
โ Firebase
CloudDB
๋ฐฑ์ ์ฉ์ผ๋ก CloudBD๊ฐ ํ์ํ๊ณ , Firebase๋ฅผ ์ฌ์ฉํ์๋ค. Firebase ์ฌ์ดํธ์ ๋ค์ด๊ฐ๋ฉด Apple ํ๋ก์ ํธ์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ์ด ์์ธํ ๋์์๋ค.
- Firebase์ ํ๋ก์ ํธ ์์ฑํ๊ธฐ
- Firebase์ ์ฑ ๋ฑ๋กํ๊ธฐ (์ด๋, ์ฑ ๋ฒ๋ค ID๊ฐ ํ์ํ๋ค)
- Firebase ๊ตฌ์ฑ ํ์ผ ์ถ๊ฐ
- GoogleService-Info.plist ๋ค์ด๋ก๋ํ๋ค.
- XCode์ ์ถ๊ฐํ๋ค.
- ์ฑ์ Firebase SDK๋ฅผ ์ถ๊ฐํ๋ค.
- SPM์ ์ด์ฉํ์ฌ Firebase ์ข ์ํญ๋ชฉ์ ์ค์นํ๊ณ ๊ด๋ฆฌํ๋ค.
- Xcode -> File -> Add Packages
- ์ฑ์์ Firebase๋ฅผ ์ด๊ธฐํํ๋ค.
- UIApplicationDelegate์์ ์ด๊ธฐํํด์ค
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
๋๋ ๋์์์ ๋ฐฑ์ ํด์ผํ๋ฏ๋ก Firebase Storage๋ฅผ ํ์ฉํ๋ค.
- SPM์์ FirebaseFireStore, FirebaseStorage๋ฅผ ์ ํํ๋ค.
FirebaseStorage๋ฅผ ๊ด๋ฆฌํ๋ ๊ฐ์ฒด๋ฅผ ํ๋ ์์ฑํ๊ณ ๊ทธ ๊ฐ์ฒด๋ฅผ ํตํด ์ ์ฅ๊ณผ ์ญ์ ๋ฅผ ๊ด๋ฆฌํ์๋ค.
import FirebaseStorage
import Firebase
final class FirebaseStorageManager {
static func uploadVideo(_ video: Data, id: UUID) {
let metaData = StorageMetadata()
metaData.contentType = ".mp4"
let firebaseReference = Storage.storage().reference().child("\(id).mp4")
firebaseReference.putData(video, metadata: metaData)
}
static func deleteVideo(id: UUID) {
let firebaseReference = Storage.storage().reference().child("\(id).mp4")
firebaseReference.delete { error in
if let error = error {
print("๋์์ ์ญ์ ์คํจ: \(error)")
} else {
print("๋์์ ์ญ์ ์ฑ๊ณต")
}
}
}
}
- ์ ์ฅํ ๋ child์์ ๋ค์ด๊ฐ๋ ๊ฒ์ด ์ด๋ฆ์ด ๋๊ณ .mp4๋ฅผ ์ด์ฉํด ํ์ฅ์๋ฅผ ์ค์ ํ์๋ค.
- ์ญ์ ํ ๋๋ title๋ก ๋น๊ตํ๋ฉด ๊ฐ์ ์ด๋ฆ์ ๋์์์ ๊ตฌ๋ถํ๊ธฐ ์ด๋ ค์ธ ๊ฒ ๊ฐ์ ๊ณ ์ ํ ์๋ณ์์ธ UUID๋ฅผ ์ด์ฉํ์ฌ ์ญ์ ํ๋๋ก ํ์๋ค.
ํ์ฌ localDB์ 3๊ฐ์ ํ์ผ์ด ์ ์ฅ๋์ด ์๊ณ ๋ฐฑ์
ํ์ผ๋ 3๊ฐ๊ฐ ์กด์ฌํ๋ค.
- AppleDeveloper - AVPlayerViewController
- AppleDeveloper - write(to:options)
- AppleDeveloper - AVAssetImageGenerator
- AppleDeveloper - copyCGImage(at:actualTime)
- AppleDeveloper - Creating images from a video asset
- AppleDeveloper - AVFoundation
- AppleDeveloper - Capture setup
- AppleDeveloper - Future
- Firebase ๊ณต์ํํ์ด์ง