How to refresh storage reference data in tableView cached with SDWebImage? - swift

I have a reference to a project image in Firebase, and I want to edit the photo by replacing the data in the storage reference, without changing the reference link. I am successfully doing this, and the image view is updating as well when a user selects a new image. However, when I go back in the navigation controller, my tableView still shows the same image - and when I click on the tableView item - the project image does not seem to be updated.
The only way the image and tableview data successfully update is when I delete the simulator from my computer and re-add it. Here is some of my code for how I populate the tableView - the code works great for new projects being added but the modified portion is not working - I call this function in viewwillappear to populate the tableview:
func checkForUpdates(uid: String) {
handler = db.collection("Projects").whereField("UID", isEqualTo: "\(uid)").addSnapshotListener { snapShot, error in
guard let document = snapShot else {return}
if document.count > 0 {
document.documentChanges.forEach { difference in
if difference.type == .added {
if let project = Project(dictionary: difference.document.data()) {
self.projectArray.append(project)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
else {return}
}
if difference.type == .modified {
DispatchQueue.main.async {
self.loadData(uid: uid)
self.tableView.reloadData()
}
return
}
if difference.type == .removed {return}
else {return}
}
}
else {
print("no documents to show")
}
}
}
When I segue into viewing a particular project (by clicking on tableviewcell) - here is my code
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "viewProject" {
if let vc = segue.destination as? ViewMyProject {
vc.selectedProject = self.selectedProject!
}
else {return}
}
else {return}
}
Here is my code for how I set cells within a xib file
func setProject(project: Project) {
projectName.text = project.title
projectCategory.text = project.category
projectDate.text = project.timeStamp.dateValue().dateToString(style: .short)
projectImage!.sd_setImage(with: URL.init(string: project.imageLink)) { (image, error, cacheType, url) in
if error != nil {
print ("Error setting project cell image")
return
}
else {
print("Successfuly set project cell image")
}
}
}
And here is how I load the project after clicking on the cell (I call this in viewWillAppear)
func loadProject(project: Project) {
filterProjectFeedback(project: project)
if projectImage.image == nil {projectImage.sd_setImage(with: URL.init(string: project.imageLink))}
projectDescription.text = project.description
}
Thanks so much in advance - I have a feeling it has to do with a strong reference - but I cannot make my "projectArray" variable weak

Here is a code in swift 3 to refresh cache everytime:
imgCardBack.sd_setImage(with: URL(string: objUserData.back_image!), placeholderImage:UIImage(named: "cardBack"), options: .refreshCached)
How to update image in cache when image changed on server with SDWebImage

Related

Progress Bar doesnt animate in UIKIt, Swift

I am using PHPicker for the first time, using loadFileRepresentation method, which asynchronously writes a copy of a selected file and returns a progress object. I am attaching this progress object to a parent progress where I track progress of copying of all files selected by user, so that I can drive one single progress bar view.
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
var tasks = [Progress]()
let parentTask = Progress()
DispatchQueue.global(qos: .unspecified).async {
for itemProvider in results.map({ $0.itemProvider }) {
if itemProvider.canLoadObject(ofClass: UIImage.self) {
guard let identifier = itemProvider.registeredTypeIdentifiers.first else { return }
guard let filenameExtension = URL(string: identifier)?.pathExtension else { return }
let newTask = itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { tempPathForFileCopying, error in
if (error != nil) {
print("Error while copying files \(String(describing: error))")
}
let targetPath = self.viewModel.galleryManager.selectedGalleryPath.appendingPathComponent(UUID().uuidString).appendingPathExtension(filenameExtension)
if let tempPathForFileCopying {
do {
try FileManager.default.copyItem(at: tempPathForFileCopying, to: targetPath)
} catch {
print("Error \(error)")
}
self.viewModel.galleryManager.buildThumb(forImage: AlbumImage(fileName: targetPath.lastPathComponent, date: Date()))
self.imagesToBeAdded.append(AlbumImage(fileName: targetPath.lastPathComponent, date: Date()))
}
}
tasks.append(newTask)
parentTask.addChild(newTask, withPendingUnitCount: newTask.totalUnitCount - newTask.completedUnitCount)
}
}
}
for task in tasks {
parentTask.totalUnitCount += task.totalUnitCount
}
self.screenView.progressView.observedProgress = parentTask
self.showLoading(task: parentTask)
}
I than invoke showLoading function, which should end up in while loop, until task.isFinished is true. Strangely, printing progress into the console works fine, and I can see that progress there, but no matter what, I cannot update UI progress bar view from here. Entire UI is stuck, until all the copies of the files are created. Even though that happens on background thread and I am trying to call progressView.setProgress on main thread. Again, printing current progress into the console works fine, but no matter what, UI doesnt update until all that copying is done, which just by that time is useless
func showLoading(task: Progress) {
DispatchQueue.main.async {
while !task.isFinished {
var oldFraction = task.fractionCompleted
usleep(300)
if task.fractionCompleted != oldFraction {
let progress = Float(task.fractionCompleted)
self.screenView.progressView.setProgress(progress, animated: false)
print(progress)
}
}
self.viewModel.addPhotos(images: self.imagesToBeAdded)
}
}

How to upload multiple image on firebase using Swift's PHPickerController [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
so in Swift, you have the ability to upload an image/video with ease using UIImageViewController. I did research and came across PHPickerController and I am trying to incorporate that into my code - for the reasoning being that I want multiple images/videos selected and once user presses "button" it pushes that batch to firebase cloud. I have been struggling with this for sometime now. Any sample swift file of doing just this would be much appreciated.
This worked for me.
Note: Make sure that the images you are selecting from the photoLibrary are not the default ones that come with xCode. Some of the default images do not work because they don't have a file location.
SwiftUI Solution
Here is how you call the PHPickerViewController:
struct PHPicker: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.selectionLimit = 5
config.filter = PHPickerFilter.images
let pickerViewController = PHPickerViewController(configuration: config)
pickerViewController.delegate = context.coordinator
return pickerViewController
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
class something: NSObject, PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
var fileName: Int = 5
for result in results {
// Get all the images that you selected from the PHPickerViewController
result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
// Check for errors
if let error = error {
print("Sick error dawg \(error.localizedDescription)")
} else {
// Convert the image into Data so we can upload to firebase
if let image = object as? UIImage {
let imageData = image.jpegData(compressionQuality: 1.0)
// You NEED to make sure you somehow change the name of each picture that you upload which is why I am using the variable "count".
// If you do not change the filename for each picture you upload, it will try to upload the file to the same file and it will give you an error.
Storage.storage().reference().child("fileName").child("\(fileName)").putData(imageData!)
fileName += 1
print("Uploaded to firebase")
} else {
print("There was an error.")
}
}
}
}
}
}
func makeCoordinator() -> something {
return something()
}
}
Here is how I present the sheet:
struct PresentMyPicker: View {
#State var presentSheet: Bool = false
var body: some View {
VStack {
Button {
presentSheet.toggle()
} label: {
Text("Click me")
}
}
.sheet(isPresented: $presentSheet) {
PHPicker()
}
}
}
UIKit solution
This is how I present the PHPickerViewController when they tap the button:
func setupView() {
var config = PHPickerConfiguration()
config.selectionLimit = 5
config.filter = PHPickerFilter.images
let pickerViewController = PHPickerViewController(configuration: config)
pickerViewController.delegate = self
view.addSubview(button)
button.addAction(UIAction() { _ in
self.present(pickerViewController, animated: true)
}, for: .touchUpInside)
}
Here is my delegate function that runs after you click "Add" with the selected images you want to upload.
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
var fileName: Int = 1
for result in results {
// Get all the images that you selected from the PHPickerViewController
result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
// Check for errors
if let error = error {
print("Sick error dawg \(error.localizedDescription)")
} else {
// Convert the image into Data so we can upload to firebase
if let image = object as? UIImage {
let imageData = image.jpegData(compressionQuality: 1.0)
// You NEED to make sure you somehow change the name of each picture that you upload which is why I am using the variable "fileName".
// If you do not change the filename for each picture you upload, it will try to upload all the selected images to the same file location and give you an error.
Storage.storage().reference().child("CollectionName").child("\(fileName)").putData(imageData!)
fileName += 1
} else {
print("There was an error.")
}
}
}
}
}
Also if you are wanting to upload videos to firebase and having trouble take a look at this example it took me forever to figure this out. Uploading Videos to firebase correctly.

Tableview not loading data after popping View Controller

When I pop the view controller stack, I need a table view in the first view controller to reload. I am using viewWillAppear (I already tried viewDidAppear and it didn't work).
override func viewWillAppear(_ animated: Bool) {
print("will appear")
loadData()
}
Once the view controller has re-appeared, I need to query the API again which I am doing in another service class with a completion handler of course and then reloading the table view:
#objc func loadData() {
guard let userEmail = userEmail else { return }
apiRequest(userId: userEmail) { (queriedArticles, error) in
if let error = error {
print("error in API query: \(error)")
} else {
guard let articles = queriedArticles else { return }
self.articlesArray.removeAll()
self.articleTableView.reloadData()
self.articlesArray.append(contentsOf: articles)
DispatchQueue.main.async {
self.articleTableView.reloadData()
}
}
}
}
What happens is that I am able to pop the stack and see the first view controller BUT it has the same data as it did before. I expect there to be one more cell with new data and it doesn't appear. I have to manually refresh (using refresh control) to be able to query and load the new data.
Any idea what I am doing wrong?

Download single Object of Firestore and save it into an struct/class object

I am coding since January 2019 and this is my first post here.
I am using Swift and Firestore. In my App is a tableView where I display events loaded out of a single Document with an array of events inside as [String: [String:Any]]. If the user wants to get more infos about an event he taps on it. In the background the TableViewController will open a new "DetailEventViewController" with a segue and give it the value of the eventID in the tapped cell.
When the user is on the DetailViewController Screen the app will download a new Document with the EventID as key for the document.
I wanna save this Data out of Firestore in a Struct called Event. For this example just with Event(eventName: String).
When I get all the data I can print it directly out but I can't save it in a variable and print it out later. I really don't know why. If I print the struct INSIDE the brackets where I get the data its working but if I save it into a variable and try to use this variable it says its nil.
So how can I fetch data out of Firestore and save in just a Single ValueObject (var currentEvent = Event? -> currentEvent = Event.event(for: data as [String:Any]) )
I search in google, firebaseDoc and stackoverflow but didn't find anything about it so I tried to save all the singe infos of the data inside a singe value.
// Struct
struct Event {
var eventName: String!
static func event(for eventData: [String:Any]) -> Event? {
guard let _eventName = eventData["eventName"] as? String
else {
print("error")
return nil
}
return Event(eventName: _eventName)
}
// TableView VC this should work
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowEventDetailSegue" {
if let ShowEvent = segue.destination as? DetailEventViewController, let event = eventForSegue{
ShowEvent.currentEventId = event.eventID
}
}
}
// DetailViewController
var currentEvent = Event()
var currentEventId: String?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let _eventID = currentEventId else {
print("error in EventID")
return}
setupEvent(eventID: _eventID) /* currentEvent should be set here */
setupView(event: currentEvent) /* currentEvent has after "setupEvent" the value of nil */
}
func setupEvent(eventID: String) {
let FirestoreRef = Firestore.firestore().collection("events").document(eventID)
FirestoreRef.getDocument { (document, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
SVProgressHUD.showError(withStatus: "Error in Download")
}else {
if let document = document, document.exists {
guard let data = document.data() else {return}
let eventData = Event.event(for: data as [String:Any])
print(eventData)
//here all infos are printed out - so I get them
self.currentEvent = eventData!
//Here is the error.. I can't save the fetched Data in my single current Event
} else {
SVProgressHUD.showError(withStatus: "Error")
}
}
}
}
func setupView(event: Event) {
self.titleLabel.text = event.eventName
}
I expect that the function setupEvents will give the currentEvent in the DetailViewController a SINGLEvalue cause its a SINGLE document not an array. So I can use this single Eventvalue for further actions. Like starting a new segue for a new ViewController and just push the Event there not

How to do an undo function within a tableviewcell?

I am having troubles getting my undo button working. I'm trying to get it to where if you press the delete button on a tableview cell, the undo can re-enter the cell.
My undo:
#IBAction func undoBtnWasPressed(_ sender: Any) {
undoItem()
undoView.isHidden = true
}
func undoItem() {
undoManager?.registerUndo(withTarget: GoalCell.self, selector: #selector(removeGoal(atIndexPath:)), object: nil)
undoManager?.undo()
}
My remove:
#objc func removeGoal(atIndexPath indexPath: IndexPath) {
guard let managedContext = appDelegate?.persistentContainer.viewContext else { return }
managedContext.delete(goals[indexPath.row])
undoView.isHidden = false
do {
try managedContext.save()
print("Successfully removed goal.")
} catch {
debugPrint("Could not save: \(error.localizedDescription)")
}
}
You can just save the data from the datasource of the deleted cell in a property or an array and if the undo button is pressed, you re-add the data into the datasource and reload the row or the full data of the tableview.
EDIT
So for instance, in your example you had the delete function delete data from core data based on goals[indexPath.row]
Before deleting, save the content from goals[indexPath.row] into a separate array.
When undo is tapped, just grab the value from the separate array and add it back to goals and add it back into core data.
Then just do a tableview.reloadData()
EDIT 2:
let dataSource: [CustomObject] = [Object1, Object2, Object3]
var undoSource: [CustomObject] = []
func removeRow(indexPath) {
let object = dataSource[indexPath.row]
undoSource.append(object)
dataSource.remove(object)
}
func undo() {
for object in undoSource {
dataSource.append(object)
}
tableView.reloadData()
}