본문 바로가기
iOS & macOS

[macOS] 파일 선택 기능 - Drag & Drop, NSOpenPanel

by Bokoo14 2024. 1. 12.

macOS에서 파일 업로드하는 기능을 개발하였습니다. 

File 업로드 기능 구현 방식

1) Drag & Drop 방식

2) NSOpenPanel 방식


1. Drag & Drop 방식

공식 문서 

https://developer.apple.com/documentation/swiftui/drag-and-drop

 

Drag and drop | Apple Developer Documentation

Enable people to move or duplicate items by dragging them from one location to another.

developer.apple.com

onDrop 방식 예제 코드

struct ContentView: View {
    @State private var droppedImages: [UIImage] = []

    var body: some View {
        VStack {
            // 드롭 영역
            Rectangle()
                .frame(width: 200, height: 200)
                .foregroundColor(.gray)
                .onDrop(of: [.image]) { providers in
                    // 드롭된 항목 처리 클로저
                    for provider in providers {
                        // 이미지 항목 처리
                        if let item = provider.loadObject(ofClass: UIImage.self) {
                            if let image = item as? UIImage {
                                droppedImages.append(image)
                            }
                        }
                    }
                    return true // 처리 완료
                }
            
            // 처리된 이미지 목록 표시
            ScrollView {
                LazyVStack {
                    ForEach(droppedImages, id: \.self) { image in
                        Image(uiImage: image)
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 100, height: 100)
                    }
                }
            }
        }
    }
}

Rectangle영역에 이미지를 드래그 드롭하여 뷰에 보여주는 코드

 

DispatchGroup을 사용하여 파일 처리 순서를 보장한 onDrop 방식

요구 조건

1. pdf파일만 드래그 드롭

2. 파일 400MB용량 제한

.onDrop(of: ["public.file-url"], isTargeted: $isTargeted) { providers in
            let totalSizeLimit: Int64 = 400 * 1024 * 1024 // 400MB 제한 (1 MB = 1024 * 1024 bytes)
            var totalSize: Int64 = 0
            isFileTypePDF = true
            
            // DispatchGroup 생성
            let group = DispatchGroup()
            
            for provider in providers {
                if provider.canLoadObject(ofClass: URL.self) {
                    group.enter() // 그룹에 진입 표시
                    
                    provider.loadObject(ofClass: URL.self) { object, error in
                        defer { // 코드 블록을 빠져나갈 때 실행
                            group.leave() // 그룹에서 나감
                        }
                        
                        if let url = object {
                            if url.pathExtension == "pdf" { // PDF 파일인 경우, 배열에 추가
                                let pdfFileName = getPDFFileName(savePath: url.path)
                                DispatchQueue.main.async {
                                    // 코드 생략
                                }
                                
                                if let fileSize = fileSizeForURL(url) {
                                    totalSize += fileSize
                                } else {
                                    print("파일 크기를 확인할 수 없습니다.")
                                }
                            } else { // 다른 파일 형식이 드롭되었을 경우
                                print("지원하지 않는 파일 형식입니다.")
                                // 코드 생략
                                isFileTypePDF = false // 모든 파일이 pdf가 아님
                            }
                        }
                    } // END: provider.loadObject
                }
            }
            
            // 모든 파일 처리 완료 대기 (모든 파일을 처리 한 후 실행)
            // DispatchGroup에 속한 모든 작업이 완료된 후에 실행
            // .main: 주로 UI 업데이트 및 메인 스레드에서 실행해야 하는 작업에 사용
            group.notify(queue: .main) {
                // 파일 전체 크기 check
                if totalSize > totalSizeLimit {
                    print("선택한 파일의 크기가 400MB를 초과합니다 - totalSize: \(totalSize)")
                    isExceedMaxFileSize = true
                } else {
                    print("선택한 파일의 크기가 400MB를 초과하지 않습니다 - totalSize: \(totalSize)")
                    isExceedMaxFileSize =  false
                }
                // 코드 생략
            } // END: group.notify(queue: .main)
            return true
        } // END: onDrop - 파일 드래그 드롭 형식

DispatchGroup생성하지 않고  여러 개의 파일을 드래그 드롭하면?

파일 처리 순서가 보장되지 않음!

DispatchGroup을 사용하지 않을 경우 파일 처리 작업이 동시에 실행되거나 완료되는 순서에 따라 파일 용량을 확인하는 타이밍이 달라질 수 있음.

 

DispatchGroup을 사용하면?  각 파일 처리 작업이 그룹에 속하게 되고, 그룹 내의 모든 작업이 완료될 때까지 기다린 후에 파일 용량을 확인하고 조건에 따라 처리합니다. 이렇게 함으로써 파일 처리 작업의 순서와 상관없이 모든 파일이 정확히 처리되었는지를 확인할 수 있습니다.

DispatchGroup을 사용하지 않으면? 파일 처리 작업이 동시에 실행되거나 완료되는 순서에 따라 파일 용량을 확인하는 타이밍이 달라질 수 있습니다. 예를 들어, 파일 처리 작업이 모두 완료되기 전에 파일 용량을 확인한다면 아직 처리되지 않은 파일의 용량은 반영되지 않을 것입니다. 이로 인해 예상과 다른 결과가 나올 수 있습니다.

 

DispatchGroup은 비동기 작업을 그룹화하고, 모든 작업이 완료될 때까지 대기할 수 있는 기능을 제공합니다. 코드에서 DispatchGroup을 제거하면 파일 용량을 정확하게 계산할 수 없게 됩니다. DispatchGroup을 사용하는 이유는 파일 처리 작업이 비동기적으로 이루어지기 때문에, 모든 파일 처리가 완료된 후에 파일 용량을 확인하고 조건에 따라 처리하는 것입니다. 따라서 DispatchGroup을 삭제하면 파일 처리의 순서와 상관없이 파일 용량을 확인하게 되므로, 예상과 다른 결과가 나올 수 있습니다.

 

따라서 DispatchGroup을 유지하여 파일 처리 작업의 완료 여부를 체크하고, 모든 파일이 정확히 처리된 후에 파일 용량을 확인하는 것이 코드의 정확성을 보장하는 데 도움이 됩니다.

 

2. NSOpenPanel 방식

/// 파일 선택 버튼을 누르면 pdf파일만 선택할 수 있는 panel이 나옵니다
    private func showFilePicker() {
        let openPanel = NSOpenPanel()
        openPanel.allowedContentTypes = [.pdf]
        openPanel.allowsMultipleSelection = true // 다중 선택을 허용하도록 설정
        
        openPanel.begin { (response) in
            if response == .OK {
                let urls = openPanel.urls
                print("파일 선택 버튼 - \(urls)")
                
                let totalSizeLimit: Int64 = 400 * 1024 * 1024 // 400MB 제한 (1 MB = 1024 * 1024 bytes)
                var totalSize: Int64 = 0 // 선택된 모든 파일의 크기
                
                // 여러 개의 url을 돌며 모든 파일 크기의 합 계산
                for url in urls {
                    if let fileSize = fileSizeForURL(url) {
                        totalSize += fileSize
                    }
                }
                
                // 제한된 용량(400MB)을 넘지 않았다면
                // 코드 생략
            }
        } // openPanel
    } // END: func showFilePicker

 

 


DispatchGroup

DispatchGroup은 주로 비동기적인 작업을 관리하고 조정하기 위해 사용되는 기능입니다. DispatchGroup을 사용하여 여러 비동기 작업을 그룹화하고, 작업이 완료될 때까지 기다릴 수 있습니다.

DispatchGroup 주요 기능

  1. 그룹에 작업 진입 및 퇴장 표시: DispatchGroup을 사용하여 작업이 시작되면 group.enter()를 호출하여 그룹에 진입했음을 표시합니다. 작업이 완료되면 group.leave()를 호출하여 그룹에서 퇴장했음을 표시합니다. 그룹에 속한 작업이 완료될 때까지 대기하기 위해 group.wait() 메서드를 사용할 수도 있습니다.
  2. 그룹의 작업 완료 여부 확인: DispatchGroup은 작업이 완료되었을 때 통지를 받을 수 있는 group.notify 클로저를 제공합니다. 이를 사용하여 그룹에 속한 모든 작업이 완료되었을 때 추가적인 작업을 수행할 수 있습니다. group.notify는 지정된 DispatchQueue에서 비동기적으로 실행되므로 메인 큐에서 UI 업데이트와 같은 작업을 수행할 수 있습니다.

DispatchGroup 쓰레드 관리를 위한 기능은 아니지만, 비동기 작업의 진행 상황을 추적하고 조정하는 사용될 있습니다. 비동기적인 작업을 병렬로 실행하고, 모든 작업이 완료될 때까지 대기하거나 추가 작업을 수행하는 등의 기능을 제공하여 쓰레드 관리에 도움을 있습니다