继续上一话中的计算器Demo,上一话讲到类必须被初始化,类中的属性也必须被初始化,所以你不能只声明而不给它一个处置,那么问题来了,我们从storyboard中拖拽的@IBOutlet为什么只有声明而不需要初始化呢,这是因为它的类型依旧是一个optional,在你初始化之前已经被赋值为nil了,这也就是为什么你不需要再初始化它的原因。
@IBOutlet weak var display: UILabel!
我们当然可以给display添加一个!来解包
display!.text = digit因为拖拽生成的原因,左边的xib界面初始化之后和右边的viewcontroller关联,那么这些@IBOutlet就已经被初始化了并且是永久初始化,如果我们每次用的时候都要加一个“!”,那实在太耽误事了,所以拖拽生成的变量类型后面是自动增加的“!”表达了这个变量虽然是一个optional,但是它已经被解包了,我们在使用这个变量的时候就可以不加!
现在我们需要一个return按钮,用来表示输入完了一个待操作的数,我们复制一个按钮,改变它的值。
凡是Unicode的字符都可以被我们使用,包括汉字和表情。把这个按钮与vc相关联,它不需要传参数,所以参数类型可以是AnyObject的,方法取名enter
@IBAction func enter() { }
@IBAction func enter() { userIsInTheMiddleOfTypingANumber = false }
它清除显示屏的功能实现了但是为什么回车会出现在label中呢,相信你已经猜到了因为我们之前复制按钮的关系,回车键依旧关联在appendDigit方法中,我们只需要右键点击按钮在弹出的菜单中点“X”取消这个关联就OK了:
现在我们创建一个数组来存储运算的值,你可以看到如何定义以及初始化一个数组
var appendStack:Array<Double> = Array<Double>()因为Swift语言是强语言类型,所以我们可以不声明类型:
var appendStack = Array<Double>()
appendStack.append(display.text!)你会发现报错,因为appendStack是Double类型的,而display.text即便拆封之后依旧是个String类型的,那么怎么办呢。这里要引入一个新的东西,我们叫它计算属性:
var displayValue:Double { get{ } set{ } }
var displayValue:Double { get{ return NSNumberFormatter().numberFromString(display.text!)!.doubleValue } set{ display.text = "\(newValue)" userIsInTheMiddleOfTypingANumber = false } }
@IBAction func enter() { userIsInTheMiddleOfTypingANumber = false appendStack.append(displayValue) println("appendStack = \(appendStack)") }
按回车已经不再显示了,中控台信息:
现在我们来增加运算符按钮,它们依旧公用一个action方法:
和回车一样,我们依旧可以在特殊符号中找到数学运算符:
拖拽定义一个新方法operate,记得sender类型写UIButton,获取一下按钮的值:
@IBAction func operate(sender: UIButton) { let operation = sender.currentTitle! }
接下来展示swift中的操作流,老头说swift的操作流是非常强大的,比其他语言强大太多,这一点我深有体会,我敲swift代码已经上万行了,控制流的确非常好用和高效,而且非常直观。
首先展示一下乘法运算:
@IBAction func operate(sender: UIButton) { let operation = sender.currentTitle! switch operation{ case "×": if appendStack.count >= 2 { displayValue = appendStack.removeLast() * appendStack.removeLast() enter() } // case "÷": // case "+": // case "−": default: break } }
switch operation{ case "×": if appendStack.count >= 2 { displayValue = appendStack.removeLast() * appendStack.removeLast() enter() } case "÷": if appendStack.count >= 2 { displayValue = appendStack.removeLast() * appendStack.removeLast() enter() } case "+": if appendStack.count >= 2 { displayValue = appendStack.removeLast() * appendStack.removeLast() enter() } case "−": if appendStack.count >= 2 { displayValue = appendStack.removeLast() * appendStack.removeLast() enter() } default: break }
func performOperation(operation:(Double,Double)-> Double) { if appendStack.count >= 2 { displayValue = operation(appendStack.removeLast() , appendStack.removeLast()) enter() } }
case "×": performOperation({ $0 * $1 })
还有更酷的!如果你有一个类似于performOperation的方法,那么在调用时方法中的最后一个参数可以写到括号外面,比如:
case "×": performOperation( ){$0 * $1}
performOperation {$0 * $1}完整的方法代码:
@IBAction func operate(sender: UIButton) { let operation = sender.currentTitle! switch operation{ case "×": performOperation {$0 * $1} case "÷": performOperation {$1 / $0} case "+": performOperation {$0 + $1} case "−": performOperation {$1 - $0} default: break } }
现在我们新增一排按钮,首先是平方根:
我们只需要在控制流中增加一个case就好了,如下:
case "√": performOperation {sqrt($0)}
func performOperation(operation:Double-> Double) { if appendStack.count >= 1 { displayValue = operation(appendStack.removeLast() ) enter() } }
实现了,就是这么简单。现在还对界面做一些优化,你会发现在竖屏状态屏幕浪费了太多的空间,而你不可能拖动每一个按钮去设置相互之间的间距,这样太浪费时间。
首先左下角有点空,我们新增一个空按钮来占位,并且保证它不触发任何action
我们希望这些按钮能根据屏幕的大小自动稀疏布局,保持按钮群到各边框的距离相等。我们应用布局按钮,这排按钮在屏幕下方:
点开第一个按钮我们可以看到一些选项:
我们可以选择左对齐、上对齐、居中等等,但是这不是我们想要的选项,我们点开第二个按钮:
我们选中这两项标示所有按钮都有相同的宽和高:
现在来设置间距:
还记得我们对齐时候看到的蓝线么,这个8像素点得间距就是蓝线对齐的默认像素点间距,另外要保证红线是亮的状态而不是虚线状态,这样约束才能被加上。加上约束的界面:
哇偶!是不是眼花了,点开视图大纲,可以看到这里的小黄圈,如果它是红色的证明我们添加的约束有冲突的地方。
还记得我们上一话中介绍的么?这里的黄色标示有些约束和我们预想的不同,那么点开黄色小圆圈,随便点击一个条目,作如下设置:
记得勾选下面的Apply to all views in container,这样所有的警告都会被施以相同的操作。
现在警告没有了,看起来很不错呦。运行一下看看:
在Iphone6上显得很修长。
切换到横屏:
很赞对不对?
如果要清除约束使用第三个按钮:
上面部分是清除所选项的约束,下面部分是清除所有的约束。
但是实际我们的计算器是有问题的,因为我们的处理引擎这部分是独立的,这就牵涉到了MVC设计模式的问题,之后我们所有的代码都要满足MVC设计模式,现在来了解一下MVC:
MVC是一个基本机制,用于分类,左边的Model(模型层)包括我们的模型,在计算其中,计算是模型。控制层控制视图如何显示,而视图是控制要用到的模型,在view中用到的时相当常规的界面元素。我们用道路上的指示线来作比:
控制器和模型之间是虚线说明允许临时跨越,但是过去之前你需要观察一下,控制器(C)必须完全掌握模型(M),因为控制器的职责就是展示模型给用户,所以控制器拥有完全的访问权限,这是个单向的箭头
同样的控制器(C)也可以到达视图(V),因为控制器要向视图发送命令,因为控制器设计视图,我们在单向箭头上加了一行绿色小字,outlet,因为当我们在控制器中有一个属性指向视图,这个属性的名字就是Outlet。
那么模型(M)和视图(V)之间呢?答案是永远不能!因为模型是完全独立于UI的。这也是为什么他们之间采用了双黄线隔开。
那么视图(V)向控制器(C)发送信息么?它们之间是一种盲通信,并不是任意的通信,他们之间的建立通信的方式是控制器生成一个Target,然后视图使用action向控制器反馈信息,比如我们点击了一个按钮或者其他页面上的操作。页面并不知道控制器是一个什么样的控制器,它只知道页面上产生了动作,并反馈给控制器,这是一种盲目的、简单的、结构化的通信方式。
那么有一些复杂的操作怎么办呢,比如should、will和did。我拖动屏幕这是一个did的动作,我按住了屏幕准备拖动它的时候这是一个will的动作,遇到这样复杂的动作时怎么办呢,视图的做法是它把这些问题抛给了它的代理,代理依旧是控制器中的东西,这些代理来回答will、did、should怎么做这样的问题。
另外一个很重要的点是:视图不应该持有它所展示的数据!数据不能作为它的属性。比如你的iphone中有一万首歌,你不能期望它持有一万首歌展示给你看。第一,这样做很低效,第二,这一万首歌应该在模型层,一些控制器来负责选择展示哪些歌曲,然后从模型中取到并在视图层中展示。
那么当我们滑动屏幕期望能获取更多歌曲的时候我们需要怎么做呢?这是另外一种代理,我们叫它数据源DataSource,数据源并不去处理诸如will、should这样的处理,他回答有多少歌曲并把数量返回给视图这样的工作,此时视图为这一万首歌开辟空间。所以控制器(C)的作用就是给视图(V)解释并格式化这些模型(M)提供的数据。
那么问题又来了,模型可以和控制器通信么?显然不行,但是如果数据改变了如何通知我们的控制器呢?它依旧使用了这种盲通信的方法。我们把模型想象成一个电台,它通过广播的方式告知别人自己的变化,IOS把这种技术叫做Notification和Key Value Observing(KVO),一旦控制器接收到了模型变化的消息,它会通过那个绿箭头向模型索取它的变化信息。那么视图能接受模型的广播么?也许可以但不要这么做,这违背了MVC模式。
我们可以利用这些知识做些小应用,如果工程很复杂呢?我们需要多个MVC,多个MVC的叠加可以实现复杂功能。