马上着手开发|马上着手开发 iOS 应用程序 (六) - 实现自定义控件
重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。
翻译自苹果官网:
https://developer.apple.com/library/prerelease/ios/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson5.html#//apple_ref/doc/uid/TP40015214-CH19-SW1
在本课中,为 app 实现评分功能。完成的样子如下:
[图片上传失败...(image-ed9a4d-1608214851935)]
学习目标:
课程的最后,你讲能够:
- 创建自定义源代码文件并与 storyboard 中控件关联
- 定义自定义类
- 实现自定义类的构造器
- 使用 UIView 作为容器
- 理解如何在程序中显示视图
我们需要一个控件来让用户为美食评分。虽然有多种实现方式,但是本节重点是使用自定义类的方式。
这是你即将要实现的评分控件:
[图片上传失败...(image-5a7bae-1608214851936)]
评分控件让用户为美食选择 0、1、2、3、4或5分。当用户点击星星,在所点星星左边的所有星星都将被填充颜色。计算填充的星星数目作为评分,而空的星星不算。
【马上着手开发|马上着手开发 iOS 应用程序 (六) - 实现自定义控件】通过创建一个 UIView 的子类来构建控件的界面、交互和行为。
创建 UIView 的子类
- 选择
File > New > File
(或按 Command-N)。
- 选择
Cocoa Touch Class
然后点击 Next。
- 在 Class 区域,输入 RatingControl。
- 在
SubClass of
区域,选择 UIView。
- 确保语言设置成了 Swift。
[图片上传失败...(image-781de4-1608214851936)]
- 点击 Next。
默认会存到你的项目目录中。
Group 选项默认是你的 app 名字,FoodTracker。
在 Targets 区域,app 选项是选中的而 tests 未选中。
- 其他都按照默认的来,点击 Create。
Xcode 创建了 RatingControl.swift 文件。RatingControl 是 UIView 的子类。
- 删除文件中模板自动添加的注释以便轻装上阵。
import UIKit class RatingControl: UIView { }
由于使用 storyboard 来加载 view,所以一开始请覆写父类的
init?(coder:)
构造方法。覆写构造方法
- 在 RatingControl.swift 的 class 行下面,添加如下注释。
// MARK: Initialization
- 在注释下面,输入 init。
弹出代码提示框。
[图片上传失败...(image-d1c2fd-1608214851936)]
- 选择列表的第三个 init?(coder:) 构造方法,回车确认。
init?(coder aDecoder: NSCoder) { }
Xcode 为你生成基本的构造方法。
- 点击红色圆圈添加 required 关键字来修复错误。
required init?(coder aDecoder: NSCoder) { }
每个 UIView 子类实现自定义构造方法同时必须实现 init?(coder:)。
Swift 编译器知道这个规则,所以提供了 fix-it 功能为错误提供潜在的解决方案。
- 在子类的构造器中添加如下行。
super.init(coder: aDecoder)
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
显示自定义视图
为了显示自定义视图,需要向界面添加视图并确保视图关联了 RatingControl 类。
显示视图
- 打开 storyboard。
- 在 storyboard 中,使用对象库找到 View 对象并拖到堆栈视图中 image view 的下面。
- 选中视图,在实用工具区打开尺寸检查器。
[图片上传失败...(image-71bf43-1608214851936)]
- 在 Intrinsic Size 区域的弹出菜单中,选择 Placeholder。
- 在 Instrinsic Size 下面的 Height 区域输入 44 ,Width 区域输入 240 。回车确认,界面最后应该像这样:
[图片上传失败...(image-c76766-1608214851936)]
- 选中视图,打开识别检查器(Identity inspector)。
[图片上传失败...(image-d3e5ed-1608214851936)]
- 在识别检查器中,找到名为 Class 的区域选择 RatingControl。
[图片上传失败...(image-50b30f-1608214851936)]
下一步是添加按钮到视图来允许用户选择评分。
步骤:
- 在 init?(coder:) 构造方法中,添加如下代码行来创建红色按钮:
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) button.backgroundColor = UIColor.redColor()
使用 redColor() 更容易让你看到视图的样子。如果你喜欢可以换成其他 UIColor 的值,如 blueColor() 和 greenColor()。
- 在方法最后添加如下代码:
addSubview(button)
addSubView() 方法向 RatingControl 添加刚才创建的按钮。
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)
}
为了告诉堆栈视图如何布局控件,需要提供固有内容尺寸。如下面这样覆写
intrinsicContentSize
方法。override func intrinsicContentSize() -> CGSize {
return CGSize(width: 240, height: 44)
}
检验:运行 app。应该能够看到视图中有个小红色正方形。它是刚才在构造方法中添加的按钮。
[图片上传失败...(image-4360ea-1608214851936)]
给按钮添加动作
- 在 RatingController.swift 最后 } 的前面添加这行注释:
// MARK: Button Action
- 在注释下面添加如下代码:
func ratingButtonTapped(button: UIButton) { print("Button pressed ") }
使用 print() 方法检查 ratingButtonTapped(_:) 动作连接了预期的按钮。这个函数打印消息到 Xcode 调试控制台中,而控制台是在编辑区底部非常有用的调试工具。
稍后会用真实实现来替代这行调试代码。
- 找到 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) }
- 在 addSubView(button) 行下面,添加如下代码:
button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
你应该很熟悉 target-action 设计模式因为之前已经使用它连接过控件和方法。现在让我们再次创建连接。连接 ratingButtonTapped: 动作和按钮对象,当用户按下了按钮,会触发这个方法。
注意因为使用了界面构造器(Interface Builder),就可以像普通方法一样定义,而不需要使用 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)
}
检验:运行 app。当点击了红色正方形,应该看到控制台打印了 Button Pressed。
[图片上传失败...(image-cb94a3-1608214851936)]
使用整形变量来表示评分值,它的范围是0到5,使用数组来存放所有的按钮。
添加评分属性
- 在 RatingControl.swift 中,找到类定义行:
class RatingControl: UIView {
- 在行的下面,添加如下代码:
// MARK: Properties var rating = 0 var ratingButtons = [UIButton]()
创建五个按钮
- 在 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) }
- 在最后四行添加 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 确保 for-in 中的行缩进正确。
半闭区间运算符(..<) 不包括最大的数,所以范围是从 0 到 4 五次循环来添加五个按钮。当不需要知道当前循环变量使用通配符 _。
- 在 addSubView(button) 行上面,添加如下代码:
ratingButtons += [button]
添加每个按钮到 ratingButtons 数组。
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)
}
}
检验:运行 app。还是只看到了一个按钮。因为 for-in 循环仅仅把各个按钮堆叠到一起。需要调整这些按钮的布局。
[图片上传失败...(image-7a228e-1608214851936)]
调整按钮的布局
- 在 RatingControl.swift 的
// MARK: Initialization
区域添加如下方法:
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 ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (44 + 5)) button.frame = buttonFrame }
使用 for-in 循环遍历按钮来设置它们的 frames。
enumerate() 方法返回 ratingButtons 中所有控件的集合。集合中每个元组包含一个 index 和 button,分别代表遍历的下标和按钮。使用 index 计算新的 frame 并设置给对应的按钮。frame 的 x 值等于 44 点的标准按钮大小加上 5 点的空隙然后乘以 index 。
最后的 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 ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (44 + 5)) button.frame = buttonFrame } }
[图片上传失败...(image-382550-1608214851936)]
使用
Debug toggle
收缩控制台。[图片上传失败...(image-e34a5f-1608214851936)]
为按钮大小添加常量
注意在代码中使用了 44 的值。而在代码的各处使用硬编码值是很不好的做法。如果需要一个大一点的按钮,就得在各处修改这个 44 的值。相反,定义常量来表示按钮的大小,这样更方便修改因为值只需修改一处。
现在,根据容器视图的高度调整按钮来适配不同尺寸的容器视图.
定义一个常量作为按钮的大小
- 在 layoutSubviews() 方法第一行前面添加如下代码:
// Set the button's width and height to a square the size of the frame's height. let buttonSize = Int(frame.size.height)
这让布局更加灵活。
- 修改方法剩下部分使用 buttonSize 常量取代 44:
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 }
- 和第一次添加按钮一样,你需要更新控件的固有内容尺寸来让堆栈视图正确的布局。使用 intrinsicContentSize 方法来计算控件的大小并返回,代码如下:
- 修改 init?(coder:) 方法中 for-in 循环的第一行为如下代码 :
let button = UIButton()
因为你在 layoutSubviews() 中设置了按钮的 frame,你不再需要在创建按钮时候设置了。
最后的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 ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (buttonSize + 5)) button.frame = buttonFrame } }
override func intrinsicContentSize() -> CGSize {
let buttonSize = Int(frame.size.height)
let width = (buttonSize + spacing) * starsreturn CGSize(width: width, height: buttonSize)
}
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)
}
}
检验:运行 app。一切和之前一样。按钮应该并排显示。此刻点击任何一个按钮都会调用 ratingButtonTapped(_:) 并打印信息到控制台。
[图片上传失败...(image-24503b-1608214851936)]
向按钮中添加星星的图片
下一步,添加空的和填充的星星图片到按钮中。
[图片上传失败...(image-27ef0d-1608214851936)]
可以在课程最终项目的 Images/ 文件夹中找到这两张图片,或者用你自己的。
添加图片到项目中
- 选择项目导航的 Assets.xcassets 查看 asset catalog。
回忆一下 asset catalog 是存放和管理 app 图片资源的地方。
- 点击左下角的 + 按钮,从弹出的菜单中选择 New Folder。
[图片上传失败...(image-ca95d8-1608214851936)]
- 双击文件夹名字并重命名为 Rating Images。
- 选中文件夹,点击左下角的 + 按钮,在弹出菜单中选择 New Image Set。
一个图片集合虽然只能代表单张图片,但是包含在不同屏幕分辨率上显示的不同版本图片。
- 双击图片集合的名字并重命名为 emptyStar。
- 选择电脑中空的星星图片。
- 拖动图片到图片集合的 2x 槽中。
[图片上传失败...(image-54cea1-1608214851936)]
2x 是 iPhone 6 模拟器显示的分辨率,图片在这个分辨率显示最好。
- 点击左下角的 + 按钮,在弹出的菜单中选择 New Image Set。
- 双击图片集合名字并重命名为 filledStar。
- 在电脑上,选择想要添加的填充状态的星星图片。
- 拖动图片到图片集合的 2x 槽中。
[图片上传失败...(image-8ab7f7-1608214851936)]
asset catalog 应该像这样:
[图片上传失败...(image-3c4c98-1608214851936)]
下一步,在适当时候写代码为按钮设置正确的图片。
- 打开 RatingControl.swift。
- 在 init?(coder:) 构造方法的 for-in 循环前面添加这两行:
let filledStarImage = UIImage(named: "filledStar") let emptyStarImage = UIImage(named: "emptyStar")
- 在 for-in 循环按钮初始化那一行的后面,添加如下代码:
button.setImage(emptyStarImage, forState: .Normal) button.setImage(filledStarImage, forState: .Selected) button.setImage(filledStarImage, forState: [.Highlighted, .Selected])
为按钮的不同状态设置两张不同的图片这样当按钮选中的时候就能看到状态变化了。按钮未选中(.Normal 状态)显示空的星星图片。按钮被选中(.Selected 状态)显示填充的星星图片。在用户点击按钮不松手时候按钮同时处于选中和高亮状态。
- 删除设置背景颜色为红色的那行代码:
button.backgroundColor = UIColor.redColor()
因为按钮已经有图片,所以把背景颜色去掉。
- 添加如下代码:
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 = falsebutton.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
ratingButtons += [button]
addSubview(button)
}
}
检验:运行 app。应该看到星星已经取代红色按钮了。此刻点击任意一个按钮仍然会调用 ratingButtonTapped(_:) 并打印信息到控制台上,但是按钮图片并没有改变。下一步会修复这个问题的。
[图片上传失败...(image-2e0a7f-1608214851936)]
实现按钮动作
用户需要点击星星的时候选择评分,所以使用真实的 ratingButtonTapped(_:) 方法替代调试实现。
实现评分动作
- 找到 RaingControl.swift 文件的 ratingButtonTapped(_:) 方法:
- 使用下面这行替代 print 语句:
rating = ratingButtons.indexOf(button)! + 1
indexOf(_:) 方法尝试找出数组中选中的按钮并返回它的下标。
方法返回可选的 Int 因为搜索的对象或许在集合中不存在。然而,因为触发动作的按钮是你自己添加和创建到数组中的,所以确定会返回正确的下标。这时,使用 ! 来访问内在的下标值。因为数组下标是从0开始所以要加1才是合适的评分.
- 在 RatingControl.swift 的最后一个 } 前面,添加如下代码:
func updateButtonSelectionStates() { }
使用帮助方法来更新按钮的选中状态。
- 在 updateButtonSelectionStates() 方法中,添加 for-in 循环:
for (index, button) in ratingButtons.enumerate() { // If the index of a button is less than the rating, that button should be selected. button.selected = index < rating }
代码遍历按钮数组根据按钮的下标是否小于评分来设置每个按钮的状态。如果 index < rating 设置按钮的状态为选中并让它显示填充的星星图片。否则,按钮不选中显示空的星星图片。
- 在 ratingButtonTapped(_:) 方法中,添加 updateButtonSelectionStates() 的调用作为最后一行的实现:
func ratingButtonTapped(button: UIButton) { rating = ratingButtons.indexOf(button)! + 1updateButtonSelectionStates() }
- 在 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 ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (buttonSize + 5)) button.frame = buttonFrame } updateButtonSelectionStates() }
- 在
// MARK: Properties
区域,找到 rating 属性:
var rating = 0
- 更新 rating 属性让它包含属性观察器:
var rating = 0 { didSet { setNeedsLayout() } }
属性观察器观察和响应属性值的修改。在设置属性值的前后它会被立即调用。具体来讲,当设完值后 didSet 属性观察器立刻执行。在这里调用 setNeedsLayout() 让每次评分修改后触发布局更新。确保 UI 显示正确的 rating 属性值。
func updateButtonSelectionStates() {
for (index, button) in ratingButtons.enumerate() {
// If the index of a button is less than the rating, that button shouldn't be selected.
button.selected = index < rating
}
}
检验:运行 app。应该能看到五颗星,请尝试点击一个来修改评分。点击第三颗星来修改评分为3,例如。
[图片上传失败...(image-273d8b-1608214851936)]
为间距和星星数量添加属性
确保在代码中没有任何硬编码的值,为评分的数量和评分之间的间距添加属性。这样,你只需修改代码的一处。
用属性代替硬编码的值
- 找到 RatingControl.swift 的
// MARK: Properties
区域。
// MARK: Properties var rating = 0 { didSet { setNeedsLayout() } } var ratingButtons = [UIButton]()
点击编辑区顶部文件名字使用 functions menu 快速跳过去。
- 在已存在的属性下面,添加如下代码:
var spacing = 5
使用这个属性给你的按钮增加一些额外间距。
- 在 layoutSubviews 中,使用 spacing 属性替代那个作为间距的常量。
buttonFrame.origin.x = CGFloat(index * (buttonSize + spacing))
- 在 spacing 属性下面,添加另外一个属性:
var stars = 5
可以使用这个属性来控制控件的星星数量。
- 在
init?(coder:)
中,使用 stars 属性替代之前你为星星数量设置的常量。
for _ in 0..
连接评分控件和视图控制器
最后一件要做的事情就是给 ViewController 类添加一个评分控件的引用。
连接评分控件 outlet 到 ViewController.swift 中
- 打开 storyboard。
- 点击 Xcode 工具栏中的 Assistant 按钮打开辅助编辑器。
[图片上传失败...(image-bca606-1608214851936)]
- 如果想要更多的空间来工作,点击 Xcode 工具栏中的 Navigator 和 Utilties 按钮来收缩项目导航和实用工具区。
[图片上传失败...(image-4a797d-1608214851936)]
同样可以收缩大纲视图。
- 选择评分控件。
ViewConroller.swift 会显示在右边的编辑器。(如果没有,在右边的编辑器选择栏中选择 Automatic > ViewController.swift)
- 按住 Control 从画板中的评分控件拖动到右边的编辑器中,在 photoImageView 属性的下面停止拖动。
[图片上传失败...(image-8d53cb-1608214851936)]
- 在出现的对话框中,输入名字 ratingControl。
忽略剩下的选项,对话框最后应该像这样:
[图片上传失败...(image-80bfa3-1608214851936)]
- 点击 Connect。
清理项目
现在已经很接近最后的食物场景界面了,但是还是需要做一些清理工作。app 正在逐渐实现更多牛逼的功能且拥有与前面课程完全不同的界面。需要删掉一些不需要的代码,同时把控件集中到堆栈视图中来平衡界面。
清理界面
- 点击 Standard 按钮返回标准编辑器。
[图片上传失败...(image-48437d-1608214851936)]
点击 Xcode 工具栏的 Navigator 和 Utiliites 按钮展开项目导航和实用工具区。
- 打开 storyboard。
- 选择 Set Default Label Text 按钮,点击 Delete 键删除它。
堆栈视图重新排放 UI 控件来填充按钮留下的空间。
[图片上传失败...(image-80ff66-1608214851936)]
- 如果需要,打开大纲视图。选择 Stack View 对象。
[图片上传失败...(image-7b62c4-1608214851936)]
- 打开属性检查器。
- 在属性检查器中,找到 Alignment 区域并选择 Center。
堆栈视图中所有控件应该水平居中了:
[图片上传失败...(image-15509a-1608214851936)]
清理代码
- 打开 ViewController.swift。
- 删除 ViewController.swift 中的 setDefaultLabelText(_:) 动作方法。
@IBAction func setDefaultLabelText(sender: UIButton) { mealNameLabel.text = "Default Text" }
重要提示 如果运行出现问题,尝试按 Command-Shift-K 来 clear 项目。
[图片上传失败...(image-91ad7a-1608214851936)]
注意: 为了查看本课的完整实例项目, 下载文件并在 Xcode 中查看它。
推荐阅读
- 深入理解Go之generate
- 标签、语法规范、内联框架、超链接、CSS的编写位置、CSS语法、开发工具、块和内联、常用选择器、后代元素选择器、伪类、伪元素。
- 我的软件测试开发工程师书单
- echart|echart 双轴图开发
- NPDP拆书(三)(新产品开发战略(经营与创新战略))
- 芯灵思SinlinxA33开发板Linux内核定时器编程
- 常用git命令总结
- 藏族开发的修路人——记致富援乡的斯定那珠
- ASP.NET|ASP.NET Core应用开发思维导图
- VueX(Vuex|VueX(Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式)