How to call PHPhotoLibrary presentLimitedLibraryPicker from SwiftUI?

4.8k views Asked by At

What would be the correct way of calling PHPhotoLibrary's presentLimitedLibraryPicker method from SwiftUI?

The method requires a UIViewController, which I don't have in SwiftUI.

I have tried to use UIViewControllerRepresentable in order to create a UIViewController, and it works, but the result is that two View Controllers are presented, the one I create with UIViewControllerRepresentable, and the Limited Library Picker.

Both View Controllers need to be dismissed in order to get to the original screen, which is not desirable.

To sum up the issue I see:

  1. presentLimitedLibraryPicker works by passing a ViewController.
  2. This makes me create and present a dummy ViewController, just so I can call the method.
  3. There's no way to obtain a reference to the Limited Library Picker, also it doesn't offer a delegate. So I can't detect when the Limited Library Picker controller is dismissed.

This is my attempt (It shows the picker, but when you dismiss it, you need to dismiss the extra, dummy, view controller too:

import Foundation
import SwiftUI
import Photos
import PhotosUI

struct TestView: View {

   @State var showLibraryPicker = false

var body: some View {
    NavigationView {
        VStack {
            Button("Open Library Picker") { showLibraryPicker = true }
        }
        .navigationBarTitle("Test", displayMode: .inline)
        .navigationViewStyle(StackNavigationViewStyle())
        .sheet(isPresented: $showLibraryPicker, onDismiss: { print("Dismissed") }) {
            TestLimitedLibraryPicker()
            }
        }
    }
}


struct TestLimitedLibraryPicker: UIViewControllerRepresentable {

     func makeUIViewController(context: Context) -> UIViewController {
         let controller = UIViewController()
         PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller)
          return controller
     }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {

     }
}
4

There are 4 answers

2
Asperi On BEST ANSWER

Here is some demo approach. Prepared & tested with Xcode 12 / iOS 14.

struct TestPhotosView: View {

    @State var showLibraryPicker = false

    var body: some View {
        NavigationView {
            VStack {
                Button("Open Library Picker") { self.showLibraryPicker = true }
            }
            .navigationBarTitle("Test", displayMode: .inline)
            .navigationViewStyle(StackNavigationViewStyle())
            .background(Group {
                if self.showLibraryPicker {
                    TestLimitedLibraryPicker(isPresented: $showLibraryPicker)
                }
            })
        }
    }
}


struct TestLimitedLibraryPicker: UIViewControllerRepresentable {
    @Binding var isPresented: Bool

    func makeUIViewController(context: Context) -> UIViewController {
        let controller = UIViewController()

        DispatchQueue.main.async {
            PHPhotoLibrary.requestAuthorization() { result in
                PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller)
                context.coordinator.trackCompletion(in: controller)
            }
        }
        
        return controller
    }

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

    func makeCoordinator() -> Coordinator {
        Coordinator(isPresented: $isPresented)
    }
    class Coordinator: NSObject {
        private var isPresented: Binding<Bool>
        init(isPresented: Binding<Bool>) {
            self.isPresented = isPresented
        }

        func trackCompletion(in controller: UIViewController) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self, weak controller] in
                if controller?.presentedViewController == nil {
                    self?.isPresented.wrappedValue = false
                } else if let controller = controller {
                    self?.trackCompletion(in: controller)
                }
            }
        }
    }
}
1
mbxDev On

Just encountered this issue! Here's my take.

  • There is no SwiftUI way to invoke the limited photo picker. So we must create a View Controller (UIViewControllerRepresentable) which calls .presentLimitedLibraryPicker. But if you present this as a sheet, it will immediately show the picker, and you'll get a sheet on a sheet.
  • Instead, embed the wrapped View Controller directly in the View's body and pass it a binding to the trigger conditional.
  • In the wrapped View Controller, look for a change to the conditional in updateUIViewController. If the value is true, present the picker, then immediately toggle the value off.
import SwiftUI
import UIKit
import PhotosUI

struct ContainingView: View {
    @State var showLimitedPicker: Bool = false

    var body: some View {
        HStack {
            Button("Load moreā€¦") { showLimitedPicker = true }
            LimitedPicker(isPresented: $showLimitedPicker)
                .frame(width: 0, height: 0)
        }
    }
}

struct LimitedPicker: UIViewControllerRepresentable {
    @Binding var isPresented: Bool

    func makeUIViewController(context: Context) -> UIViewController {
        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        if isPresented {
            PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: uiViewController)
            DispatchQueue.main.async {
                isPresented = false
            }
        }
    }
}

0
mdonati On

To keep adding to the code, my take on this adds a view extension to make it work similar to other presentation components like sheets, fullscreen covers, etc. It honors the binding by dismiss the image picker when it is set to false from outside the representable.

import PhotosUI
import SwiftUI
import UIKit

extension View {
  func limitedPhotosLibrary(isPresented: Binding<Bool>) -> some View {
    background(
      LimitedPhotosLibraryPicker(isPresented: isPresented)
    )
  }
}

private struct LimitedPhotosLibraryPicker: UIViewControllerRepresentable {
  @Binding var isPresented: Bool

  func makeUIViewController(context: Context) -> some UIViewController {
    UIViewController()
  }

  func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
    if isPresented {
      if !uiViewController.isPresentingImagePicker {
        PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: uiViewController) { _ in
          isPresented = false
        }
      }
    } else {
      if uiViewController.isPresentingImagePicker, uiViewController.presentedViewController?.isBeingDismissed == false {
        uiViewController.dismiss(animated: true)
      }
    }
  }
}

private extension UIViewController {
  var isPresentingImagePicker: Bool {
    presentedViewController?.isKind(of: UIImagePickerController.self) == true
  }
}
0
Aleksey Shevchenko On

Indeed no need to have coordinator here. Just use completion of presentLimitedLibraryPicker to set isPresented to false.

import Photos
import PhotosUI
import SwiftUI

struct LimitedLibraryPickerWrapper: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    
    func makeUIViewController(context: Context) -> UIViewController {
        let controller: LoaderViewController = .init()
        DispatchQueue.main.async {
            PHPhotoLibrary.requestAuthorization() { result in
                PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller) { _ in
                    withAnimation {
                        isPresented = false
                    }
                }
            }
        }
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}