Swift dispatch_async not updating UIView until iOS notification - ios

I have a UIView that is updated from a server once a button is pushed.
The UIView update happens within a dispatch_async(). Through debugging I can see that the server response has been received and the new subview is created and applied to the UIView.
However the view remains unchanged UNTIL another iOS notification occurs (e.g. the device receives an email). As soon as the notification banner is displayed the UIView is refreshed and displays the image from the server.
What am I missing? How do I get the UIView to update as soon as the new subview is added?
Note: I have tried the following within and outside the dispatch_async() and tried calling them on the same queue after the initial change takes place without luck.
self.view.setNeedsDisplay()
self.view.bringSubviewToFront(view)
self.view.layoutIfNeeded() (thanks for the suggestion #buxik)
EDIT added code (small version of larger class, only relevant section added):
class networkedImage: UIViewController {
#IBOutlet var viewArea: UIView = UIView()
let originalWidth: CGFloat = 200
let originalHeight: CGFloat = 100
let imageURL: NSURL = NSURL(string: "www.example.com/image.jpg")!
func updateViewArea() {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
dispatch_async(dispatch_get_main_queue()) {
self.viewArea.subviews.forEach({ $0.removeFromSuperview() })
let newImageView = UIImageView(frame: CGRectMake(0, 0, self.originalWidth, self.originalHeight))
let data = NSData(contentsOfURL: self.imageURL)
if data != nil {
let image = UIImage(data: data!)
if image != nil {
newImageView.image = image
self.viewArea.addSubview(newImageView)
self.viewArea.bringSubviewToFront(newImageView)
self.viewArea.setNeedsDisplay()
}
NSNotificationCenter.defaultCenter().addObserverForName("NotificationIdentifier", object: nil, queue: nil, usingBlock: {
[unowned self] note in
print("I thought this might work, it didn't.")
})
}
}
}
}
}

To resolve the issue I added an Observer to viewDidLoad()
NSNotificationCenter.defaultCenter().addObserverForName("imageReadyNotification", object: nil, queue: NSOperationQueue.mainQueue(), usingBlock: {
[unowned self] note in
self.updateViewArea()
})
Then at the bottom of updateViewArea() I post the notification
if self.newImage == true {
NSNotificationCenter.defaultCenter().postNotificationName("imageReadyNotification", object: nil, userInfo: nil)
}
The if condition is to ensure the notification isn't fired continually. I have no idea why calling this function twice works, but it does.

Related

Swift having image in background not changing with subview

I have a view that displays 1 picture at a time and that picture is supposed to be the background image. I also have other elements on top of that image that are supposed to change on touch . The view that I have is called VideoView and I have elements on top . All of this works my problem is that when I use insertSubview all the elements on top change (like they are supposed to) but the image stays the same (VideoView) and it is supposed to change . If I replace the InsertSubView with addSubview then the image changes correctly on click but my top elements do not show . I know that what I'm supposed to use is insertSubview and on touch I can see the image URL changing but the screen stays with the first default image .
I think I know what might be going on . On every tap I am adding a new InsertSubview and the new image might be behind the old image . I verified by uploading a video . I first had an image and when I tapped to go to the next screen the image stayed however the video sound came on .
class BookViewC: UIViewController{
#IBOutlet weak var VideoView: UIView!
var imageView: UIImageView?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// This is how I change images, touchCount number is linked with unique image depending on the number passed in .
for touch in touches {
let location = touch.location(in: self.VideoView)
if location.x < self.VideoView.layer.frame.size.width / 2 {
if touchCount == 0 || touchCount < 0 {
touchCount = 0
} else {
touchCount = touchCount - 1
}
reloadTable(Offset: touchCount)
}
else {
touchCount = touchCount + 1
reloadTable(Offset: touchCount)
}
}
}
func ShowImage(s_image: String) {
let imgURL: NSURL = NSURL(string: s_image)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
bookviewImage.imageView = UIImageView(image: UIImage(named: s_image))
bookviewImage.imageView?.frame = VideoView.bounds
bookviewImage.imageView?.contentMode = UIViewContentMode.scaleAspectFill
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
self.bookviewImage.imageView?.image = UIImage(data: data!)
self.VideoView.insertSubview(self.bookviewImage.imageView!, at: 0)
// self.VideoView.addSubview(self.bookviewImage.imageView!)
} else {
self.bookviewImage.imageView!.image = nil
}
})
});
task.resume()
}
func reloadTable(Offset: Int) {
// send http request
session.dataTask(with:request, completionHandler: {(data, response, error) in
if error != nil {
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
if let Streams = parsedData["Results"] as? [AnyObject]?
{
if self.streamsModel.Locations.count >= 0 {
// Get Results
}
DispatchQueue.main.async {
for Stream in Streams! {
if let s_image = Stream["stream_image"] as? String {
self.ShowImage(s_image: s_image)
}
}
}
This background Image on InsertSubview does not change on click however when I add AddSubview the background image changes so I know there is something wrong in my code since the InsertSubview background image is not updating
[![ .][2]][2]
I'm not sure how you are changing pictures and might have to see the code to provide a better explanation. In the meantime, you should understand the difference between insertSubview and addSubview.
addSubview:
addSubview adds a view at the end of the list. In the hierarchical structure, this would be the bottom most item. On screen, however, it would be the top most item covering everything underneath it.
The hierarchy would look like
View
Video View
Profile Image
Time
Full Name
Post
Button
imageView
insertSubview:
insertSubview inserts a view at a given position in the list. Your code is inserting at position 0. In the hierarchical structure, this would be the top most item. On screen, however, it would be the bottom most item covered by everything on top of it.
The hierarchy would look like
View
Video View
imageView
Profile Image
Time
Full Name
Post
Button
Hope that helps.

Where to Put DispatchQueue Swift

I am a new developer. I'm using Swift 4.2 and Xcode 10.2.
I'm trying to show a spinner while a photo is being saved. I am simulating a slow connection on my iPhone in Developer Mode to test it. Using the code below the spinner does not show up. The view goes right to the end where the button shows and says the upload is complete (when it isn't). I tried putting it all into a DispatchQueue.global(qos: .userinitiated).async and then showing the button back on the main queue. I also tried putting the showSpinner on a DispatchQueue.main and then savePhoto on a .global(qos: .utility). But I cleary don't understand the GCD processes.
Here's my code:
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// Can show the loading bar here.
}
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
What types of DispatchQueues should I use and where should I put them?
Here is the savePhoto code:
static func savePhoto(image:UIImage, progressUpdate: #escaping (Double) -> Void) {
// Get data representation of the image
let photoData = image.jpegData(compressionQuality:0.1)
guard photoData != nil else {
print("Couldn't turn the image into data")
return
}
// Get a storage reference
let userid = LocalStorageService.loadCurrentUser()?.userId
let filename = UUID().uuidString
let ref = Storage.storage().reference().child("images/\(String(describing: userid))/\(filename).jpg")
// Upload the photo
let uploadTask = ref.putData(photoData!, metadata: nil) { (metadata, error) in
if error != nil {
// An error during upload occurred
print("There was an error during upload")
}
else {
// Upload was successful, now create a database entry
self.createPhotoDatabaseEntry(ref: ref, filename: filename)
}
}
uploadTask.observe(.progress) { (snapshot) in
let percentage:Double = Double(snapshot.progress!.completedUnitCount /
snapshot.progress!.totalUnitCount) * 100.00
progressUpdate(percentage)
}
}
Since the code that saves the photo is asynchronous , so your current code removes the spinner directly after it's added before the upload is complete
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// remove spinner when progress is 100.0 = upload complete .
if pct == 100.0 {
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
}
}
Here you don't have to use GCD as firebase upload runs in another background thread so it won't block the main thread

How can I display image on fullscreen when user taps on my UIImage in Swift?

I'm displaying a photo in my app and I'm using UIImage for that. So far this is how I'm doing that:
func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
completion(data: data, response: response, error: error)
}.resume()
}
func downloadImage(url: NSURL){
getDataFromUrl(url) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
print("Download Finished")
self.requestPhoto.image = UIImage(data: data)
}
}
}
and then in viewDidLoad() I'm doing:
if let checkedUrl = NSURL(string: photo) {
requestPhoto.contentMode = .ScaleAspectFit
downloadImage(checkedUrl)
}
That leaves the UIImage filled with my photo, but it's not clickable and it's the size of the original component. Is there a way of adding some kind of listener or something that will display the photo on fullscreen when user taps the UIImage component?
What you need is to add a UITapGestureRecognizer to your requestPhoto image view. Something like this :
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tapHandler"))
self.requestPhoto.addGestureRecognizer(tapGestureRecognizer)
self.requestPhoto.userInteractionEnabled = true
The last line is needed as UIImageView has it turned of by default, so the touches get canceled.
And then in the same class :
func tapHandler(sender: UITapGestureRecognizer) {
if sender.state == .Ended {
// change the size of the image view so that it fills the whole screen
}
}
The whole thing could also benefit from a flag saying if the view is fullscreen or not, so that when the user taps the fullscreen image you can reverse the process.
As always, you can read more in the docs.
How to enlarge thumbnail photo into fullscreen photo view on iOS?
I resolved this problem by creating new UIImageView object with the same image in it and set the frame of it equal to thumbnail image:
let fullscreenPhoto = UIImageView(frame: thumbnailImage.frame)
Then add image as sublayer into main window:
UIApplication.sharedApplication().windows.first!.addSubview(fullscreenPhoto)
Resizing is made with animation using block:
let windowFrame = UIApplication.sharedApplication().windows.first!.frame
UIView.animateWithDuration(0.4, delay: 0.0, options: .CurveEaseInOut, animations: {
fullscreenPhoto.frame = windowFrame
fullscreenPhoto.alpha = 1
fullscreenPhoto.layoutSubviews()
}, completion: { _ in
})
You can find ready solution here: https://github.com/lukszar/SLImageView
There is provided gesture recognizers for show and hide fullscreen.
You just need to set your UIImageView to SLImageView class in Storyboard and all magic is done.

Backend Completion Handler crashes app if completed after PushViewController

What I have
I don't understand the nature of the problem.
I have 2 View Controllers :
1) FeedViewController ,which shows events in tableview
2) EventViewController, pushed when you press on event.
When Feed Loads, it start asynchronous loadings of all images of all events.
It's done for each event by following function:
EventsManager().loadProfilePictureById(event.profilePictureID as String, currentProgress: event.profilePictureProgress, completionHandler: {
(progress:Double, image:UIImage!, error:NSError!) -> Void in
event.profilePictureProgress = progress
if image != nil {
event.profilePicture = image
}
if (error == nil){
if (self.tableView.headerViewForSection(index) != nil){
var header:eventHeaderView = self.tableView.headerViewForSection(index) as! eventHeaderView
header.updateProfilePicture(
self.eventsManager.events[index].profilePictureID as String,
progress: self.eventsManager.events[index].profilePictureProgress,
image: self.eventsManager.events[index].profilePicture)
}
}else{
println("Error:" + error.description)
}
})
Here is how I push EventViewController:
func PushEventViewController(sender:UITapGestureRecognizer)->Void{
let ViewSender = sender.view!
let selectedRow = ViewSender.tag
//let Cell:HomeEventTableViewCell = TimelineEventTable.cellForRowAtIndexPath(SelectedIndexPath) as HomeEventTableViewCell
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
let VC:EventViewController = self.storyboard?.instantiateViewControllerWithIdentifier("EventViewController") as! EventViewController
VC.event = self.eventsManager.events[selectedRow]
self.navigationController?.pushViewController(VC, animated: true)
})
}
Problem
The problem is that if I press on event and push EventViewController before image is downloaded (completion handlers are still getting called) it crashes app.
Assumptions
I struggled with this for days and couldn't find a solution,
but my assumptions are that completion handler called after
Crash happens when it tries to execute following line after EventViewController was pushed:
event.profilePictureProgress = progress
if image != nil {
event.profilePicture = image
}
I assume when new view controller is pushed , event object which is used in completion handler is being deallocated
Found out where the problem was, the issue was that variable event.profilePictureProgress was declared as dynamic var (I was going to take advantage of that to add observer to it after).

Proper usage of dispatch_async (GCD) call

i am trying to load set of images from the server and updating UI with returned images and displaying it by fade animation. it will be repeated forever.
Code snippet:
override func viewDidLoad(animated: Bool) {
super.viewDidLoad(animated)
self.loadImages()
}
func loadImages(){
var urlImage:UIImage?
//self.array_images contains preloaded images (not UIImage) objects which i get from different api call.
if imageCount >= self.array_images!.count{
imageCount = 0
}
var img = self.array_images[imageCount]
var url = NSURL(string: img.url!)
var data = NSData(contentsOfURL: url!)
if (data != nil){
urlImage = UIImage(data: data!)
UIView.transitionWithView(self.imageView_Promotion, duration: 1.2, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
println("Begin Animation")
self.imageView_Promotion.image = realImage
}, completion:{ finished in
println("Completed")
self.imageCount++
self.loadImages() //another issue: it calls the function more than couple of times
})
}
}else{
var image:UIImage = UIImage(named: "test_Image.jpg")!
imageView_Promotion.image = image
}
The above one hangs the UI. i have tried to call loadImages in dispatch_async and animation in dispatch_main queue but the issue still persist.
let priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.loadImages()
}
dispatch_async(dispatch_get_main_queue(),{
//UIView animation
})
What is the proper way to handle this.
Thanks
Thread-safe problem
Basically, UIKit APIs are not thread-safe. It means UIKit APIs should be called only from the main thread (the main queue). But, actually, there are some exceptions. For instance, UIImage +data: is thread-safe API.
Your code includes some unsafe calls.
UIView.transitionWithView(self.imageView_Promotion, duration: 1.2, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
I don't think UIView.transitionWithView is thread-safe.
var image:UIImage = UIImage(named: "test_Image.jpg")!
UIImage +named: is not thread-safe, it uses a global cache or so on. According to UIImage init(named:) Discussion.
You can not assume that this method is thread safe.
Block the main thread
If the thread-safe problem was fixed, it would block the main thread.
Your code calls loadImages method, that downloads an image from the server or loads an image from the file system, from the completion block of UIView.transitionWithView. UIView.transitionWithView should be called from the main thread, so the completion block also will be called from the main thread. It means downloading or loading an image on the main thread. It blocks the main thread.
Example code
Thus, loadImages should be like the following.
Swift Playground code:
import UIKit
import XCPlayground
func loadImage(file:String) -> UIImage {
let path = NSBundle.mainBundle().pathForResource(file, ofType: "")
let data = NSData(contentsOfFile: path!)
let image = UIImage(data: data!)
return image!
}
var index = 0
let files = ["image0.png", "image1.png", "image2.png"]
let placementImage = loadImage(files[0])
let view = UIImageView(image:placementImage)
// loadImages function
func loadImages() {
let priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND
dispatch_async(dispatch_get_global_queue(priority, 0)) {
/*
* This block will be invoked on the background thread
* Load an image on the background thread
* Don't use non-background-thread-safe APIs like UIImage +named:
*/
if index >= files.count {
index = 0
}
var file = files[index++]
let image = loadImage(file)
dispatch_async(dispatch_get_main_queue()) {
/*
* This block will be invoked on the main thread
* It is safe to call any UIKit APIs
*/
UIView.transitionWithView(view, duration: 1.2, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
/*
* This block will be invoked on the main thread
* It is safe to call any UIKit APIs
*/
view.image = image
}, completion:{ finished in
/*
* This block will be invoked on the main thread
* It is safe to call any UIKit APIs
*/
loadImages()
})
}
}
}
// call loadImages() from the main thread
XCPShowView("image", view)
loadImages()
XCPSetExecutionShouldContinueIndefinitely()
Your UI freezes as the comletion block for transitionWithView is called on the main thread (a.k.a. UI thread)- that's basically how you end up running "loadImages()" on the main thread. Then, when the load method is being called on the main thread, you create an instance of NSData and initialize it with contents of a URL - this is being done synchronously, therefore your UI is freezing. For what it's worth, just wrap the loadImages() call (the one which is inside the completion block) into a dispatch_async call to a background queue and the freeze should be gone.
P.S. I would recommend queuing calls of loadImages() instead of relying on the completion blocks of UI animations.
You have to separate loadImages & update View into 2 GCD thread. Something like that:
override func viewDidLoad(animated: Bool) {
super.viewDidLoad(animated)
dispatch_async(dispatch_queue_create("com.company.queue.downloadImages", nil {
() -> Void in
self.loadImages()
}))
}
/**
Call this function in a GCD queue
*/
func loadImages() {
... first, download image
... you have to add stop condition cause I see that self.loadImages is called recursively in 'completion' clause below
... then, update UIView if data != nil
if (data != nil) {
dispatch_async(dispatch_get_main_queue(),nil, {
() -> Void in
UIView.transitionWithView.... {
}, completion: { finished in
dispatch_async(dispatch_queue_create("com.company.queue.downloadImages", nil, {
() -> Void in
self.imageCount++
self.loadImages()
}))
})
})
}
}

Resources