Creating Interface Elements Programmatically Using PureLayout (Part 2)

Original author: Aly Yaka
  • Transfer
Hello, Habr! I present to you the translation of the article Creating UI Elements Programmatically Using PureLayout by Aly Yaka.

image

Welcome to the second part of the article on programmatically creating an interface using PureLayout. In the first part, we created the user interface of a simple mobile application completely in code, without using Storyboards or NIBs. In this guide, we will cover some of the most commonly used user interface elements in all applications:

  • UINavigationController / Bar
  • UITableView
  • Self-sizing UITableViewCell


UINavigationController


In our application, you probably need a navigation bar so that the user can go from the contact list to detailed information about a specific contact, and then return back to the list. UINavigationControllerEasily solve this problem using the navigation bar.

UINavigationController- it’s just a stack where you move a lot of views. The user sees the uppermost view (the one that was last moved) right now (except when you have another view presented on top of this, let's say favorites). And when you press the top view controllers of the navigation controller, the navigation controller automatically creates a “back” button (upper left or right side depending on the current language preferences of the device), and pressing this button returns you to the previous view.

All this is handled out of the box by the navigation controller. And adding one more will take only one additional line of code (if you do not want to customize the navigation bar).
Go to AppDelegate.swift and add the following line of code below, letviewController = ViewController ():

let navigationController = UINavigationController(rootViewController: viewController)

Now change self.window? .RootViewController = viewController на self.window? .RootViewController = navigationController. In the first line, we created an instance UINavigationController and passed it to us viewController in quality rootViewController, which is the view controller at the very bottom of the stack, which means that there will never be a back button on the navigation bar of this view. Then we give our window a navigation controller like rootViewController, because now it will contain all the views in the application.

Now run your application. The result should look like this:

image

Unfortunately, something went wrong. It appears that the navigation bar overlaps our upperView, and we have several ways to fix this:

  • Increase our size upperViewto fit the height of the navigation bar.
  • Set the property of isTranslucent the navigation bar to false. This will make the navigation bar opaque (if you haven’t noticed it is a bit transparent), and now the top edge of the superview will become the bottom of the navigation bar.

I personally will choose the second option, but, you will study the first. I also recommend checking and carefully reading Apple's docs on UINavigationController and UINavigationBar:


Now go to the viewDidLoad method and add this line self.navigationController? .NavigationBar.isTranslucent = false ниже super.viewDidLoad ()so that it looks like this:

override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationController?.navigationBar.isTranslucent = false
        self.view.backgroundColor = .white
        self.addSubviews()
        self.setupConstraints()
        self.view.bringSubview(toFront: avatar)
        self.view.setNeedsUpdateConstraints()
    }

You can also add this line self.title = "John Doe" в viewDidLoad, which will add a “Profile” to the navigation bar so that the user knows where he is currently located. Run the application and the result should look like this:

image

Refactoring our View Controller


Before continuing, we need to reduce our file ViewController.swiftso that we can use only real logic, and not just code for user interface elements. We can do this by subclassing UIView and moving all our user interface elements there. The reason we do this is to follow the Model-View-Controller or MVC architectural pattern for short. Learn more about MVC Model-View-Controller (MVC) on iOS: a modern approach .

Now right-click on the folder ContactCard in Project Navigator and select “New File”:

image

Click on Cocoa Touch Class and then Next. Now write “ProfileView” as the class name, and next to “Subclass of:” be sure to enter “UIView”. It just tells Xcode to automatically make our class inherit from UIView, and it will add some boilerplate code. Now click Next, then Create and delete the commented out code:

/*
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
    }
*/

And now we are ready for refactoring.

Cut and paste all the lazy variables from the view controller into our new view.
Below the last pending variable, override init(frame :)by typing initand then selecting the first autocomplete result from Xcode.

image

There will be an error saying that the “required” initializer “init (coder :)” should be provided by a subclass of “UIView”:

image

You can fix this by clicking on the red circle and then Fix.

image

In any overridden initializer, you should almost always call the superclass initializer, so add this line of code at the top Method: super.init (frame: frame).
Cut and paste the method addSubviews()under the initializers and delete self.viewbefore each call addSubview.

func addSubviews() {
    addSubview(avatar)
    addSubview(upperView)
    addSubview(segmentedControl)
    addSubview(editButton)
}

Then call this method from the initializer:

override init(frame: CGRect) {
    super.init(frame: frame)
    addSubviews()
    bringSubview(toFront: avatar)
}

For restrictions, override updateConstraints()and add a call at the end of this function (where it will always remain):

override func updateConstraints() {
    // Insert code here  
    super.updateConstraints() // Always at the bottom of the function
}

When overriding any method, it is always useful to check its documentation by visiting Apple documents or, more simply, holding down the Option (or Alt) key and clicking on the function name:

image

Cut and paste the restriction code from the view controller into our new method:

override func updateConstraints() {
    avatar.autoAlignAxis(toSuperviewAxis: .vertical)
    avatar.autoPinEdge(toSuperviewEdge: .top, withInset: 64.0)
    upperView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
    segmentedControl.autoPinEdge(toSuperviewEdge: .left, withInset: 8.0)
    segmentedControl.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0)
    segmentedControl.autoPinEdge(.top, to: .bottom, of: avatar, withOffset: 16.0)
    editButton.autoPinEdge(.top, to: .bottom, of: upperView, withOffset: 16.0)
    editButton.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0)
    super.updateConstraints()
}

Now go back to the view controller and initialize the instance ProfileView over the viewDidLoadmethod let profileView = ProfileView(frame: .zero), add it as a subview to the view ViewController .

Now our view controller has been reduced to a few lines of code!

import UIKit
import PureLayout
class ViewController: UIViewController {
    let profileView = ProfileView(frame: .zero)
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationController?.navigationBar.isTranslucent = false
        self.title = "Profile"
        self.view.backgroundColor = .white
        self.view.addSubview(self.profileView)
        self.profileView.autoPinEdgesToSuperviewEdges()
          self.view.layoutIfNeeded()
    }
}

To make sure that everything works as intended, launch your application and check how it looks.

Having a thin, neat review controller should always be your goal. This may take a lot of time, but it will save you from unnecessary trouble during maintenance.

UITableView


Next we will add a UITableView to present contact information such as phone number, address, etc.

If you haven’t already done so, head over to the Apple documentation for a look at UITableView, UITableViewDataSource, and UITableViewDelegate.


Browse to ViewController.swiftand add lazy varfor tableView above viewDidLoad():

lazy var tableView: UITableView = {
    let tableView = UITableView()
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.delegate = self
    tableView.dataSource = self
    return tableView
}()

If you try to run the application, Xcode will complain that this class is neither a delegate nor a data source for UITableViewController, and therefore we will add these two protocols to the class:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
.
.
.

Once again, Xcode will complain about a class that does not comply with the protocol UITableViewDataSource, which means that there are mandatory methods in this protocol that are not defined in the class. To find out which of these methods you should implement while holding Cmd + Control, click on the protocol UITableViewDataSource in the class definition and you will go to the protocol definition. For any method that is not preceded by a word optional, a class corresponding to this protocol must be implemented.

Here we have two methods that we need to implement:

  1. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int - This method tells the table view how many rows we want to show.
  2. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell- This method requests a cell in each row. Here we initialize (or reuse) the 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 go back to ViewController.swift, start typing numberOfRowsInSection, and when autocomplete appears, select the first option.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        <#code#>
    }

Delete the word code and return now 1.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

Under this function, start typing cellForRowAt and select the first method from autocomplete again.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        <#code#>
    }

And, again, bye, bring it back UITableViewCell.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }

Now, to connect our table view internally ProfileView, we will define a new initializer that takes the table view as a parameter so that it can add it as a subview and set the appropriate restrictions for it.

Go to ProfileView.swiftand add the attribute for the table view directly above the initializer: it

var tableView: UITableView!is defined, therefore we are not sure that it will be permanent.

Now replace the old implementation init (frame :)with:

init(tableView: UITableView) {
    super.init(frame: .zero)
      self.tableView = tableView
    addSubviews()
    bringSubview(toFront: avatar)
}

Xcode will now complain about missing init (frame :)for ProfileView, so go back to ViewController.swiftand replace let profileView = ProfileView (frame: .zero)with

lazy var profileView: UIView = {
    return ProfileView(tableView: self.tableView)
}()

Now we ProfileView have a link to the table view, and we can add it as a subview and set the correct restrictions for it.
Back to ProfileView.swift, add addSubview(tableView)to the end addSubviews()and set these restrictions to updateConstraints()above super.updateConstraints:

tableView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
        tableView.autoPinEdge(.top, to: .bottom, of: segmentedControl, withOffset: 8)

The first line adds three restrictions between the table view and its superview: the right, left, and bottom sides of the table view are attached to the right, left, and bottom sides of the profile view.

The second line attaches the top of the table view to the bottom of the segmented control with an interval of eight points between them. Launch the application and the result should look like this:

image

Great, now everything is in place, and we can begin to introduce our cells.

UITableViewCell


To implement UITableViewCell, we will almost always need to subclass this class, so right-click the folder ContactCardin the Project Navigator, then “New file ...”, then “Cocoa Touch Class” and “Next”.

Enter “UITableViewCell” in the “Subclass of:” field, and Xcode will automatically populate the class name “TableViewCell”. Enter “ProfileView” before autocomplete so that the final name is “ProfileInfoTableViewCell”, then click “Next” and “Create”. Go ahead and delete the created methods, since we will not need them. If you want, you can first read their descriptions to understand why we do not need them right now.

As we said earlier, our cell will contain basic information, which is the name of the field and its description, and therefore we need labels for them.

lazy var titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = "Title"
    return label
}()
lazy var descriptionLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = "Description"
    label.textColor = .gray
    return label
}()

And now we will redefine the initializer so that the cell can be configured:

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
      contentView.addSubview(titleLabel)
    contentView.addSubview(descriptionLabel)
}
required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

Regarding the limitations, we are going to do a little different, but, nevertheless, very useful:

override func updateConstraints() {
    let titleInsets = UIEdgeInsetsMake(16, 16, 0, 8)
    titleLabel.autoPinEdgesToSuperviewEdges(with: titleInsets, excludingEdge: .bottom)
    let descInsets = UIEdgeInsetsMake(0, 16, 4, 8)
    descriptionLabel.autoPinEdgesToSuperviewEdges(with: descInsets, excludingEdge: .top)
    descriptionLabel.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 16)
    super.updateConstraints()
}

Here we begin to use UIEdgeInsetsto set the spacing around each label. An object UIEdgeInsetscan be created using the method UIEdgeInsetsMake(top:, left:, bottom:, right:). For example, for titleLabelwe say that we want the upper limit to be four points, and the right and left to eight. We do not care about the bottom, because we exclude it, since we attach it to the top of the description mark. Take a minute to read and visualize all the constraint in your head.

Ok, now we can start drawing cells in our table view. Let's move on to ViewController.swiftand change the lazy initialization of our table view to register this class of cells in the table view and set the height for each cell.

let profileInfoCellReuseIdentifier = "profileInfoCellReuseIdentifier"
lazy var tableView: UITableView = {
    ...
    tableView.register(ProfileInfoTableViewCell.self, forCellReuseIdentifier: profileInfoCellReuseIdentifier)
    tableView.rowHeight = 68
    return tableView
}()

We also add a constant for the cell reuse identifier. This identifier is used to remove cells from the table view when they are displayed. This is an optimization that can (and should) be used to help UITableView reuse cells that were previously presented to display new content instead of redrawing the new cell from scratch.
Now let me show you how to reuse cells in one line of code in a method cellForRowAt:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: profileInfoCellReuseIdentifier, for: indexPath) as! ProfileInfoTableViewCell
    return cell
}

Here we inform the table view of the withdrawal of a reusable cell from the queue using the identifier under which we registered the path to the cell that the user is about to appear. Then we force the cell to ProfileInfoTableViewCellto be able to access its properties, so that we can, for example, set the title and description. This can be done using the following:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    ...
    switch indexPath.row {
    case 0:
        cell.titleLabel.text = "Phone Number"
        cell.descriptionLabel.text = "+234567890"
    case 1:
        cell.titleLabel.text = "Email"
        cell.descriptionLabel.text = "john@doe.co"
    case 2:
        cell.titleLabel.text = "LinkedIn"
        cell.descriptionLabel.text = "www.linkedin.com/john-doe"
    default:
        break
    }
    return cell
}

Now install numberOfRowsInSectionto return “3” and launch your application.

image

Amazing right?

Self-Sizing Cells


Perhaps, and most likely, there will be a case when you want different cells to have heights in accordance with the information inside them, which is not known in advance. To do this, you need a table view with automatically calculated dimensions, and in fact there is a very simple way to do this.

First of all, ProfileInfoTableViewCelladd this line to the lazy initializer descriptionLabel:

label.numberOfLines = 0

Go back to ViewControllerand add these two lines to the table view initializer:

lazy var tableView: UITableView = {
    ...
    tableView.estimatedRowHeight = 64
    tableView.rowHeight = UITableViewAutomaticDimension
    return tableView
}()

Here we inform the table view that the row height should have an automatically calculated value based on its contents.

Regarding 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 ViewDidLoadwe need to reload the table view for these changes to take effect:

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    DispatchQueue.main.async {
        self.tableView.reloadData()
    }
}

Now go ahead and add another cell, increasing the number of rows to four and adding another statement switchto cellForRow:

case 3:
    cell.titleLabel.text = "Address"
    cell.descriptionLabel.text = "45, Walt Disney St.\n37485, Mickey Mouse State"

Now run the application, and it should look something like this:

image

Conclusion


Amazing right? And as a reminder of why we actually code our user interface, here is a whole blog post written by our mobile team about why we are not using storyboards in Instabug .

What you did in two parts of this lesson:

  • Deleted a file main.storyboardfrom your project.
  • Created UIWindow programmatically and assigned it rootViewController.
  • Created various user interface elements in the code, such as labels, image views, segmented controls, and table views with their cells.
  • Invested UINavigationBarin your application.
  • Created a dynamic size UITableViewCell.

Also popular now: