View Controller

View Controller

我们已经和类UIViewController打过一次照面,它被称为视图控制器,被用来装入和释放视图、管理视图交互、并且和其他视图控制器一起协作完成整体的App界面。为了术语一致,后文会直接使用它的英文名:View Controller。

View Controller可以管理一个视图层级体系。首先,每个View Controller内含一个view属性,作为所有视图的根视图。如果在一个视图控制器内加入一个按钮视图和一个标签视图,分别名为button、label,那么层次系统如下:

ViewController:UIViewController
    view:UIView
        button:UIButton
        label:UILabel

为了演示View Controller的视图管理能力,本文引入一个微小的App作为案例,界面和功能如下:

  1. 界面包括一个按钮和一个标签,标签初始值为0
  2. 当点击按钮时,标签的数字会被累加1

代码如下:

import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        let page = Page1()
        self.window!.rootViewController = page
        self.window?.makeKeyAndVisible()
        return true
    }
}
class Page1: UIViewController {
    var count = 0
    var label : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        label   = UILabel()
        label.frame = CGRect(x: 100, y: 100, width: 20, height: 50)
        label.text =  "0"
        view.addSubview(label)
        let button   = UIButton(type: .system)
        button.frame = CGRect(x: 120, y: 100, width: 20, height: 50)
        button.setTitle("+",for: .normal)
        button.addTarget(self, action: #selector(Page1.buttonAction(_:)), for: .touchUpInside)
        view.addSubview(button)
    }
    func buttonAction(_ sender:UIButton!){
        self.count +=  1
        label.text =  self.count.description
    }
}


编译运行后,你可以看到界面上的按钮和标签,当点击按钮时,每次点击,标签的值会加1。

代码解释下:

  1. 属性UIViewController.view是所有子视图的根视图,可以把其他view加入其内
  2. 按钮类为UIButton ,可以通过其属性frame设置位置和大小,可以通过view.addSubview把按钮加入为view的子视图
  3. 标签类为UILabel,可以如同Button一样设置位置和大小并加入到视图层次化体系内
  4. button可以添加事件,通过方法:

    button.addTarget(self, action: #selector(Page1.buttonAction(_:)), for: UIControlEvents.touchUpInside)

View Controller根据管理对象的不同,分为两种类型:

  1. Content View Controller ,内容型
  2. Container View Controller,容器型

内容型的视图控制器只能加入和管理视图。而容器型的则还可以管理其他的View Controller。容器型存在的目的是为了更好的做View Controller之间的导航,比如UINavigationController, UITabBarController就是容器型的。

如下案例演示一个Container View Controller:

import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window!.rootViewController = Page1()
        self.window?.makeKeyAndVisible()
        return true
    }
}
class Page1: UIViewController{
    var c : UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()
        c = UIButton()
        c.setTitle("Page1",for: .normal)
        c.frame = CGRect(x: 10, y: 50, width: 200, height: 50)
        view.addSubview(c)
        c.backgroundColor = .blue
        let page2 = Page2()
        self.addChildViewController(page2)
        page2.view.frame = CGRect(x: 10, y: 100, width: 200, height: 50)
        view.addSubview(page2.view)
    }
}
class Page2: UIViewController{
    var c : UIButton!
    var count = 0
    override func viewDidLoad() {
        super.viewDidLoad()
        c = UIButton()
        c.backgroundColor = .red
        c.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
        c.setTitle("0",for: .normal)
        c.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
        view.addSubview(c)
    }
    func buttonAction(_ sender:UIButton!){
        self.count +=  1
        c.setTitle(self.count.description,for: .normal)
    }
}

本案例涉及到2个View Controller类,分别为Page1、Page2。其中Page1就是一个Container View Controller。

  1. Page1作为Root View Controller,嵌入到Window内,占据全部屏幕大小,它内嵌一个按钮和Page2——也就是另外一个View Controller。
  2. Page2中有一个按钮,点击时会给按钮的标题的数字加1。

为了让Page2的视图可以显示在Page1内,需要把Page2.view属性作为子视图加入到Page1.view内,并且通过:

self.addChildViewController(page2)

把Page2设置为Page1的Child View Controller,目的是为了重用Page2的已有的逻辑。

直接使用UIViewController的不多,更多的是直接使用特定目的的View Controller,常用的特定目的的类有这些:

  1. AlertController,警示框控制器
  2. NavigationController,导航控制器
  3. PageViewController,分页控制器
  4. TabBarController,标签页控制器

后文会继续介绍。

AlertController

根据选择的风格的不同,UIAlertController可以用来向用户显示一个警告信息、或者是显示一个可触控的操作列表。

首先查看一个警告信息的代码:

import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window!.rootViewController = Page()
        self.window?.makeKeyAndVisible()
        return true
    }
}
class Page:UIViewController{
    override func viewDidAppear(_ animated: Bool) {
        Alert()
    }
    func Alert(){
        let alert = UIAlertController(title: "title", message: "message", preferredStyle:.alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!) in
            print("OK")
        }))
        alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler:nil))
        self.present(alert,animated: true,completion: nil)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.black
    }
}

执行后可以显示一个有标题和内容的警告消息,可以点击警告消息框内的按钮来执行指定的代码。UIAlertController可以通过构造函数:

     UIAlertController(title: "title", message: "message", preferredStyle:.alert)

来指定警告信息的标题、正文和样式。可以通过类似如下的代码:

       alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!) in
                print("OK")
            }))

在此警告框内创建一个可以点击的按钮,点击此按钮后要执行的代码以匿名函数的形式通过handle参数添加。

可以使用:

    UIViewController.present(_:animated:completion:) 

来显示此警告框。

设定preferredStyle为.alert创建的就是警告信息框,如果想创建操作列表框的话,那么请设定preferredStyle为.actionSheet即可。操作列表框就是一个在屏幕下方显示一个类似菜单的选择器,每个选项都可以单独触控并执行代码。实例如下:

class Page:UIViewController{
    override func viewDidAppear(_ animated: Bool) {
        ActionSheet()
    }
    func ActionSheet() {
        let sheet: UIAlertController = UIAlertController(title:nil, message:nil, preferredStyle:UIAlertControllerStyle.actionSheet)
        sheet.addAction(UIAlertAction(title:"Do something 1", style:UIAlertActionStyle.default, handler:{ action in
            print ("Do something 1")
        }))
        sheet.addAction(UIAlertAction(title:"Do something 2", style:UIAlertActionStyle.default, handler:{ action in
            print ("Do something 2")
        }))
        sheet.addAction(UIAlertAction(title:"Cancel", style:UIAlertActionStyle.cancel, handler:nil))
        self.present(sheet, animated:true, completion:nil)
    }
}

操作列表框的创建方法警告信息框的创建方法是非常类似的。

NavigationController

类NavigationController常用于层次化界面导航。

NavigationController可以包含多个ViewController、一个UINavigationBar、一个可选的UIToolbar。

本文演示一个三层导航的案例,操作和UI描述如下:

  1. 共三层层次化界面
  2. 每一级页面内有一个按钮,可以继续导航到下一级页面
  3. 每一个页面的导航条的左侧按钮可以返还到上一级

代码如下:

import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var nav : Nav?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        nav = Nav()
        self.window!.rootViewController = nav
        self.window?.makeKeyAndVisible()
        return true
    }
}
class Nav: UINavigationController {
    var count = 0
    var label : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        self.pushViewController(Level1(), animated: true)
    }
}

class Level1: UIViewController {
    var count = 0
    var label : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Level 1"
        self.view.backgroundColor = .white
        let button   = UIButton(type: .system)
        button.frame = CGRect(x: 120, y: 100, width: 100, height: 50)
        button.setTitle("Dive Into 2",for: .normal)
        button.addTarget(self, action: #selector(Level1.buttonAction(_:)), for: .touchUpInside)
        view.addSubview(button)
    }
    func buttonAction(_ sender:UIButton!){
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        appDelegate.nav?.pushViewController(Level2(), animated: true)
    }
}

class Level2: UIViewController {
    var count = 0
    var label : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Level 2"
        self.view.backgroundColor = .white
        let button   = UIButton(type: .system)
        button.frame = CGRect(x: 120, y: 100, width: 100, height: 50)
        button.setTitle("Dive Into 3",for: .normal)
        button.addTarget(self, action: #selector(Level2.buttonAction(_:)), for: .touchUpInside)
        view.addSubview(button)
    }
    func buttonAction(_ sender:UIButton!){
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        appDelegate.nav?.pushViewController(Level3(), animated: true)
    }
}
class Level3: UIViewController {
    var count = 0
    var label : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Level 3"
        self.view.backgroundColor = .white
        let button   = UIButton(type: .system)
        button.frame = CGRect(x: 120, y: 100, width: 100, height: 50)
        button.setTitle("End",for: .normal)
        button.addTarget(self, action: #selector(Level3.buttonAction(_:)), for: .touchUpInside)
        view.addSubview(button)
    }
    func buttonAction(_ sender:UIButton!){

    }
}


解释下代码:

  1. 此代码共有5个类:AppDelegate、Nav、Level1、Level2、Level3
  2. 类Nav继承于UINavigationController,实例化此类并设置到window.rootViewController上
  3. 类Level1、Level2、Level3都继承于UIViewController
  4. Nav类视图加载完成后,把Level1的实例压入导航视图
  5. UINavigationController本身默认提供页面的顶部条状区域,它被称为导航条(UINavigationBar),点击此导航条的左按钮可以返还到上一级页面;
  6. 导航条中间显示一个字符串,默认值为当前层次ViewController.title的属性值

PageViewController

PageViewController可以管理多个页面,每个页面都是一个ViewController,可以使用程序控制或者用户手势来切换页面。

我们来看案例。案例App外观和交互描述如下:

  1. App共有两个页面
  2. 默认进来的是Page1类实例代表的页面,此页面内有一个标签,标签显示为"#1",
  3. 另外一个页面是Page2类实例代表的页面,此页面内有一个标签,标签显示为"#2"
  4. 可以使用手势(左划和右划)切换也的显示两个页面
  5. 屏幕下方有一个蓝色背景的矩形,内有两个圆点,一个白色一个灰色,当切换页面时,圆点会切换颜色,以便指示当前页面

如下代码,展示了PageViewController的使用:

import UIKit
class VCBase: UIViewController
{
    var label : UILabel?
    override func viewDidLoad()
    {
        super.viewDidLoad()
        label = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 200))
        label!.textAlignment = .center
        view.addSubview(label!)
        view.backgroundColor = UIColor.white
    }
}
class Page1: VCBase
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        label!.text = "#1"
    }
}
class Page2: VCBase
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        label!.text = "#2"
    }
}
class PageViewController :UIPageViewController,UIPageViewControllerDataSource{
    var vcs :[UIViewController]
    required init(){
        vcs = [Page1(),Page2()]
        super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
    }
    required  init?(coder: NSCoder){
        fatalError()
    }
    override func viewDidLoad() {
        self.dataSource = self
        setViewControllers( [vcs[0]], direction: .forward, animated: false, completion: nil)
    }
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
    {
        let v = (viewController)
        let i = vcs.index(of: v)! - 1
        if i < 0 {
            return nil
        }
        return vcs[i]
    }
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
    {
        let v = (viewController)
        let i = vcs.index(of: v)! + 1
        if i > vcs.count - 1 {
            return nil
        }
        return vcs[i]
    }
    func presentationCount(for pageViewController: UIPageViewController) -> Int
    {
        return vcs.count
    }
    func presentationIndex(for pageViewController: UIPageViewController) -> Int
    {
        return 0
    }
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window!.rootViewController = PageViewController()
        self.window!.rootViewController!.view.backgroundColor = .blue
        self.window?.makeKeyAndVisible()
        return true
    }
}

此处代码的前两个类Page1,Page2都是UIViewController的子类,每个初始化一个标签(UILabel),标签的内容是不同的,以示两个页面的区别。类PageViewController是UIPageViewController的子类,实现了UIPageViewControllerDataSource并在装入时设置数据源为自身。通过:

     setViewControllers( [vcs[0]], direction: .forward, animated: false, completion: nil)

设置PageViewController的当前的ViewController,值为Page1的实例,因此App启动后显示的初始页面就是Page1。

协议UIPageViewControllerDataSource的内容如下:

    public protocol UIPageViewControllerDataSource : NSObjectProtocol {
        public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
        public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
        optional public func presentationCount(for pageViewController: UIPageViewController) -> Int 
        optional public func presentationIndex(for pageViewController: UIPageViewController) -> Int 
    }

其中前两个函数会由UIKit传入当前视图,希望App返回此视图的前一个视图或者是后一个视图。后两个函数分别返回总页面数量和初始页面对应的选择项索引,这两个函数是可选实现的,如果实现了,就会在页面底部显示页面指示器,也就是我们最早提到的,在屏幕下方显示的两个圆点。

TabBarController

类UITabBarController可以包含多个UIViewController的实例,并且在页面底部显示一个Tabbar作为UIViewController的切换显示开关。

案例界面和交互是这样的:

  1. 屏幕下方矩形内显示两个正方形操作图标,且图标下有文字,分别为Page1、Page2
  2. 左上角显示一个标签为“Page #1”
  3. 点击操作图标的Page 2,则左上角显示一个标签为“Page #2”
  4. 可以触控对应操作图标,在Page1、Page2之间切换

代码如下:

import UIKit
class Page1: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let cc = UILabel(frame: CGRect(x: 10,y: 50,width: 200,height: 50))
        cc.text = "Page #1"
        cc.textColor = UIColor.black
        self.view.addSubview(cc)
    }
}
class Page2: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let cc = UILabel(frame: CGRect(x: 10,y: 50,width: 200,height: 50))
        cc.text = "Page #2"
        cc.textColor = UIColor.black
        self.view.addSubview(cc)
    }
}
class Tabbar: UITabBarController, UITabBarControllerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }

    override func viewWillAppear(_ animated: Bool) {
        viewControllers = [Page1(),Page2()]
        let r = UIImage.imageWithColor(UIColor.black)
        viewControllers![0].tabBarItem = UITabBarItem(title: "Page 1",
                        image:r,
            tag:0)
        viewControllers![1].tabBarItem = UITabBarItem(title: "Page 2",
            image: r,
             tag:1)

    }
    //Delegate methods
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
        print("did select viewController: \(viewController.tabBarItem.tag)")
    }
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window!.rootViewController = Tabbar()
        self.window!.rootViewController!.view.backgroundColor = UIColor.white
        self.window?.makeKeyAndVisible()
        return true
    }
}
extension UIImage {
    class func imageWithColor(_ color: UIColor) -> UIImage {
        let rect = CGRect(x: 0.0, y: 0.0, width: 10.0,height: 10.0 )
        UIGraphicsBeginImageContext(rect.size)
        let context = UIGraphicsGetCurrentContext()
        context?.setFillColor(color.cgColor)
        context?.fill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }
}

本案例中,共有三个需要关注的类:

  1. Page1。UIViewController的子类,内含一个UILabel,显示字符串为Page #1
  2. Page2。UIViewController的子类,内含一个UILabel,显示字符串为Page #2
  3. Tabbar是UITabBarController的子类,实现协议UITabBarControllerDelegate,并在viewDidLoad时设置委托到自身;其属性viewControllers是一个数组,用于装入多个UIViewController的实例,当执行:

    func viewWillAppear(_ animated: Bool)
    
    

初始化此数组,创建需要选择的UIViewController实例进来即可。

把Tabbar的实例它赋值给self.window!.rootViewController上,这样就把它设置为App的根视图控制器了。

目录