【我们都爱Paul Hegarty】斯坦福IOS8公开课个人笔记3 Xcode、Auto Layout及MVC

  1. 云栖社区>
  2. 博客>
  3. 正文

【我们都爱Paul Hegarty】斯坦福IOS8公开课个人笔记3 Xcode、Auto Layout及MVC

cwift-tal 2015-03-31 09:53:02 浏览600
展开阅读全文

   继续上一话中的计算器Demo,上一话讲到类必须被初始化,类中的属性也必须被初始化,所以你不能只声明而不给它一个处置,那么问题来了,我们从storyboard中拖拽的@IBOutlet为什么只有声明而不需要初始化呢,这是因为它的类型依旧是一个optional,在你初始化之前已经被赋值为nil了,这也就是为什么你不需要再初始化它的原因。

@IBOutlet weak var display: UILabel!

然而既然它是一个optional类型的,那为什么UILabel后面是“!”而不是“?”呢。对于实际语言而言,它们没有区别,都是可能为一个UILabel的意思,但是实际的用法不同,这都是编译器帮你做的,如果我们把后面的感叹号改成问号会怎么样?下面的部分会报错,提示它没有text这个成员:


我们当然可以给display添加一个!来解包

display!.text = digit
因为拖拽生成的原因,左边的xib界面初始化之后和右边的viewcontroller关联,那么这些@IBOutlet就已经被初始化了并且是永久初始化,如果我们每次用的时候都要加一个“!”,那实在太耽误事了,所以拖拽生成的变量类型后面是自动增加的“!”表达了这个变量虽然是一个optional,但是它已经被解包了,我们在使用这个变量的时候就可以不加!

现在我们需要一个return按钮,用来表示输入完了一个待操作的数,我们复制一个按钮,改变它的值。


凡是Unicode的字符都可以被我们使用,包括汉字和表情。把这个按钮与vc相关联,它不需要传参数,所以参数类型可以是AnyObject的,方法取名enter

 @IBAction func enter() {
    }

enter的作用是把我们label现实的数字存入栈中,以待后续的操作。代码中该如何写呢?点击了回车键,那么我们之后的输入将是一个新的数,所以需要把标志位修改:

    @IBAction func enter() {
        userIsInTheMiddleOfTypingANumber = false
    }

运行看看:


它清除显示屏的功能实现了但是为什么回车会出现在label中呢,相信你已经猜到了因为我们之前复制按钮的关系,回车键依旧关联在appendDigit方法中,我们只需要右键点击按钮在弹出的菜单中点“X”取消这个关联就OK了:


现在我们创建一个数组来存储运算的值,你可以看到如何定义以及初始化一个数组

var appendStack:Array<Double> = Array<Double>()
因为Swift语言是强语言类型,所以我们可以不声明类型:

var appendStack = Array<Double>()

你想要在点击回车的时候把当前label的值加入栈中,如果你这样写:

appendStack.append(display.text!)
你会发现报错,因为appendStack是Double类型的,而display.text即便拆封之后依旧是个String类型的,那么怎么办呢。这里要引入一个新的东西,我们叫它计算属性:

  var displayValue:Double {
        get{
        }
        set{
        }
    }

在定义的时候后面加一对花括号,然后在其中在get和set方法,这个属性的作用是计算display.text的值转成Double,完整的displayValue代码:

var displayValue:Double {
        get{
            return NSNumberFormatter().numberFromString(display.text!)!.doubleValue
        }
        set{
            display.text = "\(newValue)"
            userIsInTheMiddleOfTypingANumber = false
        }
    }

现在我们在enter方法中做修改:

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

removeLast()方法会删除数组的最后一个元素,同时返回它的值,我们可以运行一下,输入顺序每输入一个操作数,点击一下回车,然后这个数就会被加到栈里,输入两个数之后点击乘号按钮进行运算,结果如下:


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()
        }
    
    }

可以看到有些有意思的东西,我们这个方法的参数operation的类型是一个函数,没错,在swift中函数经常作为参数类型被传递。那么在case中我们该如何写呢,可能会让你大吃一惊:

 case "×":
            performOperation({
              $0 * $1
            })

这里应用了闭包的简化形式,我们无须指定参数类型,因为Swift是强类型的,它可以自动识别类型,而如果你没有取参数名的话,那么$0和$1代表前两个参数,闭包中的结构是一个方法,有两个参数,它的返回值是两个参数的乘积,这也符合performOperation的定义。

还有更酷的!如果你有一个类似于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)}

sqrt是swift自己的求根方法,你会发现报错了,原因是我们之前的performOperation中的闭包都是有两个参数的,而这个case中只有一个参数,那么该如何修改呢,我们需要复制performOperation的代码然后做如下修改:

  func performOperation(operation:Double-> Double)
    {
        if appendStack.count >= 1 {
            displayValue = operation(appendStack.removeLast() )
            enter()
        }
        
    }

你会发现这两个方法的名字完全一样,但是我们不用担心,因为swift会根据参数的不同自动调用应该调用的方法,现在我们来试验一下,输入36,点击开方按钮:


实现了,就是这么简单。现在还对界面做一些优化,你会发现在竖屏状态屏幕浪费了太多的空间,而你不可能拖动每一个按钮去设置相互之间的间距,这样太浪费时间。



首先左下角有点空,我们新增一个空按钮来占位,并且保证它不触发任何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的叠加可以实现复杂功能。


网友评论

登录后评论
0/500
评论