오티스의개발일기

[SwiftUI] AVFoundation을 사용하여 나만의 카메라 프레임워크 및 라이브러리 만들기😁 [코드 제공] 본문

IOS 개발/CameraManager

[SwiftUI] AVFoundation을 사용하여 나만의 카메라 프레임워크 및 라이브러리 만들기😁 [코드 제공]

안되면 될때까지.. 2024. 12. 17. 19:18
728x90

 

 

https://github.com/1domybest

 

1domybest - Overview

email : dhstjrxo123@gmail.com blog : https://otis.tistory.com/ - 1domybest

github.com

 

안녕하세요!

오늘부터 AVFoundation을 사용하여
나만의 카메라를 만들어보는 시간을 가져보겠습니다.!!!

 

기본적으로 제가 기존에만든 CameraFramework 라이브러리를 사용하여
제작할예정입니다.!

 

CameraFramework 의 코드도 오픈소스로 풀어놨으니 많은 관심 부탁드립니다!!!!
소스 코드 깃헙 주소는 글 가장 아래에 위치해있습니다!


궁금한점은 
아래 댓글 혹은 깃험 프로필에있는 연락처로 연락주시면

설명드리겠습니다!! 언제든 연락주세요!

 

자 그럼 시작하겠습니다!!

 

 

 

산출물

 

멀티 카메라
포지션 변경, 썸네일 노출 기능

 

 

 

 

일단 진행하기에 앞서 

기능소개와

목차를 대략적으로 보여드리고

각기능을 어떻게 사용하는지 하나하나 알려드리겠습니다.

 

기능

1. 싱글카메라, 멀티카메라 사용

2. 카메라 전,후면 변경

3. 미러모드 기능

4. 노출 (빛) 정도 조절

5. 미니 스크린 둥글기 조절

6. 오디오 세션

7. 썸네일 추가 기능

8. 플레쉬 ON/OFF

9. 자동초점 (ON/OFF)기능

10. 줌 IN/OUT

 

목차

1. 프로젝트 생성

2. info.list에 퍼미션 추가하기

3. 프로젝트 구조잡기

4. LazyView 제작

5. 카메라 View, ViewModel 제작

 

 

 

1. 프로젝트 생성

 

프로젝트는 IOS App 으로 생성하시고 이름은 원하시는 이름으로 사용하시면 됩니다!

 

 

 

 

제작한 라이브러리의 미니멈 타겟은 14.0 입니다!
그이상의 버전으로 사용하셔도되구요
IOS 14.0 아래는 사용이 불가능합니다!

 

 

 

2.  info.list 에 카메라, 마이크 권한 추가하기

 

 

퍼미션 추가하는 방법은 왼쪽 프로젝트 클릭 -> info 클릭후 -> 리스트에 마우스를 가져다 대면 +버튼이 나옵니다 + 버튼을 누르시고 추가하시면 됩니다.!

 

카메라 권한추가

Privacy - Camera Usage Description

 

마이크 권한추가

Privacy - Microphone Usage Description

 

3.  라이브러리 추가

1. 프로젝트를 클릭합니다.

 

 

2. General 을 클릭하고 아래부분에 아래 사진과같은 목록이있습니다 여기에서 + 버튼을 클릭해주세요

 

 

3. + 를 클릭하고 아래 사진처럼 Add Package Dependency 를 클릭해주세요

 

 

4. 왼쪽 상단에

https://github.com/1domybest/CameraManagerLibrary

 

를 검색하시고 엔터를 클릭하게되면 아래와 같은 라이브러리가 나옵니다!

 

 

 

5. AddPackage를 눌러 프로젝트에 라이브러리를 추가시켜주세요

 

6. 추가에 성공하셨다면 아래와같은 모습을 볼수있습니다!

5.  프로젝트 구조

5.  전체 코드

위에 구조순서대로 파일코드를 아래 접어두었으니

"더보기" 를 통해 확인해주세요!

 

ContentView - 앱이 실행될때 처음 호출될 View   

"아래 더보기 클릭"

더보기
//
//  ContentView.swift
//  Example
//
//  Created by 온석태 on 10/24/24.
//

import SwiftUI
import CameraManagerFrameWork

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            ZStack {
                VStack {
                    Spacer()
                    
                    NavigationLink(destination: LazyView(SingleCameraView())) {
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(.blue)
                            .frame(width: 200, height: 100)
                            .overlay(
                                Text("SingleCameraView")
                                    .bold()
                                    .foregroundColor(.white)
                            )
                    }
                    
                    Spacer().frame(height: 30)
                    
                    NavigationLink(destination: LazyView(MultiCameraView())) {
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(.red)
                            .frame(width: 200, height: 100)
                            .overlay(
                                Text("MultiCameraView")
                                    .bold()
                                    .foregroundColor(.white)
                            )
                    }
                    
                    Spacer()
                }
            }
            
        }
    }
}

LazyView - 간단하게 말씀드리면 렌더링이 되기전까지 객체를 미리 생성하지않게 하기위해제작된 View입니다.

일단 왜 그냥 View를 사용하지않고 굳이 Lazy한 View를 만들었는가????????

그이유는 바로 메모리와 관련되어있는데요

기본적으로 저희같은 모바일 개발자들은 메모리관리가 철저해야합니다.

그렇게 되지않으면 세션이 정상적으로 종료되지않기때문에 다시 카메라를 켰을때 정상적으로 동작하지않을수도있구요

가장중요한건 메모리 누수에대한 문제가 발생하면 어느정도 시간이지나 메모리가 꽉차게된다면 앱크래쉬가 발생되기때문에

항상 작업을하고 메모리가 잘 빠져나갔는지 확인하는 습관이 필요합니다.!

 

이러한 이유때문에 LazyView를 생성했고 이게 없는 상태에서는 카메라 뷰로 스택을 쌓기전에도 객체가 생성되면 해제되지않는 

문제점이 있기때문에 사용하였습니다.!!! 이것과 관련해서 UIKit과 SwiftUI를 적절히 섞어 만든 프로젝트도 있는데 

이것과 관련한 글은 다음에 쓰고 참조하도록 하겠습니다!!!

 

 

"아래 더보기 클릭"

더보기
//
//  LazyView.swift
//  Example
//
//  Created by 온석태 on 10/25/24.
//

import Foundation
import SwiftUI

// LazyView 구현
struct LazyView<Content: View>: View {
    let build: () -> Content
    
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    
    var body: Content {
        build()
    }
}

 

 

UIKitViewRepresentable - UIKit에서 만든 View를 SwiftUI에서 사용가능하도록 브릿지 역할을 해주는 파일입니다.

"아래 더보기 클릭"

더보기
//
//  UIKitViewRepresentable.swift
//  HypyG
//
//  Created by 온석태 on 8/30/24.
//

import Foundation
import SwiftUI
import UIKit

struct UIKitViewRepresentable: UIViewRepresentable {
    let view: UIView?
    var width: CGFloat?
    var height: CGFloat?
    
    func makeUIView(context: Context) -> UIView {
        let uiView = UIView()
        return view ?? uiView
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        guard let width = width , let height = height else {
            return
        }
        uiView.frame.size = CGSize(width: width, height: height)
    }
}

 

 

 

MultiCamera, SingleCamera - 이 두개의 폴더안에는 MVVM형식으로 라이브러리를 적절히 호출하고 함수들을 만든 파일들입니다!

 

 

멀티 카메라

MultiCameraView

"아래 더보기 클릭"

더보기
//
//  MultiCameraView.swift
//  Example
//
//  Created by 온석태 on 10/25/24.
//

import Foundation
import SwiftUI

import CameraManagerFrameWork

struct MultiCameraView: View {
    @ObservedObject var vm: MultiCameraViewModel = MultiCameraViewModel()
    
    var body: some View {
        ZStack {
            UIKitViewRepresentable(view: vm.cameraMananger?.multiCameraView)
                .frame(height: (UIScreen.main.bounds.width / 9)  * 16 )
                .overlay(
                    VStack {
                        Spacer().frame(height: 10)
                        HStack {
                            Spacer()
                            
                            Button(action: {
                                self.vm.toggleTorch()
                            }, label: {
                                Text("Torch \(self.vm.isTorchOn ? "OFF" : "ON")")
                                    .foregroundColor(.white)
                                    .bold()
                                    .padding(.horizontal, 10)
                                    .padding(.vertical, 5)
                                    .background(
                                        RoundedRectangle(cornerRadius: 10)
                                            .foregroundColor(self.vm.isTorchOn ? .black : .red)
                                    )
                            })
                        }
                        
                        Spacer()
                        
                        VStack {
                            HStack {
                                Text("UV Exposure Control")
                                    .foregroundColor(.white)
                                    .bold()
                                    .padding(.horizontal, 10)
                                    .padding(.vertical, 5)
                                    .background(
                                        RoundedRectangle(cornerRadius: 10)
                                            .foregroundColor(.orange)
                                    )
                                Spacer()
                            }
                            Spacer().frame(height: 10)
                            
                            Slider(value: $vm.brightness, in: -8...8, step: 0.1)
                                
                        }.frame(width: UIScreen.main.bounds.width - 30)
                    
                        
                        
                        Spacer().frame(height: 20)
                        
                        HStack {
                            VStack {
                                Button(action: {
                                    self.vm.toggleCamera(showThumbnail: true)
                                }, label: {
                                    Text("Camera \(self.vm.isCameraOn ? "OFF With Thumbnail" : "ON")")
                                        .foregroundColor(.white)
                                        .bold()
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .background(
                                            RoundedRectangle(cornerRadius: 10)
                                                .foregroundColor(self.vm.isCameraOn ? .black : .red)
                                        )
                                    
                                })
                                
                                Spacer().frame(height: 20)
                                
                                Button(action: {
                                    self.vm.toggleThumbnail()
                                }, label: {
                                    Text("Thumbnail \(self.vm.isShowThumnnail ? "OFF" : "Show")")
                                        .foregroundColor(.white)
                                        .bold()
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .background(
                                            RoundedRectangle(cornerRadius: 10)
                                                .foregroundColor(self.vm.isShowThumnnail ? .blue : .red)
                                        )
                                })
                                Spacer().frame(height: 10)
                            }
                            
                            Spacer()
                            
                            VStack {
                                
                                Button(action: {
                                    self.vm.toggleCameraScreenMode()
                                }, label: {
                                    Text("\(self.vm.currentScreenMode == .doubleScreen ? "SingleScreen" : "DoubleScreen")")
                                        .foregroundColor(self.vm.currentScreenMode == .doubleScreen ? .white : .black)
                                        .bold()
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .background(
                                            RoundedRectangle(cornerRadius: 10)
                                                .foregroundColor(self.vm.currentScreenMode == .doubleScreen ? .black : .white)
                                        )
                                })
                                
                                Spacer().frame(height: 10)
                                
                                Button(action: {
                                    self.vm.toggleMainCameraPosition()
                                }, label: {
                                    
                                    Text("MainCamera [\(self.vm.isFrontMainCamera ? "Front" : "Back")]")
                                        .foregroundColor(self.vm.isFrontMainCamera ? .white : .black)
                                        .bold()
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .background(
                                            RoundedRectangle(cornerRadius: 10)
                                                .foregroundColor(self.vm.isFrontMainCamera ? .black : .white)
                                        )
                                })
                            }
                          
                            
                            Spacer().frame(width: 10)
                        }
                        
                       
                    }
                )
                .onChange(of: vm.brightness) { value in
                    self.vm.changeExposure()
                }
        }
        .onDisappear {
            self.vm.unrference()
        }
    }
}

 

 

MultiCameraViewModel

"아래 더보기 클릭"

더보기
//
//  MultiCameraViewModel.swift
//  Example
//
//  Created by 온석태 on 10/25/24.
//

import Foundation
import CameraManagerFrameWork
import UIKit

class MultiCameraViewModel:ObservableObject {
    
    @Published var cameraMananger: CameraManager?
    @Published var isShowThumnnail: Bool = false
    @Published var isCameraOn: Bool = true
    @Published var brightness:Float = 0.0
    
    @Published var isFrontMainCamera:Bool = false
    @Published var isTorchOn:Bool = false
    
    @Published var currentScreenMode: CameraScreenMode = .doubleScreen
    
    init () {
        var cameraOption = CameraOptions()
        cameraOption.cameraSessionMode = .multiSession
        cameraOption.cameraScreenMode = .doubleScreen
        cameraOption.enAblePinchZoom = true
        cameraOption.cameraRenderingMode = .normal
        cameraOption.tapAutoFocusAndExposure = true
        cameraOption.showTapAutoFocusAndExposureRoundedRectangle = true
        cameraOption.startPostion = .back
        
        cameraOption.onChangeMainScreenPostion = { currentPosition in
            self.isFrontMainCamera = currentPosition == .front ? true : false
        }
        
        cameraOption.onChangeScreenMode = { currentScreenMode in
            guard let currentScreenMode = currentScreenMode else { return }
            self.currentScreenMode = currentScreenMode
        }
        
        self.cameraMananger = CameraManager(cameraOptions: cameraOption)
        self.cameraMananger?.initialize()
        self.cameraMananger?.setThumbnail(image: UIImage(named: "testThumbnail")!)
    }
    
    deinit {
        print("MultiCameraViewModel deinit")
    }
    
    func unrference () {
        self.cameraMananger?.unreference()
        self.cameraMananger = nil
    }
    
    func toggleCameraScreenMode () {
        
        if !self.isCameraOn { return }
        self.currentScreenMode = currentScreenMode == .doubleScreen ? .singleScreen : .doubleScreen
        self.cameraMananger?.setCameraScreenMode(cameraScreenMode: self.currentScreenMode)
    }
    
    func toggleTorch() {
        self.isTorchOn = self.isTorchOn ? false : true
        self.cameraMananger?.setTorch(onTorch: self.isTorchOn)
    }
    
    func toggleCamera (showThumbnail: Bool) {
        if isCameraOn {
            self.isCameraOn = false
            self.cameraMananger?.pauseCameraSession(showThumbnail: showThumbnail)
        } else {
            self.isCameraOn = true
            self.isShowThumnnail = false
            self.cameraMananger?.startCameraSession()
        }
        
    }
    
    func toggleThumbnail () {
        if isShowThumnnail {
            self.isShowThumnnail = false
            self.cameraMananger?.setShowThumbnail(isShow: false)
        } else {
            self.isShowThumnnail = true
            self.cameraMananger?.setShowThumbnail(isShow: true)
        }
    }
    
    func toggleMainCameraPosition() {
        if !self.isCameraOn { return }
        self.isFrontMainCamera = isFrontMainCamera ? false : true
        self.cameraMananger?.setMainCameraPostion(mainCameraPostion: self.isFrontMainCamera ? .front : .back)
        
        if self.isFrontMainCamera {
            self.isTorchOn = false
        }
    }
    
    func changeExposure() {
        if !self.isCameraOn { return }
        self.cameraMananger?.changeExposureBias(to: self.brightness)
    }
    
    
}

 

 

싱글 카메라

SingleCameraView

"아래 더보기 클릭"

더보기
//
//  SingleCamera.swift
//  Example
//
//  Created by 온석태 on 10/25/24.
//

import Foundation
import SwiftUI

import CameraManagerFrameWork

struct SingleCameraView: View {
    @ObservedObject var vm: SingleCameraViewModel = SingleCameraViewModel()
    
    var body: some View {
        ZStack {
            UIKitViewRepresentable(view: vm.cameraMananger?.singleCameraView)
                .frame(height: (UIScreen.main.bounds.width / 9)  * 16 )
                .overlay(
                    VStack {
                        Spacer().frame(height: 10)
                        HStack {
                            Spacer()
                            
                            Button(action: {
                                self.vm.toggleTorch()
                            }, label: {
                                Text("Torch \(self.vm.isTorchOn ? "OFF" : "ON")")
                                    .foregroundColor(.white)
                                    .bold()
                                    .padding(.horizontal, 10)
                                    .padding(.vertical, 5)
                                    .background(
                                        RoundedRectangle(cornerRadius: 10)
                                            .foregroundColor(self.vm.isTorchOn ? .black : .red)
                                    )
                            })
                        }
                        
                        Spacer()
                        
                        VStack {
                            HStack {
                                Text("UV Exposure Control")
                                    .foregroundColor(.white)
                                    .bold()
                                    .padding(.horizontal, 10)
                                    .padding(.vertical, 5)
                                    .background(
                                        RoundedRectangle(cornerRadius: 10)
                                            .foregroundColor(.orange)
                                    )
                                Spacer()
                            }
                            Spacer().frame(height: 10)
                            
                            Slider(value: $vm.brightness, in: -8...8, step: 0.1)
                                
                        }.frame(width: UIScreen.main.bounds.width - 30)
                    
                        
                        
                        Spacer().frame(height: 20)
                        
                        HStack {
                            VStack {
                                Button(action: {
                                    self.vm.toggleCamera(showThumbnail: true)
                                }, label: {
                                    Text("Camera \(self.vm.isCameraOn ? "OFF With Thumbnail" : "ON")")
                                        .foregroundColor(.white)
                                        .bold()
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .background(
                                            RoundedRectangle(cornerRadius: 10)
                                                .foregroundColor(self.vm.isCameraOn ? .black : .red)
                                        )
                                    
                                })
                                
                                Spacer().frame(height: 20)
                                
                                Button(action: {
                                    self.vm.toggleThumbnail()
                                }, label: {
                                    Text("Thumbnail \(self.vm.isShowThumnnail ? "OFF" : "Show")")
                                        .foregroundColor(.white)
                                        .bold()
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .background(
                                            RoundedRectangle(cornerRadius: 10)
                                                .foregroundColor(self.vm.isShowThumnnail ? .blue : .red)
                                        )
                                })
                                Spacer().frame(height: 10)
                            }
                            
                            Spacer()
                            
                            Button(action: {
                                self.vm.changePosition()
                            }, label: {
                                
                                Text("\(self.vm.isFront ? "Back" : "Front")")
                                    .foregroundColor(self.vm.isFront ? .white : .black)
                                    .bold()
                                    .padding(.horizontal, 10)
                                    .padding(.vertical, 5)
                                    .background(
                                        RoundedRectangle(cornerRadius: 10)
                                            .foregroundColor(self.vm.isFront ? .black : .white)
                                    )
                            })
                            
                            Spacer().frame(width: 10)
                        }
                        
                       
                    }
                )
                .onChange(of: vm.brightness) { value in
                    self.vm.changeExposure()
                }
        }
        .onDisappear {
            self.vm.unrference()
        }
    }
}

 

SingleCameraViewModel

"아래 더보기 클릭"

더보기
//
//  SingleCameraViewModel.swift
//  Example
//
//  Created by 온석태 on 10/25/24.
//

import Foundation
import CameraManagerFrameWork
import UIKit

class SingleCameraViewModel:ObservableObject {
    
    @Published var cameraMananger: CameraManager?
    @Published var isShowThumnnail: Bool = false
    @Published var isCameraOn: Bool = true
    @Published var brightness:Float = 0.0
    
    @Published var isFront:Bool = false
    @Published var isTorchOn:Bool = false
    
    init () {
        var cameraOption = CameraOptions()
        cameraOption.cameraSessionMode = .singleSession
        cameraOption.cameraScreenMode = .singleScreen
        cameraOption.enAblePinchZoom = true
        cameraOption.cameraRenderingMode = .normal
        cameraOption.tapAutoFocusAndExposure = true
        cameraOption.showTapAutoFocusAndExposureRoundedRectangle = true
        cameraOption.startPostion = .back
        
        self.cameraMananger = CameraManager(cameraOptions: cameraOption)
        self.cameraMananger?.initialize()
        self.cameraMananger?.setThumbnail(image: UIImage(named: "testThumbnail")!)
    }
    
    deinit {
        print("SingleCameraViewModel deinit")
    }
    
    func unrference () {
        self.cameraMananger?.unreference()
        self.cameraMananger = nil
    }
    
    func toggleTorch() {
        if isTorchOn {
            self.isTorchOn = false
        } else {
            if !self.isFront {
                self.isTorchOn = true
            }
        }
        
        self.cameraMananger?.setTorch(onTorch: self.isTorchOn)
    }
    
    func toggleCamera (showThumbnail: Bool) {
        if isCameraOn {
            self.isCameraOn = false
            self.cameraMananger?.pauseCameraSession(showThumbnail: showThumbnail)
        } else {
            self.isCameraOn = true
            self.isShowThumnnail = false
            self.cameraMananger?.startCameraSession()
        }
        
    }
    
    func toggleThumbnail () {
        if isShowThumnnail {
            self.isShowThumnnail = false
            self.cameraMananger?.setShowThumbnail(isShow: false)
        } else {
            self.isShowThumnnail = true
            self.cameraMananger?.setShowThumbnail(isShow: true)
        }
    }
    
    func changeExposure() {
        if !self.isCameraOn { return }
        self.cameraMananger?.changeExposureBias(to: self.brightness)
    }
    
    func changePosition() {
        if !self.isCameraOn { return }
        self.isFront = isFront ? false : true
        self.cameraMananger?.setPosition(self.isFront ? .front : .back)
        
        if self.isFront {
            self.isTorchOn = false
        }
    }
}

 

 

 

전체 코드와 예제코드는 아래 깃헙에 있에서 확인해주세요!

도큐멘트

https://1domybest.github.io/CameraManagerLibrary/documentation/cameramanagerframework/

 

Documentation

 

1domybest.github.io

 

전체코드 (즉시 실행및 테스트 가능)

https://github.com/1domybest/OTIS_CameraManager_Example

 

GitHub - 1domybest/OTIS_CameraManager_Example

Contribute to 1domybest/OTIS_CameraManager_Example development by creating an account on GitHub.

github.com

 

728x90
Comments