Using ImagePicker in SwiftUI - ios

I am new to SwiftUI. I want to make an app that has an image picker.
I found this article: https://ishtiz.com/swiftui/image-picker-in-swiftui
It says:
To implement an image picker in SwiftUI, you can use the ImagePicker struct provided by the SwiftUI framework. This struct has a pickImage() method that presents the image picker to the user and returns the selected image as a UIImage object.
Providing this example code:
struct ContentView: View {
#State private var image: UIImage?
var body: some View {
VStack {
if image != nil {
Image(uiImage: image!)
.resizable()
.scaledToFit()
}
Button("Select Image") {
self.image = ImagePicker.pickImage()
}
}
}
}
I added the code to my project but it doesn’t build:
Cannot find 'ImagePicker' in scope
Do I need to import something?

try import PhotosUI
For iOS Versions lower than 16.0
you can refers tutorial
For iOS 16.0 + you can use PhotoPicker
import PhotosUI
import SwiftUI
#available(iOS 16.0, *)
struct PhotosPickerDemo: View {
#State private var selectedItem: PhotosPickerItem? = nil
#State private var selectedImageData: Data? = nil
var body: some View {
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
Text("Select a photo")
}
.onChange(of: selectedItem) { newItem in
Task {
// Retrieve selected asset in the form of Data
if let data = try? await newItem?.loadTransferable(type: Data.self) {
selectedImageData = data
}
}
}
if let selectedImageData,
let uiImage = UIImage(data: selectedImageData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
}

Related

SwiftUI how to lazy load a stack and async changing their value

I want to use lazyStack to load my data and use DispatchQueue to update its value after a specific time.
But the view doesn't change and I don't know how to refresh the value in the view
import SwiftUI
struct CustomImages{
var image:Image
var id = 0
init(){
print("loading")
self.image = Image("UnknownAlbum")
self.id = 1
}
}
struct SwiftUIView: View {
var body: some View {
VStack{
ScrollView {
LazyVStack {
ForEach(0..<100){row in
var i = CustomImages()
HStack{
i.image
Text("\(i.id)")
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now()){
print("adding")
i.id += 2
}
}
}
}
}
}
}
}
}
Variables in Custom Images should be linked through #Binding.
In SwiftUI, a typical declaration cannot detect variation.
I've used the example code, and I think you can change it according to your purpose.
In the example code, the logic changes to the second image after 3 seconds.
import SwiftUI
struct ContentView: View {
#State private var image = Image("farnsworth")
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<30) { row in
let id = Binding<Int>(get: { row }, set: {_ in})
let customImages = CustomImages(image: $image, id: id)
HStack {
customImages.image
.resizable()
.aspectRatio(contentMode: .fit)
Text("\(customImages.id)")
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
image = Image("farnsworth2")
}
}
}
}
}
}
}
struct CustomImages{
#Binding var image: Image
#Binding var id: Int
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Adding multiple images into a view from photo library - SwiftUI

I want to add images from phone's photo library into a collage layout that I made. First I made the collage layout as a separate view in SwiftUI called CollageLayoutOne.
import SwiftUI
struct CollageLayoutOne: View {
var uiImageOne: UIImage
var uiImageTwo: UIImage
var uiImageThree: UIImage
var body: some View {
Rectangle()
.fill(Color.gray)
.aspectRatio(1.0, contentMode: .fit)
.overlay {
HStack {
Rectangle()
.fill(Color.gray)
.overlay {
Image(uiImage: uiImageOne)
.resizable()
.aspectRatio(contentMode: .fill)
}
.clipped()
VStack {
Rectangle()
.fill(Color.gray)
.overlay {
Image(uiImage: uiImageTwo)
.resizable()
.aspectRatio(contentMode: .fill)
}
.clipped()
Rectangle()
.fill(Color.gray)
.overlay {
Image(uiImage: uiImageThree)
.resizable()
.aspectRatio(contentMode: .fill)
}
.clipped()
}
}
.padding()
}
}
}
Then I have a separate view (PageView) where I want to show the CollageLayoutOne view and it also hosts the button to get to the image library.
struct PageView: View {
#State private var photoPickerIsPresented = false
#State var pickerResult: [UIImage] = []
var body: some View {
NavigationView {
ScrollView {
if pickerResult.isEmpty {
} else {
CollageLayoutOne(uiImageOne: pickerResult[0], uiImageTwo: pickerResult[1], uiImageThree: pickerResult[2])
}
}
.edgesIgnoringSafeArea(.bottom)
.navigationBarTitle("Select Photo", displayMode: .inline)
.navigationBarItems(trailing: selectPhotoButton)
.sheet(isPresented: $photoPickerIsPresented) {
PhotoPicker(pickerResult: $pickerResult,
isPresented: $photoPickerIsPresented)
}
}
}
#ViewBuilder
private var selectPhotoButton: some View {
Button(action: {
photoPickerIsPresented = true
}, label: {
Label("Select", systemImage: "photo")
})
}
}
My problem is that for some unknown reason the app crashes every time I select the photos and try to add them. If I do pickerResult[0] for all three it works just fine, but displays only the first selected photo on all 3 spots. Also if I start with all 3 as pickerResult[0] and then change them to [0], [1], [2] while the preview is running it doesn't crash and displays correctly.
I'm just starting with Swift and SwiftUI, so excuse me if it's some elementary mistake. Below I am also adding my code for PhotoPicker that I got from an article I found.
PhotoPicker.swift:
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
#Binding var pickerResult: [UIImage]
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> some UIViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = .images // filter only to images
if #available(iOS 15, *) {
configuration.selection = .ordered //number selection
}
configuration.selectionLimit = 3 // ignore limit
let photoPickerViewController = PHPickerViewController(configuration: configuration)
photoPickerViewController.delegate = context.coordinator
return photoPickerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
private let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.pickerResult.removeAll()
for image in results {
if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
image.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] newImage, error in
if let error = error {
print("Can't load image \(error.localizedDescription)")
} else if let image = newImage as? UIImage {
self?.parent.pickerResult.append(image)
}
}
} else {
print("Can't load asset")
}
}
parent.isPresented = false
}
}
}
image.itemProvider.loadObject is an asynchronous function, and it loads images one by one.
When the first image is processed, you add it to pickerResult and your pickerResult.isEmpty check becomes false, but your array contains only one item so far.
The safe thing to do here is to check the count:
if pickerResult.count == 3 {
CollageLayoutOne(uiImageOne: pickerResult[0], uiImageTwo: pickerResult[1], uiImageThree: pickerResult[2])
}
Also, in such cases, it's a good idea to wait until all asynchronous requests are complete before updating the UI, for example, like this:
var processedResults = [UIImage]()
var leftToLoad = results.count
let checkFinished = { [weak self] in
leftToLoad -= 1
if leftToLoad == 0 {
self?.parent.pickerResult = processedResults
self?.parent.isPresented = false
}
}
for image in results {
if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
image.itemProvider.loadObject(ofClass: UIImage.self) { newImage, error in
if let error = error {
print("Can't load image \(error.localizedDescription)")
} else if let image = newImage as? UIImage {
processedResults.append(image)
}
checkFinished()
}
} else {
print("Can't load asset")
checkFinished()
}
}

SwiftUI: Cannot Delete List Item If Just Viewed

I have a very odd issue. I have a list app that crashes when I delete a list item that I just viewed. I can delete an item that is different than the one I just viewed without the crash. The crash error is:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/XXX/Documents/Xcode Projects Playground/Test-Camera-CloudKit/Test-Camera-CloudKit/DetailView.swift, line 20
Line 20 of the DetailView.swift file is the line that displays the image/photo [Image(uiImage: UIImage(data: myItem.photo!) ?? UIImage(named: "gray_icon")!)]. Below are the files from my stripped down app to try to run this issue to ground. I am using CoreData and CloudKit.
ContentView.swift:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]) var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(myItem: item)) {
HStack {
Text(item.name ?? "Unknown name")
}
}
}.onDelete(perform: delete)
}
.navigationBarTitle("Items")
.navigationBarItems(trailing:
Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.moc)
}
}
}
func delete(at offsets: IndexSet) {
for index in offsets {
let item = items[index]
moc.delete(item)
}
do {
try moc.save()
} catch {
print("Error deleting objects")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddItemView.swift:
import SwiftUI
struct AddItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#State private var image : Data = .init(count: 0)
#State private var name = ""
#State private var show = false
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $name)
}
Section {
VStack {
Button(action: {self.show = true}) {
HStack {
Image(systemName: "camera")
}
}
Image(uiImage: UIImage(data: self.image) ?? UIImage(named: "gray_icon")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300, alignment: Alignment.center)
.clipped()
}
.sheet(isPresented: self.$show, content: {
ImagePicker(show: self.$show, image: self.$image)
})
}
Section {
Button("Save") {
let newItem = Item(context: self.moc)
newItem.name = self.name
newItem.photo = self.image
try? self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var myItem: Item
var body: some View {
VStack {
Text(myItem.name ?? "Unknown")
Image(uiImage: UIImage(data: myItem.photo!) ?? UIImage(named: "gray_icon")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300, alignment: Alignment.center)
.clipped()
}
.navigationBarTitle(Text(myItem.name ?? "Unknown"), displayMode: .inline)
}
}
struct DetailView_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let item = Item(context: moc)
item.name = "Test"
return NavigationView {
DetailView(myItem: item)
}
}
}
ImagePicker.swift
import SwiftUI
import Combine
struct ImagePicker : UIViewControllerRepresentable {
#Binding var show : Bool
#Binding var image : Data
func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(child1: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var child : ImagePicker
init(child1: ImagePicker) {
child = child1
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.child.show.toggle()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let image = info[.originalImage]as! UIImage
let data = image.jpegData(compressionQuality: 0.45)
self.child.image = data!
self.child.show.toggle()
}
}
}
I am really struggling with how does the app have an error in a view that is not being shown when an item is deleted. The list items does get deleted and removed from CloudKit. The delete operation works. The crash and error happens whether the coredata attribute for the photo has a photo or not. In other words, it has the error even when the item does have a photo and is not nil. Where am I going wrong? How do I get it to allow me to delete an item that I just viewed in the DetailView without a nil error? Any help is greatly appreciated. Thanks.

Core Data NSManagedObject - ObservedObjects not updating

I have a data entry view that lets the user add an image and see the preview after doing so.
The ImagePickerView I have returns a UIImage which I save to Core Data as type Data through the .pngData converter. However, after selecting the image, the view does not update to show it even though I am using #ObservedObject and objectWillChange
I can't use #State because the draft object is an NSManagedObject
import SwiftUI
import CoreData
struct AddItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment (\.presentationMode) var presentationMode
#State var showImagePicker: Bool = false
#ObservedObject var draft: Item //Core Data entity
var body: some View {
NavigationView {
VStack {
if (draft.image != nil) {
Image(uiImage: UIImage(data: draft.image!)!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.width - 32, height: UIScreen.main.bounds.height / 4)
.clipShape(RoundedRectangle(cornerRadius: 20))
} else {
Button(action: {
self.showImagePicker = true
}, label: {
RoundedRectangle(cornerRadius: 20)
})
.padding([.all], 20)
}
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(sourceType: .photoLibrary) { image in
draft.objectWillChange.send()
draft.image = image.pngData()
}
}
}
}
}
Try using objectWillChange.send() after you make your changes:
.sheet(isPresented: $showImagePicker) {
ImagePickerView(sourceType: .photoLibrary) { image in
draft.image = image.pngData() // #1
draft.objectWillChange.send() // #2
}
}

Firebase image is empty after switching views in SwiftUI

I'm trying to display images from a list of objects stored in Firebase. Initially the image loads fine, but if I switch to a different view and return to the list view the image never loads again.
Gif of the described bug
The image data seems to be saved as expected on both load attempts:
here
Below is my code for the image loader, which uses a url to fetch the images from Firebase Storage, and the list row that contains the image.
ImageLoader.swift
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
class ImageLoader: ObservableObject {
#Published var dataIsValid = false
var data:Data?
func loadImage(url: String) {
let imageRef = Storage.storage().reference(forURL: url)
imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
print(self.dataIsValid)
self.dataIsValid = true
self.data = data
}
}
}
func imageFromData() -> UIImage {
UIImage(data: self.data!)!
}
}
ListRow.swift
import SwiftUI
import Combine
struct EventRow: View {
#ObservedObject var imageLoader = ImageLoader()
var imageUrl: String
var body: some View {
HStack {
Image(uiImage: self.imageLoader.dataIsValid ? self.imageLoader.imageFromData() : UIImage())
.resizable()
.frame(width: 100.0, height: 140.0)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 5.0))
}
.onAppear {
self.imageLoader.loadImage(url: self.imageUrl)
}
}
}
The way I fixed this was by creating a custom ImageView and handling the image loading within this view. I figured this out by following this tutorial and realized that was the step I was missed. If anyone can explain why using the built-in SwiftUI Image() causes this issue I would really appreciate it.
ListRow.swift
import SwiftUI
struct ListRow: View {
var imageUrl: String
var body: some View {
HStack {
FBURLImage(url: imageUrl)
}
}
}
FBURLImage.swift
import SwiftUI
struct FBURLImage: View {
#ObservedObject var imageLoader: ImageLoader
init(url: String) {
imageLoader = ImageLoader()
imageLoader.loadImage(url: url)
}
var body: some View {
Image(uiImage:
imageLoader.data != nil ? UIImage(data: imageLoader.data!)! : UIImage())
.resizable()
.frame(width: 100.0, height: 140.0)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 5.0))
}
}
ImageLoader.swift
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
class ImageLoader: ObservableObject {
#Published var data: Data?
func loadImage(url: String) {
let imageRef = Storage.storage().reference(forURL: url)
imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
}
}

Resources