使用Swift编程语言开发iOS应用(五)

实现一个定制化控件

本文的学习目标

  • 创建和关联定制化源代码到storyboard的界面元素
  • 定义一个定制类
  • 实现一个定制类的构造器
  • 使用UIView作为容器
  • 理解如何程序化显示视图

创建一个定制化视图

为了能够给一个菜品打分,需要一个控件(control)来选择给一个菜品赋予星级的数量,本文关注于在storyboard上创建一个定制视图和编写定义代码。

5_2_ratingcontrol_2x.png

这个打分控件让用户给一个菜品选择0、1、2、3、4或者5颗星,点击其中一颗星时,包括被点击的星和其前面的星将被填充,被填充的星的数量作为打分值。

创建一个UIView类的子类
  1. 选择File/New/File(或按键)
  2. 在提示对话框中选择Source为iOS
  3. 选择Cocoa Touch Class,点击Next
  4. 在Class字段,输入RatingControl
  5. 在SubClass of字段,选择UIKit
  6. 选择语言选项为Swift。


    5_3_newviewclass_2x.png
  7. 点击“Next”
  8. 使用所有缺省,点击“Create”,
    生成了定义RatingControl类的RatingControl.swift文件。
  9. 代码如下

import UIKit class RatingControl: UIView { }

典型创建一个视图的方法有两种:一种是初始化(initializing)视图的框架然后人工添加视图到界面上,第二种使用storyboard来加载一个视图,分别对应于:初始化框架方法init(frame:)和storyboard的init?(coder:)。
本文使用storyboard加载视图,需要重写(override)父类的init?(coder:)方法的实现。

重写构造器
  1. 在RatingControl.swift中,添加//MARK注释;

    //MARK: Initialization

  2. 在评论下,输入init后出现命令自动完成窗口;

5_4_initcoder_codecompletion_2x.png
  1. 选择对应于init?(coder:)的方法;

  2. 点击错误提示来消除错误,添加required关键字;

    required init?(coder aDecoder: NSCoder) { }

    每个UIView子类实现构造器必须包含一个init?(coder:)构造器的实现,因此在该构造器前面需要required。

  3. 增加父类构造器的调用。

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

5_5_initcoder_fixit_2x.png

显示定制视图

为了显示定制视图,在界面上增加一个视图并在代码和视图之间建立连接。

显示视图
  1. 打开storyboard文件
  2. 在对象库(Object library)中找到视图对象,将其拖拽到storyboard场景堆栈视图中图像视图的下面
  3. 选中定制视图的同时,打开尺寸编辑器(Size inspector),修改Height字段为44,修改Width字段为240,修改Intrinsic Size字段为Placeholder
5_6_inspector_size_2x.png
5_7_viewwithplaceholdersize_2x.png
  1. 选中定制视图的同时,打开标示编辑器(Identity inspector),在Class字段选择RatingControl
5_8_inspector_identity_2x.png
5_9_identity_ratingcontrol_2x.png

增加按钮到视图中

在UIView类的子类定制视图RatingControl中添加按钮,让用户可以选择点击打分。

在视图中创建一个按钮
  1. 在构造器init?(coder:)中,添加创建一个红色按钮的代码,并添加到RatingControl视图中

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

  2. 设置按钮所在视图的固有尺寸(intrinsic size),用于堆栈视图对按钮进行布局。

    override func instrinsicContentSize() ->CGSize { return CGSize(width: 240, height: 44) }

增加按钮的动作方法
  1. 在RatingControl.swift文件中,在代码块中添加注释说明到最后一行。

    // MARK: Button Action

  2. 在注释下面添加代码

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

  3. 在构造器init?(coder:)中代码addSubview(button)之前添加代码

    button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(_:)), forControlEvents: .TouchDown)

    前面教程有讲述连接storyboard上的界面元素到代码中的动作方法,采用了目标-动作(target-action)模式,同样这里绑定ratingButtonTapped(_:)动作方法到button对象上,当button对象出现TouchDown事件时会触发其绑定的动作方法。这里目标是self指的是RatingControl类。
    完整的代码如下:

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: #selector(RatingControl.ratingButtonTapped(_:)), forControlEvents:.TouchDown) addSubview(button) }

5_11_console_buttonpressed_2x.png
增加打分属性
  1. 在RatingControl.swift文件中,在代码块中添加属性注释说明和以下代码。

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

创建5个按钮
  1. 在RatingControl.swift文件中,在构造器init?(coder:)中增加for-in循环,增加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:#selector(RatingControl.ratingButtonTapped(_:)), forControlEvents: .TouchDown) ratingButtons += [button] addSubview(button) } }

UIView类的布局方法是layoutSubviews,在子类中重写该方法。

布局多个按钮
  1. 在RatingControl.swift文件中构造器后面添加layoutSubviews方法

    override func layoutSubviews() { var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44) for (index, button) in ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (44 + 5)) button.frame = buttonFrame } }

使用for-in循环遍历ratingButtons数组,调用其enumerate()方法得到数组中的对值(index, button)对象,index为数组的下标。

增加属性用于Star间隔和数量

增加属性
  1. 在RatingControl.swift文件中// MARK: Properties段添加一行代码

    let spacing = 5

  2. 在layoutSubviews方法中,修改代码

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

  3. 在spacing属性下面添加代码

    let starCount = 5

  4. 在构造器init?(coder:)中修改代码

    for _ in 0..<starCount {

声明一个按钮尺寸的常量

让按钮根据所在容器视图的高度来调整尺寸,使用常量保存容器视图的高度。

声明一个常量用于按钮的尺寸
  1. 在layoutSubviews()方法中,增加以下代码

    let buttonSize = Int(frame.size.height)

  2. 修改layoutSubviews()方法的代码

    var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize) for (index, button) in ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (buttonSize + 5)) button.frame = buttonFrame }

  3. 在instrinsicContentSize()方法修改代码

    let buttonSize = Int(frame.size.height) let width = (buttonSize * starCount) return CGSize(width: width, height: buttonSize)

  4. 在init?(coder:)构造器中for-in循环修改第一行代码

    let button = UIButton()

按钮的框架尺寸在layoutSubviews()方法中完成。完整的代码如下:

override func layoutSubviews() { let buttonSize = Int(frame.size.height) var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize) for (index, button) in ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (buttonSize + 5)) button.frame = buttonFrame } }

intrinsicContentSize方法代码如下:

`override func intrinsicContentSize() -> CGSize {

let buttonSize = Int(frame.size.height)
let width = (buttonSize * starCount) + (spacing * (starCount - 1))
return CGSize(width: width, height: buttonSize)

}`

init?(coder:)构造器完整代码如下:

required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) for _ in0..<5 { let button = UIButton() button.backgroundColor = UIColor.redColor() button.addTarget(self, action:#selector(RatingControl.ratingButtonTapped(_:)), forControlEvents: .TouchDown) ratingButtons += [button] addSubview(button) } }

在模拟器上运行APP

5_13_sim_5redbuttons_2x.png

增加星型图像到按钮上

添加图像到项目中
  1. 在项目导航栏中选中Assets.xcassets,右侧显示资源目录(asset catalog);

  2. 在点击左下角的“+”按钮,在弹出菜单中选择“New Folder”;

    5_14_assetcatalog_addfolder_2x.png
  3. 双击目录名并改名为”Rating Images”;

  4. 在目录选中的状态下,在点击左下角的“+”按钮,在弹出菜单中选择“New Image Set”,一个图像集合(image set)代表一组包含用于显示不同屏幕分辨率的不同版本图像的单一图像资源;

  5. 双击资源集合名并改名为”emptyStar“;

  6. 将电脑中empty star图像拖拽到图像集合中,2x表示用于iPhone 6的显示分辨率;

    5_17_emptystar_drag_2x.png
  7. 同上方法创建filledStar图像集合。

5_18_filledstar_drag_2x.png
5_19_assetcatalog_final_2x.png
设置按钮为星型图像
  1. 打开RatingControl.swift文件
  2. 在init?(coder:)构造器中,添加以下代码

let filledStarImage = UIImage(named: “filledStar”) let emptyStarImage = UIImage(named: “emptyStar”)

  1. 在for-in循环中,在按钮被初始化代码后面增加以下代码

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

  1. 修改部分代码,完整的init?(coder:)构造器代码如下

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

在模拟器上运行APP

实现按钮动作方法

点击一个星完成实际的打分,实现ratingButtonTapped(_:)方法。

实现打分动作
  1. 在RatingControl.swift中,找到ratingButtonTapped(_:)方法,添加代码;

`func ratingButtonTapped(button: UIButton) {

   rating = ratingButtons.indexOf(button)! + 1

}`

indexOf(_:)方法在按钮数组中找到被点击按钮所在的数组下标(数组下标从0开始,打分计数要加1)。

  1. 添加updateButtonSelectionStates()方法和代码;

func updateButtonSelectionStates() { for (index, button) in ratingButtons.enumerate() { button.selected = index < rating } }

更新按钮的选中状态,当小于点击按钮的数组下标时设置为选中,否则为未选中。
  1. 修改ratingButtonTapped(_:)方法如下;

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

  1. 修改layoutSubviews()方法,增加updateButtonSelectionStates()的调用;

override func layoutSubviews() { let buttonSize = Int(frame.size.height) var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize) for (index, button) in ratingButtons.enumerate() { buttonFrame.origin.x = CGFloat(index * (buttonSize + 5)) button.frame = buttonFrame } updateButtonSelectionStates() }

  1. 为rating属性增加属性侦听(property observer)方法didSet。

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

属性侦听当属性发生改变后会调用didSet,这里会调用setNeedsLayout()触发布局更新事件。
在模拟器上运行APP:
5_21_sim_filledstars_2x.png

连接打分控件到视图控制器

在ViewController类中建立一个打分控件的引用。

使用outlet连接打分控件到ViewController.swift
  1. 打开storyboard文件;
  2. 点击右上角的助手编辑器;
  3. 在界面选中打分控件,拖拽到ViewController.swift中属性photoImageView下面;
5_22_ratingcontrol_dragoutlet_2x.png
  1. 在提示对话框中Name字段输入ratingControl;

    5_23_ratingcontrol_addoutlet_2x.png
  2. 点击“Coonect”按钮。

清理项目代码

清理界面
  1. 打开storyboard;
  2. 选中“Set Default Label Text”按钮,按Delete键将其删除;
5_24_deletebutton_2x.png
  1. 打开outline视图,选中StackView对象;
2_AL_outlineview_2x.png
  1. 打开属性编辑器(Attributes inspector);
  2. 找到Alignment字段,选择Center。
5_25_centerstack_2x.png
清理代码
  1. 打开ViewController.swift文件
  2. 删除setDefaultLabelText(_:)方法
    在模拟器上运行APP:
5_26_sim_finalUI_2x.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容