🌐 KO

πŸ“· PhotosUI

⭐ Difficulty: ⭐⭐ ⏱️ Est. Time: 1-2h πŸ“‚ Graphics & Media

사진 선택을 μœ„ν•œ μ΅œμ‹  μ‹œμŠ€ν…œ μΈν„°νŽ˜μ΄μŠ€

iOS 14+ν”„λΌμ΄λ²„μ‹œ μš°μ„ 

✨ PhotosUI?

PhotosUI is a framework that provides a modern interface for selecting photos and videos from the user's library. It protects privacy by only accessing user-selected items without requiring full library access, and can be easily implemented via SwiftUI's PhotosPicker and UIKit's PHPickerViewController.

πŸ’‘ Key Features: Photo/Video Selection Β· Multi-Selection Β· Filtering Β· Live Photos Support Β· Privacy Protection Β· No Permission Required Β· SwiftUI/UIKit Integration

πŸ–ΌοΈ 1. PhotosPicker (SwiftUI)

Select photos using PhotosPicker in SwiftUI.

PhotosPickerView.swift β€” SwiftUI Photo Picker
import SwiftUI
import PhotosUI

struct PhotosPickerView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var selectedImage: Image?

    var body: some View {
        VStack(spacing: 20) {
            if let selectedImage {
                selectedImage
                    .resizable()
                    .scaledToFit()
                    .frame(width: 300, height: 300)
                    .cornerRadius(12)
            } else {
                Image(systemName: "photo.on.rectangle.angled")
                    .font(.system(size: 60))
                    .foregroundStyle(.secondary)
            }

            // 단일 사진 선택
            PhotosPicker("사진 선택", selection: $selectedItem, matching: .images)
                .buttonStyle(.borderedProminent)
                .onChange(of: selectedItem) {
                    Task {
                        if let data = try? await selectedItem?.loadTransferable(type: Data.self) {
                            if let uiImage = UIImage(data: data) {
                                selectedImage = Image(uiImage: uiImage)
                            }
                        }
                    }
                }
        }
        .padding()
    }
}

// 닀쀑 선택 예제
struct MultiplePhotosPickerView: View {
    @State private var selectedItems: [PhotosPickerItem] = []
    @State private var selectedImages: [UIImage] = []

    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
                    ForEach(selectedImages, id: \.self) { image in
                        Image(uiImage: image)
                            .resizable()
                            .scaledToFill()
                            .frame(width: 100, height: 100)
                            .clipped()
                            .cornerRadius(8)
                    }
                }
                .padding()
            }

            PhotosPicker(
                selection: $selectedItems,
                maxSelectionCount: 10,
                matching: .images
            ) {
                Label("사진 선택 (μ΅œλŒ€ 10개)", systemImage: "photo.on.rectangle")
            }
            .buttonStyle(.borderedProminent)
            .padding()
            .onChange(of: selectedItems) {
                Task {
                    selectedImages = []
                    for item in selectedItems {
                        if let data = try? await item.loadTransferable(type: Data.self),
                           let image = UIImage(data: data) {
                            selectedImages.append(image)
                        }
                    }
                }
            }
        }
    }
}

🎬 2. λΉ„λ””μ˜€ 및 Live Photos

λΉ„λ””μ˜€μ™€ Live Photosλ₯Ό μ„ νƒν•˜κ³  처리.

VideoPickerView.swift β€” Video/Live Photos
import SwiftUI
import PhotosUI
import AVKit

struct VideoPickerView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var videoURL: URL?
    @State private var player: AVPlayer?

    var body: some View {
        VStack {
            if let player {
                VideoPlayer(player: player)
                    .frame(height: 300)
                    .cornerRadius(12)
            } else {
                Image(systemName: "video.circle")
                    .font(.system(size: 60))
                    .foregroundStyle(.secondary)
            }

            // λΉ„λ””μ˜€λ§Œ 선택
            PhotosPicker("λΉ„λ””μ˜€ 선택", selection: $selectedItem, matching: .videos)
                .buttonStyle(.borderedProminent)
                .onChange(of: selectedItem) {
                    Task {
                        if let movie = try? await selectedItem?.loadTransferable(type: Movie.self) {
                            player = AVPlayer(url: movie.url)
                        }
                    }
                }
        }
        .padding()
    }
}

// Movie νƒ€μž… μ •μ˜
struct Movie: Transferable {
    let url: URL

    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(contentType: .movie) { movie in
            SentTransferredFile(movie.url)
        } importing: { received in
            let copy = URL.documentsDirectory.appending(path: "movie.mov")
            if FileManager.default.fileExists(atPath: copy.path()) {
                try FileManager.default.removeItem(at: copy)
            }
            try FileManager.default.copyItem(at: received.file, to: copy)
            return Self.init(url: copy)
        }
    }
}

// Live Photo 선택
struct LivePhotoPickerView: View {
    @State private var selectedItem: PhotosPickerItem?

    var body: some View {
        VStack {
            // Live Photos 포함 선택
            PhotosPicker(
                "사진 선택 (Live Photos 포함)",
                selection: $selectedItem,
                matching: .any(of: [.images, .livePhotos])
            )
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

πŸ› οΈ 3. PHPickerViewController (UIKit)

Use PHPickerViewController in UIKit.

PHPickerView.swift β€” UIKit Photo Picker
import SwiftUI
import PhotosUI

struct PHPickerView: UIViewControllerRepresentable {
    @Binding var selectedImages: [UIImage]
    let selectionLimit: Int
    let filter: PHPickerFilter

    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration()
        configuration.selectionLimit = selectionLimit
        configuration.filter = filter

        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        let parent: PHPickerView

        init(_ parent: PHPickerView) {
            self.parent = parent
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)

            var images: [UIImage] = []

            let group = DispatchGroup()

            for result in results {
                group.enter()

                result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
                    defer { group.leave() }

                    if let image = object as? UIImage {
                        images.append(image)
                    }
                }
            }

            group.notify(queue: .main) {
                self.parent.selectedImages = images
            }
        }
    }
}

// μ‚¬μš© 예제
struct PHPickerExampleView: View {
    @State private var selectedImages: [UIImage] = []
    @State private var showPicker = false

    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
                    ForEach(selectedImages, id: \.self) { image in
                        Image(uiImage: image)
                            .resizable()
                            .scaledToFill()
                            .frame(width: 100, height: 100)
                            .clipped()
                    }
                }
            }

            Button("사진 선택") {
                showPicker = true
            }
            .buttonStyle(.borderedProminent)
        }
        .sheet(isPresented: $showPicker) {
            PHPickerView(
                selectedImages: $selectedImages,
                selectionLimit: 5,
                filter: .images
            )
        }
    }
}

πŸ” 4. 필터링 및 μ„€μ •

νŠΉμ • λ―Έλ””μ–΄ νƒ€μž…λ§Œ μ„ νƒν•˜λ„λ‘ 필터링.

FilteredPickerView.swift β€” Filtering
import SwiftUI
import PhotosUI

struct FilteredPickerView: View {
    @State private var selectedItems: [PhotosPickerItem] = []
    @State private var filterType: PHPickerFilter = .images

    var body: some View {
        VStack(spacing: 20) {
            Text("ν•„ν„° 선택")
                .font(.headline)

            // μ΄λ―Έμ§€λ§Œ
            PhotosPicker(
                "μ΄λ―Έμ§€λ§Œ 선택",
                selection: $selectedItems,
                matching: .images
            )
            .buttonStyle(.bordered)

            // λΉ„λ””μ˜€λ§Œ
            PhotosPicker(
                "λΉ„λ””μ˜€λ§Œ 선택",
                selection: $selectedItems,
                matching: .videos
            )
            .buttonStyle(.bordered)

            // Live Photos만
            PhotosPicker(
                "Live Photos만 선택",
                selection: $selectedItems,
                matching: .livePhotos
            )
            .buttonStyle(.bordered)

            // νŒŒλ…ΈλΌλ§ˆ μ‚¬μ§„λ§Œ
            PhotosPicker(
                "νŒŒλ…ΈλΌλ§ˆ μ‚¬μ§„λ§Œ",
                selection: $selectedItems,
                matching: .panoramas
            )
            .buttonStyle(.bordered)

            // μŠ€ν¬λ¦°μƒ·λ§Œ
            PhotosPicker(
                "μŠ€ν¬λ¦°μƒ·λ§Œ",
                selection: $selectedItems,
                matching: .screenshots
            )
            .buttonStyle(.bordered)

            // 슬둜λͺ¨μ…˜ λΉ„λ””μ˜€λ§Œ
            PhotosPicker(
                "슬둜λͺ¨μ…˜ λΉ„λ””μ˜€λ§Œ",
                selection: $selectedItems,
                matching: .slomoVideos
            )
            .buttonStyle(.bordered)

            // μ—¬λŸ¬ ν•„ν„° μ‘°ν•©
            PhotosPicker(
                "이미지 λ˜λŠ” λΉ„λ””μ˜€",
                selection: $selectedItems,
                matching: .any(of: [.images, .videos])
            )
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

// κ³ κΈ‰ 필터링
struct AdvancedFilterView: View {
    @State private var selectedImages: [UIImage] = []
    @State private var showPicker = false

    var body: some View {
        VStack {
            Button("PNG μ΄λ―Έμ§€λ§Œ 선택") {
                showPicker = true
            }
            .buttonStyle(.borderedProminent)
        }
        .sheet(isPresented: $showPicker) {
            PHPickerView(
                selectedImages: $selectedImages,
                selectionLimit: 10,
                filter: .any(of: [.images])
            )
        }
    }
}

πŸ“Έ 5. Complete Example

PhotoGalleryView.swift β€” Photo Gallery App
import SwiftUI
import PhotosUI

struct PhotoGalleryView: View {
    @State private var selectedItems: [PhotosPickerItem] = []
    @State private var photos: [PhotoItem] = []
    @State private var isLoading = false

    var body: some View {
        NavigationStack {
            ScrollView {
                if photos.isEmpty {
                    ContentUnavailableView(
                        "사진이 μ—†μŠ΅λ‹ˆλ‹€",
                        systemImage: "photo.on.rectangle.angled",
                        description: Text("μ•„λž˜ λ²„νŠΌμ„ 눌러 사진을 μΆ”κ°€ν•˜μ„Έμš”")
                    )
                    .frame(height: 400)
                } else {
                    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 8) {
                        ForEach(photos) { photo in
                            ZStack(alignment: .topTrailing) {
                                Image(uiImage: photo.image)
                                    .resizable()
                                    .scaledToFill()
                                    .frame(width: 100, height: 100)
                                    .clipped()
                                    .cornerRadius(8)

                                Button {
                                    photos.removeAll { $0.id == photo.id }
                                } label: {
                                    Image(systemName: "xmark.circle.fill")
                                        .foregroundStyle(.white)
                                        .background(Circle().fill(Color.black.opacity(0.5)))
                                }
                                .padding(4)
                            }
                        }
                    }
                    .padding()
                }

                if isLoading {
                    ProgressView("사진 λ‘œλ”© 쀑...")
                        .padding()
                }
            }
            .navigationTitle("사진 가러리")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    PhotosPicker(
                        selection: $selectedItems,
                        maxSelectionCount: 20,
                        matching: .images
                    ) {
                        Label("μΆ”κ°€", systemImage: "plus")
                    }
                }

                ToolbarItem(placement: .bottomBar) {
                    Text("\(photos.count)개 사진")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            .onChange(of: selectedItems) {
                Task {
                    isLoading = true
                    defer { isLoading = false }

                    for item in selectedItems {
                        if let data = try? await item.loadTransferable(type: Data.self),
                           let image = UIImage(data: data) {
                            photos.append(PhotoItem(image: image))
                        }
                    }
                    selectedItems = []
                }
            }
        }
    }
}

struct PhotoItem: Identifiable {
    let id = UUID()
    let image: UIImage
}

πŸ’‘ HIG Guidelines

🎯 Practical Usage

πŸ“š Learn More

⚑️ Performance Tips: maxSelectionCount to prevent excessive selection, and load large images on a background thread. PhotosPicker handles permission management automatically, so no separate permission request is needed.

πŸ“Ž Apple Official Resources

πŸ“˜ Documentation πŸ’» Sample Code 🎬 WWDC Sessions