实现自定义控制

本章将会实现对FoodTracker APP的评级控制,当你完成时,你的APP看起来像这样:

学习目标

在课程结束时,你将能够:

创建并关联自定义源代码文件和在storyboard中的元素
定义一个自定义类
在实现自定义类的初始化
使用的UIView作为容器
了解如何以编程方式显示views 

创建一个自定义View

为了能评级一个菜谱,用户需要一个控制,让他们能选择给想要菜谱多少星星数量。有许多方法实现这个,但我们会专注于涉及创建一个自定义view,用过在代码中定义,并使用storyboard。实现的效果如下:

 

评级控件将让用户为一个菜谱选择0-5个星星。当用户点击一个星星时,所有被填充的星星就是目前的星星数。填充的星星数量就是评级的数量,空心的星星就不是。为了开始设置这个UI,交互和控制行为,我们要创建一个UIView的子类。

创建UIView的子类步骤如下

1.选择File>New>File(或Command+N)

2.在对话框的左边,选择iOS下方的Source

3.选择Cocoa Touch Class,然后点击下一步

4.在Class标签后,输入RatingControl

5.Subclass of标签后,选择UIView

6.确定语言选择的是Swift

 

7.点击Next。

保存的位置是默认项目目录,Group选择默认的是你APP的名字,FoodTracker

在Targets字段,你的APP是被选择的,App的Test是未选择的

8.保留这些默认值,然后点击Create

Xcode会创建一个RatingControl类的文件:RatingControl.swift,RatingControl是一个自定义的UIView的子类

9.在RatingControl.swift中,删除注释,来到类中,就像一个白板一样

import UIKit
 
class RatingControl: UIView {
    
}

你通常创建一个view有两种方式,一种是通过View的初始化frame来手动添加View到你的UI中,另一种是允许view,在storyboard中加载。每一个方法对应一个初始程序:对于第一种,是使用init(frame:),第二种,使用init(coder:)。回想一下initializer方法,用于准备一个类的实例,它涉及到为每个属性设置初始值并执行一些其他设置。

因为我们这里会在storyboard中使用init方法,所以我们覆盖子类init(coder:)的实现,

下面是覆盖初始化程序的步骤

1.在RatingControl.swift中的class下面,添加注释

// MARK: Initialization

2.在注释的下方,输入init。代码完成功能会出现

3.选择第二个方法,即init(coder:),然后按下Return

init(coder aDecoder: NSCoder!) {
}

4.你会发现有个错误,但能修复它,会自动导入required关键字

每个实现了initializer的UIView的子类,必须包含一个init(coder:)的实现。Swift编译器知道这个,并提供自动修复工具,来改变你的代码,对于你代码中的错误,它提供一个潜在的解决方法

5.在init方法中,添加父类的初始化程序

super.init(coder: aDecoder)

最终 init(coder:)的方法,如下

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
}

显示自定义View

为了显示你的自定义View,你需要添加一个View到你的UI中并在view的代码中建立一个连接。显示View的步骤如下:

1.打开你的storyboard

2.在storyboard中,使用Object library找到View对象,并拖动到storyboard场景中,它在image view的下方

3.选中View,打开Size inspector

 

4.在Intrinsic Size标签旁,选择Placeholder

5.在Intrinsic Size内输入44的Height 和240的Width,按下Return,然后界面如下:

6.在View选中的情况下,选择Identity inspector

7.在Identity inspector中,找到Class 标签,选择RatingControl

添加按钮到View中 

此刻,你获取到了自定义的UIView子类,名为RatingControl。接下来我们需要为这个View添加按钮,来允许用户选择一个评级。先从简单的开始,获取一个红色的按钮显示在你的view中。步骤如下:

1.在init(coder:)内,添加以下代码来创建一个红色的按钮

let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
button.backgroundColor = UIColor.redColor()

你使用的是redColor(),所以按钮为红色。如果你喜欢,你可以改成其他颜色如blueColor() 或者greenColor()

2.接着添加下一行

addSubview(button)

addSubview()表示,把Button添加到你创建的RatingControl View中

你的 init(coder:)完整代码,应该如下所示:

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    button.backgroundColor = UIColor.redColor()
    addSubview(button)
}

检查站:执行你的APP,你应该能看到一个View中有一个红色的正方形。这个红色的正方形就是你添加的按钮

你需要这个按钮,最终还有其他按钮,在View中执行点击动作。这个动作你用来改变菜谱的评级。

下面让我们添加一个动作到按钮中

1.在RatingControl.swift类中大括号}上方,添加如下代码:

 

// MARK: Button Action

 

2.在注释下方添加如下代码:

func ratingButtonTapped(button: UIButton) {
    print("Button pressed  ")
}

现在,我们使用 print()函数来检查ratingButtonTapped动作是否如预期那样链接到按钮上。这个函数打印一个消息,会输出在Xcode的控制台。控制台是一个有效的调试机制。

3.找到init(coder:) initializer:

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    button.backgroundColor = UIColor.redColor()
    addSubview(button)
}

4.在 addSubview(button)上方,添加如下代码:

button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)

你已经熟悉目标 - 动作( target-action )模式了,因为你已经多次用它在storyboard中链接代码和元素的动作方法。上面,我们在做同样的事情,只不过你在代码中创建连接。你要附加的动作ratingButtonTapped:到button 对象,每当.TouchDown事件发生时将被触发。这个事件表明用户在按钮中已按下。设置目标self,在这种情况下是RatingControl类,因为的动作定义在这里。
需要注意的是,因为你没有使用界面生成器,你不需要定义你的操作方法为IBAction属性;你就像定义其他方法一样定义动作。

最终代码看起来如下:

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    button.backgroundColor = UIColor.redColor()
    button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    addSubview(button)
}

检查站:运行你的应用程序。当你单击红色正方形,你应该在控制台中看到了“Button pressed”的消息。

是时候想想为了展示评级,RatingControl类需要一些什么信息了。你需要保持并记录评级的值0-5,也就是用户点击来设置的评级按钮。你可以使用Int来展示评级的值,并且这些按钮作为UIButton对象数组

添加评级属性 

1.RatingControl.swift中, 找到class声明的这行:

 

class RatingControl: UIView {

 

2.在这行代码下面,添加如下代码:

// MARK: Properties
 
var rating = 0
var ratingButtons = [UIButton]()

此时,你在View中有一个按钮,但你需要5个这样的按钮。为了创建一整个按钮集,我们使用for-in循环。一个for-in循环遍历一个序列,如数字范围,多次执行一组代码。现在我们就用它来创建一个按钮,循环创建五个。

创建5个按钮

1.在RatingControl.swift中,找到init(coder:):

 

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    button.backgroundColor = UIColor.redColor()
    button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    addSubview(button)
}

 

2.添加for-in循环代码

for _ in 0..<5 {
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    button.backgroundColor = UIColor.redColor()
    button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    addSubview(button)
}

你可以全选他们,然后按下Control+I来缩进。(..<)这个Swift语法中的操作符,不会包含<后的数字,也就是说循环(0..<5)就是0,1,2,3,4。你可以使用下划线(_)表示通配符,意思是你不需要知道当前执行的循环迭代。

3.在addSubview(button)上方添加如下代码:

 

ratingButtons += [button]

 

你创建的每个按钮,需要把它存放在ratingButtons数组中,因为我们将要用它它们。

完整的init(coder:)函数,代码如下:

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    for _ in 0..<5 {
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
        button.backgroundColor = UIColor.redColor()
        button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
        ratingButtons += [button]
        addSubview(button)
    }
}

检查站:运行你的应用程序。你会注意到它看起来像有只有一个按钮。这是因为for-in循环只是堆叠在彼此顶部的按钮。你需要调整按钮的位置布局。

 

布局方法名为layoutSubviews的方法,这个方法在UIView类中已定义。layoutSubviews会由系统在合适的时候调用,给UIView的子类一个可以自行实现准确布局的地方。你需要在重写这个方法来把按钮放置在合适的地方

写布局按钮的代码

1.在RatingControl.swift中的init(coder:)函数下方,添加一个方法

override func layoutSubviews() {
}

 你可以使用代码完成功能迅速添加方法

2.在方法中,添加如下代码

var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44)
 
// Offset each button's origin by the length of the button plus spacing.
for (index, button) in ratingButtons.enumerate() {
    buttonFrame.origin.x = CGFloat(index * (44 + 5))
    button.frame = buttonFrame
}

这个代码创建了一个框,使用for-in循环遍历所有的框(frame),enumerate()方法返回一个集合,包含ratingButtons数组中的元素和索引。这个集合包含一个元组,每个元组包含一个索引和一个按钮。正好我们需要用到索引来计算按钮的位置。你的layoutSubviews方法应该如下所示:

override func layoutSubviews() {
    var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44)
    
    // Offset each button's origin by the length of the button plus spacing.
    for (index, button) in enumerate(ratingButtons) {
        buttonFrame.origin.x = CGFloat(index * (44 + 5))
        button.frame = buttonFrame
    }
}

检查站:运行你的应用程序。现在,按钮应该是并排的了。点击任何按钮,可以在控制台收到信息。

 

声明一个常量表示按钮大小

注意我们使用了44这个值在代码中,这一般来说是不好的做好,我们使用了硬编码。如果你想要一个稍微大点的按钮,你就必须在每个44出现的地方去修改,这样很麻烦,相反我们使用一个常量,来表示按钮的大小,这样其他地方引用这个常量,我们要修改按钮大小的时候,只需要修改这个常量即可。现在我们可以通过检索容器View的高度,来调整我按钮的大小。

声明一个常量为保存按钮的尺寸

1.在layoutSubviews()方法中,添加如下代码:

 

// Set the button's width and height to a square the size of the frame's height.
let buttonSize = Int(frame.size.height)

 

这使的布局更灵活

2.改变方法中,把44变成buttonSize:

var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
 
// Offset each button's origin by the length of the button plus spacing.
for (index, button) in ratingButtons.enumerate() {
    buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
    button.frame = buttonFrame
}

3.在init(coder:)初始化函数中,改变循环中的第一行let button = UIButton()

 

let button = UIButton()

因为我们layoutSubviews()方法中设置了按钮的frames ,所以这我们你不再需要在创建按钮时,进行设置尺寸。

你的layoutSubviews()方法现在看起来应该是这样:

override func layoutSubviews() {
    // Set the button's width and height to a square the size of the frame's height.
    let buttonSize = Int(frame.size.height)
    var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
    
    // Offset each button's origin by the length of the button plus some spacing.
    for (index, button) in enumerate(ratingButtons) {
        buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
        button.frame = buttonFrame
    }
}

init(coder:)方法中,现在看起来应该是这样:

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    for _ in 0..<5 {
        let button = UIButton()
        button.backgroundColor = UIColor.redColor()
        button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
        ratingButtons += [button]
        addSubview(button)
    }
}

检查点:运行你的应用程序。一切都应该和以前一样工作。按钮应该是并排的。点击任何按钮,可以在控制台收到信息。

添加Star图像到Buttons中

接下来我们将添加星星到按钮中

 

上面图片,你可以直接右键下载

 

添加图片到项目中

1.打开项目导航,选择Images.xcassets ,这个目录,这一系列的操作,在上一章我们已经做过了,你是否还记得呢?

2.在底部的左下角,点击(+)按钮,然后选择New Folder(上一章是直接选择New Image Set,因为我们这里有两个星星图片,所以我们使用文件夹,方便分类)

 

3.双击这个文件夹名字然后重命名为Rating Images

4.选择文件夹,点击(+)选择New Image Set。

5.双击image然后重命名为emptyStar

6.在电脑中,选择空的星星图片。

7.拖动到2x槽中

8.重复4-7步骤,把emptyStar换成filledStar,拖动填充的星星图片到2x槽中

最后你的asset目录应该如下所示:

下一步,我们会在适当的时间,通过写代码来设置Button的图像

为按钮设置图片

1.打开RatingControl.swift

2.在init(coder:)中,循环之前,添加如下代码:

 

let filledStarImage = UIImage(named: "filledStar")
let emptyStarImage = UIImage(named: "emptyStar")

 

3.在循环中的最后一行添加如下代码:

button.setImage(emptyStarImage, forState: .Normal)
button.setImage(filledStarImage, forState: .Selected)
button.setImage(filledStarImage, forState: [.Highlighted, .Selected])

根据不同的状态,你设置两个不同的图像,所以你可以看到,当按钮被选中。当按钮处于未选中状态(Normal)空星星的图像出现 。当按钮处于选中状态(Selected状态),实星星的图像出现。当用户是在敲击按钮时,按钮突出显示(.Selected和.Highlighted状态)。

4.删除设置背景色为红色的,这行代码

button.backgroundColor = UIColor.redColor()

5.添加以下这行代码:

button.adjustsImageWhenHighlighted = false

这是为了确保图像不会在状态变化过程中显示高亮。

现在的init(coder:)函数看起来应该如下:

required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
    
    let emptyStarImage = UIImage(named: "emptyStar")
    let filledStarImage = UIImage(named: "filledStar")
    
    for _ in 0..<5 {
        let button = UIButton()
        
        button.setImage(emptyStarImage, forState: .Normal)
        button.setImage(filledStarImage, forState: .Selected)
        button.setImage(filledStarImage, forState: [.Highlighted, .Selected])
        
        button.adjustsImageWhenHighlighted = false
        
        button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
        ratingButtons += [button]
        addSubview(button)
    }
}

检查站:运行你的应用程序。您应该看到星星,而不是红色的按钮。但你的按钮不改变图像呢。我们下面会解决这个问题。

实现按钮的动作

用户需要能够通过点击星星来选择一个评级,所以我们需要更换先前ratingButtonTapped()方法中的内容。

实现评级动作

1.RatingControl.swift中,找到ratingButtonTapped(_:)方法:

 

func ratingButtonTapped(button: UIButton) {
    print("Button pressed 👍")
}

 

2.替换print语句为下面的代码:

rating = ratingButtons.indexOf(button)! + 1

indexOf(_:)方法尝试找到按钮数组中已选择的按钮,然后返回这个按钮的索引。这个方法返回可选的Int,因为搜索的实例可能不存在于你的集合中。然而,因为仅仅只在会你创建并添加到数组中的一个按钮才会触发动作,你能搜索按钮并返回有效的索引。在这种情况下你能使用强制解包操作符(!)来访问潜在的索引值。你索引的值加1表示这个评级。因为数组下标是从0(0-4)开始的,我们需要评级为1-5。

3.在RatingControl.swift中,(})前面,添加如下代码

func updateButtonSelectionStates() {
}

这个帮助方法,你将用于和更新按钮的选择状态

4.在updateButtonSelectionStates()方法内,添加如下for-in循环:

for (index, button) in enumerate(ratingButtons) {
    // If the index of a button is less than the rating, that button should be selected.
    button.selected = index < rating
}

这段代码通过按钮数组来遍历设置每一个按钮的状态,根据数组中的索引是否小于评级来判断按钮是否选中。如果index < ratin的值为true,那么这个按钮的状态就是已选择的,同时使之显示为填充的星星图片。否则,其他的按钮为为选中状态,显示为空心的星星。如果你的Swift版本找不到indexOf方法,那么给你一个提示使用一个全局函数(寻找)可以解决这个问题

5.在ratingButtonTapped()方法中,添加一个调用方法为updateButtonSelectionStates(),在方法实现的最后一行

 

func ratingButtonTapped(button: UIButton) {
    rating = ratingButtons.indexOf(button)! + 1
    
    updateButtonSelectionStates()
}

6.在layoutSubviews方法()中,也添加updateButtonSelectionStates()方法,到方法实现的最后一行

override func layoutSubviews() {
    // Set the button's width and height to a square the size of the frame's height.
    let buttonSize = Int(frame.size.height)
    var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
    
    // Offset each button's origin by the length of the button plus some spacing.
    for (index, button) in enumerate(ratingButtons) {
        buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
        button.frame = buttonFrame
    }
    updateButtonSelectionStates()
}

当载入view时,更新按钮的选择状态很重要,不只是当评级改变时。

7.在 // MARK: Properties,找到rating属性

var rating = 0

8.更新rating属性,包含这个观察者:

var rating = 0 {
didSet {
    setNeedsLayout()
}
}

一个属性观察者,用来观察和响应属性值的变化。一个属性的值每一次被设置时,属性观察者会被调用,可用于值改变前后立即执行一些工作。didSet属性观察者是在属性值被设置后哦立即调用。在这里,我们调用了setNeedsLayout()方法,表示每次评级改变时,出发一个布局更新。确保UI会一直准确的显示评级属性。

现在我们的updateButtonSelectionStates()看起来应该是这样:

func updateButtonSelectionStates() {
    for (index, button) in enumerate(ratingButtons) {
        // If the index of a button is less than the rating, that button shouldn't be selected.
        button.selected = index < rating
    }
}

检查站:运行你的应用程序。您应该看到五颗星,并能点击一个改变评级。点击第三颗星的评级更改为3,如。

添加星星的间距和数量属性

确保你不会有任何的硬编码,创建评级星星的数量和间距属性。这样,如果你需要改变这些值,你只需要在一个地方修改

使评级控件的属性,来界面构造器中可查

1.在 RatingControl.swift中,找到 // MARK: Properties

// MARK: Properties
 
var rating = 0
var ratingButtons = [UIButton]()

2.在已存在的属性下方,添加如下代码:

var spacing = 5

这个属性是用于你按钮的间距

3.在layoutSubviews中,替换原来间距的常量,改为使用spacing属性:

buttonFrame.origin.x = CGFloat(index * (buttonSize + spacing))

4.在spacing属性的下方,添加另一个属性:

var stars = 5

这个属性用来控制你显示星星的数量

5.在 init(coder:)中替换先前的代码:

for _ in 0..<stars {

检查点:运行你的应用程序。一切都应该看起来跟以前完全一样。

连接Rating Control到View Controller

我们最后需要做的事情就是为rating control,在ViewController中设置一个引用

连接到一个rating control outlet到ViewController.swift

1.打开你的storyboard

2.打开assistant editor

3.选择rating control

4.按住Control从画布中拖动rating control到代码中,然后在photoImageView下方,松开

5.在弹出的对话框中,Name字段旁输入ratingControl。其他不变,然后点击Connect

清理项目

你即将完成菜谱的场景UI,但首先你需要做一些清理工作。相比先前的章节,现在,FoodTracker应用程序实现更先进的特性和不同的用户界面,你要删除你不需要的部分。你还可以让元素居中,以平衡的UI元素。

清理UI

1.返回到standard editor

 

2.打开你的storyboard

3.选择 Set Default Label Text按钮,然后按下Delete键删除它。

 

4.选择Select View(如果你不是在Xcode7下,没有Stack View的,后面可以略过不看)

5.打开Attributes inspector

6.在Attributes inspector,找到Alignment字段,然后选择Center,stack view会水平居中

下面我们需要移除按钮点击响应的动作方法

清理代码

1.打开ViewController.swift

2.在ViewController.swift中,删除setDefaultLabelText()方法

后面的章节我们会修改mealNameLabel的outlet

检查站:运行你的应用程序。一切都应该像以前一样工作,但设置默认标签文本按钮消失了,而元素水平居中。点击任何按钮,依然会有消息显示在控制台。

posted @ 2015-07-01 19:11  jy02432443  阅读(822)  评论(0编辑  收藏  举报