iOS编程之UIScrollView的使用[AutoLayOut+NavigationBar+Keyboard](Xcode6.3 iOS8.x Swift Storyboard)

本篇文章介绍IOS编程中如何使用UIScrollView,包括UIScrollView的基本原理、AutoLayOut以及视图内容被Keyboard遮挡时的自动滚动等等。

1. Understanding UIScrollView

关于IOS中各种内容是如何显示到屏幕上的,即视图渲染(rendering)的过程,这篇文章有详细的介绍;关于UIScrollView的基本原理(可能用原理这个词不太适合)比如各种尺寸(size)、偏移(offset)等等的定义,这篇文章中有非常深入详细生动的讲解;关于UIScrollView的各种方法属性等等,可以查看apple的相关开发文档。以上参考内容是本节的主要依据。

1.1 Rasterization and Composition

这小节的内容主要是说明在IOS中视图(view)是如何显示到屏幕上的,即Rendering(渲染)的过程。

首先,IOS中界面的Rendering(渲染)过程由Rasterization(光栅化、点阵化)和 Composition(合成)组成。Rasterization的含义是:

Rasterisation (or rasterization) is the task of taking an image described in a vector graphics format (shapes) and converting it into a raster image (pixels or dots) for output on a video display or printer, or for storage in a bitmap file format.

即将矢量图形转化成点阵图形的过程。Composition(合成)则是将不同的内容的图形进行叠加组合的过程。

第二,任意要显示的内容(视图、按钮等等)都具有两个非常重要的矩形边界量:bounds和frame。在布置界面的时候,我们通过frame来定义内容的位置和尺寸。一般情况下,frame和bounds大小一样,但原点通常不同。这两个矩形边界是UIScrollView工作的核心内容。

在Rasterization的时候,我们并不关心一个显示内容会在Composition阶段被怎样合成,所以,frame矩形此时没有作用。内容的Rasterization通过drawRect方法实现,drawRect的边界即以bounds矩形定义,由左上角点{x:0, y:0}和右下角点{x:width, y:hight}共同决定。

在Composition的时候,各个内容点阵化画得图形叠加到其上级内容的点阵化图形上,这时候就需要通过frame来确定各个内容之间的位置关系。首先是frame的原点,它表示子内容相对其上级内容在左上角位置上的偏移量,比如,一个{x:20, y:15}的偏移量表示该子内容的左上角原点相对于其上级内容往右、下分别偏移了20和15个像素点;然后是frame的尺寸,一般和bounds中的尺寸相同,即为{x:width, y:hight}。

下图是以UIButton为子内容UIView为上级内容时的一个例子。

ios-uniscrollview-bounds-frame

UIButton的bounds矩形为{x:0, y:0, w:80, h:30},UIView的bounds矩形为{x:0, y:0, w:110, h:70},由于UIButton的原点相对UIView的原点有一个右20下15的偏移量,所以,UIButton的frame矩形为{x:20, y:15, w:110, h:70}。

这里UIButton只有一个上级内容—-UIView,所以关系比较简单,如果UIView还有上级内容(当然UIView的上级只能是View级别的内容),则继续向上嵌套,如滚雪球一般(snowball effect)。

上面提到的那篇文章为了解释后面的UIScrollView,定义了两个公式:

CompositedPosition.x = View.frame.origin.x – Superview.bounds.origin.x;

CompositedPosition.y = View.frame.origin.y – Superview.bounds.origin.y;

这两个公式定义得非常难以理解,所以我将其修改为:

CompositedPosition.x = This.frame.origin.x + Super.frame.origin.x;

CompositedPosition.y = This.frame.origin.y + Super.frame.origin.y;

对照上图中的例子,This即是指UIButton,Super则是指UIView,于是:

This.frame.origin.x = 20;

This.frame.origin.y = 15;

Super.frame.origin.x = 0;

Super.frame.origin.y = 0;

所以,Composition后UIButton的原点位置就是{x:20, y:15}。

进一步,如果UIView还有一个上级的UIView,那么,UIButton的直接上级UIView的frame的原点就可能不是{x:0, y:0}了,就需要先计算出来。

1.2 UIScrollView的各种尺寸

首先,UIScrollView本身对于IOS的rendering来说,也是一个上文所述的“内容”,所以,它也首先具有bounds和frame所定义的矩形,其中bounds主要定义其尺寸,而frame定义其在上级“内容”(这里当然只可能是一个View级别的内容)中的位置。

当UIScrollView的尺寸位置确定后,就可以进一步确定其内容(content)的尺寸和位置。这里和前文所述的确定子内容的frame的方法有所区别,这里UIScrollView的子内容一般不用bounds和frame来定义其尺寸和位置,而是直接通过使用UIScrollView的专门方法来定义。这些方法主要有:contentSize、contentOffset和contentInset,下图非常清楚地展示了各个方法的含义。

ios-uiscrollview-contentsize-contentoffset-contentinset

图中以一张图片作为UIScrollView的子内容(其实这个子内容是UIImageView,也是一个View,所以当然也有bounds和frame,但是一般不关心它们),其中:

contentSize包含两个项目width和hight,意为子内容的宽度和高度;

contentOffset包含两个项目x和y,意为子内容原点相对UIScrollView原点的左方和上方的距离(注意这里的取值正负,在以左以上为正);

contentInset包含四个值top、left、bottom、right,意为Edge insets,即对内容在四个边上的缩小(负值)或扩大(正值)。

2. Manipulating UIScrollView

此节结合实例,介绍UIScrollView的具体用法,例子中由于需要在一个界面上布置很多文本和输入框,而屏幕高度不够,所以需要使用UIScrollView。主要涉及AutoLayOut(Dashboard),弹出键盘(Keyboard)时被遮挡内容自动滚动等。

2.1 创建名为IosUIScrollViewDemo的project

具体操作此处略去,参考这篇文章

2.2 将View嵌到Navigation Controller中

为了说明如何使UIScrollView在Navigation Controller中正确显示,故将View嵌入Navigation Controller中。方法是选中View,点击工具栏Editor->Embed In->Navigation Controller。如下图所示。

ios-editor-embbed-in-navigation-controler

添加Navigation Item,并修改名称为“Navigation Bar”,如下图。

ios-Navigation-bar-item-change-name

2.3 修改View的名称、尺寸

一般我们保留初始的View,把UIScrollView等其他内容布置到这个View上,为了不致混淆,我们将这个在Dashboard中的document outline中显示为View的视图改名为Main View,如下图。

ios-uiscrollview-change-view-to-main-view
为了能够容纳足够的内容,需要将Main View的尺寸改变,方法是选中View Controller,选择Size inspector,改simulated size为free form,并修改hight值为800。如下图。

ios-uiscrollview-change-view-controller-simu-size

2.4 添加UIScrollView

从Object library中选择UIScrollView拖拽到Main View中,如下图。

ios-uiscrollview-add

调整UIScrollView的尺寸和位置,并添加AutoLayout约束,如下图。

ios-UIScrollView-autolayout-constraints

2.5 添加Content View

由于我们需要在UIScrollView中滚动显示众多内容,一般地选择将这些内容先一起放到一个View中,然后再把这个View放到UIScrollView中。可以直接从Object library中拖拽一个View到UIScrollView中,也可以全部选中已经放到UIScrollView中的各种内容,然后点击工具栏Editor->Embed In->View,这里我们先在UIScrollView中添加一个View,并在Dashboard中的document outline中修改其名称为Content View。如下图。

ios-UIScrollView-ContentView-change-name

我们将Content View的尺寸调整到与Scroll View一样,即宽高各为600、736。然后为Content View添加AutoLayout约束。如下图。

ios-UIScrollView-Contentview-autolayout-contraints

由于Content View的尺寸与Scroll View一样,故各方向边距为0。

2.6 补全AutoLayout尺寸constraints

我们觉得已经为Scroll View和Content View都添加好了约束,于是应该一切就绪了,但是却发现还有Issue在,于是我们点开Issue navigator,查看到下图中的问题。

ios-UIScrollView-issue

Issue的意思是有两个view的水平方向尺寸定义不明,可是我们看上图右侧Scroll View和Content View不是都完整地定义了四个约束了吗?由于UIScrollView的特殊性,ios中不能通过Scroll View和Content View与Main View的相对位置关系让系统推断其各自的实际大小,必须明确指定出来。但是有意思的是,我们可以指定Content View与Main View和Main View等宽,方法是在上图右侧所显示的document outline中,选中Scroll View或Content View,按住右键移动鼠标到Main View上释放,然后在弹出选项中选择equal with,如下图。

ios-UIScrollView-scrollview-mainview-equal-width

设定好宽度后,我们发现Issue又变成有1个View的高度尺寸模糊不清。没错,这里的View指的就是Content View,他的高度可以直接指定具体的数值,比如736。于是,经过新添3个约束,Issue终于全部消失了。最终Scroll View和Content View的约束如下图。

ios-UIScrollView-view-final-constraints

2.7 Navigation Bar对Scroll View的影响

这个时候我们运行程序,却发现Scroll View和上面的Navigation Bar之间存在一个空白的区域,原因是Xcode自以为聪明地替我们完成了让Scroll View避开Navigation Bar这样的工作,但是却完成地不好。解决这个问题的方法是,选中View Controller,在其Attributes Inspector中将Adjust Scroll View Insets不选中,如下图。
ios-UIScrollView-uncheck-adjsut-insets
至此,Scroll View已经可以愉快地工作了。

2.8 布置若干textField及Keyboard的遮挡效果

为了说明textField内容被遮挡时UIScrollView自动滚动的方法,我们先在Content View内布置若干的textField。为让显示更清楚,将Content View的背景设定为dark grey,并添加两个黄色背景的Label,定义各自的constraints。效果如下图。
ios-UIScrollView-textfields-uncovered

这时如果点击一个textField输入文字,keyboard的出现会遮挡了下方部分的内容,如下图,这是非常影响体验的一个问题,所以必须解决它。

ios-UIScrollView-textfields-covered

我们会想到一种非常直接的解决方法,那就是让Content View自动向上滚动一段距离。但是,仔细想想,这里存在一个问题,就是:Content View最下面的一个textField怎么办?无论怎么滚动它都不可能跑到Keyboard的上面去。这个时候我们就能理解为什么UIScrollView有个contentInset的方法了,这是一个非常聪明的解决办法。

3. UIScrollView contentInset for Keyboard

解决Keyboard遮挡内容的问题的思路是这样的,首先,一个textField被点击,于是系统知道Keyboard要弹出来,然后需要判断Keyboard是否会对当前textField产生遮挡,如果不遮挡,则无需其他动作,如果产生遮挡,则将Scroll View的contentInset的bottom置为Keyboard的高(至于为什么下面详述),用户输入完成时(点击界面空白处或回车),系统明白Keyboard即将消失,于是将contentInset的bottom再置为初始值。

3.1 关于Keyboard即将出现和即将隐藏的消息

根据上面的过程,Keyboard即将出现和即将隐藏是非常重要的两个时间点,所以我们需要定义这两个消息的响应函数。方法如下:

    func registerForKeyboardNotifications() {
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: "keyboardWillBeShown:", name: UIKeyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
    }

根据上面的代码,可以看出,我们将Keyboard即将出现(UIKeyboardWillShowNotification)这一消息的处理函数定义为keyboardWillBeShown(注意书写时需要在引号中函数名后加一个冒号);将Keyboard即将隐藏(UIKeyboardWillHideNotification)这一消息的处理函数定义为keyboardWillBeHidden。

在View Controller初始化时调用这个函数即可,如:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    self.registerForKeyboardNotifications()
}

然后在Main View消失时我们需要注销这个通知中心(notificationCenter),代码如下:

override func viewDidDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    NSNotificationCenter.defaultCenter().removeObserver(self)
}

这样我们就定义好了Keyboard即将出现和即将隐藏这两个消息的响应函数,当这两个消息出现时就会执行keyboardWillBeShown和keyboardWillBeHidden这两个函数。这两个函数也写在Main View的Class中,我们先写两个空的函数,内容之后再添加,这两个函数如下:

func keyboardWillBeShown(sender: NSNotification) {
}
func keyboardWillBeHidden(sender: NSNotification) {
}

3.2 知道哪个textField正被编辑

在滚动Scroll View前,我们还需要知道哪个textField正在被编辑,以及它的位置,从而判断Keyboard的出现是否会对其产生遮挡效果,实现这一方法的过程如下。

首先,我们需要为所有的textField指定delegate,方法是;

  1. 在Main View的Class名称后添加“UITextFieldDelegate”,代码如下:
    import UIKit
    
    class ViewController: UIViewController, UITextFieldDelegate {
    // codes...
    }
    
  2. 在storyboard中选中一个textField,点开右侧的Connection inspector,点击outlet下的delegate,拖拽鼠标至storyboard中view controller的icon上,如下图:ios-UIScrollView-set-textField-delegate然后为所有textField重复这个过程即可。

然后,我们需要定义一个UITextField的变量,用来标记当前被编辑的UITextField,代码如下:

var activeTextField: UITextField?

最后,需要在某个textField被编辑时将当前的textField赋值给activeTextField,这不需要定义消息处理函数,因为UITextField的delegate有现成的method,代码如下:

func textFieldDidBeginEditing(textField: UITextField) {
    activeTextField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
    activeTextField = nil
}

3.3 keyboard出现和隐藏时的处理

最后,我们将keyboardWillBeShown和keyboardWillBeHidden这两个函数补充完整。代码如下:

// Called when the UIKeyboardDidShowNotification is sent.
func keyboardWillBeShown(sender: NSNotification) {
    let info: NSDictionary = sender.userInfo!
    let value: NSValue = info.valueForKey(UIKeyboardFrameBeginUserInfoKey) as! NSValue
    let keyboardSize: CGSize = value.CGRectValue().size
    let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0)
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
    
    //If active text field is hidden by keyboard, scroll it so it's visible
    var aRect: CGRect = self.view.frame
    aRect.size.height -= keyboardSize.height
    let activeTextFieldRect: CGRect? = activeTextField?.frame
    let activeTextFieldOrigin: CGPoint? = activeTextFieldRect?.origin
    if (!CGRectContainsPoint(aRect, activeTextFieldOrigin!)) {
        scrollView.scrollRectToVisible(activeTextFieldRect!, animated:true)
    }
}

// Called when the UIKeyboardWillHideNotification is sent
func keyboardWillBeHidden(sender: NSNotification) {
    let contentInsets: UIEdgeInsets = UIEdgeInsetsZero
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
}

最终的效果如下图:

ios-uiscrollview-keyboard-autoscroll-final-demo

iOS编程之UITableView的使用(Xcode6.3 iOS8.x Swift Storyboard)

1. 创建新的project

打开Xcode,点击“create a new Xcode project”,如下图。

ios-demo-create-new-xcode-project

 

填写“project name”等相关信息,如下图。

 

ios-demo-create-new-xcode-project-input-name

 

2. 在ViewController中定义TableView的一个outlet

共有两种方法:

  1. 直接在ViewController的Class中以如下语句定义
    @IBOutlet var mainTableView: UITableView!
    
  2. 先在视图中创建一个TableView,然后右键拖拽其到ViewController中释放,定义outlet。

这里采用第一种方法。

3. 在视图中创建一个TableView

方法是在“object library”中找到TableView,左键点击按住,拖拽其到视图上释放,如下图。

ios-demo-insert-tableview

4. 连接TableView和outlet

需要将TableView和它的outlet连接起来,首先,点击ViewController的图标,在右侧选择显示“Connections Inspector”,我们发现在2中定义的outlet已经被显示出来,如下图:

ios-demo-tableview-outlet

连接的方法是点击outlet中“mainTableView”右侧的空心圆圈,按住左键,拖动鼠标至TableView上后释放,如下图:

ios-demo-tableview-define-outlet

5. 定义TableView的dataSource和delegate

除outlet外,还需定义TableView的dataSource和delegate,分两步进行:

  1. 在ViewController的Class中声明,即在
    class ViewController: UIViewController {...}
    

    处加入“UITableViewDelegate, UITableViewDataSource”使之成为

    class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {...}
    
  2. 需要连接dataSource和delegate到ViewController,方法是首先在视图中选中TableView,然后在右侧的“Connections Inspector”中可以看到其dataSource和delegate,如下图ios-demo-tableview-attributes,之后点击dataSource和delegate右侧的空心圆圈,按住鼠标左键,拖动鼠标至ViewController图标上释放,如下图ios-demo-tableview-datasource-outlet

完成以上两步后,dataSource和delegate的定义完成。

6. 相关代码编写

完成以上操作后,即可在ViewController中编写代码,控制TableView显示的内容等等,代码如下:

//
//  ViewController.swift
//  TableViewDemo
//
//  Created by Xu Jie on 15/5/1.
//  Copyright (c) 2015年 J.Xu. All rights reserved.
//

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    // 定义TableView的outlet,可自由定义,不一定必须是“tableView”
    @IBOutlet var mainTableView: UITableView!
    
    // 需要显示的内容
    var items: [String] = ["武汉","上海","北京","深圳","广州","重庆","香港","台海","天津"]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 定义cell的类
        self.mainTableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
    
    // 指定TableView共有多少行
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items.count;
    }
    
    // 设定TableView列表每个单元(行)的内容
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        var cell:UITableViewCell = self.mainTableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
        cell.textLabel?.text = self.items[indexPath.row]
        
        return cell
    }
    
    // 选中某行时执行
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        println("You selected cell #\(indexPath.row)!")
    }
    
    // 删除某行时执行
    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath){
        var index=indexPath.row as Int
        self.items.removeAtIndex(index)
        self.mainTableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Top)
        NSLog("删除\(indexPath.row)")
    }
    
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

iOS编程之UnwindSegue的使用(Xcode6.3 iOS8.x Swift Storyboard)

1. 什么是UnwindSegue以及UnwindSegue的基本用法

1.1 UnwindSegue 的基本概念

关于UnwindSegue的基本概念可以在Apple的document中查看,同时,在stack overflow上也有关于它的详细例子。
以下是apple的ios开发文档中对UnwindSegue的说明。

Unwind segues give you a way to “unwind” the navigation stack back through push, modal, popover, and other types of segues. You use unwind segues to “go back” one or more steps in your navigation hierarchy. Unlike a normal segue, which create a new instance of their destination view controller and transitions to it, an unwind segue transitions to an existing view controller in your navigation hierarchy. Callbacks are provided to both the source and destination view controller before the transition begins. You can use these callbacks to pass data between the view controllers.

大意是:UnwindSegue可以让我们以push, modal, popover等其他segue的形式“放松地(自由地)”在导航堆列中跳转。我们用UnwindSegue“回跳”至navigation层级中得一个或多个界面前的一个界面。与一般的segue不同的是,UnwindSegue开创了一种新的(定义)目的视图控制器的方法并跳转到那个视图,当然,这个视图必须存在于navigation层级中。在跳转开始前,为源视图和目的视图均提供了Callback函数,我们可以通过这些Callback函数来传递数据。

从以上信息中我们可以看出,UnwindSegue和一般的Segue的最大区别是:

  1. UnwindSegue可以跳转到之前的一个或多个视图,其实也就是之前的任意一个视图,当然,这个视图必须在navigation堆列(stack)之中;而一般的Segue只能跳转到相邻的一个视图。
  2. UnwindSegue只能跳转到已经在navigation stack中存在的视图,还未创建的视图无法跳转;而一般的Segue则无此限制。

所以,一般的,在需要跳转到前两个或三个以上的视图时才需要用到UnwindSegue。

1.2 UnwindSegue的用法

如前文中提及的Apple的documentstack overflow中的例子,均有对UnwindSegue用法的详细解说,这里稍作总结。

使用UnwindSegue大概可分为以下几步:

    1. 在目标视图中创建一个action,以提供给源视图作为识别和接口。代码形如
      @IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
      }
      
    2. 在源视图中定义exit的关联项目,这个项目一般是一个按钮,方法是在exit图标上按右键,这时会弹出一个选择目标视图action的列表,这个action即是1中定义的action,点击对应的action后面的空心小圆圈,按住左键拖拽到按钮上释放,源视图中这个按钮到目标视图的UnwindSegue就建立起来了非常方便,如下图UnwindSegue-exit-button
    3. 当然,2中的那个项目不一定是个按钮,也可以是其它东西,也可以没有这个项目,比如,我们想在源视图控制器中做个判断,当满足一定条件时就进行UnwindSegue,那么这个时候的操作是,在exit上按住右键拖拽到视图控制器图标上释放,如下图UnwindSegue-exit-vc这时,这个UnwindSegue就创造出来了,但是,我们还需要指定这个UnwindSegue的id,然后我们在源视图控制器中就可以通过这个id来执行这个UnwindSegue,语法是:
      self.performSegueWithIdentifier("unwindtoDestinationVC", sender: self)

      其中unwindtoDestinationVC即是UnwindSegue的id。

2. 在UnwindSegue前执行一些任务的方法

有时候我们需要在UnwindSegue前进行一些操作,比如,从当前视图转出时,需要先将当前视图中的某些输入的内容存入数据库,然后再执行跳转。此时,就需要注意各种操作和语句执行的时序。

2.1 UnwindSegue与普通按钮关联

当exit与一个UnwindSegue和一个按钮绑定起来的时候,如1.2中2所表示的那样,这时候的执行顺序是这样的:

  1. 按钮被按下,向系统发送这一事件消息;
  2. 执行按钮的action;
  3. 执行prepareForSegue;
  4. 执行UnwindSegue。

2.2 UnwindSegue与toolbar上的item关联

在实践中发现,当exit与一个UnwindSegue和一个toolbar(底部toolbar/顶部navigation bar)按钮绑定起来的时候,执行的顺序和视图中的普通按钮有所不同,具体是这样的:

  1. 按钮被按下,向系统发送这一事件消息;
  2. 执行prepareForSegue;
  3. 执行UnwindSegue。

也即是说,该bar上的按钮所绑定的action并不会被执行,也就是说他们是segue优先的。那么如果非得在此之前执行一点儿任务怎么办呢?

可以这么做:

  1. 在目标视图控制器中创建一个unwindsegue;
  2. 将源视图的exit绑定到view上,而非一个具体的按钮;
  3. 定义bar按钮的action;
  4. 在bar按钮的action中先执行一定的任务,然后在合适的位置执行
    self.performSegueWithIdentifier("unwindtoDestinationVC", sender: self)

这样,即能实现在unwindsegue前完成一定的任务。


相关代码