home.


Tagged: swift


Swift 3 and iOS: Programatically create a UINavigationController

First create the UINavigationController, giving it a rootViewController as your controller to display.

Then create a UIBarButtonItem with a button and action select target as a method in your class.

Then add that to your UINavigationController’s topViewController’s navigationItem.

let navigationController = UINavigationController(rootViewController: YOUR_CONTROLLER)
let btnDone = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(dismissNav))
navigationController.topViewController?.navigationItem.rightBarButtonItem = btnDone

The referenced selector would look like this:

func dismissNav() {
    self.dismiss(animated: true, completion: nil)
}
swift ios ios-UINavigationController

Swift 3 and iOS: Add views programmatically with NSLayoutConstraint and AutoLayout

You can add a view programmatically in a fixed position easily enough:

self.edgesForExtendedLayout = [] // This ensures your view is below the navigation bar
...
let rect = CGRect(origin: CGPoint(x:0, y:0), size: CGSize(width:50, height:50))
var label = UILabel(frame: rect)
label.backgroundColor = UIColor.red
label.text = "label text"
self.view.addSubview(label)

However, if you use autolayout with NSLayoutConstraint then it’s the same as above but without the rect stuff, i.e. var label = UILabel(). You need to set translatesAutoresizingMaskIntoConstraints on the label to false to enable autolayout apparently. You then create layout constraints, for example:

NSLayoutConstraint(item: label, attribute: .centerX, relatedBy: .equal,
                   toItem: view, attribute: .centerX,
                   multiplier: 1.0, constant: 0.0)

The first line mentions the view you want it applied to, the attribute you want to constrain, and how it’s related to another. The second line says what we’re relating to, that relation’s attribute. We’ll ignore the last line for the moment.

So you’re saying “for the item label, relate its central x position to equals the central x position on view.” The below is saying “For the item label, relate its height to equal the height of view, for 95%.”

NSLayoutConstraint(item: label!, attribute: .height, relatedBy: .equal,
                   toItem: view, attribute: .height,
                   multiplier: 0.95, constant: 0.0)

Finally you add the contraints to your view: view.addConstraints([constraint1, constraint2, constraint3, constraint4]). The full example is:

 override func viewDidLoad() {
      super.viewDidLoad()

      self.edgesForExtendedLayout = []

      self.label = UILabel()
      self.label?.translatesAutoresizingMaskIntoConstraints = false
      self.label?.backgroundColor = UIColor.red
      self.label?.text = "label text"
      self.view.addSubview(self.label!)

      let horConstraint = NSLayoutConstraint(item: label!, attribute: .centerX, relatedBy: .equal,
                                             toItem: view, attribute: .centerX,
                                             multiplier: 1.0, constant: 0.0)
      let verConstraint = NSLayoutConstraint(item: label!, attribute: .centerY, relatedBy: .equal,
                                             toItem: view, attribute: .centerY,
                                             multiplier: 1.0, constant: 0.0)
      let widConstraint = NSLayoutConstraint(item: label!, attribute: .width, relatedBy: .equal,
                                             toItem: view, attribute: .width,
                                             multiplier: 0.95, constant: 0.0)
      let heiConstraint = NSLayoutConstraint(item: label!, attribute: .height, relatedBy: .equal,
                                             toItem: view, attribute: .height,
                                             multiplier: 0.95, constant: 0.0)

      view.addConstraints([horConstraint, verConstraint, widConstraint, heiConstraint])
  }
ios swift ios-autolayout

Swift 3 and iOS: Simple UITableView and UITableViewController with expandable sections

You can animate expanding sections by adding or removing all the rows in that section.

Create a UIViewController in your storyboard with a UITableView and a UITableViewCell named LabelCell. Give it a custom class like below.

class SomeViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet var tableView:UITableView?
    var hidden:[Bool] = [true, true]

    func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
        cell.textLabel?.text = "A row!"
        return cell
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 30
    }
...

This is fairly basic. It has an table view IBOutlet. It returns two sections, creates a reusable row cell as named above, and defines the section height as 30 pixels (I’m sure there’s an autolayout fix here…) The hidden property is the only interesting one. It’s a list of the hidden sections. We’re hiding everything currently.

Let’s look at the code that creates the section header:

  func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
      let label = UILabel()
      label.textAlignment = .left
      label.text = "I'm a test label"
      label.tag = section

      let tap = UITapGestureRecognizer(target: self, action: #selector(SomeViewController.tapFunction))
      label.isUserInteractionEnabled = true
      label.addGestureRecognizer(tap)

      return label
  }

It creates a UILabel which has a tag as the section number, and gives that a tap recogniser. Let’s look at the tap recogniser method:

  func tapFunction(sender:UITapGestureRecognizer) {
      let section = sender.view!.tag
      let indexPaths = (0..<3).map { i in return IndexPath(item: i, section: section)  }

      hidden[section] = !hidden[section]

      tableView?.beginUpdates()
      if hidden[section] {
          tableView?.deleteRows(at: indexPaths, with: .fade)
      } else {
          tableView?.insertRows(at: indexPaths, with: .fade)
      }
      tableView?.endUpdates()
  }

This toggles the section’s hidden property, then depending on that animates adding or removing that section’s rows. It gets the section number from the view tag that we set above, and creates index paths to define the rows we want to remove or add.

You’ll notice the 0..<3. The three would vary depending on your section’s rows. In our case we’re hard coding three as seen below:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if hidden[section] {
        return 0
    } else {
        return 3
    }
}

In numberOfRowsInSection when we’re hiding a section we show zero rows for that, otherwise we show three, or however many are in your table.

And voila.

ios swift ios-uitableview

Swift 3, iOS and Xcode 8: UISplitViewController

UISplitViewController is similar to the UINavigationController on the iPhone: it shows a master page and you can navigate to child views. However, on the iPad and suchlike the master and detail pages can be shown side-by-side.

Drag one onto your storyboard. It will create four controllers. The UISplitViewController has a “master view controller” relationship to a UINavigationController and a “detail view” relationship to a normal UIViewController. The UINavigationController has a “root view controller” relationship to a UITableViewController.

Let’s create a custom class for the UITableViewController which will just show a normal table:

class SplitTableController : UITableViewController {

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

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

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
        cell.textLabel?.text = "Section \(indexPath.section) Row \(indexPath.row)"
        return cell
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "Section \(section)"
    }
}

Opening the app won’t do something, except show the detail view controller first, and you’ll be able to back out to our table as populated above.

Let’s drag a “show detail” segue, called “showDetail”, from the UITableViewController to normal view controller working as our detail view. Now create a method that does something with that segue:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    self.performSegue(withIdentifier: "showDetail", sender: nil)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetail" {
        let index = self.tableView.indexPathForSelectedRow! as NSIndexPath
        let vc = segue.destination as! SplitDetailController
        vc.name = "hiya: "+String(index.row)
    }
}

This assumes you’ve created a custom class for our destination view controller called SplitDetailController and it has a outlet called name, which affects the view somehow. So create that now.

When you open the app now, the same will happen as before, except when you back out into the master view controller, you’ll be able to click on a cell and affect, and go to, the destination view controller as above.

On the iPad, in landscape view, you’ll see two panes. In iPad portrait, however, you’ll only see one, and you’ll have to drag from the left to pull out the table view. In the viewDidShow method in your table view controller, this will ensure it always shows both: self.splitViewController?.preferredDisplayMode = .allVisible

Finally, on the iPhone, we always go to the detail view controller first. To go to the mater view controller, our table view, first ensure the table view has the UISplitViewControllerDelegate protocol, and then override this method to say the master view controller should always collapse onto the detail view controller:

func splitViewController(_ splitViewController: UISplitViewController,
                         collapseSecondary secondaryViewController: UIViewController,
                         onto primaryViewController: UIViewController) -> Bool {
    return true
}
swift ios

Swift 3, iOS and Xcode 8: UIPageController and UIPageControl

These two elements allow you to swipe through UIViewControllers with little dots at the bottom of the screen.

First drag a UIPageController onto your storyboard. And give it a custom class. That custom class will look like this:

class MyPagesViewController : UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {

    let pages = ["PagesContentController1", "PagesContentController2"]

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        self.dataSource = self

        ...
    }

    func pageViewController(_ pageViewController: UIPageViewController,
                            viewControllerBefore viewController: UIViewController) -> UIViewController? {
      ...
    }

    func pageViewController(_ pageViewController: UIPageViewController,
                            viewControllerAfter viewController: UIViewController) -> UIViewController? {
      ...
    }

    func presentationCount(for pageViewController: UIPageViewController) -> Int {
      ...
    }

    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
      ...
    }
}

We’re extending from UIPageViewController and we have a delegate and data source protocols for our page view controller. We’re setting ourself as the delegate and datasource.

The pages array references view controller restoration identifiers. So create two new view controllers in your story board and ensure they have those identifiers.

The first thing we want to do is initialise our first view controller to show in viewDidShow.

  let vc = self.storyboard?.instantiateViewController(withIdentifier: "PagesContentController1")
  setViewControllers([vc!], // Has to be a single item array, unless you're doing double sided stuff I believe
                     direction: .forward,
                     animated: true,
                     completion: nil)

If you start the app now, you’ll get your first screen. Our first two delegate methods will allow us to page to the next screen, however:

func pageViewController(_ pageViewController: UIPageViewController,
                        viewControllerBefore viewController: UIViewController) -> UIViewController? {
    if let identifier = viewController.restorationIdentifier {
        if let index = pages.index(of: identifier) {
            if index > 0 {
                return self.storyboard?.instantiateViewController(withIdentifier: pages[index-1])
            }
        }
    }
    return nil
}

func pageViewController(_ pageViewController: UIPageViewController,
                        viewControllerAfter viewController: UIViewController) -> UIViewController? {
    if let identifier = viewController.restorationIdentifier {
        if let index = pages.index(of: identifier) {
            if index < pages.count - 1 {
                return self.storyboard?.instantiateViewController(withIdentifier: pages[index+1])
            }
        }
    }
    return nil
}

In both, we look for the restoration identifiers we set previously, then get get the index of such in our pages array, and we then return the previous view controller, if we’re in the method that says viewControllerAfter, otherwise we attempt to go forwards.

The other two delegate methods deal with the UIPageControl, which is automatically given to us when we created the UIPageViewController, although it does not appear in the storyboard.

func presentationCount(for pageViewController: UIPageViewController) -> Int {
    return pages.count
}

func presentationIndex(for pageViewController: UIPageViewController) -> Int {
    if let identifier = viewControllers?.first?.restorationIdentifier {
        if let index = pages.index(of: identifier) {
            return index
        }
    }
    return 0
}

We’re returning the number of pages, and in the latter we look for our current view controller, get its restoration id, and return the index of that to designate the page we’re on currently.

Finally, in your storyboad, on the UIPageViewController set the transition type to ‘scroll’, thereby showing those little dots on the bottom of the screen. To make them transparent this voodoo code that I found in a youtube video seems to work:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    for view in view.subviews {
        if view is UIScrollView {
            view.frame = UIScreen.main.bounds // Why? I don't know.
        }
        else if view is UIPageControl {
            view.backgroundColor = UIColor.clear
        }
    }
}

And voila.

swift ios

Page 2 of 7
Previous Next