本章将会实现对FoodTracker APP的评级控制,当你完成时,你的APP看起来像这样:编程
学习目标swift
在课程结束时,你将可以:数组
建立并关联自定义源代码文件和在storyboard中的元素
定义一个自定义类
在实现自定义类的初始化
使用的UIView做为容器
了解如何以编程方式显示views app
建立一个自定义Viewless
为了能评级一个菜谱,用户须要一个控制,让他们能选择给想要菜谱多少星星数量。有许多方法实现这个,但咱们会专一于涉及建立一个自定义view,用过在代码中定义,并使用storyboard。实现的效果以下:ide
评级控件将让用户为一个菜谱选择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
检查站:运行你的应用程序。一切都应该像之前同样工做,但设置默认标签文本按钮消失了,而元素水平居中。点击任何按钮,依然会有消息显示在控制台。