• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/6/20

Feed the data to the interface

Log in or subscribe for free to enjoy all this course has to offer!

We’ve got the data (content) for our application! Except the user can’t see it...! 😕

Let’s feed it to the interface!

Populating the grid

Loading the list

In this example, we'll be loading the list within our list view controller. Let's add a method for that:

func loadData() {
MediaService.getMediaList(term: "mix") { (success, list) in
if success, let list = list {
DataManager.shared.mediaList = list
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
else {
// show no data alert
}
}
}

Here we are calling a media service method to fetch media list from iTunes, providing a search term - mix (feel free to play around with the term!).

One of the parameters there is the completion closure that gets executed once our service receives a response from the server.

In case of success, we are assigning that new media list to the data manager and then reloading the collection view so that it can be populated with the new content.

What's interesting here is that we are doing so within this structure:

DispatchQueue.main.async {
// code to execute
}

This command requests that the code be executed on the main thread asynchronously. 😱

And what's the main thread and asynchronous execution? 🤔

Multithreading sneak peek

As the code gets executed, it's being organized into sequences of commands; these sequences are called execution threads.

There's always at least one thread per application (or, more generally, per process). That Is called the main thread

When an application is run in the form of more than one thread that can be executed in parallel, that's called multithreading. Those threads, while being executed simultaneously, share the same resources.

What is it for? 🤔

Multithreading is used to execute parts of code in parallel, making it appear that things happen faster. Most of the time this is due to the fact one piece of the code doesn't have to wait for everything before it gets completed.

Is multithreading another responsibility of a developer? 🤔

Yes! Well, and no. 🤨 Developing applications that do not require sophisticated processing does not require programmatic thread management (with a few exceptions).

One of the exceptions is that UI related operations must be executed on the main thread in iOS. Since updating collection view is UI related, we must explicitly request it to be executed on the main thread. Which is exactly what the command  DispatchQueue.main.async takes care of! 

Ok, back to our code.

What happens if the getMediaList method returns without success? We need to notify the user. For this, lets add a helper method that will display a no data alert:

func presentNoDataAlert(title: String?, message: String?) {
let alertController = UIAlertController(title: title,
message: message,
preferredStyle: .alert)
let dismissAction = UIAlertAction(title: "Got it", style: .cancel, handler: { (action) -> Void in
})
alertController.addAction(dismissAction)
present(alertController, animated: true)
}

And alter the loadData method to use it:

if success, let list = list {
// ...
}
else {
self.presentNoDataAlert(title: "Oops, something happened...",
message: "Couldn't load any fun stuff for you:(")
}

Next we need to call loadData() in viewDidLoad():

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
config()
loadData()
}

Let's try running the app:

No images
No images

We see that the text has changed, but the artwork still has placeholders. That's because we never loaded any images!

Loading the images

When and where do we load the images? 

One of the major advantages of using collection view (same for table view) is that cells are prepared and rendered on demand - when they are about to appear. Those that remain hidden from the user forever (if the user never scrolls the list), will never have to be prepared and rendered on the screen - hence, no wasted resources!

We're going to request to load an image for each cell in the cellForItem method. Here we get to use our helper method to get images:

if let imageURL = URL(string: mediaBrief.artworkUrl) {
MediaService.getImage(imageUrl: imageURL, completion: { (success, imageData) in
if success, let imageData = imageData,
let artwork = UIImage(data: imageData) {
DispatchQueue.main.async {
cell.setImage(image: artwork)
}
}
})
}

Notice that assigning an image is a UI-related activity and therefore must be executed on the main thread!

You may try debugging it, setting a breakpoint in this method, and notice that it happens a number of times in the beginning - to display the initial set to cover the device screen. That will continue happening for the new cells when the user scrolls.

You'll also notice that we are now getting proper artwork displayed:

Remote images loaded
Remote images loaded

This is wonderful! However, we are now loading images every time we scroll through a cell 😠 - that's a waste of resources.

Instead, we are going to utilize an extra data property we've created in MediaBrief class -  artworkData : 

if let artworkData = mediaBrief.artworkData,
let artwork = UIImage(data: artworkData) {
cell.setImage(image: artwork)
}
else if let imageURL = URL(string: mediaBrief.artworkUrl) {
MediaService.getImage(imageUrl: imageURL, completion: { (success, imageData) in
if success, let imageData = imageData,
let artwork = UIImage(data: imageData) {
mediaBrief.artworkData = imageData
DispatchQueue.main.async {
cell.setImage(image: artwork)
}
}
})
}

Let's check that property first. If the image data is already there, we use it to create an image and pass it to the cell. Otherwise, we request it from the service and assign it to that property, so that next time we can reuse it! 😎

Here's the complete code of the cell-serving method:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListMediaCell", for: indexPath) as! ListCollectionViewCell
let mediaBrief = dataSource[indexPath.item]
cell.populate(mediaBrief: mediaBrief)
if let artworkData = mediaBrief.artworkData,
let artwork = UIImage(data: artworkData) {
cell.setImage(image: artwork)
}
else if let imageURL = URL(string: mediaBrief.artworkUrl) {
MediaService.getImage(imageUrl: imageURL, completion: { (success, imageData) in
if success, let imageData = imageData,
let artwork = UIImage(data: imageData) {
mediaBrief.artworkData = imageData
DispatchQueue.main.async {
cell.setImage(image: artwork)
}
}
})
}
return cell
}

One view controller is complete! One more left!

Populating the detail view

Here too, we are going to implement a  loadData() method along with two helper methods to populate the media and swag elements. This view does not employ a collection view, and the images are needed right away without waiting for scrolling, so we'll incorporate them into the loading method: 

func loadData() {
MediaService.getMedia(id: mediaId) { (success, media) in
if success, let media = media {
self.media = media
self.swag = DataManager.shared.swagForMedia(media)
DispatchQueue.main.async {
self.populateMedia()
self.populateSwag()
}
}
else {
self.presentNoDataAlert(title: "Oops, something happened...",
message: "Couldn't load media details")
}
}
}
func populateMedia() {
guard let media = self.media else {
return
}
titleLabel.text = media.title
artistLabel.text = media.artistName
collectionLabel.text = media.collection
if let imageURL = URL(string: media.artworkUrl) {
MediaService.getImage(imageUrl: imageURL, completion: { (success, imageData) in
if success, let imageData = imageData,
let artwork = UIImage(data: imageData) {
DispatchQueue.main.async {
self.artworkImageView.image = artwork
}
}
})
}
}
func populateSwag() {
guard let swag = self.swag else {
return
}
swagTitleLabel.text = swag.title
if let imageURL = URL(string: swag.artworkUrl) {
MediaService.getImage(imageUrl: imageURL, completion: { (success, imageData) in
if success, let imageData = imageData,
let artwork = UIImage(data: imageData) {
DispatchQueue.main.async {
self.swagImageView.image = artwork
}
}
})
}
}

Let's review the above:

  • loadData()

    • Calling the  getMedia()  method of the media service class. 

    • If the media request returns a successful result, we are populating media and swag data, otherwise showing a no-data alert.

  • Then we are implementing two separate functions for each data section. Each method is responsible for requesting their image:

    • populateMedia()  -  populates media elements

    • populateSwag()  - populates swag elements

  • Here, too, in both image assigning code fragments, we are requesting that it be executed on the main thread. 

Notice, we also got to utilize the no-data alert helper function!

Do you see the difference in image handling compared with the list using the collection view?

In this scenario, we are not utilizing the artworkData property of data objects because, in the current configuration of the detail view,  it will never get an opportunity to be reused. 😠

And finally, alter the viewDidLoad method:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
config()
loadData()
}

Very nice - let's run it:

Detail view images loaded!
Detail view images loaded!

 Feel free to also test linking to Safari - the More on iTunes button and Swag view tap!

Well done! 😎

Let's recap!

  • Multithreading is sharing resources between sequences of code. 

  • There's always one main thread per application.

  • UI-related functionality in iOS must be executed on the main thread.

Example of certificate of achievement
Example of certificate of achievement