iPad开发之Split Views

来源:互联网 发布:dnf鬼泣紫阵减防数据 编辑:程序博客网 时间:2024/06/17 00:48

一个Split View在iPad上通常是作为两个视图的结合体展示的,底层是用UISplitViewController来实现的;两个视图是它的子视图控制器的主视图。子视图控制器是split view控制器的viewControllersUISplitViewController的子视图控制器都通过spliteViewController属性来引用UISplitViewController。

split view控制器会根据设备的方向来管理它的子视图控制器的视图。

1.iPad处于横屏方向(landscape)
两个视图并列出现
2.iPad处于竖屏方向(portrait)
这时,有两种可能性:

  • 两个视图并列出现;如苹果的”设置”应用
  • 只有第二个视图出现,但是有一个选项可以召唤第一个视图从左边出现,作为覆盖物。例如苹果的”邮件”应用

一个split view通常是master-detail架构。第一个视图是UITableView,向用户展示列表,这是master;用户点击列表的某项来声明第二个视图应该展示什么内容,这是detail。因此我们将split view视图控制器的两个子视图控制器说成是master视图控制器和detail视图控制器。有时候苹果也称它们为primary视图控制器和secondary视图控制器。

在iPhone上,master-detail类型的界面通常会表现为导航式的界面:用户看到占据整个界面的master列表,当用户点击列表的某一项时就会导航到对应的占据整个界面detail视图,此时detail视图控制器被push到了导航栈中。而在iPad上,因为有足够的空间,所以按照上文所说的那样来管理视图的。

在iOS7及以前版本中,写一个在iPad上使用split view的universal类型的应用的话,你必须要通过在iPhone上使用navigation控制器和在iPad上使用split view控制器来实现完全不同的界面。

在iOS8及以后版本中,split view界面是具有适应性的,也就是说UISplitViewController可以在iPhone和iPad上创建和管理master-detail架构。而且split view控制器在结构上比之前更灵活;例如,在并列安排上,视图的宽度由你决定。

如果split view 控制器是根视图控制器的话,它就决定应用的旋转行为。为了不子类化UISplitViewController就能参与哪个决定,可以让某个对象来成为split view控制器的代理,即UISplitViewControllerDelegate,实现某些方法:

splitViewControllerSupportedInterfaceOrientations:
splitViewControllerPreferredInterfaceOrientationForPresentation:

注意:split view控制器不会决定它子视图控制器的状态栏外观。因此,当split view控制器是根视图控制器时,如果你想要隐藏状态栏,你得子类化UISplitViewController,或者将split view控制器包装在自定义的容器视图控制器中。

一.Expanded Split View Controller(iPad)
注:下文的Expanded翻译成”扩展的”

Xcode的Master-Detail应用程序模板会自动为你创建一个具有适配性的UISplitViewController。这里我们从一个空的应用工程开始,来创建一个split view controller。

示例代码如下:

AppDelegate.swift文件代码:

import UIKit@UIApplicationMainclass AppDelegate: UIResponder, UIApplicationDelegate {    var window: UIWindow?    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {        self.window = UIWindow(frame: UIScreen.mainScreen().bounds)        let svc = UISplitViewController()        let master = MasterController()        master.title = "Pep"        let nav1 = UINavigationController(rootViewController: master)        let detail = DetailController()        let nav2 = UINavigationController(rootViewController: detail)        svc.viewControllers = [nav1,nav2]        self.window!.rootViewController = svc        self.window!.backgroundColor = UIColor.whiteColor()        self.window!.makeKeyAndVisible()        let b = svc.displayModeButtonItem()        //UISplitViewController的displayModeButtonItem()创建一个用于在竖屏下展示第一个视图的barButtonItem        detail.navigationItem.leftBarButtonItem = b        return true    }    func applicationWillResignActive(application: UIApplication) {    }    func applicationDidEnterBackground(application: UIApplication) {    }    func applicationWillEnterForeground(application: UIApplication) {    }    func applicationDidBecomeActive(application: UIApplication) {    }    func applicationWillTerminate(application: UIApplication) {    }}

MasterController.swift文件代码

import UIKitclass MasterController: UITableViewController {    let model = ["Manny","Moe","Jack"]    override func viewDidLoad() {        super.viewDidLoad()        self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")    }    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {        let cell = tableView.dequeueReusableCellWithIdentifier("cell",forIndexPath: indexPath)        cell.textLabel!.text = model[indexPath.row]        return cell;    }    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {        return model.count    }    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {        let detail = DetailController()        detail.boy = model[indexPath.row]        let b = self.splitViewController!.displayModeButtonItem()        detail.navigationItem.leftBarButtonItem = b        detail.navigationItem.leftItemsSupplementBackButton = true//leftItemsSupplementBackButton属性默认为false;如果设为true,则leftBarButton和returnBarButton同时存在时,可以一起出现        let nav = UINavigationController(rootViewController: detail)        self.showDetailViewController(nav, sender: self)        //showDetailViewController()用于显示新的detail view控制器    }}

DetailController.swift文件代码:

import UIKitclass DetailController: UIViewController {    var lab : UILabel!    var boy : String = "" {        didSet {            if self.lab != nil {                self.lab.text = self.boy            }        }    }    override func viewDidLoad() {        super.viewDidLoad()        self.view.backgroundColor = UIColor.whiteColor()        let lab = UILabel(frame: CGRectMake(100,100,100,30))        self.view.addSubview(lab)        self.lab = lab        self.lab.text = self.boy    }}

二.Collapsed Split View Controller(iPhone)
注:下文的Collapsed翻译成”折叠的”

如果我们在iPhone上运行刚刚创建的应用,它也会运行:展示导航控制器的界面,通过pop或push来执行master视图控制器和detail视图控制器间的切换。

唯一的缺点就是应用启动的时候,首先展示的是detail视图,而不是master视图。为了修改这个问题,我们将应用的delegate(即AppDelegate类)作为UISplitViewController的代理,如下:

class AppDelegate: UIResponder, UIApplicationDelegate,UISplitViewControllerDelegate....svc.delegate = self

然后我们要实现方法:splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:,返回true

func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {        return true    }

这样,在iPhone上,应用运行的非常完美!

那么,这个是如何实现的呢?为了理解split view控制器做了哪些事情,你需要知道它采用了哪种状态:”折叠的”还是非”折叠的”。这种差别分别会对环境的trait collection是否有.Compacthorizontal size class作出反应。如果有的话,split view控制器就会折叠。因此,split view控制器在iPhone上启动就会折叠。

一个折叠的split view视图控制器只有一个子视图控制器。那么,问题就来了,如何从扩展的状态变为折叠的状态呢?当split view控制器折叠时,它会询问它的代理如何去处理。通常,会调用下面代理方法:

  • primaryViewControllerForCollapsingSplitViewController::折叠的split view控制器只有一个子视图控制器。那么这个控制器应该是哪个呢?默认是返回当前的第一个视图控制器,但是你可以实现这个方法来返回不同的视图控制器。
  • splitViewController:collapseSecondaryViewController:ontoPrimaryViewController::正在折叠的split view控制器将会移除它的第二个视图控制器,让它的第一个视图控制器作为它唯一的子视图控制器。返回true来允许这样进行。如果这个方法返回false(默认是false),split view控制器就会发送collapseSecondaryViewController:forSplitViewController:消息给第一个视图控制器。那么,第二个视图控制器发生什么就取决于第一个视图控制器。

结合下我们的上述代码:

我们的第一个视图控制器是UINavigationController,通过将特定的第二个视图控制器push到它自己的栈中来响应collapseSecondaryViewController:forSplitViewController:消息。这可以解释我们所看到的结果;如果我们没有实现splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:代理方法返回true的话,iPhone上运行的结果就会是展示detail视图。因此,我们实现了该代理方法返回true,从而允许split view控制器移除它的第二个视图控制器,所以我们在iPhone上运行时可以直接看到master视图。

应用现在启动了,它的根视图控制器是split view控制器,而split view 控制器有一个子视图控制器,即UINavigationController,同时UINavigationController也有一个子视图控制器,就是MasterViewController。因此table view就会出现在导航界面上,用户点击某一行时,代码就会发送showDetailViewController:sender:消息给MasterViewController,结果就是遍历视图控制器层次,来寻找某个控制器来处理这个方法。当然,遍历到UISplitViewController时,变回处理这个消息,方式是如下其中一种:

  • 如果split view控制器没被折叠,它接收指定的视图控制器作为自己的第二个视图控制器。第二个视图控制器的视图被展示了,所以用户将它看作是detail视图。这是iPad上所发生的;
  • 如果split view控制器被折叠,它就会发送showViewController:sender:给第一个视图控制器。而第一个视图控制器刚好是导航控制器,所以导航控制器就会push特定的控制器到它的栈中。

三.Expanding Split View Controller(iPhone 6 Plus)
注:下文的Expanding翻译成”正在扩展的”

iPhone 6 Plus是一个有趣的混合例子:竖屏方向上,水平是compact(紧凑)的;而横屏方向上,水平是regular的。所以,split view控制器会认为iPhone 6 Plus处于竖屏方向是iPhone,处于横屏方向是iPad。那么,split view控制器在同一个设备上会切换collapsed的值(true或false),在竖屏上,split view 控制器会显示navigation界面:master视图控制器作为根视图控制器,像iPhone一样;在横屏上,master视图和detail视图会并排显示,像iPad一样。

应用运行在iPhone 6 Plus上,如果处于竖屏方向,split view控制器会像在iPhone上执行那样;当设备旋转处于竖屏方向式,split view控制器执行与collapsing(折叠)相反的操作,苹果称为expanding(扩展)。当split view控制器扩展时,它会询问它的代理如何处理:

  • primaryViewControllerForExpandingSplitViewController:折叠的split view只有一个子视图控制器;而扩展的split view控制器有两个子视图控制器。哪个视图控制器应当是它的第一个子视图控制器呢?默认是当前的子视图控制器,但是你可以实现这个方法来返回不同的。
  • splitViewController:separateSecondaryViewControllerFromPrimaryViewController : 哪个视图控制器是扩展的split view控制器的第二个子视图控制器?实现这个方法可以提供。如果你没有实现这个方法或者这个方法返回nil,split view控制器就会发送separateSecondaryViewControllerForSplitViewController:消息给第一个视图控制器。如果这个方法返回一个视图控制器,split view控制器就会将其设为它的第二个视图控制器。

当我们在iPhone 6 Plus上从横屏旋转成竖屏时,一个有趣的问题发生了。横屏上处于detail视图时,如果旋转成为竖屏时,就会处于master视图,而不是原来的detail视图。为什么呢?返回到之前的代理方法:

func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {        return true    }

就是因为这个方法导致出现的这种情况,那么如何解决呢?我们可以让split view控制器的代理跟踪用户是否选择了detail视图。可以使用一个实例属性self.didChooseDetail

var didChooseDetail = false    func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController : UIViewController, ontoPrimaryViewController : UIViewController) -> Bool {        return !didChooseDetail    }
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {....        let del = UIApplication.sharedApplication().delegate as! AppDelegate        del.didChooseDetail = true    }

四.自定义Split View控制器

UISplitViewController的属性和代理方法允许我们自定义:

通过属性自定义:

1.preferredDisplayMode:设置这个属性来改变扩展的split view控制器的当前展示模式,或者将它设置为.Automatic来让展示模式采取默认值。一个扩展的split view控制器有三种可能的展示模式(UISplitViewControllerDisplayMode):

  • PrimaryHidden
  • AllVisible
  • PrimaryOverlay

默认的automatic行为(即将preferredDisplayMode设置为.Automatic):

  • iPad横屏:displayModeButtonItem隐藏,preferredDisplayMode.AllVisible
  • iPad竖屏:displayModeButtonItem显示,preferredDisplayMode.AllVisible.PrimaryHidden之间切换

2.preferredPrimaryColumnWidthFraction:设置master视图在.AllVisible.PrimaryOverlay模式下的宽度,以整个split view的百分比形式(0到1之间)。你的设置可能没有用处除非你也通过maximumPrimaryColumnWidthminimumPrimaryColumnWidth属性来设置宽度的范围。为了知道使用的实际宽度,使用primaryColumnWidth属性。

通过代理方法自定义:

1.splitViewController:willChangeToDisplayMode::一个扩展的split view控制器的displayMode即将改变时调用这个方法,意味着第一个视图控制器的视图展示或隐藏。

2.targetDisplayModeForActionInSplitViewController::该方法返回一个UISplitViewControllerDisplayMode来声明用户点击displayModeButtonItem后要做什么。影响displayMode的事情发生时调用该方法:

  • split view控制器第一次展示时
  • 界面旋转时
  • 用户召唤或者移走primary视图时

当折叠或扩展后,UISplitViewController会发送UIViewControllerShowDetailTargetDidChangeNotification

五.设置折叠状态(Collapsed State)

split view控制器可以处于折叠或者扩展状态,它的collapsed属性可为true或false。但是这个属性是可读的,那么该如何设置折叠的状态呢?因为有时候,我们想要在iPhone上也可以并列展示两个子视图控制器的视图。

split view控制器会根据具体环境来决定采取哪种状态,即当前的trait collection的水平size class是否是.Compact。因此,我们可以”骗”split view控制器,告诉假的trait collection环境,从而使split view 控制器相信它就是在iPad上。你可以实现那样的效果通过将自定义的视图控制器容器放在split view控制器之上,即作为split view控制器的直接父视图控制器。然后,你可以向自定义的视图控制器容器发送svc.setOverrideTraitCollection:forChildViewController:消息,它就会将消息中的参数trait collection传递给split view 控制器。

下面的示例中,自定义的视图控制器容器是应用的根视图控制器,而它的子视图控制器是split view控制器。在应用声明周期的早些阶段,视图控制器容器配置了split view控制器。

var didInitialSetup = falseoverride func viewWillLayoutSubviews() {        if !self.didInitialSetup {            self.didInitialSetup = true            self.view.backgroundColor = UIColor.greenColor()            let svc = self.childViewControllers[0] as! UISplitViewController            svc.preferredDisplayMode = .AllVisible            svc.preferredPrimaryColumnWidthFraction = 0.5            svc.maximumPrimaryColumnWidth = 500                        let traits = UITraitCollection(traitsFromCollections: [          UITraitCollection(horizontalSizeClass: .Regular)        ])                self.setOverrideTraitCollection(traits, forChildViewController: svc)            }        }

结果就是在设备横屏或竖屏上,split view控制器并列展示它的两个子视图控制器的视图。

还有一种可能性,就是让iPhone表现得像iPhone 6 Plus一样,横屏的水平size class是.Regular,而竖屏的水平size class是.Compact

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {        let svc = self.childViewControllers[0] as! UISplitViewController            if  size.width > size.height {                let traits = UITraitCollection(traitsFromCollections: [                    UITraitCollection(horizontalSizeClass: .Regular)                    ])                self.setOverrideTraitCollection(traits, forChildViewController: svc)            } else {                self.setOverrideTraitCollection(nil, forChildViewController: svc)            }        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)    }

上面的两端代码都可以实现在iPhone上并排显示两个子视图控制器的视图,但是第二段代码更加灵活。

六.取代子视图控制器
下面把”percolate”翻译成”过滤”

在前面的讨论中,我们并没有直接把showDetailViewController:sender:消息发送给split view 控制器;相反,我们把它发送给self,即master view控制器,我们能确保消息可以层层过滤,传递给split view控制器。

iOS提供了一种架构,消息(应该是特定的消息,并不是每个消息都可以这样)可以沿着视图控制器层次向上过滤。这个架构的核心是svc.targetViewControllerForAction:sender:方法,该方法返回一个视图控制器。这个方法中,action:参数是方法的selector,这个方法会查看接收消息的视图控制器是否对讨论中的方法(即showDetailViewController:sender:)进行了实现,如果实现的话,就返回self;如果没有的话,将对视图控制器层次递归,返回它的父视图控制器或者它的展示视图控制器调用该方法的结果;如果没有视图控制器实现的话,就返回nil。

其实,我们的自定义方法也可以参与”过滤机制”(percolation mechanism),通过扩展UIViewController来实现你的自定义方法,它可以调用targetViewControllerForAction:sender:

extension UIViewController {    fun showHide(sender:anyObject?){   let target = self.targetViewControllerForAction("showHide:",sender:sender)   if target !=nil {   target!.showHide(self)  } }}
0 0
原创粉丝点击