当你还不能写出自己满意的程序时,你就不要去睡觉。
创建项目
首先,打开Xcode,新建一个项目,Xcode将提示选择一个工程模板。由于我们将从零开始学习,请在左侧窗口选则iOS/Application ,右侧窗口选择Empty Application ,点击Next ,然后在Product Name 项填入SwiftCounter ,Language 注意选择Swift ,再点击Next ,选择项目保存的路径,最后点击Create 即可完成项目创建。
项目新建完成后,我们可以看到工程中已经自动生成了AppDelegate.swift 文件。
应用代理类(AppDelegate)
AppDelegate 类中定义了app进入不同生命周期(包括app启动动、闲置、进入后台、进入前台、激活、完全退出)时的回调方法。实际上在app启动时,app会自动执行一个叫main 的入口函数,它通过调用UIApplicationMain 函数来创建出AppDelegate 类实例,并委托其实现app在不同生命周期的定制行为。
屏幕(Screen)、窗口(Window)和视图(View)
在app启动完成的回调方法application:didFinishLaunchingWithOptions 中,首先创建一个UIWindow 对象。在此,我先简单介绍一下iOS开发中基本UI元素:
UIScreen 代表一块物理屏幕;
UIWindow 代表一个窗口,在iPhone上每个app一般只有一个窗口,而在Mac上一个app经常有多个窗口;
UIView 代表窗口里某一块矩形显示区域,用来展示用户界面和响应用户操作;
UILabel 和UIButton ,继承自UIView 的特定UI控件,实现了特定的样式和行为。
继续看application:didFinishLaunchingWithOptions 中的默认实现:
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
window!backgroundColor UIColorwhiteColor()
makeKeyAndVisible()
首先,它通过获取主屏幕的尺寸,创建了一个跟屏幕一样大小的窗口;然后将其背景色为白色;并调用makeKeyAndVisible() 方法将此窗口显示在屏幕上。
视图控制器(ViewController)
在iOS开发中,主要使用ViewController 来管理与之关联的View、响应界面横竖屏变化以及协调处理事务的逻辑。每个ViewController 都有一个view 对象,定制的UI对象都将添加到此view 上。
为了给计时器创建页面并实现功能,我们需要新建一个视图控制器,命名为CounterViewController :点击文件,新建文件,类型选择Swift ,然后输入类名CounterViewController ,确定。
我们先为其添加基础的结构代码:
import UIKit
class CounterViewController UIViewController {
override func viewDidLoad() {
super()
}
}
其中重载的方法viewDidLoad 非常重要,它在控制器对应的view 装载入内存后调用,主要用来创建和初始化UI。
在介绍如何定制计时器UI之前,我们需要先将CounterViewController 的view 跟app中唯一的窗口Window 关联起来。完成此操作只需在application:didFinishLaunchingWithOptions 中加一行代码:
rootViewController CounterViewController 这样view 会自动添加到Window 上,用户启动app后直接看到的将是CounterViewController 中view 的内容。
创建计时器UI界面
在Xcode中创建UI的方法有很多种,包括使用Nib 或Storyboard 文件的可视化方式,以及使用纯代码的方式。由于代码创建UI是可视化创建UI的基础,本篇教程只介绍使用代码创建UI的方式。
先看一下我们最终的UI效果图:
在view上方,我们将定义一个标签UILabel 来显示剩余时间;
在view下方,我们定义了一排橘黄色的按钮UIButton ,接受用户点击操作;
在view最下方,我们定义了启动/停止 按钮和复位 按钮,接受用户点击操作。
为了能保存和引用这些UI控件,我们要为它们创建属性:
///UI Controls
var timeLabelUILabel? //显示剩余时间
timeButtons: UIButton[]? //设置时间的按钮数组
startStopButton//启动/停止按钮
clearButton//复位按钮
注意,所有的UI变量类型后面都带了? 号,表示它们是Optional 类型,其变量值可以为nil 。Optional 类型实际上是一个枚举enum ,里面包含None 和Some 两种类型。nil 其实是Optional.None ,非nil 是Option.Some ,它通过Some(T) 来包装(wrap )原始值,详细解释请参考Swift之?和!。
Swift中非Optional 类型(任何不带? 号的类型)必须通过设置默认值或在构造函数init 中完成初始化。前面有提到,ViewController 中的UI控件主要是在viewDidLoad 方法中进行创建和初始化的,所以我们必须将UI控件设置为Optional 类型。
其次,考虑到时间按钮的样式和功能基本相同,而且以后可能会增加或删减类似按钮,我们使用一个数组timeButtons 来保存所有的时间按钮。同时,因为每个按钮的显示标题跟点击后增加的时间不同,我们还需要定义一个数组timeButtonInfos 来保存不同按钮的信息:
let timeButtonInfos = [("1分", 60), ("3分"180"5分"300"秒"1)]
timeButtonInfos在初始化之后不会再有改变,所以我们使用let 将其定义为常量。它是一个数组,其中每个元素是一个元组(tuple ),包含了对应按钮的标题和点击后增加的秒数。
由于UI创建的代码比较多,全部写到viewDidLoad 中会很乱。所以接下来,我们定义了3个方法,分别用来完成3部分UI控件的创建:
///UI Helpers
setupTimeLabel() {
timeLabel ()
timeLabeltextColor font UIFont(namenilsize: 80)
blackColortextAlignment NSTextAlignmentCenter
viewaddSubviewtimeLabel)
这个方法首先使用默认构造函数UILabel() 创建了一个UILabel实例并赋值给timeLabel属性。然后将timeLabel 文本颜色设置为白色、字体设置为80号大小的默认字体、背景色设为黑色,标签中的文本设置为居中对齐。具体UILabel的使用方法,请参考UILabel。
需要注意的是,在赋值时,每个timeLabel 后都带上了一个! 号,这是因为timeLabel 实际上是Optional 类型,它像一个黑盒子一样包装了(wrap )原始值,所以在使用它的原始值时必须先用! 操作符来拆包(unwrap ),详细解释请参考Swift之?和!。
最后,我们将timeLabel 添加到了控制器对应的view 上。
setuptimeButtons{
var buttons[] []
for indextitle_)) in enumeratetimeButtonInfos) {
let buttonUIButton ()
buttontag index //保存按钮的index
setTitle"(title)"forStateUIControlStateNormal)
orangeColorsetTitleColor(),242)">)
HighlightedaddTargetaction: "timeButtonTapped:"forControlEventsUIControlEventsTouchUpInsidebuttons += button
)
}
timeButtons buttons
首先我们创建了一个空数组,用来临时保存生成的按钮。接下来,考虑到在timeButtons 中指定位置index 的button对应的是timeButtonInfos 中相同位置的信息,我们需要获得这个index 。
通过使用enumerate 全局函数我们可以为timeButtonInfos 创建一个包含index 以及数组中元素(也是元组)的元组(index,(title,_)) 。由于暂时用不到timeButtonInfos 中元组的第二个参数(点击增加的时间),我们使用_ 替代命名,表示不生成对应的变量。
接着在每次循环开始,我们创建了一个UIButton 实例。每个继承自UIView 的类(包括UIButton )都继承了属性tag ,它主要用一个整数来标记某个view。此处,我们将按钮信息所在的index 赋值给button.tag ,用来标记button对应的信息所处的位置。
接着我们设置了按钮的标题、背景色、不同点击状态下的标题颜色等,具体UIButton的使用方法请参考UIButton。
除了显示作用,按钮还可以响应用户的点击操作。我们通过addTarget:action:forControlEvents: 方法给button 添加了可以响应按下按钮并抬起 操作的回调方法:timeButtonTapped: 。
最后我们将这个临时按钮加入buttons 数组,并将此按钮添加到视图上。
当所有按钮创建完毕,我们将这个临时按钮数组赋值给timeButtons ,以方便日后引用。
setupActionButtons {
//create start/stop button
startStopButton startStopButtonredColor"启动/停止""startStopButtonTapped:")
clearButton clearButton"复位""clearButtonTapped:")
上面方法中分别为startStopButton 设置了点击后的回调方法startStopButtonTapped: ;为clearButton 设置了点击后的回调方法clearButtonTapped: 。
接着,我们简单定义了这几个所需的按钮按下回调方法:
///Actions & Callbacks
startStopButtonTappedsender{
}
clearButtonTappedtimeButtonTappedupdateTimertimerNSTimer 然后在viewDidLoad 方法中,我们通过调用上面定义好的UI创建方法来创建主页面:
///Overrides
{
()
()
()
在Xcode中使用快捷键CMD+R 运行app,发现预想中的界面并没有出现,这是因为我们还没有设置每个UI控件的位置和大小。
实际上,所有继承自UIView 的UI控件类都可以使用init:frame: 构造函数来创建指定位置、大小的UI控件。
如果你的app只支持一种方向的屏幕(比如说竖屏),这样做是没问题的;但如果你的app需要同时支持竖屏和横屏,那么最好重载ViewController 中的viewWillLayoutSubviews 方法。这个方法会在ViewController 中的视图view 大小改变时自动调用(横竖屏切换会改变视图控制器中view 的大小),也提供了最好的时机来设置UI控件的位置和大小。
所以我们在CounterViewController 中重载此方法,并为每个UI控件设置了合适的位置和大小:
viewWillLayoutSubviews frame CGRectMake(1040sizewidth-20120)
gap ( width - 10*2 - CGFloattimeButtonscount) * 64/ count )
) {
buttonLeft = 10 + 64 + gap* buttonLeftheight44}
20100)
+100+)
在iOS设备中,视图坐标系是以左上角(0,0) 为原点,使用CGRect 结构体来表示一个矩形位置,使用CGRectMake 全局函数来创建矩形结构体的实例。每个继承自UIView 的UI类都有一个类型为CGRect 的frame 属性,用来保存其在父view中的位置。
我们首先设置了timeLabel 的frame,其左上角为(10,40) ,宽度为整个view 的宽度-20(为右边也留出10的边),高度为120;
其次我通过循环整个时间按钮数组,为每个按钮设置了合适的frame。这里为了让时间按钮的排列能够自动适应不同屏幕的宽度,我们先计算中每个按钮之间的间距gap ,然后根据gap 、按钮的宽度64 来确定每个按钮的左边距buttonLeft ,并最终得到每个按钮的frame 。
最后,我们为startStopButton 按钮和clearButton 也设置了合适的frame。
竖屏模式iPhone4S下,各控件通过计算后得出的frame值如下:
此时,再次CMD+R 运行app,就能看到预期的界面了。
添加逻辑功能
界面已开发完毕,现在我们考虑为计时器app添加以下功能:
1. 设置时间
在使用倒计时器时,我们发现每次点击时间按钮,当前倒计时的时间会累加;而当开始倒计时时,倒计时的时间又会递减。这些操作都牵扯到一个重要的状态:当前倒计时的时间,而且这个状态是不断变化的。
所以我们考虑为这个状态定义一个变量,表示当前倒计时剩余的秒数 :
remainingSeconds Int 0
我们期望当用户点击时间按钮时,app内部会增加remainingSeconds 的值;当点击复位按钮时,会设置remainingSeconds 的值为0;当计时开始时,会逐秒减少remainingSeconds 的值。并且当remainingSeconds 发生变化时,能够及时更新UI,在timeLabel 上显示正确的剩余时间。
为实现这些功能,首先我们在时间按钮和复位按钮点击的回调方法中对remainingSeconds 值作调整:
{
remainingSeconds 0
{
let seconds[sendertag]
seconds
按钮回调方法中的唯一参数sender ,代表触发此回调方法的控件。在timeButtonTapped: 方法中,我们通过控件的tag来找到对应的按钮的信息。按钮信息是一个元组,其中第二个参数存储着每次点击按钮需要增加的秒数,我们将此秒数增加到remainingSeconds 上。
现在在设置或复位时间时,remainingSeconds 的值可以正常更新了,我们需要考虑如何让UI界面也能及时显示剩余时间。通常的做法,是在按钮的回调方法中,除了设置remainingSeconds 的值,也同时通过设置timeLabel 的text 属性来更新UI。这种做法可以解决问题,但并不最佳方案,因为我们除了需要在timeButtonTapped: 中设置UI;也需要在clearButtonTapped: 中设置UI;还需要在计时器启动后,在适当的回调中逐秒递减时设置UI。这样会造成很多重复的代码,且难于管理。
其实我们可以使用更Swift 的方式来解决状态跟UI的同步问题:使用属性的willSet 和/或didSet 方法,请参考Property Observers。
0 {
willSetnewSecondsmins newSeconds/60
seconds %60
text NSStringformat:"%02d:%02d"mins)
在此,我们给remainingSeconds 属性添加了一个willSet 方法,这个方法会在remainingSeconds 的值将要变化的时候调用,并传入变化后的新值作为唯一参数。
在这个方法里,我们先通过整除/ 和取余% 的方式得到倒计时秒数对应的分钟,和除分钟数外的秒数。假如新值为80(秒),那么计算后,mins值为1,second值为20。
然后,我们通过使用Objective-C中定义的字符串类型NSString 来格式化这两个数值,让其显示为分钟:秒钟 的形式:比如新值为80,那么格式化后的字符串为01:20 。这里多提一句,Swift中提供了自带的String 类,它能跟Obejctive-C 定义的NSString 互相兼容。在使用Swift 编程时,我们主要使用String 来处理字符串,但由于String 类目前还没有提供格式字符串相关的方法,我们只能求助于NSString 类型。
2.启动和停止倒计时
通过点击启动/停止 按钮,我们可以启动或停止倒计时。这种操作能让计时器呈现2种不同的状态:正在计时 和 没有计时。为了实现此功能,我们定义了一个布尔类型变量isCounting :
isCounting Bool = false
同时,在启动计时器后,我们需要每间隔1秒钟就更新一次UI界面的剩余时间。为实现这种定时触发的功能,我们需要用到Foundation 库中定义的NSTimer。
为此我们定义了一个NSTimer 类型变量timer :
?
接着,在用户点击启动/停止 按钮时触发的回调方法startStopButtonTapped: 中,我们切换了isCounting 的状态:
isCounting = !isCounting
同样,为了实现界面同步,我们为isCounting 属性添加了willSet 方法:
false newValue{
if newValue timer scheduledTimerWithTimeIntervaltargetselector"updateTimer:"userInforepeats: true} else timer?.invalidatenil
}
setSettingButtonsEnabled()
}
}
当isCounting 的新值newValue 为true 时,我们将通过调用NSTimer 的类方法scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 创建并启动一个每1秒钟调用1次updateTimer: 方法的timer,并将返回的实例保存到timer 中。
同时我们定义了updateTimer: 方法来更新当前的倒计时时间:
-= 1
false时,我们将暂停timer 并将timer 设置为nil 。此处对timer使用了? 修饰符,意思是只有timer 是非nil时才做拆包,并调用后面的方法,否则什么也不做。? 的用处很多,善用它能写出更安全的代码。
此外,由于时间按钮和复位按钮只在计时器停止时起作用,在计时器启动时无效,我们还提供了辅助方法setSettingButtonsEnabled: 用来设置这些按钮在不同isCounting 状态下的样式(settingButtons是指在设置时间时,也就是计时器停止时可以操作的按钮):
enabled Boolfor button ! enabled enabled
alpha enabled ? 1.0 : 0.3
}
enabled
0.3
}
3.在计时完成后进行提醒
当倒计时自然结束时(不是人为点击启动/停止 按钮来停止),如果当前app还处于激活状态(用户没有按Home 键退出),那么我们此时将弹出一个警告窗口(UIAlertView),来提示倒计时已完成:
1
<= alert UIAlertViewalerttitle = "计时完成!"
message ""
addButtonWithTitle"OK"show}
我们使用通用构造函数创建了一个UIAlertView 实例,设置好标题和按钮,最后调用show() 方法将其显示出来。效果参照下图:
很多情况下,用户在app计时未结束时就离开了计时器app(计时器处于未激活状态),那么当计时完成时,我们如何来通知用户呢?对这种情况,我们可以使用系统的本地通知UILocalNotification。我们先定义一个辅助方法createAndFireLocalNotificationAfterSeconds: 来创建和注册一个N秒钟后的本地提醒事件:
createAndFireLocalNotificationAfterSeconds secondsIntUIApplicationsharedApplicationcancelAllLocalNotifications()
notification UILocalNotification()
timeIntervalSinceNow bridgeToObjectiveCdoubleValue
notificationfireDate NSDatetimeIntervalSinceNow:timeIntervalSinceNow);
timeZone NSTimeZonesystemTimeZone();
alertBody "计时完成!";
scheduleLocalNotification);
在方法实现中,我们先调用cancelAllLocalNotifications 取消了所有当前app已注册的本地消息。之后创建了一个新的本地消息对象notification 。
接下来我们要为notifcation设置消息的激活时间。我们通过NSDate(timeIntervalSinceNow: double) 构造器创建了从当前时间往后推N秒的一个时间。由于方法接受的参数timeIntervalSinceNow 是double 类型,我们先将Int 类型seconds 通过bridgeToObjectiveC() 方法转换成兼容的NSNumber 对象,再调用其doubleValue 方法获得对应的值。
如同String 之于NSString ,Swift中的Int 、Float 类都能跟Objective-C 中的NSNumber 类互相兼容。由于Swift中没有提供将Int 转换为double 类型的方法,我们也不得不求助于NSNumber ,通过bridgeToObjectiveC() 方法将Int 对象转换成对应的NSNumber 对象。
之后我们将本地消息的时区设置为系统时区,提示消息为“计时完成!”。并最终完成此消息的注册。
接下来,我们更新了启动/停止 按钮响应的startStopButtonTapped: 回调方法:
isCounting
isCounting remainingSeconds}
在启动计时器时创建并注册计时完成时的本地提醒;当计时器停止时,取消当前app所注册的所有本地提醒。
值得注意的是,在iOS8中使用本地消息也需要先获得用户的许可,否则无法成功注册本地消息。因此,我们将询问用户许可的代码片段也添加到了app启动后的入口方法中(AppDelegate 中的didFinishLaunchingWithOptions ):
//register notification
applicationregisterUserNotificationSettingsUIUserNotificationSettingsforTypesUIUserNotificationTypeSound | Alert |
Badgecategoriesnil
))
最终本地消息提醒的效果参考下图:
至此,一个完整的倒计时app已开发完毕。
后记
通过完成此教程,我对Swift 语言的理解也更进了一步。Swift 是一门全新的语言,作为开发者,我们需要不断加深对这门语言的理解,并灵活使用语言提供的特性来编程。虽然在开发中还需要大量使用Cocoa(Touch) 中提供的Objective-C 类库,但编程的方式已经完全改变了,不仅仅是将Objective-C 代码翻译成Swift 代码,而需要在代码层面进行重新思考。 (编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|