Upload SwiftUI Image to Firebase with Data - swiftui

I'm creating simple CRUD operations in an app and I came across a hairy quirk, given SwiftUI's Image design.
I'm trying to upload an image to Firebase, except I need to convert an Image to a UIImage, and further into Data.
Here's my code:
Image Uploader
import UIKit
import Firebase
import FirebaseStorage
import SwiftUI
struct ImageUploader {
static func uploadImage(with image: Data, completion: #escaping(String) -> Void) {
let imageData = image
let fileName = UUID().uuidString
let storageRef = Storage.storage().reference(withPath: "/profile_images/\(fileName)")
storageRef.putData(imageData, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading image to Firebase: \(error.localizedDescription)")
return
}
print("Image successfully uploaded to Firebase!")
}
storageRef.downloadURL { url, error in
guard let imageUrl = url?.absoluteString else { return }
completion(imageUrl)
}
}
}
View Model
import SwiftUI
import Firebase
func uploadProfileImage(with image: Data) {
guard let uid = tempCurrentUser?.uid else { return }
let imageData = image
ImageUploader.uploadImage(with: imageData) { imageUrl in
Firestore.firestore().collection("users").document(uid).updateData(["profileImageUrl":imageUrl]) { _ in
print("Successfully updated user data")
}
}
}
ImagePicker
import SwiftUI
import PhotosUI
#MainActor
class ImagePicker: ObservableObject {
#Published var image: Image?
#Published var imageSelection: PhotosPickerItem? {
didSet {
if let imageSelection {
Task {
try await loadTransferrable(from: imageSelection)
}
}
}
}
func loadTransferrable(from imageSelection: PhotosPickerItem?) async throws {
do {
if let data = try await imageSelection?.loadTransferable(type: Data.self) {
if let uiimage = UIImage(data: data) {
self.image = Image(uiImage: uiimage)
}
}
} catch {
print("Error: \(error.localizedDescription)")
}
}
}
View
if let image = imagePicker.image {
Button {
viewModel.uploadProfileImage(with: image)
} label: {
Text("Continue")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340.0, height: 50.0)
.background(.blue)
.clipShape(Capsule())
.padding()
}
.shadow(radius: 10.0)
.padding(24.0)
}
As you can see, I have a problem in the view: Cannot convert value of type 'Image' to expected argument type 'Data', which makes sense. The images are all of type Data.
How can I do it?

don't use #Published var image: Image? in your class ImagePicker: ObservableObject, Image is a View and is for use in other Views.
Use #Published var image: UIImage? and adjust your code accordingly.
Then use image.pngData() to get the Data to upload.

Related

nested loadObject call issue

I write some code to show an image dragged from other apps(such as web browser, photos, etc). I make a delegate to perform the drop.
If DropInfo has an image item, I will try to retrieve the data as uiimage by NSItemProvider.loadObject first. If errors occur during loading, I will ask DropInfo again whether it has a url item. If the answer is YES, I will try to retrieve URL by NSItemProvider.loadObject.
It means the second loadObject will be nested in the first one. When running the simulator, I find the second loadObject completion handler is never called which is supposed to be called when I try to retrieve URL. Do I miss anything?
import SwiftUI
import Combine
struct ContentView: View{
#State private var img: UIImage?
var body: some View{
Image(uiImage: img != nil ? img! : UIImage(systemName: "photo")!)
.resizable()
.scaledToFit()
.onDrop(of: [.image], delegate: ImageDropController(img: $img))
}
}
class ImageDropController: DropDelegate{
#Binding var img: UIImage?
init(img: Binding<UIImage?>) {
_img = img
}
func performDrop(info: DropInfo) -> Bool {
if getImgFromImage(info: info){
return true
}else{
return false
}
}
func getImgFromImage(info: DropInfo) -> Bool{
guard info.hasItemsConforming(to: [.image]) else {
return getImgFromUrl(info: info)
}
createImgFromImage(from: info.itemProviders(for: [.image]).first!,info: info)
return true
}
func createImgFromImage(from provider: NSItemProvider, info: DropInfo){
provider.loadObject(ofClass: UIImage.self) { image, error in
var unwrappedImage: UIImage?
if let error = error {
print("unwrapImage failed: ", error.localizedDescription)
_ = self.getImgFromUrl(info: info)
} else {
unwrappedImage = image as? UIImage
}
if let image = unwrappedImage{
DispatchQueue.main.async {
self.img = image
}
}
}
}
func getImgFromUrl(info: DropInfo) -> Bool{
guard info.hasItemsConforming(to: [.url]) else {
return false
}
createImgFromUrl(from: info.itemProviders(for: [.url]).first!)
return true
}
private func createImgFromUrl(from provider: NSItemProvider){
var fetchUrl: URL?
print("create from url")
_ = provider.loadObject(ofClass: URL.self) { url, error in
print("nested handler") <<------- never be called
if let error = error {
print("unwrapUrl failed: ", error.localizedDescription)
} else {
fetchUrl = url
print("url", fetchUrl?.description)
}
if let url = fetchUrl{
// Do some data fetch work using url
}
}
}
}

how to fetch data from array by indexing in SwiftUI?

This is my Json file and i don't understand how to fetch data and set
the Image in our SwiftUI code. please help me resolve this problem.
And this is My Model, is this model correct?
This is My API and wants to fetch only value images array
import Foundation
public struct BannerImages {
public let images: [String]
public init(images: [String]) {
self.images = images
}
}
try this approach to fetch your images and display them in a View:
import Foundation
import SwiftUI
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack {
Text("Fetching the data...")
List (vm.images, id: \.self) { url in
AsyncImage(url: URL(string: url)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 111, height: 111)
} placeholder: {
ProgressView()
}
}
}
.task {
await vm.getData()
}
}
}
class ViewModel: ObservableObject {
#Published var images = [String]()
func getData() async {
guard let url = URL(string: "apiurl") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
Task{#MainActor in
let results = try JSONDecoder().decode(APIResponse.self, from: data)
self.images = results.images
}
} catch {
print("---> error: \(error)")
}
}
}
struct APIResponse: Codable {
let images: [String]
}
1. First you need to have a variable with data type of Data like this: var imageData: Data?
2. Then you have to fetch the image data from the link in the array like this:
func getImageData() {
// Check image url isnt nill
guard imageUrl(your image url) != nil else {
return
}
// Download the data for the image
let url = URL(string: imageUrl(your image url)!)
if let url = url {
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { data, response, error in
if error == nil {
DispatchQueue.main.async {
self.imageData = data
}
}
}
dataTask.resume()
}
}
3. Once this is done go to the view file where you want to display the image and create
let uiImage = UIImage(data: put the var in which you stored the image data in previous step ?? Data())
Image(uiImage: uiImage ?? UIImage())

AsyncImage not rendering all images in a List and producing error Code =-999 "cancelled"

I'm having trouble getting all images to show in a List using AsyncImage. When you first run the app, it seems fine but if you start scrolling, some Images aren't shown. Either the ProgressView doesn't update until the row is scrolled outside of the screen and back into view or I get an error Code=-999 "cancelled".
I can get the all the images to show and ProgressView to update correctly using a regular Image View and going through the whole setup process of downloading and showing an image. It's only when I try to use AsyncImage that all the images aren't shown. How can I get all AsyncImages to show in a List?
struct ContentView: View {
#StateObject private var viewModel = ListViewModel()
var body: some View {
List {
ForEach(viewModel.images) { photo in
HStack {
AsyncImage(url: URL(string: photo.thumbnailUrl)) { phase in
switch phase {
case .success(let image):
image
case .failure(let error):
let _ = print(error)
Text("error: \(error.localizedDescription)")
case .empty:
ProgressView()
#unknown default:
fatalError()
}
}
VStack(alignment: .leading) {
Text(photo.title)
Text(photo.thumbnailUrl)
}
}
}
}
.task {
await viewModel.loadImages()
}
}
}
#MainActor
class ListViewModel: ObservableObject {
#Published var images: [PhotoObject] = []
func loadImages() async {
do {
let photos = try await AsyncImageManager.downloadImages()
images = photos
} catch {
print("Could load photos: \(error)")
}
}
}
struct AsyncImageManager {
static func downloadImages() async throws -> [PhotoObject] {
let url = URL(string: "https://jsonplaceholder.typicode.com/photos")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([PhotoObject].self, from: data)
}
}
struct PhotoObject: Identifiable, Codable {
let albumId: Int
let id: Int
let title: String
let url: String
let thumbnailUrl: String
}
This is still a bug in iOS 16.
The only solution that I found is to use ScrollView + LazyVStack instead of List.

swiftui list doens't appear but array isn't empty

I am working on a Swiftui file that loads data from Firebase.
It did work but when I added things it suddenly stopt working...
I tried to strip it back down but I can't get it working again.
Does anyone know what I do wrong?
import SwiftUI
import Firebase
struct Fav: View {
#StateObject var loader = Loader()
var body: some View {
ScrollView {
if loader.userfav.count != 0 {
List (loader.userfav, id: \.id) { fav in
Text(fav.name.capitalized)
}
}
else
{
Text("You haven't added favorits yet...")
}
}
.onAppear{
loader.loadfav(loadfavorits: "asd")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
func deletefav (docid: String) {
print(docid)
}
}
struct Fav_Previews: PreviewProvider {
static var previews: some View {
Fav()
}
}
and the loader file
import Foundation
import Firebase
import FirebaseFirestore
class Loader : ObservableObject {
private var db = Firestore.firestore()
#Published var userfav = [fav]()
func loadfav (loadfavorits: String) {
userfav = [fav]()
db.collection("favo").whereField("user", isEqualTo: loadfavorits).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting favorits: \(err.localizedDescription)")
}
else
{
for fav in querySnapshot!.documents {
let brand = fav.get("brand") as! String
let store = fav.get("store") as! String
let name = fav.get("name") as! String
let type = fav.get("type") as! String
let docid = fav.get("docid") as! String
self.userfav.append(fav(brand: brand, store: store, name: name, type: type, docid: docid))
}
}
}
}
}
It doesn't show the Text("You haven't added favorits yet...")
So that means dat loader.userfav.count is not empty
Having a List embedded in a ScrollView (which also scrolls) can lead to layout problems. Remove the outer ScrollView and the issue will be solved.

SwiftUI 2.0: export group of images with .fileExporter modifier

This is a follow up on this thread:
SwiftUI 2.0: export images with .fileExporter modifier
Goal: export a group of images in SwiftUI
What I did:
I am using the .fileExporter modifier, with the FileDocument struct.
Also open to other approach, like . fileMover modifier for example.
Problem:
When setting the FileDocument for multiple images struct I am getting am error on func fileWrapper (check code bellow).
Question:
How can I export multiple images in SwiftUI (could be any method)?
//file exporter
.fileExporter(isPresented: $exportFile, document: ImageDocument(
image: UIImage(data: product.cover ?? Data())!,
image2: UIImage(data: product.cover2 ?? Data())!)
,
contentType: .jpeg, onCompletion: { (result) in
if case .success = result {
print("Success")
} else {
print("Failure")
}
})
//export group of images
struct ImageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.jpeg] }
var image: UIImage
var image2: UIImage
init(
image: UIImage?,
image2: UIImage?
) {
self.image = image ?? UIImage()
self.image2 = image2 ?? UIImage()
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = UIImage(data: data),
let image2 = UIImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
self.image2 = image2
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents:
image.jpegData(compressionQuality: 0.80)!,
image2.jpegData(compressionQuality: 0.80)!//<----- getting an "extra argument error here
)
}
}
Update:
Tried to rewrite Max answer to UIImage, but this only exports 1 image:
import SwiftUI
class AppContext: ObservableObject {
#Published var fileSaveDialogShown = false
}
#main
struct FocalApp: App {
#StateObject var appContext = AppContext()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.appContext)
.fileExporter(
isPresented: $appContext.fileSaveDialogShown,
documents: [
ImageDocument(image: UIImage(named: "1")),
ImageDocument(image: UIImage(named: "2"))
],
contentType: .jpeg // Match this to your representation in ImageDocument
) { url in
print("Saved to", url) // [URL]
}
}
}
}
import SwiftUI
import UniformTypeIdentifiers
struct ImageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.jpeg, .png, .tiff] }
var image: UIImage
init(image: UIImage?) {
self.image = image ?? UIImage()
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = UIImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
// You can replace tiff representation with what you want to export
return FileWrapper(regularFileWithContents: image.jpegData(compressionQuality: 1)!)
}
}
struct ContentView: View {
#EnvironmentObject var appContext: AppContext
var body: some View {
VStack {
Button(action: {
appContext.fileSaveDialogShown.toggle()
}, label: {
Text("Button")
})
}
.frame(width: 200, height: 200)
}
}
You need to use fileExporter with documents instead of document, which takes in a Collection
Here's how I'm doing it on macOS, adapting it should be straightforward:
import SwiftUI
import UniformTypeIdentifiers
struct ImageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.jpeg, .png, .tiff] }
var image: NSImage
init(image: NSImage?) {
self.image = image ?? NSImage()
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = NSImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
// You can replace tiff representation with what you want to export
return FileWrapper(regularFileWithContents: image.tiffRepresentation!)
}
}
#main
struct FocalApp: App {
#StateObject var appContext = AppContext()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(self.appContext)
.fileExporter(
isPresented: $appContext.fileSaveDialogShown,
documents: [
ImageDocument(image: NSImage(named: "testimage1")),
ImageDocument(image: NSImage(named: "testimage2"))
],
contentType: .tiff // Match this to your representation in ImageDocument
) { url in
print("Saved to", url) // [URL]
}
}
}
}