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)
}
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])
}
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.
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
}
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.