Welcome to part two of Creating UI Elements Programmatically Using PureLayout. In the first part, we built a simple mobile application’s UI completely in code without the use of Storyboards or NIBs. In this tutorial, we will cover some of the most used UI elements in all applications:
3. Self-sizing UITableViewCell
In an app like this, you’ll probably want a navigation bar so that a user can navigate from the list of contacts to a detail view for a certain contact and then go back to the list. A UINavigationController would easily solve this with its navigation bar.
A UINavigationController is simply a stack on which you push many views as you go further into the application. The upper most view (the one pushed or navigated into last) is probably the one that the user sees right now (except if you have another view presented on top of that). And as you push view controllers on top of the navigation controller, a back button will be automatically be created by the navigation controller (the upper left or right side depending on the device’s current language preference) and pressing that button will take you to the previous view controller.
All of this is handled, out of the box, by the navigation controller. And adding one to your application would only take one extra line of code (if you don’t want customizations to the navbar).
Navigate to AppDelegate.swift and add this line of code below let viewController =ViewController()
And now change self.window?.rootViewController = viewController to self.window?.rootViewController = navigationController. What we did in the first line is create an instance of a UINavigationController and give it our viewController as the rootViewController, which is the view controller at the very bottom of the stack, meaning that there will never be a back button on that view’s navbar. Then we give our window the navigation controller as it’s rootViewController since it will now contain all of the view in the application, or that tab if that app has a tabbar.
Now run your application. The result should look like this:
Oops, something doesn’t look right. It seems like the navigation bar is overlaying our upperView and we have a couple of ways here to fix this:
Increase the size of our upperView to accommodate the navigation bar’s height.
Set the navigation bar’s isTranslucent property to false. Doing this will make the navigation bar opaque (in case you didn’t notice, it is a little bit transparent) and now the superview’s top edge will be the bottom of the navigation bar.
I will personally go with the second choice, but please, go ahead and explore the first one. I also recommend to check out, and read thoroughly, Apple’s docs on the UINavigationController and UINavigationBar:
Now, go to viewDidLoad method and add this line self.navigationController?.navigationBar.isTranslucent = false below super.viewDidLoad() so it will look like this:
You can also add this line self.title = "John Doe" in viewDidLoad. This will add the word “Profile” to the navigation bar so the user knows where they currently stand. Go ahead and do this before running the application and the result should look like this:
Refactoring Our View Controller
Before proceeding to creating UI elements programmatically, we’ll need to slim down our ViewController.swift file to be able to only accommodate actual logic instead of just code for UI elements. We can do this by subclassing UIView and moving all of our UI elements there. The reason why we’re doing this is to follow the Model-View-Controller architectural pattern, or MVC for short. More on MVC Model-View-Controller (MVC) in iOS: A Modern Approach.
Now right click on the ContactCard folder in the Project Navigator and choose New File:
Click on Cocoa Touch Class and then click Next. Now write “ProfileView” as the name of the class and next to “Subclass of:” make sure to type in “UIView”. This is just to tell Xcode to automatically make our class inherit from UIView and it will add some boilerplate code (that we’ll probably remove anyway in the case of UIView). Now click Next, then Create, and remove the commented code:
And now we’re ready for refactoring.
Cut and paste all the lazy vars from the view controller to our new view.
Below the last lazy var, override init(frame:), by typing init and then choosing the first autocomplete result from Xcode.
An error will come up saying `'required' initializer 'init(coder:)' must be provided by subclass of 'UIView':
You can fix this by clicking on the red circle and then Fix.
In any overridden initializer, you should almost always call the super class’s initializer, so add this line of code at the top of the method: super.init(frame: frame).
Cut and paste the addSubviews() method underneath the initializers and remove the self.view before each addSubview call.
Then call this method from the initializer:
For the constraints, override updateConstraints() and add the call to its super at the end of that function (where it will always stay).
When overriding any method, its always good to check its documentation, either by visiting the Apple docs, or more easily, by holding down the Option (or Alt) key and clicking on the function’s name
Cut and paste the constraints code from the view controller to our new method:
Now go back to the view controller and initialize an instance of ProfileView above the viewDidLoad method let profileView = ProfileView(frame: .zero), add it as a subview to the ViewController’s view and pin all its edges to the superview’s edges.
Now our view controller has slimmed down to just a few lines of code after being 100 lines long!
To make sure everything’s working as intended, run your app and check how it looks. Now everything doesn’t just look good, but also its code belongs where it should be.
Having a slim, neat view controller should always be your goal. It might take a little bit more time to do so, but it will save you so much hassle later on during maintenance.
Moving on, we will now add a UITableView to present a contact’s information such as their phone number, address, etc.
If you haven’t before, head to the Apple docs to read up on UITableView, UITableViewDataSource and UITableViewDelegate
Navigate to ViewController.swift and add a lazy var for the tableView above the viewDidLoad():
If you try to build the app, Xcode will complain saying that this class is neither a delegate nor a data source for a UITableViewController and so we will add these two protocols to the class:
Once again, Xcode will complain about the class not conforming to the UITableViewDataSource protocol, which means that there are required methods in this protocol that are not defined in the class. To find out which of these methods you should implement, hold down Cmd + Control and click on the UITableViewDataSource protocol in the class’s definition, this will navigate to the protocol’s definition. There, any method that is not prefixed by the word optional must be implemented by the class conforming to that protocol.
Here we have two methods that we need to implement:
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int -- This method tells the table view how many rows we want to show.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell -- This method asks for the cell at each row. This is where we will initialize (or reuse) a cell and insert the information that we want to show to the user. For example, the first cell will display the phone number, the second cell will display the address and so on.
Now head back to ViewController.swift, start typing numberOfRowsInSection, and when the autocomplete appears, choose the first hit.
Delete the word code and return 1 for now.
Underneath this function, start typing cellForRowAt and, again, choose the first method from autocomplete.
And, again, for now, return a UITableViewCell.
Now, to plug in our table view inside the ProfileView, we will define a new initializer for ProfileView that takes a table view as a parameter so it can add it as a subview and setup the proper constraints for it.
Head to ProfileView.swift and add an attribute for the table view right above the initializer:
var tableView: UITableView! is defined so we’re not really sure that there would be one all the time.
Now replace the old implementation of init(frame:) with:
Xcode will now complain about the missing init(frame:) for ProfileView so head back to ViewController.swift and replace let profileView = ProfileView(frame: .zero) with
Now our ProfileView has reference to the table view and we can add it as a subview and set up the right constraints for it.
Back to ProfileView.swift, add addSubview(tableView) to the bottom of addSubviews() and set up these constraints in updateConstraints() above super.updateConstraints:
The first line adds three constraints between the table view and its superview: The right, left and bottom sides of the table view are pinned to the right, left and bottom sides of the profile view.
The second line pins the top of the table view to the bottom of the segmented control with a spacing of eight points between them. Run the application and the result should look like this:
Great, now everything is in place and we can start implementing our cells.
To implement a UITableViewCell, we will almost always need to subclass this class, so go ahead and right click on ContactCard folder in the Project Navigator, then New file…, then Cocoa TouchClass, then Next.
Type "UITableViewCell" in the "Subclass of:" field and Xcode will autofill the class’s name with "TableViewCell". Type "ProfileView" before the autofill so the final name would be "ProfileInfoTableViewCell" then click Next and Create. Go ahead and delete the created methods as we will not need them. If you want, you can read their descriptions first to get an idea of why we wouldn’t need them right now.
As we said earlier, our cell will contain basic information, which is the title of the field and its description, and so we will need labels for these.
And now we will override the initializer so we can set up the cell:
And as for the constraints, we’re going to do something a little bit different but very useful nonetheless:
Here we start using UIEdgeInsets to set the spacings around each label. A UIEdgeInsets object can be created using the UIEdgeInsetsMake(top:, left:, bottom:, right:) method. For the titleLabel for example, we say that we want the top constraint to be four points long and the right and left to be eight. We don’t care about the bottom because we’re excluding it as we will pin it to the top of the description label. Take a minute to read and visualize the constraints in your head.
Fantastic, we can now start rendering cells in our table view. Let’s head to ViewController.swift and modify the lazy initialization of our table view in order to register this cell class with the table view and set a height for each cell.
We also add a constant for the cell’s reuse identifier. This identifier is used to dequeue cells from the table view when they’re being rendered. This is an optimization that can (and should) be used to help the UITableView reuse cells that were presented before in order to show new content instead of redrawing a new cell from scratch. Brilliant!
Now let me show you how to reuse cells in just one line of code in the cellForRowAt method:
Here we tell the table view to dequeue a reusable cell using the identifier that we registered the cell under for the index path that is about to appear for the user. We then force cast the cell to ProfileInfoTableViewCell in order to be able to access its properties so we can, for example, set the title and description. This can be done through the following:
And now set the numberOfRowsInSection to return 3 and run your application.
There might, and most probably, will be a case where you want your different cells to have different heights according to the information inside of them, which you might not know beforehand. For this you will need your table view to accommodate their sizes automatically, and there is actually a very simple way to do so.
First of all, in ProfileInfoTableViewCell, add this line to the descriptionLabel’s lazy initializer: label.numberOfLines = 0.
Go back to ViewController and add these two lines to the table view’s initializer:
Here, we tell the table view that the row height should have an automatically calculated value based on its content.
As for the estimated row height:
"Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view." -- Apple Docs
In ViewDidLoad, we need to reload the table view for these changes to take effect:
Now go and add another cell by increasing the number of rows to four and adding another switch statement to the cellForRow:
Now run the application and it should look something like this: