CollectionView Tutorial Swift
Project Setup
Our first step will be to create and setup our project, to do this open Xcode and select Create New Xcode Project
Since our project will only feature a single view we can select Single View App
Give your project a name and make sure the Language selected is set to Swift.
Once you have created your project you should see something like the image below.
Adding Pods
Before we can begin creating our app we will install CocoaPods a dependency manager for Swift and Objective-C Cocoa projects. It has over 48 thousand libraries and is used in over 3 million apps. You can read about CocoaPods on their website: https://cocoapods.org/
Install pods to your machine with the following command.
[cce_SWIFT] sudo gem install cocoapods [/cce_SWIFT]
To add Pods to our project open Terminal and CD to your project directory, once in your project directory enter the following command and press enter, this will create the necessary files for Pods to function.
[cce_SWIFT] pod init [/cce_SWIFT]
Open your project directory in finder and open the Podfile file added by Pods with your preferred text editor, add an entry for SDWebImage, this will give us an async image downloader and cache our images for us. you can read more about SDWebImage here: https://github.com/rs/SDWebImage, your Podfile should look like the one below.
[cce_SWIFT] # Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'collectionview-tutorial' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for collectionview-tutorial pod 'SDWebImage', '~> 4.0' end [/cce_SWIFT]
Once your PodFile is setup we can then use the command below to install our pod.
[cce_SWIFT] pod install [/cce_SWIFT]
.
Creating the UI
Now switch back to Xcode, to create the UI for our app, we will need to include a collection view and a collection view cell.
Begin by making sure you have Main.storyboard open, you should see a screen like below.
Drag a Collection View from the bottom right tool box into the View.
Since our collection view will take up the entire screen space we can set our constraints to the full width and height of the view minus 8, we do this by selecting the Add New Constraints button on the bottom right of the main view while we have our collection view selected, your constraints should look like mine below.
Next we will need a cell to display in our collection view we do this by creating a xib, to create the cell right click on your project folder and select New File, select Cocoa Touch Class, Give the new class a name and tick the Also Create XIB File box, this XIB will be used to create the visible part of our cell for later.
Once you have done this two new files will have been added to the project folder, these will be your class file and xib.
Now we need to create our layout for the cell in the xib, select the xib you just created to open it, now add a Image View and 3 Label, Drag them into the XIB view just like we did with the CollectionView from the toolbox in the bottom right.
Arrange your layout however you want your cell to look, below is the cell I have created.
Now we have our cell complete we just need to reference the parts of the cell to our cell class we created earlier, to do with we first need to make sure our Xcode view is correctly setup, select the assistant editor button(two rings) from the top right ribbon like below.
Once you have the assistant editor open you should see a split view with one side having your xib design and the other your class, it should look something like below.
With your editor now configured correctly we can now add our references, we do this by holding CTRL and dragging from our selected item, do this for each of the labels and Image View to the class on the right, you will see something like below.
Once all of our references are setup and assigned we can begin coding our collection view to display our cell with custom text based on Json data downloaded from the News API.
But first we need to assign our cell a reuse identifier, this identifier will be used to reference our cell later, you can find the entry for Collection Reusable View in the top right, see below.
Creating the code
Now we have our interface setup lets add the code that will make the whole thing work, to begin with open your main view controller, by default this will be called ViewController.swift.
Before we can add any content we need to extend our class so that it can handle the data for our collection view.
We do this by adding UICollectionViewDelegate, UICollectionViewDataSource and UICollectionViewDelegateFlowLayout as protocols to our class, your class will look something like the code below. you can read more about Swift Protocols here: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html
[cce_SWIFT] import UIKit class ViewController: UIViewController, UICollectionViewDelegate,UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { @IBOutlet weak var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad(); } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } [/cce_SWIFT]
Next we need to add our datasource and delegate to the class, this is done by telling our collection view that the delegate and datasource are to come from this class, you can express this by assigning self to each of them. Below is how the code should look.
[cce_SWIFT] override func viewDidLoad() { super.viewDidLoad(); collectionView.delegate = self; collectionView.dataSource = self; } [/cce_SWIFT]
At this point you might have a error telling you that the class does not conform to the selected protocol, to fix this we add the required methods needed to conform to the protocols added, add the following functions listed below to fix this error.
This function will dictate the number of sections in the collection view, for the purpose of this tutorial we can return just one section.
[cce_SWIFT] func numberOfSections(in collectionView: UICollectionView) -> Int { return 1; } [/cce_SWIFT]
numberOfItemsInSection will set the number of sections the collection view will have, or the number of cells we will display, this should be set dynamically based on the number of headlines we get from our API response, for now though we will return one as we have no variable to assign right now.
[cce_SWIFT] func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 1; } [/cce_SWIFT]
cellForItemAt will run for each cell visible, we also need to add a variable for our cell, this will be explained more later on.
This is where our logic will go later for modifying the contents of each cell.
[cce_SWIFT] func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ViewCollectionViewCell; return cell; } [/cce_SWIFT]
This next function will change the size of each cell based on the logic within the function, for now though we will just assign each cell with a width the size of the frame.
[cce_SWIFT] func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout,sizeForItemAt indexPath: IndexPath) -> CGSize { let cellWidth = collectionView.frame.size.width return CGSize(width: cellWidth, height: cellWidth*0.8) } [/cce_SWIFT]
Register XIB
Now we have our base methods we can register our xib and then return the cell, first though we need to register our xib, we do this during the viewDidLoad function, simply use the register method on your UICollectionView property. Your viewDidLoad function should look like this.
[cce_SWIFT] override func viewDidLoad() { super.viewDidLoad(); collectionView.delegate = self; collectionView.dataSource = self; collectionView.register(UINib(nibName: "ViewCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "cell"); } [/cce_SWIFT]
Next we need to dequeue our reusable cell in our cellForItemAt function, we do this by calling dequeueReuseableCell on our cell variable, with this we pass in the reusableIdentifier we assigned to our cell, next we pass in our indexPath, make sure to cast as a ViewColectionViewCell without the cast you will not be able to access any of the referenced components on the cell class. Your code should look something like below.
[cce_SWIFT] func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ViewCollectionViewCell; return cell; } [/cce_SWIFT]
Loading data from Json
Now we have our UI and UI logic setup we can work on getting some data for our cell, first though we should create a new class to store any constant data we intend to never change, for this example this will be a API key from https://newsapi.org/
As we did at the start of the project, create a new file, call this file Constants, once created open the file and copy the following code.
[cce_SWIFT] struct ApiDetails { static let apiKey = "API KEY HERE"; } [/cce_SWIFT]
This will create a static variable we can then reference in any classes we might need the data, if at this point you do not have an API Key you should go to the website linked above and get yourself an API key for the BBC-News API, and put it here.
Now we have our constant set and ready for use we can build our GET request, our request URL is split into two so that the key can be changed later on if needed.
In your ViewController class add the following.
[cce_SWIFT] let apiUrl = URL(string : "https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey="+ApiDetails.apiKey); [/cce_SWIFT]
This will store the completed API URL that our session will use to connect and download the Json data, as you will see the ApiDetails.apiKey contains our API Key we were given.
Now we will need a function to load our data, create a new function called loadData and add the following code.
[cce_SWIFT] func loadData(){ let session = URLSession.shared.dataTask(with: URLRequest(url : apiUrl!)) { (data, response, error) in if let httpResponse = response as? HTTPURLResponse { if(httpResponse.statusCode != 200) { //DIE AND SHOW ERROR MESSAGE } } } [/cce_SWIFT]
This will create a URLSession and give it a dataTask, we pass in a URL and get out data, response and error, these can be used to further your logic, in this tutorial we will only be using data, next we check if the response we got was anything other than a status of 200 meaning OK.
Next we create a variable called articles and give it a type of Array<Dictionary<String,Any>>
This will store our data as an Array of dictionaries with a String key and a value of Any, like below.
[cce_SWIFT] var articles: Array<Dictionary<String,Any>> = []; [/cce_SWIFT]
then we create a variable myData in an IF statement this will put the data from our session into a Mydata variable we can use later. We also have a few ELSE statements these will serve as points we can use to error out any issues as we move through the logic.
[cce_SWIFT] if let myData = data { if let json = try? JSONSerialization.jsonObject(with: myData, options: []) as! Dictionary<String,Any> { if let statusCode = json["status"] as? String { if(statusCode == "ok") { if let articles = json["articles"] as? Array<Dictionary<String,Any>> { self.articles = articles; DispatchQueue.main.async { self.uiCollectionView.reloadData() } } else { } } } else { } } else { print("Error"); } } [/cce_SWIFT]
Now we have have our Json data we need to serialise the data so we can access it, we can then loop through each of the Arrays and access each of the keys and get their values.
So by now your loadData Function should look like the below code block.
[cce_SWIFT] func loadData(){ let session = URLSession.shared.dataTask(with: URLRequest(url : apiUrl!)) { (data, response, error) in if let httpResponse = response as? HTTPURLResponse { if(httpResponse.statusCode != 200) { //DIE AND SHOW ERROR MESSAGE } } if let myData = data { if let json = try? JSONSerialization.jsonObject(with: myData, options: []) as! Dictionary<String,Any> { //PARSE IT if let statusCode = json["status"] as? String { if(statusCode == "ok") { if let articles = json["articles"] as? Array<Dictionary<String,Any>> { self.articles = articles; DispatchQueue.main.async { self.uiCollectionView.reloadData() } } else { //ERROR WITH API REQUEST NOT OK } } } else { //ERROR WITH API REQUEST NOT OK } } else { print("Error"); } } } session.resume(); } [/cce_SWIFT]
Now we have our loadData function all done we can move on to getting the data from the Json into the collectionView and then into the cell.
To do this we will go back to our cellForItem function, in here we will add a new variable called row, this variable will take the value of articles with and array index of indexPath.row, the articles variable can then be used to get any of the data from our Json we downloaded.
Now we just check each entry we want to get data from and assign it to our components in the xib cell, if you wanted to get the title of the headline we would create an IF statement checking if the row contains any data for the key title, if it does then we assign the title to our cell label using the reference we created at the beginning.
See below for an example of how the code should look
[cce_SWIFT] func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let row = articles[indexPath.row]; let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ViewCollectionViewCell; if let title = row["title"] as? String{ cell.labelTitle.text = title; } if let description = row["description"] as? String{ cell.labelSubText.text = description } if let urlToImage = row["urlToImage"] as? String{ cell.imageViewCenter.sd_setImage(with: URL(string: urlToImage), placeholderImage: UIImage(named: "placeholder")) } if let publishedDate = row["publishedAt"] as? String{ let dbDateFormatter = DateFormatter() dbDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" if let date = dbDateFormatter.date(from: publishedDate){ let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MMM dd, yyyy" let todaysDate = dateFormatter.string(from: date) cell.labelPublished.text = "Published : " + todaysDate } print(publishedDate); } return cell; } [/cce_SWIFT]
Once you have the data assigned you can then return the cell, and this will run for each cell in the number of sections assigned, this is currently one so let’s change that.
In your numberOfItemsInSection function simply add to the return the number of articles by using the count property. See below for an example.
[cce_SWIFT] func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.articles.count; } [/cce_SWIFT]
And now once you run the app you should see something that looks like this.