I'm new to gcd and multithreading / concurrency / parallelism. I wrote this example project to test some things out. I have 3 methods, a list of image urls, and an imageView in the interface.
Each method does essentially the same thing, with minor variations.
loadAllImagesSlow() loops through the image urls, creating the NSData from each url, creating a UIImage and setting the imageView's image to that image.
loadAllImagesFast() dispatches that entire task to a global concurrent queue.
newMethod() loops through the image urls and dispatches each task to a global concurrent queue. This was the fastest! :D Exciting stuff. For me anyway.
When I run loadAllImagesFast or newMethod, I see the interface quickly cycle through each image, ending at the last one. But for loadAllImagesSlow, I don't see any images; only the last image appears at the very end.
I believe loadAllImagesSlow is running on the main thread (serial queue), because i don't get responses when I tap the screen (the other functions don't block the UI). But since it's a serial queue, I don't understand why the imageView doesn't display the images one at a time. Since the main queue is serial, everything's supposed to be executed before following code. But that isn't happening with the imageView's image being set.
Any ideas why that might be? I don't need this for a particular project; I'm simply trying to understand the behaviour.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var urlArray = [
"https://img0.ndsstatic.com/wallpapers/76df262f429c799124461286c5ee64b1_large.jpeg",
"https://www.walldevil.com/wallpapers/a74/olivia-wilde-women-actresses-green-eyes-ponytails.jpg",
"https://richestcelebrities.org/wp-content/uploads/2014/11/Olivia-Wilde-Net-Worth.jpg",
"https://cdn.pcwallart.com/images/olivia-wilde-wallpaper-3.jpg",
"https://kingofwallpapers.com/olivia-wilde/olivia-wilde-006.jpg",
"https://img0.ndsstatic.com/wallpapers/b39ca6a58368a76a92d56739d6e2da31_large.jpeg"
]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.view.addGestureRecognizer(tapGesture)
print("starting")
self.loadAllImagesSlow()
}
func handleTap(sender: UITapGestureRecognizer) {
print("tap")
}
func loadAllImagesSlow() { // takes around 4 seconds. UI is unresponsive throughout. Only last image gets shown
let startTime = Date()
for each in self.urlArray {
let url = URL(string: each)
do {
let data = try Data(contentsOf: url!)
self.imageView.image = UIImage(data: data)
} catch {
print("error")
}
}
let endTime = Date()
let executionTime = endTime.timeIntervalSince(startTime)
print("Execution time: \(executionTime)")
}
func loadAllImagesFast() { // takes around 4 seconds, but UI is responsive! Cycles through the images!
DispatchQueue.global(qos: .userInitiated).async {
let startTime = Date()
for each in self.urlArray {
let url = URL(string: each)
do {
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
} catch {
print("error")
}
}
let endTime = Date()
let executionTime = endTime.timeIntervalSince(startTime)
print("Execution time: \(executionTime)")
}
}
func newMethod() { // much much faster than 'loadAllImagesFast'
// This method harnesses the power of the concurrent queue by dispatching lots of smaller tasks that run concurrently, rather than dispatching one large task that runs concorruntly alongside nothing else in that queue; essentially, sequientially, on a seaprate queue.
for each in self.urlArray {
DispatchQueue.global(qos: .userInitiated).async {
let url = URL(string: each)
do {
let data = try Data(contentsOf: url!)
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
} catch {
print("error")
}
}
}
}
}
Related
I am fetching a data source in background. There are 2 urls and I choose it with a tabBar. To know which url I need to access, I use navigationController?.tabBarItem.tag. But It throws an error of "navigationController must be used from main thread only". I've tried to wrap it with DispatchQueue.main.async but it didn't work. Any fix or new approach appreciated.
override func viewDidLoad() {
super.viewDidLoad()
performSelector(inBackground: #selector(fetchJSON), with: nil)
}
#objc func fetchJSON() {
let urlString: String
if navigationController?.tabBarItem.tag == 0 {a
urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
} else {
urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
}
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
parse(json: data)
return
}
}
performSelector(onMainThread: #selector(showError), with: nil, waitUntilDone: false)
}
Move the logic that needs to access the UI to the main thread, then pass the result as an argument to your function on the background thread.
Here, there's several issues:
The performSelector(…) methods are quite low-level and not a good solution with Swift. Avoid these, they have issues and make it cumbersome to pass arguments around. Use GCD or async/await instead.
Using the synchronous Data(contentsOf: …) is also not a good idea. If you would asynchronous solutions you wouldn't run into the threading issue in the first place.
I really suggest you look into the second problem (e.g. using a DataTask), as it completely eliminates your threading issues, but here's a simple way to refactor your existing code using GCD that should already work:
override func viewDidLoad() {
super.viewDidLoad()
let urlString: String
if navigationController?.tabBarItem.tag == 0 {a
urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
} else {
urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
}
DispatchQueue.global(qos: .utility).async {
self.fetchJSON(urlString)
}
}
func fetchJSON(_ urlString: String) {
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
parse(json: data)
return
}
}
DispatchQueue.main.async {
self.showError()
}
}
I have a tableView, each cell is loaded with an image from the internet via DispatchQueue.main.async.
I implemented a search on an array, the data from which is output to a table. Because of DDispatchQueue.main.async, the emulator starts to hang a lot, but if you remove it, everything works fine, how do I implement loading images without causing a load?
Image upload code:
DispatchQueue.main.async {
if let url = URL(string: "https://storage.googleapis.com/iex/api/logos/\(stock.displaySymbol).png") {
if let data = try? Data(contentsOf: url) {
self.stockLogoImageView.image = UIImage(data: data)
self.imageLoadingIndicator.stopAnimating()
}
}
}
Search extension code:
extension StocksViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
searchStocks(searchController.searchBar.text!)
}
func searchStocks(_ searchText: String) {
searchStocksList = stocks.filter({(stock: Stock) -> Bool in
return stock.displaySymbol.lowercased().contains(searchText.lowercased()) || stock.description.lowercased().contains(searchText.lowercased())
})
stocksTableView.reloadData()
}
}
Don't do networking on the main queue, It's the UIKit work that must be done on the main queue.
// Network on background queue
if let url = URL(string: ....),
let data = try? Data(contentsOf: url) {
let img = UIImage(data: data)
// Dispatch back to main to update UI
DispatchQueue.main.async {
self.stockLogoImageView.image = img
self.imageLoadingIndicator.stopAnimating()
}
}
I have two UICollectionView:
1 UICollectionView - Post
2 UICollectionView - Categories
Logic:
When user tap on any categories, that we send new api request and then update post collection view
In my UICollectionViewCell have two conditions.
1 Image Item
2 Video Item
Below my code:
class PopularPostsCell: UICollectionViewCell {
#IBOutlet weak var myImage : UIImageView!
func setupItems(childPostsItem: ChildPostsModel, index: Int) {
if childPostsItem.media_type != "video" {
myImage.image = nil
myImage.sd_setImage(with: URL(string: childPostsItem.media), placeholderImage: UIImage(named: ""))
} else {
DispatchQueue.global(qos: .background).async {
let locImage = Helpers.videoSnapshot(filePathLocal: childPostsItem.media)
DispatchQueue.main.async {
self.myImage.image = nil
self.myImage.image = locImage
}
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
myImage.image = nil
}
}
if video, I need get image from Video URL below my code:
static func videoSnapshot(filePathLocal: String) -> UIImage? {
do {
let asset = AVURLAsset(url: URL(string: filePathLocal)!, options: nil)
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(0, 1), actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
return thumbnail
} catch let error {
print("*** Error generating thumbnail: \(error.localizedDescription)")
return nil
}
}
Problem: If I choose several item in category very fast, my cell use random image. If I choose category slowly, that my cell use image ok.
My screen first: if I choose category fast
second: if I choose category slowly, in the screen everything is good.
If I don't use background async thread, all ok, but everything starts to slow down
Please, any help.
You are using a background thread and the main thread in a sequential manner while by definition they are parallel and don't wait for one another. Therefore in :
DispatchQueue.global(qos: .background).async {
let locImage = Helpers.videoSnapshot(filePathLocal: childPostsItem.media)
DispatchQueue.main.async {
self.myImage.image = nil
self.myImage.image = locImage
}
}
The two threads are completely asynchronous. You can't expect the background thread to be finished before the main thread starts.
There are ways to communicate thread events with signals and semaphores but in your case I think using asynchronous functions with completion handlers would be way more appropriate.
Check out how it works : https://stackoverflow.com/a/50531255/5922920
In my application, button tapping downloads data from an Internet site. The site is a list of links containing binary data. Sometimes, the first link may not contain the proper data. In this case, the application takes the next link in the array and gets data from there. The links are correct.
The problem I have is that frequently (not always though) the application freezes for seconds when I tap on the button. After 5-30 seconds, it unfreezes and downloading implements normally. I understand, something is blocking the main thread. When stopping the process in xCode, I get this (semaphore_wait_trap noted):
This is how I do it:
// Button Action
#IBAction func downloadWindNoaa(_ sender: UIButton)
{
// Starts activity indicator
startActivityIndicator()
// Starts downloading and processing data
// Either use this
DispatchQueue.global(qos: .default).async
{
DispatchQueue.main.async
{
self.downloadWindsAloftData()
}
}
// Or this - no difference.
//downloadWindsAloftData()
}
}
func downloadWindsAloftData()
{
// Creates a list of website addresses to request data: CHECKED.
self.listOfLinks = makeGribWebAddress()
// Extract and save the data
saveGribFile()
}
// This downloads the data and saves it in a required format. I suspect, this is the culprit
func saveGribFile()
{
// Check if the links have been created
if (!self.listOfLinks.isEmpty)
{
/// Instance of OperationQueue
queue = OperationQueue()
// Convert array of Strings to array of URL links
let urls = self.listOfLinks.map { URL(string: $0)! }
guard self.urlIndex != urls.count else
{
NSLog("report failure")
return
}
// Current link
let url = urls[self.urlIndex]
// Increment the url index
self.urlIndex += 1
// Add operation to the queue
queue.addOperation { () -> Void in
// Variables for Request, Queue, and Error
let request = URLRequest(url: url)
let session = URLSession.shared
// Array of bytes that will hold the data
var dataReceived = [UInt8]()
// Read data
let task = session.dataTask(with: request) {(data, response, error) -> Void in
if error != nil
{
print("Request transport error")
}
else
{
let response = response as! HTTPURLResponse
let data = data!
if response.statusCode == 200
{
//Converting data to String
dataReceived = [UInt8](data)
}
else
{
print("Request server-side error")
}
}
// Main thread
OperationQueue.main.addOperation(
{
// If downloaded data is less than 2 KB in size, repeat the operation
if dataReceived.count <= 2000
{
self.saveGribFile()
}
else
{
self.setWindsAloftDataFromGrib(gribData: dataReceived)
// Reset the URL Index back to 0
self.urlIndex = 0
}
}
)
}
task.resume()
}
}
}
// Processing data further
func setWindsAloftDataFromGrib(gribData: [UInt8])
{
// Stops spinning activity indicator
stopActivityIndicator()
// Other code to process data...
}
// Makes Web Address
let GRIB_URL = "http://xxxxxxxxxx"
func makeGribWebAddress() -> [String]
{
var finalResult = [String]()
// Main address site
let address1 = "http://xxxxxxxx"
// Address part with type of data
let address2 = "file=gfs.t";
let address4 = "z.pgrb2.1p00.anl&lev_250_mb=on&lev_450_mb=on&lev_700_mb=on&var_TMP=on&var_UGRD=on&var_VGRD=on"
let leftlon = "0"
let rightlon = "359"
let toplat = "90"
let bottomlat = "-90"
// Address part with coordinates
let address5 = "&leftlon="+leftlon+"&rightlon="+rightlon+"&toplat="+toplat+"&bottomlat="+bottomlat
// Vector that includes all Grib files available for download
let listOfFiles = readWebToString()
if (!listOfFiles.isEmpty)
{
for i in 0..<listOfFiles.count
{
// Part of the link that includes the file
let address6 = "&dir=%2F"+listOfFiles[i]
// Extract time: last 2 characters
let address3 = listOfFiles[i].substring(from:listOfFiles[i].index(listOfFiles[i].endIndex, offsetBy: -2))
// Make the link
let addressFull = (address1 + address2 + address3 + address4 + address5 + address6).trimmingCharacters(in: .whitespacesAndNewlines)
finalResult.append(addressFull)
}
}
return finalResult;
}
func readWebToString() -> [String]
{
// Final array to return
var finalResult = [String]()
guard let dataURL = NSURL(string: self.GRIB_URL)
else
{
print("IGAGribReader error: No URL identified")
return []
}
do
{
// Get contents of the page
let contents = try String(contentsOf: dataURL as URL)
// Regular expression
let expression : String = ">gfs\\.\\d+<"
let range = NSRange(location: 0, length: contents.characters.count)
do
{
// Match the URL content with regex expression
let regex = try NSRegularExpression(pattern: expression, options: NSRegularExpression.Options.caseInsensitive)
let contentsNS = contents as NSString
let matches = regex.matches(in: contents, options: [], range: range)
for match in matches
{
for i in 0..<match.numberOfRanges
{
let resultingNS = contentsNS.substring(with: (match.rangeAt(i))) as String
finalResult.append(resultingNS)
}
}
// Remove "<" and ">" from the strings
if (!finalResult.isEmpty)
{
for i in 0..<finalResult.count
{
finalResult[i].remove(at: finalResult[i].startIndex)
finalResult[i].remove(at: finalResult[i].index(before: finalResult[i].endIndex))
}
}
}
catch
{
print("IGAGribReader error: No regex match")
}
}
catch
{
print("IGAGribReader error: URL content is not read")
}
return finalResult;
}
I have been trying to fix it for the past several weeks but in vain. Any help would be much appreciated!
let contents = try String(contentsOf: dataURL as URL)
You are calling String(contentsOf: url) on the main thread (main queue). This downloads the content of the URL into a string synchronously The main thread is used to drive the UI, running synchronous network code is going to freeze the UI. This is a big no-no.
You should never call readWebToString() in the main queue. Doing DispatchQueue.main.async { self.downloadWindsAloftData() } exactly put the block in the main queue which we should avoid. (async just means "execute this later", it is still executed on Dispatch.main.)
You should just run downloadWindsAloftData in the global queue instead of main queue
DispatchQueue.global(qos: .default).async {
self.downloadWindsAloftData()
}
Only run DispatchQueue.main.async when you want to update the UI.
Your stack trace is telling you that it's stopping at String(contentsOf:), called by readWebToString, called by makeGribWebAddress.
The problem is that String(contentsOf:) performs a synchronous network request. If that request takes any time, it will block that thread. And if you call this from the main thread, your app may freeze.
Theoretically, you could just dispatch that process to a background queue, but that merely hides the deeper problem, that you are doing a network request with an API that is synchronous, non-cancellable, and offers no meaningful error reporting.
You really should doing asynchronous requests with URLSession, like you have elsewhere. Avoid using String(contentsOf:) with remote URL.
I cant work out why my arrays (amountArray, interestArray and riskbandArray) return the full set of information if I print them just after the forloop, whereas if I print them right at the end of the viewDidload method it returns nothing. I understand that its probably occurring because the network request is being executed in the background so when I ask to print my arrays, nothing is returned because it is being called in the main thread. I have tried to counteract this by bringing in Grand Central Dispatch but my arrays are still empty. SO SO frustrating.
class CollectionViewController: UICollectionViewController {
var amountArray = [Int]()
var interestArray = [Double]()
var riskbandArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let session = NSURLSession.sharedSession()
let Url = NSURL(string: "http://fc-ios-test.herokuapp.com/auctions")
let task: NSURLSessionDownloadTask = session.downloadTaskWithURL(Url!) { (url, response, error) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let data = NSData(contentsOfURL: url!)
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers) as! NSDictionary
if let array = jsonData["items"] as? [[NSObject:AnyObject]] {
for item in array {
self.amountArray.append(item["amount_cents"] as! Int)
self.interestArray.append(item["rate"] as! Double * 100)
self.riskbandArray.append(item["risk_band"] as! String)
}
}
self.collectionView?.reloadData()
} catch {
print(error)
}
})
}
task.resume()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Register cell classes
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
// Do any additional setup after loading the view.
print(self.amountArray)
}
Here's a cut-down version showing the relevant parts of your code:
let task: NSURLSessionDownloadTask = session.downloadTaskWithURL(Url!) {...}
task.resume()
print(self.amountArray)
So you're creating a task and starting it, and then immediately printing amountArray. It's important to know that the task will run asynchronously, i.e. it will get processing time periodically until it completes, but execution of your code will continue from the task.resume() before the task even really gets going. So your print() executes long before the code that would add data to amountArray ever happens.
If you want to see what's in self.amountArray, move the print() statement so that it's the last thing that happens in the task's completion block. It will then happen after the array is changed.