实现自定义控制
本章将会实现对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
检查站:运行你的应用程序。一切都应该像以前一样工作,但设置默认标签文本按钮消失了,而元素水平居中。点击任何按钮,依然会有消息显示在控制台。