Swift已经进化到了4.0版本,我才后知后觉的来学习一下。之前虽也粗粗地看过一些语法,但无奈跟现有的Objc差别一个天一个地实在太难理解就没有继续下去。这次抱着重新学习一门新语言的心态再来尝试一下希望能有所突破。
玩过Python,JS,日常工作用C++。总结下来觉得流行的编程语言其实很类似,核心的一些概念基本都有,换个名字叫法而已。所以个人觉得学习一门新的语言最好的方法就是“实践出真知”。这个UI project就是我用来学习Swift的第一个项目,原本目的只是想实现一个最简单的helloworld UI,了解一下Swift的viewcontroller和UI binding。之后稍微扩展了一下,加入了delegate的使用,property的willSet和didSet,以及String的基础用法。
要求0
创建一个SingleView,包含一个Slider,一个Label用来显示Slider当前的数值,和一个Reset按钮将Slider的滑块reset到中间并更新Label
这个简单的View可以让我了解Swift中class的定义方式;如何添加UI控件并且对用户行为做出反应。
代码及其简单,最重要的部分就是UISlider的valueChange所对应的IBAction:
@IBAction func floatSliderValueChanged(_ sender : UISlider) {
self.slideValueLabel.text = "\(sender.value)";
}
这里sender的类型默认是Any,但是实际上如果你知道这个方法的sender只可能是UISlider的话也可以直接指定为UISlider,这样在方法中就可以直接调用UISlider上的property和方法而不用做casting或者selector check。这一点跟Objc是一样的,其实不属于语言的特性。第二个点是Swift中在string literal中插入变量值的方法:使用\()来引用变量的值。
要求1
在上面的SingleView中,添加两个UITextField,其中一个的input作为base string,另一个的input作为insert string,拖动slider将insert string插入到base string相应的位置上,slider旁边的label显示slider当前的数值,底部中间的label显示出入后完整的string。Reset按钮将slider的滑块归零。
在仔细地分析过这个要求之后会发现其实还是有几个需要注意的点的。首先就是slider的范围需要根据用户输入的base string来动态调整。最小值始终是0,而最大值应该为base string的长度减1。第二,slider默认输出的是float类型的值,而插入string需要的是int型的index。slider只能snap到整数上。第三需要处理base string或者insert string为空的情况。base string为空的话slider最大最小值都是0。inset string为空的话就什么都不插入。
由于加入了textfield,所以需要引入UITextFieldDelegate。Swift中confirm的protocol和superclass一样,加在类名的冒号后面。
class ViewController: UIViewController, UITextFieldDelegate {...}
解决整数型slider的方法非常简单,就是在valueChange的action中使用slider的setValue将slider设置为最接近当前float数的整数。
self.floatSlider.setValue(newSliderValue.rounded(.down), animated:true);
在textfield的didEndEditing方法中,可以提取出当前base string的长度并以此来设置slider的最大值,这也不是什么难事。
let maxIdx = self.baseStringTextField.text== nil ? 0 : self.baseStringTextField.text!.count;
在实现插入的方法的时候遇到了一些小困难。Swift中的string支持unicode和emoji等非ASCII字符,所以string无法直接用整数的indexing来访问某一个字符。string类型自带的insert方法貌似不支持string作为要插入的字符串,只能一个一个地插入。由于不能直接使用int型的index,需要使用string自带的.index方法来算出插入的位置。
func insertString(shortStr insertStr:String?, inBaseStr baseStr:String?, after insertIdx:Int) ->String{
varoutStr = baseStr!;
vari = insertIdx;
if insertStr !=nil {
for l in insertStr! {
outStr.insert(l, at: outStr.index(outStr.startIndex, offsetBy: i));
i +=1;
}
}
return outStr;
}
另外一点需要注意的是如果某个变量是optional的,那么在引用其方法的时候需要check for nil并且使用!来将变量转变为require类型。而在Objc中,如果这个nullable变量是nil,给nil发message是一个no-op,也不会产生编译警告。
在初步完成这个要求1之后,又发现这个reset button其实需要programmatically set slider value to be 0, 并且这个步骤不仅需要更新slider value的label,还要更新最终处理过的string。其实需要做的事情和slider value change的IBAction中是一样的,但是IBAction没有办法说我要把slider的值set为多少多少。所以我需要把计算最终string和更新UI的一系列步骤单独提取出来放在一个function里,然后在需要的地方调用。这时我想到了KVO,可以定义一个private的变量sliderValue来保存slider当前的值。在每次set这个变量的值的时候在willSet和didSet中完成UI更新的一些列步骤。slider value change function中我只把slider当前的值赋给sliderValue,reset button push的时候我把0赋给sliderValue。在willSet中我计算最终的string并且更新对应的UI控件。这样做的好处是代码会非常清晰简洁,坏处是如果willSet和didSet中需要做一些heavy lifting的话对代码的运行速度会产生影响。
最终的结果自己还是比较满意的,要求1的功能全部实现,并且代码很简洁易懂。算是开了一个好头。