目标(Target)与动作(Action)是iOS和OS X应用开发的中事件处理机制。
问题提出
如图所示是一个ButtonLabelSample案例设计原型图,其中包含一个标签和一个按钮,当点击按钮的时候,标签文本会从初始的Label替换为HelloWorld。
ButtonLabelSample案例首先要解决的问题是:按钮点击事件后有谁负责响应事件?谁进行事件处理?要答这个问题,可以打开ButtonLabelSample案例故事板文件Main.storyboard,如图所示,OK按钮是在故事板文件Main.storyboard定义的,响应事件以及处理事件应该是在程序代码ViewController.Swift实现的。那么如何将OK按钮点击事件与ViewController.swift中的事件处理代码关联起来?不同的计算机语言实现的方式不同,在iOS和OS X应用开发中是通过目标和动作机制实现事件处理的。
解决方案
按钮等控件是继承自UIControl类,具有一些高级事件,目标和动作机制就是将特地的控件事件与视图控制器(或视图)中方法关联起来,这个过程称为“定义动作事件”。“目标”是响应事件对象,为了方便访问其他的控件状态,这个对象一般是视图控制器(或视图)。“动作”是控件的事件。
ButtonLabelSample案例中按钮是在故事板文件(或Xib文件)中定义的,响应按钮点击事件(动作)是在视图控制器(目标)的方法中定义的,如下图所示,通过定义动作事件方式把目标与动作连接起来。
要实现目标与动作的连接有两种方式:InterfaceBuilder连线实现和编程实现。
1. Interface Builder连线实现
InterfaceBuilder连线实现就是故事板或Xib文件中,通过连线而现实。
2. 编程实现
编程实现是通过UIControl类addTarget(_:action:forControlEvents:)方法实现的,主要代码如下:
[html] view plain copy
- class ViewController:UIViewController {
- override func viewDidLoad() {
- super.viewDidLoad()
- self.view.backgroundColor =UIColor.whiteColor()
- let screen =UIScreen.mainScreen().bounds;
- let labelWidth:CGFloat = 90
- let labelHeight:CGFloat = 20
- let labelTopView:CGFloat = 150
- let label = UILabel(frame:CGRectMake((screen.size.width
- Ê- labelWidth)/2 , labelTopView, labelWidth, labelHeight))
- label.text = "Label"
- //字体左右剧中
- label.textAlignment = .Center
- self.view.addSubview(label)
- let button = UIButton(type:UIButtonType.System)// 创建UIButton对象
- button.setTitle("OK",forState: UIControlState.Normal)
- let buttonWidth:CGFloat = 60
- let buttonHeight:CGFloat = 20
- let buttonTopView:CGFloat = 240
- button.frame =CGRectMake((screen.size.width
- Ê -buttonWidth)/2 , buttonTopView, buttonWidth, buttonHeight)
- button.addTarget(self, action:"onClick:",
- Ê forControlEvents:UIControlEvents.TouchUpInside)
- self.view.addSubview(button)
- }
- func onClick(sender: AnyObject) {
- NSLog("OK Button onClick.")
- }
- ...
- }
上述代码中创建并设置UIButton对象,其中创建UIButton对象,参数type是设置按钮的样式,UIButton样式:
- Custom。自定义类型。如果不喜欢圆角按钮,可以使用该类型。
- System。系统默认属性,表示该按钮没有边框,在iOS 7之前按钮默认为圆角矩形。
- Detail Disclosure。细节展示按钮,主要用于表视图中的细节展示。
- Info Light和Info Dark。这两个是信息按钮,样式上与细节展示按钮一样,表示有一些信息需要展示,或有可以设置的内容。
- Add Contact。添加联系人按钮 。
代码调用addTarget(_:action:forControlEvents:)方法,方法第一个参数是target,即事件处理对象,本例中是self;方法第二个参数是action,即事件处理对象中的方法,
代码中是"onClick:",方法第三个参数是事件,TouchUpInside事件是按钮的触摸点击事件。
如果调用如下无参数方法:
- func onClick() {
- }
调用代码如下:
- button.addTarget(self,action: "onClick",
- Ê forControlEvents:UIControlEvents.TouchUpInside)
区别在于action参数"onClick"方法名不同,action参数方法名的冒号暗示了方法名应该具有几个参数。如果要调用的方法是如下3个参数形式:
- func onClick(sender: AnyObject, forEvent event: UIEvent) {
- }
那么调用代码如下:
- button.addTarget(self,action: "onClick:forEvent:",
- Ê forControlEvents:UIControlEvents.TouchUpInside)
其中"onClick:forEvent:"是调用方法名,onClick表示方法名也是,forEvent表示第二个参数的外部参数名。
=========================================
实现目标与动作关联使用UIControl类addTarget(_:action:forControlEvents:)方法,示例代码如下:
- button.addTarget(self, action: "onClick:",
- forControlEvents: UIControlEvents.TouchUpInside)
其中的action参数"onClick:"事实上就是选择器(Selector)。
问题提出
任何能够将方法调用的绑定推迟到运行期,在编译时方法调用者不需要知道要调用的方法是什么,这个可以降低调用者与被调用者之间的耦合度,这样就语言就很灵活。在C语言在提供一种函数指针技术,Objective-C和Swift语言都提供选择器(Selector)类型,它是C语言函数指针的面向对象替代技术。
选择器在Cocoa和Cocoa Touch中的目标动作、通知和委托等模式中方法的调用实现的关键。
解决方案
Objective-C中选择器是SEL数据类型,使用@selector()语句调用,调用onClick:方法的Objective-C示例代码如下:
- SEL selector = @selector(onClick:);
- [button addTarget:self action: selector
- forControlEvents: UIControlEventTouchUpInside];
Swift中虽然没有提供SEL数据类型,而是提供了Selector结构体,通过方法名字符串构建Selector实例,示例代码如下:
- button.addTarget(self, action: Selector("onClick:"),
- forControlEvents: UIControlEvents.TouchUpInside)
通过选择器调用方法,关键是方法名字,它有一定规律的。穷其根本是源自于Objective-C多重参数方法命名规律。方法名的冒号暗示了方法名应该具有几个参数,下面我们看几个示例:
- //选择器为"onClick:"
- func onClick(sender: AnyObject) {
- NSLog("onClick:")
- }
- //选择器为"onClick:forEvent:"
- func onClick(sender: AnyObject, forEvent event: UIEvent) {
- NSLog("onClick:forEvent:")
- }
- //选择器为"onClickWithExtSender:forEvent:"
- func onClick(extSender sender: AnyObject, forEvent event: UIEvent) {
- NSLog("onClickWithExtSender:forEvent:")
- }
出于数据封装的需要,我们会在方法前面加private,使其变为私有方法,代码如下。
[html] view plain copy
- private func onClick(sender: AnyObject) {
- NSLog("onClick:")
- }
但是这样方法在调用时候会出现如下错误:
[html] view plain copy
- unrecognized selector sent to instance 0x7f7f81499b10‘
这个错误的意思是没有找到选择器所指定的方法,也就是没有找到onClick:方法。正确的做法是在方法前面添加@objc属性注释,这说明选择器是在objc runtime运行环境下调用的。
- //选择器为"onClick:"
- @objc private func onClick(sender: AnyObject) {
- NSLog("onClick:")
- }
==========================================
通知(Notification)机制是基于观察者(Observer)模式也叫发布/订阅(Publish/Subscribe)模式,是 MVC( 模型-视图-控制器)模式的重要组成部分。
问题提出
天气一直是英国人喜欢讨论的话题,而最近几年天气的变化也成为中国人非常关注的话题。我会根据天气预报决定是坐地铁还是开车上班,我的女儿也会根据天气预报决定明天穿哪件衣服。于是我在移动公司为我的手机定制了天气预报短信通知服务,它的工作模型如图所示。
每天气象局将天气预报信息投送给移动运营商,移动运营商的短信中心负责把天气预报发送给定制过这项服务的手机。
在软件系统中,一个对象状态改变也会连带影响其他很多对象的状态发生改变。能够实现这一需求的设计方案有很多,但能够做到复用性强且对象之间匿名通信的,观察者模式是其中最为适合的一个。
解决方案
通知机制可以实现“一对多”的对象之间的通信。如图所示,在通知机制中对某个通知感兴趣的所有对象都可以成为接收者。首先,这些对象需要向通知中心(NSNotificationCenter)发出addObserver消息进行注册通知,在投送对象通过postNotificationName消息投送通知给通知中心,通知中心就会把通知广播给注册过的接收者。所有的接收者都不知道通知是谁投送的,更不关心它的细节。投送对象与接收者是一对多的关系。接收者如果对通知不再关注,会给通知中心发出removeObserver消息注销通知,以后不再接收通知。
=================================
MVC(Model-View-Controller,模型-视图-控制器)模式是相当古老的设计模式之一,它最早出现在Smalltalk语言中。现在,很多计算机语言和架构都采用了MVC模式。
MVC模式概述
MVC模式是一种复合设计模式,由 “观察者”(Observer)模式、“策略”(Strategy)模式和“合成”(Composite)模式等组成。MVC模式由3个部分组成,如图所示,这3个部分的作用如下所示。
- 模型。保存应用数据的状态,回应视图对状态的查询,处理应用业务逻辑,完成应用的功能,将状态的变化通知视图。
- 视图。为用户展示信息并提供接口。用户通过视图向控制器发出动作请求,然后再向模型发出查询状态的申请,而模型状态的变化会通知给视图。
- 控制器。接收用户请求,根据请求更新模型。另外,控制器还会更新所选择的视图作为对用户请求的回应。控制器是视图和模型的媒介,可以降低视图与模型的耦合度,使视图和模型的权责更加清晰,从而提高开发效率。
对应于哲学中的“内容”与“形式”, 在MVC模型中,模式是“内容”,它存储了视图所需要的数据,视图是“形式”,是外部表现方式,而控制器是它们的媒介。
CocoaTouch中的MVC模式
上面我们讨论的是通用的MVC模式,而Cocoa和Cocoa Touch框架中的MVC模式与传统的MVC模式略有不同,前者的模型与视图不能进行任何通信,所有的通信都是通过控制器完成的,如图所示。
在Cocoa Touch框架的UIKit框架中,UIViewController是所有控制器的根类,如UITableViewController、UITabBarController和UINavigationController。UIView是视图和控件的根类。
===============================
应用与用户进行交互,依赖于各种各样的事件。事件响应者对象是可以响应事件并对其进行处理的对象,响应者链是由一系列链接在一起的响应者组成的。响应者链在事件处理中是非常重要的,响应者链可以把用户事件路由给正确的对象。
响应者对象与响应链
UIResponder是所有响应者对象的基类,它不仅为事件处理,而且也为常见的响应者行为定义编程接口。UIApplication、UIView(及其子类,包括UIWindow)和UIViewController(及其子类)都直接或间接地继承自UIResponder类。
第一响应者是应用程序中当前负责接收触摸事件的响应者对象(通常是一个UIView对象)。UIWindow对象以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。
响应者链是一系列链接在一起的响应者对象,它允许响应者对象将处理事件的责任传递给其他更高级别的对象。随着应用程序寻找能够处理事件的对象,事件就在响应者链中向上传递。响应者链由一系列“下一个响应者”组成。
1.第一响应者将事件传递给它的视图控制器(如果有的话),然后是它的父视图。
2.类似地,视图层次中的每个后续视图都首先传递给它的视图控制器(如果有的话),然后是它的父视图。
3.最上层的容器视图将事件传递给UIWindow对象。
4.UIWindow对象将事件传递给UIApplication单例对象。
触摸事件
触摸(UITouch)对象表示屏幕上的一个触摸事件,访问触摸是通过UIEvent对象传递给事件响应者对象的。触摸对象有时间和空间两方面。
1.时间方面
时间方面信息称为阶段(phase),表示触摸是否刚刚开始、是否正在移动或处于静止状态,以及何时结束,也就是手指何时从屏幕抬起。
在给定的触摸阶段中,如果发生新的触摸动作或已有的触摸动作发生变化,则应用程序就会发送这些消息。
- 当一个或多个手指触碰屏幕时,发送touchesBegan:withEvent:消息。
- 当一个或多个手指在屏幕上移动时,发送touchesMoved:withEvent:消息。
- 当一个或多个手指离开屏幕时,发送touchesEnded:withEvent:消息。
2.空间方面
触摸点对象还包括当前在视图或窗口中的位置信息,以及之前的位置信息(如果有的话)。下面的方法是可以获得触摸点所在窗口或视图中的位置。
- func locationInView(_ view: UIView?) -> CGPoint
获得前一个触摸点所在窗口或视图中的位置信息:
- func previousLocationInView(_ view: UIView?) -> CGPoint
==========================================
什么是设计模式。设计模式是在特定场景下对特定问题的解决方案,这些解决方案是经过反复论证和测试总结出来的。实际上,除了软件设计,设计模式也被广泛应用于其他领域,比如UI设计和建筑设计等。
下面来介绍Cocoa Touch框架中的设计模式中的单例模式。
单例模式
单例模式的作用是解决“应用中只有一个实例”的一类问题。在Cocoa Touch框架中,有UIApplication、NSUserDefaults和NSNotificationCenter等单例类。另外,NSFileManager和NSBundle类虽然属于Cocoa框架的内容,但也可以在Cocoa Touch框架中使用(Cocoa框架中的单例类有NSFileManager、NSWorkspace和NSApplication等)。
问题提出
在一个应用程序的生命周期中,有时候只需要某个类的一个实例。例如:当iOS应用程序启动时,应用的状态由UIApplication类的一个实例维护,这个实例代表了整个“应用程序对象”,它只能是一个实例,其作用是共享应用程序中的一些资源、控制应用程序的访问,以及保持应用程序的状态等。
解决方案
单例模式的实现有很多方案,苹果公司在《UsingSwift with Cocoa and Objective-C》官方文档中给出了一种单例模式的实现。最简单形式代码如下:
[java] view plain copy
- class Singleton {
- static let sharedInstance = Singleton()
- }
上述代码采用static的类属性实现单例模式,这种类属性只被延迟加载执行一次,即便是在多线程情况下也只是执行一次,并且保证是线程安全的。
如果需要进行一些初始化,可以使用如下带有闭包形式代码:
[java] view plain copy
- class Singleton {
- static let sharedInstance: Singleton = {
- let instance = Singleton()
- // 初始化处理
- return instance
- }()
- }
单例模式除了上述苹果官方给出的实现外,还有很多种实现方式。