将进酒-the Angilent Band

2015.5.31 午后 镜山公园

与友小聚,箕踞啸歌,甚欢。

友名敏,吾名杰,故为敏杰之意,自号”the Agilent Band”。

Agilent, agile(敏) + excellent(杰), 又名“安捷伦”,哈哈哈。
IMG_2831IMG_2837IMG_2839IMG_2841

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