[SwiftUI] Camera + PencilKit을 사용해서 Drawing 앱 만들기 (1)

·2024년 9월 29일

구유 미니 해커톤에서 직접 촬영한 이미지 위에 그림을 그리는 기능을 담당했다.

로직은 간단하게

사진 촬영 혹은 갤러리 이미지 가져오기 → 사진 위에 그림 그리기 → 사진 + 그림 각각 추출 및 병합 → 게시글 작성 및 업로드

로 진행될 예정이다.

  • 캔버스에 그린 그림을 image로 추출할 수 있다고 한다. 이때 배경으로 설정한 이미지와 드로잉 이미지를 병합하면 된다.

1. Drawing 기본 기능 구현

import SwiftUI
import PencilKit

struct CanvasView: UIViewRepresentable {
    
    @Binding var canvasView: PKCanvasView
    
    func makeUIView(context: Context) -> PKCanvasView {
        // 손가락 또는 애플펜슬을 통한 입력을 허용
        canvasView.drawingPolicy = .anyInput
        return canvasView
    }
    
    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        
    }
}

struct CanvasContentView: View {
    @State private var canvasView = PKCanvasView()
    @State private var toolPicker = PKToolPicker()
    
    
    var body: some View {
        VStack {
            CanvasView(canvasView: $canvasView)
                .onAppear {
                    if let window = UIApplication.shared.windows.first {
                        toolPicker.setVisible(true, forFirstResponder: canvasView)
                        toolPicker.addObserver(canvasView)
                        canvasView.becomeFirstResponder()
                    }
                }
                .background(Color.white)
        }
    }
    
}

#Preview {
    CanvasContentView()
}

Screenshot 2024-09-28 at 4.27.03 PM.png

진짜 너무 쉽게 그림판 만들 수 있어서 놀랐음 … 애플 체고

2. 카메라 연결

Screenshot 2024-09-28 at 7.23.33 PM.png

  1. info.plist에 카메라 접근 허용 추가하기 !!

  2. 카메라 객체 정의하기

    본격적으로 카메라를 사용하기 위한 기본적은 세팅 값을 정리해둔 곳이다.

    import Foundation
    import AVFoundation
    import UIKit
    
    @Observable
    class Camera: NSObject {
        var session = AVCaptureSession()
        var videoDeviceInput: AVCaptureDeviceInput!
        let output = AVCapturePhotoOutput()
        var selectedImage: UIImage?
        
        // 카메라 셋업 과정을 담당하는 함수, positio
        func setUpCamera() {
            if let device = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                    for: .video, position: .back) {
                do { // 카메라가 사용 가능하면 세션에 input과 output을 연결
                    videoDeviceInput = try AVCaptureDeviceInput(device: device)
                    if session.canAddInput(videoDeviceInput) {
                        session.addInput(videoDeviceInput)
                    }
                    
                    if session.canAddOutput(output) {
                        session.addOutput(output)
                        output.isHighResolutionCaptureEnabled = true
                        output.maxPhotoQualityPrioritization = .quality
                    }
                    session.startRunning() // 세션 시작
                } catch {
                    print(error) // 에러 프린트
                }
            }
        }
        
        func requestAndCheckPermissions() {
            // 카메라 권한 상태 확인
            switch AVCaptureDevice.authorizationStatus(for: .video) {
            case .notDetermined:
                // 권한 요청
                AVCaptureDevice.requestAccess(for: .video) { [weak self] authStatus in
                    if authStatus {
                        DispatchQueue.main.async {
                            self?.setUpCamera()
                        }
                    }
                }
            case .restricted:
                break
            case .authorized:
                // 이미 권한 받은 경우 셋업
                setUpCamera()
            default:
                // 거절했을 경우
                print("Permession declined")
            }
        }
        
        func capturePhoto() {
            // 사진 옵션 세팅
            let photoSettings = AVCapturePhotoSettings()
            
            self.output.capturePhoto(with: photoSettings, delegate: self)
            print("[Camera]: Photo's taken")
        }
    
          func savePhoto(_ imageData: Data) {
              guard let image = UIImage(data: imageData) else { return }
       
              selectedImage = image
              // 사진 저장하기
              print("[Camera]: Photo's saved")
          }
        }
    
    extension Camera: AVCapturePhotoCaptureDelegate {
      func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
      }
      
      func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
      }
      
      func photoOutput(_ output: AVCapturePhotoOutput, didCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
      }
      
      func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
          guard let imageData = photo.fileDataRepresentation() else { return }
          self.savePhoto(imageData)
          
          print("[CameraModel]: Capture routine's done")
      }
    }
    
  1. 카메라를 통해 보이는 화면을 관리하는 뷰도 만들어야 한다… 이때 UIKit를 사용해야 하기 때문에 UIViewRepresentable를 사용한다.

    import Foundation
    import AVFoundation
    import SwiftUI
    
    struct CameraPreviewView: UIViewRepresentable {
        class VideoPreviewView: UIView {
            override class var layerClass: AnyClass {
                 AVCaptureVideoPreviewLayer.self
            }
            
            var videoPreviewLayer: AVCaptureVideoPreviewLayer {
                return layer as! AVCaptureVideoPreviewLayer
            }
        }
        
        let session: AVCaptureSession
        func makeUIView(context: Context) -> VideoPreviewView {
            let view = VideoPreviewView()
            
            view.backgroundColor = .black
            view.videoPreviewLayer.videoGravity = .resizeAspectFill
            view.videoPreviewLayer.cornerRadius = 0
            view.videoPreviewLayer.session = session
            view.videoPreviewLayer.connection?.videoOrientation = .portrait
    
            return view
        }
        
        func updateUIView(_ uiView: VideoPreviewView, context: Context) {
            
        }
    }
    

3. 버튼 누를 경우 촬영하는 화면 만들기 + FullScreen

  1. 위에서 만든 카메라 객체를 뷰에 적용해준다!
    
    import SwiftUI
    import AVFoundation
    
    struct CameraView: View {
        
        @StateObject var uploadViewModel: UploadViewModel
        
        var body: some View {
            ZStack {
                uploadViewModel.cameraPreview.ignoresSafeArea()
                    .onAppear {
                        uploadViewModel.configure()
                    }
    
                
                VStack {
                    Spacer()
                    
                    cameraButton
                }
            }
        }
        
        @ViewBuilder
        var cameraButton: some View {
            if uploadViewModel.isTaken {
    
            } else {
                Button {
                    uploadViewModel.isTaken = true
                    uploadViewModel.model.capturePhoto()
                    uploadViewModel.send(action: .goToCanvas) // 카메라와 관련 X
                } label: {
                    Circle()
                        .frame(width: 60, height: 60)
                        .foregroundStyle(Color.basicWhite)
                        .overlay {
                            Circle()
                                .strokeBorder(Color.primaryOrange, lineWidth: 5)
                                .frame(width: 60, height: 60)
                                .foregroundStyle(Color.basicWhite)
                        }
                    
                }
                .padding(.bottom, 30)
            }
        }
    }
    
    struct CameraView_Previews: PreviewProvider {
        static let container: DIContainer = .stub
        
        static var previews: some View {
            CameraView(uploadViewModel: .init(container: container))
        }
    }
    
    내가 만든 카메라 화면 ... !

기본적인 캔버스 연습 + 일단 카메라 사용해서 화면에 카메라프리뷰까지 띄워봤다 ... (이건 실기기로 빌드해야 보인당)
다음은 촬영한 사진에 그림 얹어서 추출하는 기능을 정리해오겟다.

profile
SOOP

0개의 댓글