Yummypets / YPImagePicker

📸 Instagram-like image picker & filters for iOS
MIT License
4.31k stars 976 forks source link

Error while integrating in Swift UI #455

Closed TilakMaddy closed 3 years ago

TilakMaddy commented 4 years ago

Below is an attempt to integrate YPImagePicker into SwiftUI. So when I call YummyViewController inside a VStack in the main body of ContentView I get the error described below

struct YummyViewController: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<YummyViewController>) -> UIViewController {
        let controller = UIViewController()

        let config = YPImagePickerConfiguration()

        let picker = YPImagePicker(configuration: config)
        picker.didFinishPicking { [unowned picker] items, _ in
            if let photo = items.singlePhoto {
                print(photo.fromCamera) // Image source (camera or library)
                print(photo.image) // Final image selected by the user
                print(photo.originalImage) // original image selected by the user, unfiltered
                print(photo.modifiedImage ?? "not modified !") // Transformed image, can be nil
                print(photo.exifMeta ?? "no exif metadata") // Print exif meta data of original image."
            }
            picker.dismiss(animated: true, completion: nil)
        }
        controller.present(picker, animated: true, completion: nil)
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<YummyViewController>) {

    }

}

CONSOLE MESSAGE

2020-02-09 13:31:33.748928+0530 MambaLegacy[8305:840740] Warning: Attempt to present <YPImagePicker.YPImagePicker: 0x7fb4f5122a00> on <UIViewController: 0x7fb4f8949ec0> whose view is not in the window hierarchy! Picker deinited 👍

anoels commented 4 years ago

Here is the solution:

`import SwiftUI

struct ContentView : View {
    @State private var showYPImagePickerView = true

    var body: some View {
        VStack {
            Button(action: {
                self.showYPImagePickerView.toggle()
            }, label: { Text("Show Square").font(.title) })

            if showYPImagePickerView {
                YPImagePickerViewr()
            }
        }

    }
}

struct YPImagePickerView: UIViewControllerRepresentable
{
    func makeUIViewController(context: Context) -> YPImagePickerViewController {
        let vc =  YPImagePickerViewController()
        print("\nmakeUIViewController \(vc)")
        return vc
    }

    func updateUIViewController(_ uiViewController: YPImagePickerViewController, context: Context) {
        print("updateUIViewController \(uiViewController)")
    }

    static func dismantleUIViewController(_ uiViewController: YPImagePickerViewController, coordinator: Self.Coordinator) {
        print("dismantleUIViewController \(uiViewController)")
    }

}

class YPImagePickerViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.green
        print("viewDidLoad \(self)")
let picker = YPImagePicker()
picker.didFinishPicking { [unowned picker] items, _ in
    if let photo = items.singlePhoto {
        print(photo.fromCamera) // Image source (camera or library)
        print(photo.image) // Final image selected by the user
        print(photo.originalImage) // original image selected by the user, unfiltered
        print(photo.modifiedImage) // Transformed image, can be nil
        print(photo.exifMeta) // Print exif meta data of original image.
    }
    picker.dismiss(animated: true, completion: nil)
}
present(picker, animated: true, completion: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        print("viewWillDissapear \(self)")
    }

    deinit {
        print("DEINIT \(self)")
    }

}`
TilakMaddy commented 4 years ago

yeah but I figured out a much shorter solution. And I am not sure if what you are saying is correct , did you actually test it ?

anoels commented 4 years ago

Yes, I have tested. It works.

KaufmannTrofim commented 4 years ago

Yes, I have tested. It works.

Doesn't work for me. I am still getting the same warning in console. How I can fix it?

anoels commented 4 years ago

Please try the following one: "to call the struct (YummyView(image: Binding<Image?>#>)

`class YPImagePickerUIViewController: UIViewController {

var selectedItems = [YPMediaItem]()

let selectedImageV = UIImageView()
let pickButton = UIButton()
let resultsButton = UIButton()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground
    print("viewDidLoad \(self)")
   self.view.backgroundColor = .systemBackground

   selectedImageV.contentMode = .scaleAspectFit
   selectedImageV.frame = CGRect(x: 0,
                                 y: 0,
                                 width: UIScreen.main.bounds.width,
                                 height: UIScreen.main.bounds.height * 0.45)
   view.addSubview(selectedImageV)

   pickButton.setTitle("Pick", for: .normal)
   pickButton.setTitleColor(.label, for: .normal)
   pickButton.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
   pickButton.addTarget(self, action: #selector(showPicker), for: .touchUpInside)
   view.addSubview(pickButton)
   pickButton.center = view.center

   resultsButton.setTitle("Show selected", for: .normal)
   resultsButton.setTitleColor(.label, for: .normal)
   resultsButton.frame = CGRect(x: 0,
                                y: UIScreen.main.bounds.height - 100,
                                width: UIScreen.main.bounds.width,
                                height: 100)

    resultsButton.addTarget(self, action: #selector(showResults), for: .touchUpInside)
    view.addSubview(resultsButton)

}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    showPicker()
}

@objc func showResults() {
    if selectedItems.count > 0 {
        let gallery = YPSelectionsGalleryVC(items: selectedItems) { g, _ in
            g.dismiss(animated: true, completion: nil)
        }
        let navC = UINavigationController(rootViewController: gallery)
        self.present(navC, animated: true, completion: nil)
    } else {
        print("No items selected yet.")
    }
}

@objc func showPicker() {

        var config = YPImagePickerConfiguration()

        config.library.mediaType = .photoAndVideo

        config.shouldSaveNewPicturesToAlbum = false

        /* Choose the videoCompression. Defaults to AVAssetExportPresetHighestQuality */
        config.video.compression = AVAssetExportPresetMediumQuality

        config.startOnScreen = .library

        /* Defines which screens are shown at launch, and their order.
           Default value is `[.library, .photo]` */
        config.screens = [.library, .photo]

        config.video.libraryTimeLimit = 500.0

        /* Adds a Crop step in the photo taking process, after filters. Defaults to .none */
        config.showsCrop = .rectangle(ratio: (1/1))

        config.wordings.libraryTitle = "Galeri"

        /* Defines if the status bar should be hidden when showing the picker. Default is true */
        config.hidesStatusBar = false

        /* Defines if the bottom bar should be hidden when showing the picker. Default is false */
        config.hidesBottomBar = false

        config.maxCameraZoomFactor = 2.0

        config.library.maxNumberOfItems = 5
        config.gallery.hidesRemoveButton = false

        //config.library.options = options
        config.library.preselectedItems = selectedItems

        let picker = YPImagePicker(configuration: config)

        /* Change configuration directly */
        // YPImagePickerConfiguration.shared.wordings.libraryTitle = "Gallery2"

        /* Single Photo implementation. */
         picker.didFinishPicking { [unowned picker] items, _ in
             self.selectedItems = items
             self.selectedImageV.image = items.singlePhoto?.image
             picker.dismiss(animated: true, completion: nil)
         }

        present(picker, animated: true, completion: nil)
    }

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("viewWillDissapear \(self)")
}

deinit {
    print("DEINIT \(self)")
}

}

`

KaufmannTrofim commented 4 years ago

Thank you! It worked

bahampton commented 4 years ago

Please try the following one: "to call the struct (YummyView(image: Binding<Image?>#>)

`class YPImagePickerUIViewController: UIViewController {

var selectedItems = [YPMediaItem]()

let selectedImageV = UIImageView()
let pickButton = UIButton()
let resultsButton = UIButton()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground
    print("viewDidLoad \(self)")
   self.view.backgroundColor = .systemBackground

   selectedImageV.contentMode = .scaleAspectFit
   selectedImageV.frame = CGRect(x: 0,
                                 y: 0,
                                 width: UIScreen.main.bounds.width,
                                 height: UIScreen.main.bounds.height * 0.45)
   view.addSubview(selectedImageV)

   pickButton.setTitle("Pick", for: .normal)
   pickButton.setTitleColor(.label, for: .normal)
   pickButton.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
   pickButton.addTarget(self, action: #selector(showPicker), for: .touchUpInside)
   view.addSubview(pickButton)
   pickButton.center = view.center

   resultsButton.setTitle("Show selected", for: .normal)
   resultsButton.setTitleColor(.label, for: .normal)
   resultsButton.frame = CGRect(x: 0,
                                y: UIScreen.main.bounds.height - 100,
                                width: UIScreen.main.bounds.width,
                                height: 100)

    resultsButton.addTarget(self, action: #selector(showResults), for: .touchUpInside)
    view.addSubview(resultsButton)

}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    showPicker()
}

@objc func showResults() {
    if selectedItems.count > 0 {
        let gallery = YPSelectionsGalleryVC(items: selectedItems) { g, _ in
            g.dismiss(animated: true, completion: nil)
        }
        let navC = UINavigationController(rootViewController: gallery)
        self.present(navC, animated: true, completion: nil)
    } else {
        print("No items selected yet.")
    }
}

@objc func showPicker() {

        var config = YPImagePickerConfiguration()

        config.library.mediaType = .photoAndVideo

        config.shouldSaveNewPicturesToAlbum = false

        /* Choose the videoCompression. Defaults to AVAssetExportPresetHighestQuality */
        config.video.compression = AVAssetExportPresetMediumQuality

        config.startOnScreen = .library

        /* Defines which screens are shown at launch, and their order.
           Default value is `[.library, .photo]` */
        config.screens = [.library, .photo]

        config.video.libraryTimeLimit = 500.0

        /* Adds a Crop step in the photo taking process, after filters. Defaults to .none */
        config.showsCrop = .rectangle(ratio: (1/1))

        config.wordings.libraryTitle = "Galeri"

        /* Defines if the status bar should be hidden when showing the picker. Default is true */
        config.hidesStatusBar = false

        /* Defines if the bottom bar should be hidden when showing the picker. Default is false */
        config.hidesBottomBar = false

        config.maxCameraZoomFactor = 2.0

        config.library.maxNumberOfItems = 5
        config.gallery.hidesRemoveButton = false

        //config.library.options = options
        config.library.preselectedItems = selectedItems

        let picker = YPImagePicker(configuration: config)

        /* Change configuration directly */
        // YPImagePickerConfiguration.shared.wordings.libraryTitle = "Gallery2"

        /* Single Photo implementation. */
         picker.didFinishPicking { [unowned picker] items, _ in
             self.selectedItems = items
             self.selectedImageV.image = items.singlePhoto?.image
             picker.dismiss(animated: true, completion: nil)
         }

        present(picker, animated: true, completion: nil)
    }

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("viewWillDissapear \(self)")
}

deinit {
    print("DEINIT \(self)")
}

}

`

Without use of a coordinator how do you go about passing back data to the swiftUI View/Navigate to other swiftUI views on completion?

anoels commented 4 years ago

Please try the following one: "to call the struct (YummyView(image: Binding<Image?>#>) `class YPImagePickerUIViewController: UIViewController {

var selectedItems = [YPMediaItem]()

let selectedImageV = UIImageView()
let pickButton = UIButton()
let resultsButton = UIButton()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground
    print("viewDidLoad \(self)")
   self.view.backgroundColor = .systemBackground

   selectedImageV.contentMode = .scaleAspectFit
   selectedImageV.frame = CGRect(x: 0,
                                 y: 0,
                                 width: UIScreen.main.bounds.width,
                                 height: UIScreen.main.bounds.height * 0.45)
   view.addSubview(selectedImageV)

   pickButton.setTitle("Pick", for: .normal)
   pickButton.setTitleColor(.label, for: .normal)
   pickButton.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
   pickButton.addTarget(self, action: #selector(showPicker), for: .touchUpInside)
   view.addSubview(pickButton)
   pickButton.center = view.center

   resultsButton.setTitle("Show selected", for: .normal)
   resultsButton.setTitleColor(.label, for: .normal)
   resultsButton.frame = CGRect(x: 0,
                                y: UIScreen.main.bounds.height - 100,
                                width: UIScreen.main.bounds.width,
                                height: 100)

    resultsButton.addTarget(self, action: #selector(showResults), for: .touchUpInside)
    view.addSubview(resultsButton)

}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    showPicker()
}

@objc func showResults() {
    if selectedItems.count > 0 {
        let gallery = YPSelectionsGalleryVC(items: selectedItems) { g, _ in
            g.dismiss(animated: true, completion: nil)
        }
        let navC = UINavigationController(rootViewController: gallery)
        self.present(navC, animated: true, completion: nil)
    } else {
        print("No items selected yet.")
    }
}

@objc func showPicker() {

        var config = YPImagePickerConfiguration()

        config.library.mediaType = .photoAndVideo

        config.shouldSaveNewPicturesToAlbum = false

        /* Choose the videoCompression. Defaults to AVAssetExportPresetHighestQuality */
        config.video.compression = AVAssetExportPresetMediumQuality

        config.startOnScreen = .library

        /* Defines which screens are shown at launch, and their order.
           Default value is `[.library, .photo]` */
        config.screens = [.library, .photo]

        config.video.libraryTimeLimit = 500.0

        /* Adds a Crop step in the photo taking process, after filters. Defaults to .none */
        config.showsCrop = .rectangle(ratio: (1/1))

        config.wordings.libraryTitle = "Galeri"

        /* Defines if the status bar should be hidden when showing the picker. Default is true */
        config.hidesStatusBar = false

        /* Defines if the bottom bar should be hidden when showing the picker. Default is false */
        config.hidesBottomBar = false

        config.maxCameraZoomFactor = 2.0

        config.library.maxNumberOfItems = 5
        config.gallery.hidesRemoveButton = false

        //config.library.options = options
        config.library.preselectedItems = selectedItems

        let picker = YPImagePicker(configuration: config)

        /* Change configuration directly */
        // YPImagePickerConfiguration.shared.wordings.libraryTitle = "Gallery2"

        /* Single Photo implementation. */
         picker.didFinishPicking { [unowned picker] items, _ in
             self.selectedItems = items
             self.selectedImageV.image = items.singlePhoto?.image
             picker.dismiss(animated: true, completion: nil)
         }

        present(picker, animated: true, completion: nil)
    }

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("viewWillDissapear \(self)")
}

deinit {
    print("DEINIT \(self)")
}

} `

Without use of a coordinator how do you go about passing back data to the swiftUI View/Navigate to other swiftUI views on completion?

@Binding is one of SwiftUI’s less used property wrappers, it lets us declare that one value actually comes from elsewhere, and should be shared in both places.

TeeAche commented 4 years ago

Hello, Thank you all for the informations. I managed to get it working, but when I click on cancel button, the YPImagepicker always reappears as I suppose the Button toggle is still on true. How can I change the toggle state back in my contentView? Thanks a lot!

lachezartodorov commented 4 years ago

@TeeAche You have to remove the showPicker() from viewDidAppear I believe it is there for testing purposes.

Claeysson commented 4 years ago

This is my solution. Doesn't produce a blank VC in the background as in the other suggested answers.

struct MediaPicker: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> YPImagePicker {
        let config = YPImagePickerConfiguration()

        let picker = YPImagePicker(configuration: config)
        picker.didFinishPicking { [unowned picker] items, _ in
            if let photo = items.singlePhoto {
                print(photo.fromCamera) // Image source (camera or library)
                print(photo.image) // Final image selected by the user
                print(photo.originalImage) // original image selected by the user, unfiltered
                print(photo.modifiedImage ?? "not modified !") // Transformed image, can be nil
                print(photo.exifMeta ?? "no exif metadata") // Print exif meta data of original image."
            }
            picker.dismiss(animated: true, completion: nil)
        }

        return picker
    }

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

    typealias UIViewControllerType = YPImagePicker

}
aksonov commented 3 years ago

This is my solution. Doesn't produce a blank VC in the background as in the other suggested answers.

struct MediaPicker: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> YPImagePicker {
        let config = YPImagePickerConfiguration()

        let picker = YPImagePicker(configuration: config)
        picker.didFinishPicking { [unowned picker] items, _ in
            if let photo = items.singlePhoto {
                print(photo.fromCamera) // Image source (camera or library)
                print(photo.image) // Final image selected by the user
                print(photo.originalImage) // original image selected by the user, unfiltered
                print(photo.modifiedImage ?? "not modified !") // Transformed image, can be nil
                print(photo.exifMeta ?? "no exif metadata") // Print exif meta data of original image."
            }
            picker.dismiss(animated: true, completion: nil)
        }

        return picker
    }

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

    typealias UIViewControllerType = YPImagePicker

}

Good solution, thanks! Do you know how to show my custom 'next' screen after user taps "Next" for MediaPicker (like in Instagram, create new post screen is shown after image selected)?

ramluro1 commented 3 years ago

This is my solution. Doesn't produce a blank VC in the background as in the other suggested answers.

struct MediaPicker: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> YPImagePicker {
        let config = YPImagePickerConfiguration()

        let picker = YPImagePicker(configuration: config)
        picker.didFinishPicking { [unowned picker] items, _ in
            if let photo = items.singlePhoto {
                print(photo.fromCamera) // Image source (camera or library)
                print(photo.image) // Final image selected by the user
                print(photo.originalImage) // original image selected by the user, unfiltered
                print(photo.modifiedImage ?? "not modified !") // Transformed image, can be nil
                print(photo.exifMeta ?? "no exif metadata") // Print exif meta data of original image."
            }
            picker.dismiss(animated: true, completion: nil)
        }

        return picker
    }

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

    typealias UIViewControllerType = YPImagePicker

}

Good solution, thanks! Do you know how to show my custom 'next' screen after user taps "Next" for MediaPicker (like in Instagram, create new post screen is shown after image selected)?

Do you mind sharing the rest of your SwiftUI code. I am trying to figure out the following:

  1. How do I get the image back into SwiftUI
  2. How can I use SwiftUI to manage the "Select Image" button instead of having it in the view controller
  3. How can I use SwiftUI to display the selected image and not show it in the view controller

Thank you

kamrankhan07 commented 2 years ago

SwiftUI

struct SampleView: View {

    @State private var showMediaPicker: Bool = false
    @State private var image: UIImage?

    var body: some View {
        ZStack {
            Button {
                showMediaPicker = true
            } label: {
                Text("Select Picture")
            }
            .sheet(isPresented: $showMediaPicker) {
                MediaPicker(image: $image)

            }

            if let image = self.image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxHeight: 100)
            }
        }
    }   
}

MediaPicker.swift

import SwiftUI
import YPImagePicker

struct MediaPicker: UIViewControllerRepresentable {

    class Coordinator: NSObject, UINavigationControllerDelegate {
        let parent: MediaPicker

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

    typealias UIViewControllerType = YPImagePicker
    @Binding var image: UIImage?

    func makeUIViewController(context: Context) -> YPImagePicker {
        var config = YPImagePickerConfiguration()

        //Common
        config.shouldSaveNewPicturesToAlbum = true
        config.albumName = "Coding Challenge"
        config.showsPhotoFilters = false
        config.showsCrop = .none
        config.screens = [.library]
        config.startOnScreen = .library
        config.hidesStatusBar = true
        config.hidesBottomBar = true

        //library
        config.library.mediaType = .photo
        config.library.maxNumberOfItems = 1

        config.wordings.libraryTitle = "Gallery"
        config.library.skipSelectionsGallery = true
        config.targetImageSize = .cappedTo(size: 1080)

        let picker = YPImagePicker(configuration: config)
        picker.didFinishPicking { [unowned picker] items, _ in
            if let photo = items.singlePhoto {
                self.image = photo.image
            }
            picker.dismiss(animated: true, completion: nil)
        }
        picker.delegate = context.coordinator
        return picker
    }

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

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

}

References: https://www.hackingwithswift.com/books/ios-swiftui/using-coordinators-to-manage-swiftui-view-controllers https://stackoverflow.com/questions/70962237/use-ypimagepikcer-in-swiftui