Firebase image is empty after switching views in SwiftUI - ios

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
}
}
}
}

Related

Using ImagePicker in SwiftUI

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)
}
}
}

SwiftUi how can I load selected video from item identifier

I am new to Swift and have been using the new PhotosPicker in SwiftUI 4.0 . I am able to display selected images but not video . When I select a video I can get a few pieces of information like this below
PhotosPickerItem(_itemIdentifier: Optional("40F724uF-24M7-4523-9B2B-AD43FB2C7D71/L0/001"), _shouldExposeItemIdentifier: false, _supportedContentTypes: [<_UTCoreType 0x106c4cd60> com.apple.quicktime-movie (not dynamic, declared)], _itemProvider: <PUPhotosFileProviderItemProvider: 0x600003ea05a0> {types = (
"com.apple.quicktime-movie"
)})
I am wondering if there is somehow that I can use that item identifier to load the video selected . I am been looking at different examples but none of them show videos for PhotosPicker . I have a very small project that I am testing this on, any suggestions would be great
import SwiftUI
import Combine
import PhotosUI
import AVKit
struct PlayVideoView: View {
#State private var selectedItem: [PhotosPickerItem] = []
#State private var data: Data?
#State var player = AVPlayer(url: URL(string: "https://swiftanytime-content.s3.ap-south-1.amazonaws.com/SwiftUI-Beginner/Video-Player/iMacAdvertisement.mp4")!)
var body: some View {
PhotosPicker(selection: $selectedItem,
maxSelectionCount: 1,
matching: .any(of: [.images,.videos])) {
Image(systemName: "photo")
.resizable()
.foregroundColor(.blue)
.frame(width: 24, height: 24)
.padding(.top, 5.0)
}.onChange(of: selectedItem) { newMedia in
Task {
guard let item = selectedItem.first else {
return
}
item.loadTransferable(type: Data.self) { result in
switch result {
case .success(let data):
if let data = data {
print(item) // get video url here and display in videoplayer
self.data = data
} else {
print("data is nil")
}
case .failure(let failure):
fatalError("\(failure)")
}
}
}
}
VideoPlayer(player: player) // selected video url should go here
.frame(width: 400, height: 300, alignment: .center)
}
}
struct PlayVideoView_Previews: PreviewProvider {
static var previews: some View {
PlayVideoView()
}
}
You could try this approach, using the code from https://github.com/zunda-pixel/SamplePhotosPicker.
The Movie code replicated here, uses the TransferRepresentation to represent
a url from the temp file.
import Foundation
import SwiftUI
import PhotosUI
import AVKit
import CoreTransferable
// from: https://github.com/zunda-pixel/SamplePhotosPicker
struct Movie: Transferable {
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) { movie in
SentTransferredFile(movie.url)
} importing: { receivedData in
let fileName = receivedData.file.lastPathComponent
let copy: URL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: copy.path) {
try FileManager.default.removeItem(at: copy)
}
try FileManager.default.copyItem(at: receivedData.file, to: copy)
return .init(url: copy)
}
}
}
struct ContentView: View {
var body: some View {
PlayVideoView()
}
}
struct PlayVideoView: View {
#State private var selectedItem: [PhotosPickerItem] = []
#State var player = AVPlayer(url: URL(string: "https://swiftanytime-content.s3.ap-south-1.amazonaws.com/SwiftUI-Beginner/Video-Player/iMacAdvertisement.mp4")!)
var body: some View {
PhotosPicker(selection: $selectedItem,
maxSelectionCount: 1,
matching: .any(of: [.images,.videos])) {
Image(systemName: "photo")
.resizable()
.foregroundColor(.blue)
.frame(width: 24, height: 24)
.padding(.top, 5.0)
}.onChange(of: selectedItem) { newMedia in
Task {
guard let item = selectedItem.first else { return }
item.loadTransferable(type: Movie.self) { result in // <-- here
switch result {
case .success(let movie):
if let movie = movie {
player = AVPlayer(url: movie.url) // <-- here
} else {
print("movie is nil")
}
case .failure(let failure):
fatalError("\(failure)")
}
}
}
}
VideoPlayer(player: player)
.frame(width: 400, height: 300, alignment: .center)
}
}

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: How to share Core Data between main app and extension

I am making app that stores some data into CoreData and than show it in on the Custom Keyboard. I have already made everything to store that data and created Custom Keyboard Extension, but I am not sure how can I get that data in this extension. I did something with apps group but still now not sure what am I doing wrong.
My DataController:
import Foundation
import SwiftUI
import CoreData
class DataController: NSObject, ObservableObject {
let container = NSPersistentContainer(name: "DataModel")
let storeDescription = NSPersistentStoreDescription(url: URL.storeURL(for: "group.zo", databaseName: "DataModel"))
override init() {
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores { description, error in
if let error = error {
print("\(error)")
}
}
}
}
public extension URL {
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
My KeyboardViewController:
import UIKit
import SwiftUI
import CoreData
class KeyboardViewController: UIInputViewController {
#IBOutlet var nextKeyboardButton: UIButton!
override func updateViewConstraints() {
super.updateViewConstraints()
}
override func viewDidLoad() {
super.viewDidLoad()
let child = UIHostingController(rootView: KeyboardView())
child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
child.view.backgroundColor = .clear
view.addSubview(child.view)
addChild(child)
}
}
My KeyboardView:
import SwiftUI
struct KeyboardView: View {
#EnvironmentObject var dataController: DataController
#FetchRequest(sortDescriptors: []) var snip: FetchedResults<Snip>
var body: some View {
HStack{
VStack(alignment: .leading){
Text("Your Snips")
.font(.system(size: 18, weight: .bold))
if snip.isEmpty {
Text("error")
}else{
ForEach(snip){ snip in
Button {
print(snip.content ?? "error")
} label: {
Text(snip.name ?? "error")
.background(Color(snip.color ?? ""))
}
}
}
Button {
print("siema")
} label: {
Text("test")
.background(Color.red)
}
Spacer()
}.padding()
Spacer()
}
}
}
Please Help!!!

Can't show image from url in swiftUI with URLSession [duplicate]

This question already has answers here:
What is the difference between ObservedObject and StateObject in SwiftUI
(6 answers)
Saving Date/Time to UserDefaults through didSet on #Published var (from TimePicker component of SwiftUI)
(1 answer)
Closed 2 years ago.
i'm new to swiftUI and trying to create simple app for showing images from API of images
i got the images from url but can't show it in Imageview ,i have button and when i press it , it calls the api and return single image url and i don't figure what the problem is
import SwiftUI
struct ContentView: View {
#ObservedObject var NetworkManager = Network()
var body: some View {
ZStack{
VStack{
ImageDog(UIDogImage:self.NetworkManager.Dog ?? UIImage(systemName: "photo")!)
Button(action: {
self.NetworkManager.ApiCaller()
}) {
Text("GET DOG").font(.system(size: 20)).foregroundColor(Color(#colorLiteral(red: 0.5272222161, green: 0.6115953326, blue: 0.6786056161, alpha: 1))).padding()
}.background(Color(#colorLiteral(red: 0.5401973128, green: 0.9296894073, blue: 0.6209766269, alpha: 1))).cornerRadius(20)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ImageDog: View {
#State var UIDogImage:UIImage
var body: some View {
Image(uiImage:UIDogImage).resizable().frame(width: 200, height: 200, alignment: .center).padding()
}
}
and the Network caller where the image url returned and i change it to image then pass it to Dog property image
import SwiftUI
class Network:ObservableObject {
#Published var Dog:UIImage?
// This Api return random dog images every time you request it
// https://dog.ceo/api/breeds/image/random
// MARK: Network Caller
func ApiCaller(){ // used URLSession for Network requesting and Codable For parsing JSON
// 1 :: create URL
if let DogURL = URL(string: "https://dog.ceo/api/breeds/image/random") {
// 2 :: create URLSession
let session = URLSession(configuration: .default)
// 3 :: give Sesion Task
let Dogtask = session.dataTask(with: DogURL) { (data, response, error) in
if error == nil {
if let DogData = data {
do {
let DogResponseResult = try JSONDecoder().decode(DogResponse.self, from: DogData)
print(DogResponseResult.message!)
if let data = try? Data(contentsOf: URL(string: DogResponseResult.message!)!){
DispatchQueue.main.async {
self.Dog = UIImage(data: data)
}
}
}catch{
print(error)
}
}
}
}
// 4 :: Start Session Task
Dogtask.resume() // Send the request here
}
}
}

Resources