本文本人阅读@onevcat 的 《Swifter - 100 个Swift 必备 tips》后所做笔记,有兴趣可以去查看原书
创建check
func check(_ input: Any) {
print("type is \(type(of: input)), value is \(input)")
}
创建单例
class MyManager {
static let sharedInstance = MyManager()
private init() {}
}
struct MyManager {
static let sharedInstance = MyManager()
private init() {}
}
protocol 方法声明为 mutating
如果需要修改对象中的属性,需要增加mutating
protocol Vehicle
{
var numberOfWheels: Int {get}
var color: UIColor {get set}
mutating func changeColor()
}
struct MyCar: Vehicle {
let numberOfWheels = 4
var color = UIColor.blue
mutating func changeColor() {
color = UIColor.red
}
}
多元组 (tuple)
func swapMe(a: inout T, b: inout T) {
(a,b) = (b,a)
}
var a = 1
var b = 2
swapMe(a: &a, b: &b)
??
获取可选类型的值
func getOptional(_ optional: T?,defaultValue: T) -> T
{
switch optional {
case .some(let value):
return value
case .none:
return defaultValue
}
}
var level : Int?
var startLevel = 1
var c = getOptional(level, defaultValue: 1)
Optional Chaining
struct Pet {
var toy: Toy?
}
struct Child {
var pet: Pet?
}
extension Toy {
func play() {}
}
//()? (Void?) 回值可能为 nil ,是可选的
let playClosure = {(child: Child) in child.pet?.toy?.play()}
let xiaoming = Child()
if let result = playClosure(xiaoming) {
print("好开心~")
} else {
print("没有玩具可以玩 :(")
}
check(playClosure)
FUNC 的参数修饰
func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) {
func incrementor(variable: inout Int) -> () {
variable += addNumber;
}
return incrementor;
}
方法参数名称省略 {#FUNC-NAMING}
实例方法
extension Car {
func moveToX(x: Int, y: Int) {
//...
}
}
car.moveToX(x: 10, y: 20)
类方法
struct Car {}
extension Car {
static func findACar(name: String) -> Car? {
var result: Car?
result = Car()
return result
}
}
let myPorsche = Car.findACar(name: "Porsche")
全局方法
// 注意,现在不在 Car 中,而是在一个全局作用域
func findACar(name: String, color: UIColor) -> Car? {
let result: Car? = Car()
//...
return result
}
let myFerrari = findACar(name: "Ferrari",color: UIColor.red)
SWIFT 命令行工具
直接将一个 .swift 文件作为命令行工具的输入,这样里面的代码也会被自动地编译和执行。我们甚至还可以在 .swift 文件最上面加上命令行工具的路径,然后将文件权限改为可执行,之后就可以直接执行这个 .swift 文件了:
#!/usr/bin/env swift
print("hello")
// Terminal
> chmod 755 hello.swift
> ./hello.swift
// 输出:
hello
swiftc 来进行编译
// MyClass.swift
class MyClass {
let name = "XiaoMing"
func hello() {
print("Hello \(name)")
}
}
// main.swift
let object = MyClass()
object.hello()
> swiftc MyClass.swift main.swift
字面量转换
数字,字符串或者是布尔值
let aNumber = 3
let aString = "Hello"
let aBool = true
Array 和 Dictionary
let anArray = [1,2,3]
let aDictionary = ["key1": "value1", "key2": "value2"]
这些初始化方法中去调用原来的 init(name value: String),这种情况下我们需要在这些初始化方法前加上 convenience
class Person {
let name: String
init(name value: String) {
self.name = value
}
convenience init(value: String) {
self.init(name: value)
}
}
模式匹配
let contact = ("http://onevcat.com", "[email protected]")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
try ~/"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
siteRegex =
try ~/"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"
switch contact {
case (siteRegex, mailRegex): print("同时拥有有效的网站和邮箱")
case (_, mailRegex): print("只拥有有效的邮箱")
case (siteRegex, _): print("只拥有有效的网站")
default: print("嘛都没有")
}
// 输出
// 同时拥有网站和邮箱
方法嵌套
func makeIncrementor(addNumber: Int) -> ((inout Int) -> Void {
func incrementor(inout variable: Int) -> Void {
variable += addNumber;
}
return incrementor;
}
命名空间
在我们进行 app 开发时,默认添加到 app 的主 target 的内容都是处于同一个命名空间中的,我们可以通过创建 Cocoa (Touch) Framework 的 target 的方法来新建一个 module,这样我们就可以在两个不同的 target 中添加同样名字的类型了:
// MyFramework.swift
// 这个文件存在于 MyFramework.framework 中
public class MyClass {
public class func hello() {
print("hello from framework")
}
}
// MyApp.swift
// 这个文件存在于 app 的主 target 中
class MyClass {
class func hello() {
print("hello from app")
}
}
在使用时,如果出现可能冲突的时候,我们需要在类型名称前面加上 module 的名字 (也就是 target 的名字):
MyClass.hello()
// hello from app
MyFramework.MyClass.hello()
// hello from framework
使用类型嵌套的方法来指定访问的范围。常见做法是将名字重复的类型定义到不同的 struct 中,以此避免冲突。这样在不使用多个 module 的情况下也能取得隔离同样名字的类型的效果:
struct MyClassContainer1 {
class MyClass {
class func hello() {
print("hello from MyClassContainer1")
}
}
}
struct MyClassContainer2 {
class MyClass {
class func hello() {
print("hello from MyClassContainer2")
}
}
}
使用时:
MyClassContainer1.MyClass.hello()
MyClassContainer2.MyClass.hello()
ANY 和 ANYOBJECT
protocol AnyObject {
}
使用 Any 和 AnyObject 并不是什么令人愉悦的事情,正如开头所说,这都是为妥协而存在的。如果在我们自己的代码里需要大量经常地使用这两者的话,往往意味着代码可能在结构和设计上存在问题,应该及时重新审视。简单来说,我们最好避免依赖和使用这两者,而去尝试明确地指出确定的类型。
随机数生成
func randomInRange(range: Range) -> Int {
let count = UInt32(range.endIndex - range.startIndex)
return Int(arc4random_uniform(count)) + range.startIndex
}
for _ in 0...100 {
print(randomInRange(1...6))
}
TYPEALIAS 和泛型接口
typealias 是用来为已经存在的类型重新定义名字的,通过命名,可以使代码变得更加清晰。
import UIKit
typealias Location = CGPoint
typealias Distance = Double
func distanceBetweenPoint(location: Location,
toLocation: Location) -> Distance {
let dx = Distance(location.x - toLocation.x)
let dy = Distance(location.y - toLocation.y)
return sqrt(dx * dx + dy * dy)
}
let origin: Location = Location(x: 0, y: 0)
let point: Location = Location(x: 1, y: 1)
let distance: Distance = distanceBetweenPoint(origin, toLocation: point)
条件编译
#if
#elseif
#else
#endif
编译标记
// MARK: 以外,Xcode 还支持另外几种标记,它们分别是 // TODO: 和 // FIXME:。???
可变参数函数
func sum(input: Int...) -> Int {
return input.reduce(0, combine: +)
}
print(sum(1,2,3,4,5))
// 输出:15
Swift 的可变参数十分灵活
func myFunc(numbers: Int..., string: String) {
numbers.forEach {
for i in 0..<$0 {
print("\(i + 1): \(string)")
}
}
}
myFunc(1, 2, 3, string: "hello")
// 输出:
// 1: hello
// 1: hello
// 2: hello
// 1: hello
// 2: hello
// 3: hello
Swift 的 NSString 格式化的声明就是这样处理的:
extension NSString {
convenience init(format: NSString, _ args: CVarArgType...)
//...
}
@UIAPPLICATIONMAIN???
UIApplicationMain 帮助我们自动生成了 Swift 的 app 也是需要 main 函数的
初始化方法顺序
class Cat {
var name: String
init() {
name = "cat"
}
}
class Tiger: Cat {
let power: Int
override init() {
power = 10
super.init()
name = "tiger"
}
}
class Cat {
var name: String
init() {
name = "cat"
}
}
class Tiger: Cat {
let power: Int
override init() {
power = 10
// 如果我们不需要打改变 name 的话,
// 虽然我们没有显式地对 super.init() 进行调用
// 不过由于这是初始化的最后了,Swift 替我们自动完成了
}
}
那些有用的tips2
DESIGNATED,CONVENIENCE 和 REQUIRED
对于某些我们希望子类中一定实现的 designated 初始化方法,我们可以通过添加 required 关键字进行限制,强制子类对这个方法重写实现。这样做的最大的好处是可以保证依赖于某个 designated 初始化方法的 convenience 一直可以被使用。
初始化返回 NIL
Apple 已经为我们加上了初始化方法中返回 nil 的能力。我们可以在 init 声明时在其后加上一个 ? 或者 ! 来表示初始化失败时可能返回 nil。比如为 Int 添加一个接收 String 作为参数的初始化方法。我们希望在方法中对中文和英文的数据进行解析,并输出 Int 结果。对其解析并初始化的时候,就可能遇到初始化失败的情况:
extension Int {
init?(fromString: String) {
self = 0
var digit = fromString.characters.count - 1
for c in fromString.characters {
var number = 0
if let n = Int(String(c)) {
number = n
} else {
switch c {
case "一": number = 1
case "二": number = 2
case "三": number = 3
case "四": number = 4
case "五": number = 5
case "六": number = 6
case "七": number = 7
case "八": number = 8
case "九": number = 9
case "零": number = 0
default: return nil
}
}
self = self + number * Int(pow(10, Double(digit)))
digit = digit - 1
}
}
}
let number1 = Int(fromString: "12")
// {Some 12}
let number2 = Int(fromString: "三二五")
// {Some 325}
let number3 = Int(fromString: "七9八")
// {Some 798}
let number4 = Int(fromString: "吃了么")
// nil
let number5 = Int(fromString: "1a4n")
// nil
而对应地,可能返回 nil 的 init 方法都加上了 ? 标记:
convenience init?(string URLString: String)
PROTOCOL 组合
protocol A {
func bar() -> Int
}
protocol B {
func bar() -> String
}
两个接口中 bar() 只有返回值的类型不同。我们如果有一个类型 Class 同时实现了 A 和 B,我们要怎么才能避免和解决调用冲突呢?
class Class: A, B {
func bar() -> Int {
return 1
}
func bar() -> String {
return "Hi"
}
}
这样一来,对于 bar(),只要在调用前进行类型转换就可以了:
let instance = Class()
let num = (instance as A).bar() // 1
let str = (instance as B).bar() // "Hi"
STATIC 和 CLASS
protocol MyProtocol {
static func foo() -> String
}
struct MyStruct: MyProtocol {
static func foo() -> String {
return "MyStruct"
}
}
enum MyEnum: MyProtocol {
static func foo() -> String {
return "MyEnum"
}
}
class MyClass: MyProtocol {
// 在 class 中可以使用 class
class func foo() -> String {
return "MyClass.foo()"
}
// 也可以使用 static
static func bar() -> String {
return "MyClass.bar()"
}
}
现在只需要记住结论,在任何时候使用 static 应该都是没有问题的。
可选接口和接口扩展
protocol OptionalProtocol {
func optionalMethod() // 可选
func necessaryMethod() // 必须
func anotherOptionalMethod() // 可选
}
extension OptionalProtocol {
func optionalMethod() {
print("Implemented in extension")
}
func anotherOptionalMethod() {
print("Implemented in extension")
}
}
class MyClass: OptionalProtocol {
func necessaryMethod() {
print("Implemented in Class3")
}
func optionalMethod() {
print("Implemented in Class3")
}
}
let obj = MyClass()
obj.necessaryMethod() // Implemented in Class3
obj.optionalMethod() // Implemented in Class3
obj.anotherOptionalMethod() // Implemented in extension
多类型和容器
import Foundation
enum IntOrString {
case IntValue(Int)
case StringValue(String)
}
let mixed = [IntOrString.IntValue(1),
IntOrString.StringValue("two"),
IntOrString.IntValue(3)]
for value in mixed {
switch value {
case let .IntValue(i):
print(i * 2)
case let .StringValue(s):
print(s.capitalizedString)
}
}
// 输出:
// 2
// Two
// 6
内存管理,WEAK 和 UNOWNED
class Person {
let name: String
lazy var printName: ()->() = {
print("The name is \(self.name)")
}
init(personName: String) {
name = personName
}
deinit {
print("Person deinit \(self.name)")
}
}
var xiaoMing: Person? = Person(personName: "XiaoMing")
xiaoMing!.printName()
xiaoMing = nil
// 输出:
// The name is XiaoMing,没有被释放
printName 是 self 的属性,会被 self 持有,而它本身又在闭包内持有 self,这导致了 xiaoMing 的 deinit 在自身超过作用域后还是没有被调用,也就是没有被释放。为了解决这种闭包内的循环引用,我们需要在闭包开始的时候添加一个标注,来表示这个闭包内的某些要素应该以何种特定的方式来使用。可以将 printName 修改为这样:
lazy var printName: ()->() = {
[weak self] in
if let strongSelf = self {
print("The name is \(strongSelf.name)")
}
}
@AUTORELEASEPOOL
func autoreleasepool(code: () -> ())
利用尾随闭包的写法,很容易就能在 Swift 中加入一个类似的自动释放池了:
func loadBigData() {
if let path = NSBundle.mainBundle()
.pathForResource("big", ofType: "jpg") {
for i in 1...10000 {
autoreleasepool {
let data = NSData.dataWithContentsOfFile(
path, options: nil, error: nil)
NSThread.sleepForTimeInterval(0.5)
}
}
}
}
DEFAULT 参数
func sayHello1(str1: String = "Hello", str2: String, str3: String) {
print(str1 + str2 + str3)
}
func sayHello2(str1: String, str2: String, str3: String = "World") {
print(str1 + str2 + str3)
}
sayHello1(str2: " ", str3: "World")
sayHello2("Hello", str2: " ")
//输出都是 Hello World
正则表达式
一个最简单的实现可能是下面这样的:
struct RegexHelper {
let regex: NSRegularExpression
init(_ pattern: String) throws {
try regex = NSRegularExpression(pattern: pattern,
options: .CaseInsensitive)
}
func match(input: String) -> Bool {
let matches = regex.matchesInString(input,
options: [],
range: NSMakeRange(0, input.utf16.count))
return matches.count > 0
}
}
在使用的时候,比如我们想要匹配一个邮箱地址,我们可以这样来使用:
let mailPattern =
"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let matcher: RegexHelper
do {
matcher = try RegexHelper(mailPattern)
}
let maybeMailAddress = "[email protected]"
if matcher.match(maybeMailAddress) {
print("有效的邮箱地址")
}
// 输出:
// 有效的邮箱地址
如果你想问 mailPattern 这一大串莫名其妙的匹配表达式是什么意思的话..>嘛..实在抱歉这里不是正则表达式的课堂,所以关于这个问题我>推荐看看这篇很棒的正则表达式 30 分钟入门教程,如果你连 30 分钟都没有的话,打开 8 个常用正则表达式 先开始抄吧..
上面那个式子就是我从这里抄来的
现在我们有了方便的封装,接下来就让我们实现 =~ 吧。这里只给出结果了,关于如何实现操作符和重载操作符的内容,可以参考操作符一节的内容。
infix operator =~ {
associativity none
precedence 130
}
func =~(lhs: String, rhs: String) -> Bool {
do {
return try RegexHelper(rhs).match(lhs)
} catch _ {
return false
}
}
这下我们就可以使用类似于其他语言的正则匹配的方法了:
if "[email protected]" =~
"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$" {
print("有效的邮箱地址")
}
// 输出:
// 有效的邮箱地址
模式匹配
首先我们要做的是重载 ~= 操作符,让它接受一个 NSRegularExpression 作为模式,去匹配输入的 String:
func ~=(pattern: NSRegularExpression, input: String) -> Bool {
return pattern.numberOfMatchesInString(input,
options: [],
range: NSRange(location: 0, length: input.characters.count)) > 0
}
然后为了简便起见,我们再添加一个将字符串转换为 NSRegularExpression 的操作符 (当然也可以使用 StringLiteralConvertible,但是它不是这个 tip 的主题,在此就先不使用它了):
prefix operator ~/ {}
prefix func ~/(pattern: String) -> NSRegularExpression {
return NSRegularExpression(pattern: pattern, options: nil, error: nil)
}
现在,我们在 case 语句里使用正则表达式的话,就可以去匹配被 switch 的字符串了:
let contact = ("http://onevcat.com", "[email protected]")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
try ~/"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
siteRegex =
try ~/"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"
switch contact {
case (siteRegex, mailRegex): print("同时拥有有效的网站和邮箱")
case (_, mailRegex): print("只拥有有效的邮箱")
case (siteRegex, _): print("只拥有有效的网站")
default: print("嘛都没有")
}
// 输出
// 同时拥有网站和邮箱
那些有用的tips3
GCD 和延时调用
在下面我给出了一个日常里最通常会使用到的例子 (说这个例子能覆盖到日常的 GCD 使用的 50% 以上也不为过),来展示一下 Swift 里的 GCD 调用会是什么样子:
// 创建目标队列
let workingQueue = dispatch_queue_create("my_queue", nil)
// 派发到刚创建的队列中,GCD 会负责进行线程调度
dispatch_async(workingQueue) {
// 在 workingQueue 中异步进行
print("努力工作")
NSThread.sleepForTimeInterval(2) // 模拟两秒的执行时间
dispatch_async(dispatch_get_main_queue()) {
// 返回到主线程更新 UI
print("结束工作,更新 UI")
}
}
GCD 里有一个很好用的延时调用我们可以加以利用写出很漂亮的方法来,那就是 dispatch_after。最简单的使用方法看起来是这样的:
let time: NSTimeInterval = 2.0
let delay = dispatch_time(DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
print("2 秒后输出")
}
代码非常简单,并没什么值得详细说明的。只是每次写这么多的话也挺累的,在这里我们可以稍微将它封装的好用一些,最好再加上取消的功能。在 iOS 8 中 GCD 得到了惊人的进化,现在我们可以通过将一个 dispatch_block_t 对象传递给 dispatch_block_cancel,来取消一个正在等待执行的 block。取消一个任务这样的特性,这在以前是 NSOperation 的专利,但是现在我们使用 GCD 也能达到同样的目的了。这里我们将类似地来尝试实现 delay call 的取消,整个封装也许有点长,但我还是推荐一读。大家也可以把它当作练习材料检验一下自己的 Swift 基础语法的掌握和理解的情况:
import Foundation
typealias Task = (cancel : Bool) -> Void
func delay(time:NSTimeInterval, task:()->()) -> Task? {
func dispatch_later(block:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(),
block)
}
var closure: dispatch_block_t? = task
var result: Task?
let delayedClosure: Task = {
cancel in
if let internalClosure = closure {
if (cancel == false) {
dispatch_async(dispatch_get_main_queue(), internalClosure);
}
}
closure = nil
result = nil
}
result = delayedClosure
dispatch_later {
if let delayedClosure = result {
delayedClosure(cancel: false)
}
}
return result;
}
func cancel(task:Task?) {
task?(cancel: true)
}
使用的时候就很简单了,我们想在 2 秒以后干点儿什么的话:
delay(2) { print("2 秒后输出") }
想要取消的话,我们可以先保留一个对 Task 的引用,然后调用 cancel:
let task = delay(5) { print("拨打 110") }
// 仔细想一想..
// 还是取消为妙..
cancel(task)
... 和 ..<
0...3 就表示从 0 开始到 3 为止并包含 3 这个数字的范围
0..<3 -- 都写了小于号了,自然是不包含最后的 3 的意思咯
对于这样得到的数字的范围,我们可以对它进行 for...in 的访问:
for i in 0...3 {
print(i, terminator: "")
}
//输出 0123
在 Swift 中,除了数字以外另一个实现了 Comparable 的基本类型就是 String。也就是说,我们可以通过 ... 或者 ..< 来连接两个字符串。一个常见的使用场景就是检查某个字符是否是合法的字符。比如想确认一个单词里的全部字符都是小写英文字母的话,可以这么做:
let test = "helLo"
let interval = "a"..."z"
for c in test.characters {
if !interval.contains(String(c)) {
print("\(c) 不是小写字母")
}
}
// 输出
// L 不是小写字母
在日常开发中,我们可能会需要确定某个字符是不是有效的 ASCII 字符,和上面的例子很相似,我们可以使用 \0...~ 这样的 ClosedInterval 来进行 (\0 和 ~ 分别是 ASCII 的第一个和最后一个字符)。
获取对象类型
let nibName = "\(type(of: self))"
//let name = string.dynamicType 已经过时了
ANYCLASS,元类型和 .SELF
在 Cocoa API 中我们也常遇到需要一个 AnyClass 的输入,这时候我们也应该使用 .self 的方式来获取所需要的元类型,例如在注册 tableView 的 cell 的类型的时候:
self.tableView.registerClass(
UITableViewCell.self, forCellReuseIdentifier: "myCell")
.Type 表示的是某个类型的元类型,而在 Swift 中,除了 class,struct 和 enum 这三个类型外,我们还可以定义 protocol。对于 protocol 来说,有时候我们也会想取得接口的元类型。这时我们可以在某个 protocol 的名字后面使用 .Protocol 来获取,使用的方法和 .Type 是类似的。
接口和类方法中的 SELF
我们假设要实现一个 Copyable 的接口,满足这个接口的类型需要返回一个和接受方法调用的实例相同的拷贝。我们可能考虑的接口是这样的:
protocol Copyable {
func copy() -> Self
}
编译器提示我们如果想要构建一个 Self 类型的对象的话,需要有 required 关键字修饰的初始化方法,这是因为 Swift 必须保证当前类和其子类都能响应这个 init 方法。另一个解决的方案是在当前类类的声明前添加 final 关键字,告诉编译器我们不再会有子类来继承这个类型。在这个例子中,我们选择添加上 required 的 init 方法。最后,MyClass 类型是这样的:
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = self.dynamicType.init()
result.num = num
return result
}
required init() {
}
}
我们可以通过测试来验证一下行为的正确性:
let object = MyClass()
object.num = 100
let newObject = object.copy()
object.num = 1
print(object.num) // 1
print(newObject.num) // 100
而对于 MyClass 的子类,copy() 方法也能正确地返回子类的经过拷贝的对象了。
另一个可以使用 Self 的地方是在类方法中,使用起来也十分相似,核心就在于保证子类也能返回恰当的类型。
自省
首先它不仅可以用于 class 类型上,也可以对 Swift 的其他像是 struct 或 enum 类型进行判断。使用起来是这个样子的:
class ClassA { }
class ClassB: ClassA { }
let obj: AnyObject = ClassB()
if (obj is ClassA) {
print("属于 ClassA")
}
if (obj is ClassB) {
print("属于 ClassB")
}
另外,编译器将对这种检查进行必要性的判断:如果编译器能够唯一确定类型,那么 is 的判断就没有必要,编译器将会抛出一个警告,来提示你并没有转换的必要。
let string = "String"
if string is String {
// Do something
}
// 'is' test is always true
类型转换 {#TYPE-CASTING}
Swift 中使用 as! 关键字做强制类型转换
for object in self.view.subviews {
if object is UIView {
let view = object as! UIView
view.backgroundColor = UIColor.redColor()
}
}
这显然还是太麻烦了,但是如果我们不加检查就转换的话,如果待转换对象 (object) 并不是目标类型 (UIView) 的话,app 将崩溃,这是我们最不愿意看到的。我们可以利用 Swift 的 Optional,在保证安全的前提下让代码稍微简单一些。在类型转换的关键字 as 后面添加一个问号 ?,可以在类型不匹配及转换失败时返回 nil,这种做法显然更有 Swift 范儿:
for object in self.view.subviews {
if let view = object as? UIView {
view.backgroundColor = UIColor.redColor()
}
}
不仅如此,我们还可以对整个 [AnyObject] 的数组进行转换,先将其转为 [UIView] 再直接使用:
if let subviews = self.view.subviews as? [UIView] {
for view in subviews {
view.backgroundColor = UIColor.redColor()
}
}
动态类型和多方法
class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(pet: Pet) {
print("Pet")
}
func printPet(cat: Cat) {
print("Meow")
}
func printPet(dog: Dog) {
print("Bark")
}
在对这些方法进行调用时,编译器将帮助我们找到最精确的匹配:
printPet(Cat()) // Meow
printPet(Dog()) // Bark
printPet(Pet()) // Pet
对于 Cat 或者 Dog 的实例,总是会寻找最合适的方法,而不会去调用一个通用的父类 Pet 的方法。这一切的行为都是发生在编译时的,如果我们写了下面这样的代码
func printThem(pet: Pet, _ cat: Cat) {
printPet(pet)
printPet(cat)
}
printThem(Dog(), Cat())
// 输出:
// Pet
// Meow
因为 Swift 默认情况下是不采用动态派发的,因此方法的调用只能在编译时决定。
要想绕过这个限制,我们可能需要进行通过对输入类型做判断和转换:
func printThem(pet: Pet, _ cat: Cat) {
if let aCat = pet as? Cat {
printPet(aCat)
} else if let aDog = pet as? Dog {
printPet(aDog)
}
printPet(cat)
}
// 输出:
// Bark
// Meow
那些有用的tips4
属性观察
Swift 中为我们提供了两个属性观察的方法,它们分别是 willSet 和 didSet。
class MyClass {
var date: NSDate {
willSet {
let d = date
print("即将将日期从 \(d) 设定至 \(newValue)")
}
didSet {
print("已经将日期从 \(oldValue) 设定至 \(date)")
}
}
init() {
date = NSDate()
}
}
let foo = MyClass()
foo.date = foo.date.dateByAddingTimeInterval(10086)
// 输出
// 即将将日期从 2014-08-23 12:47:36 +0000 设定至 2014-08-23 15:35:42 +0000
// 已经将日期从 2014-08-23 12:47:36 +0000 设定至 2014-08-23 15:35:42 +0000
上面的例子中我们不希望 date 超过当前时间的一年以上的话,我们可以将 didSet 修改一下:
class MyClass {
let oneYearInSecond: NSTimeInterval = 365 * 24 * 60 * 60
var date: NSDate {
//...
didSet {
if (date.timeIntervalSinceNow > oneYearInSecond) {
print("设定的时间太晚了!")
date = NSDate().dateByAddingTimeInterval(oneYearInSecond)
}
print("已经将日期从 \(oldValue) 设定至 \(date)")
}
}
//...
}
更改一下调用,我们就能看到效果:
// 365 * 24 * 60 * 60 = 31_536_000
foo.date = foo.date.dateByAddingTimeInterval(100_000_000)
// 输出
// 即将将日期从 2014-08-23 13:24:14 +0000 设定至 2017-10-23 23:10:54 +0000
// 设定的时间太晚了!
// 已经将日期从 2014-08-23 13:24:14 +0000 设定至 2015-08-23 13:24:14 +0000
重写的属性并不知道父类属性的具体实现情况,而只从父类属性中继承名字和类型,因此在子类的重载属性中我们是可以对父类的属性任意地添加属性观察的,而不用在意父类中到底是存储属性还是计算属性:
class A {
var number :Int {
get {
print("get")
return 1
}
set {print("set")}
}
}
class B: A {
override var number: Int {
willSet {print("willSet")}
didSet {print("didSet")}
}
}
调用 number 的 set 方法可以看到工作的顺序
let b = B()
b.number = 0
// 输出
// get
// willSet
// set
// didSet
set 和对应的属性观察的调用都在我们的预想之中。这里要注意的是 get 首先被调用了一次。这是因为我们实现了 didSet,didSet 中会用到 oldValue,而这个值需要在整个 set 动作之前进行获取并存储待用,否则将无法确保正确性。如果我们不实现 didSet 的话,这次 get 操作也将不存在。
KVO
因为 KVO 是基于 KVC (Key-Value Coding) 以及动态派发技术实现的,而这些东西都是 Objective-C 运行时的概念。另外由于 Swift 为了效率,默认禁用了动态派发
尽量不用
局部 SCOPE
使用匿名的闭包,写代码
titleLabel = {
let label = UILabel(frame: CGRectMake(150, 30, 20, 40))
label.textColor = UIColor.redColor()
label.text = "Title"
self.view.addSubview(label)
return label
}()
PRINT 和 DEBUGPRINT
对于一个普通的对象,我们在调用 print 对其进行打印时只能打印出它的类型:
class MyClass {
var num: Int
init() {
num = 1
}
}
let obj = MyClass()
print(obj)
// MyClass
对于 struct 来说,情况好一些。打印一个 struct 实例的话,会列举出它所有成员的名字和值:比如我们有一个日历应用存储了一些会议预约,model 类型包括会议的地点,位置和参与者的名字:
struct Meeting {
var date: NSDate
var place: String
var attendeeName: String
}
let meeting = Meeting(date: NSDate(timeIntervalSinceNow: 86400),
place: "会议室B1",
attendeeName: "小明")
print(meeting)
// 输出:
// Meeting(date: 2015-08-10 03:15:55 +0000,
// place: "会议室B1", attendeeName: "小明")
使用 CustomStringConvertible 接口,这个接口定义了将该类型实例输出时所用的字符串。相对于直接在原来的类型定义中进行更改,我们更应该倾向于使用一个 extension,这样不会使原来的核心部分的代码变乱变脏,是一种很好的代码组织的形式:
extension Meeting: CustomStringConvertible {
var description: String {
return "于 \(self.date) 在 \(self.place) 与 \(self.attendeeName) 进行会议"
}
}
这样,再当我们使用 print 时,就不再需要去做格式化,而是简单地将实例进行打印就可以了:
print(meeting)
// 输出:
// 于 2015-08-10 03:33:34 +0000 在 会议室B1 与 小明 进行会议
CustomDebugStringConvertible 与 CustomStringConvertible 的作用很类似,但是仅发生在调试中使用 debugger 来进行打印的时候的输出。对于实现了 CustomDebugStringConvertible 接口的类型,我们可以在给 meeting 赋值后设置断点并在控制台使用类似 po meeting 的命令进行打印,控制台输出将为 CustomDebugStringConvertible 中定义的 debugDescription 返回的字符串。
判等
在 Swift 的字符串内容判等,我们简单地使用 == 操作符来进行:
let str1 = "快乐的字符串"
let str2 = "快乐的字符串"
let str3 = "开心的字符串"
str1 == str2 // true
str1 == str3 // false
在 Swift 中 === 只有一种重载:
func ===(lhs: AnyObject?, rhs: AnyObject?) -> Bool
它用来判断两个 AnyObject 是否是同一个引用。
哈希
Int 的 hashValue 就是它本身:
let num = 19
print(num.hashValue) // 19
除非我们正在开发一个哈希散列的数据结构,否则我们不应该直接依赖系统所实现的哈希值来做其他操作。首先哈希的定义是单向的,对于相等的对象或值,我们可以期待它们拥有相同的哈希,但是反过来并不一定成立。其次,某些对象的哈希值有可能随着系统环境或者时间的变化而改变。因此你也不应该依赖于哈希值来构建一些需要确定对象唯一性的功能,在绝大部分情况下,你将会得到错误的结果。
错误和异常处理
在 Swift 2.0 中,Apple 为这门语言引入了异常机制。现在,这类带有 NSError 指针作为参数的 API 都被改为了可以抛出异常的形式。比如上面的 writeToFile:options:error:,在 Swift 中变成了:
public func writeToFile(path: String,
options writeOptionsMask: NSDataWritingOptions) throws
我们在使用这个 API 的时候,不再像之前那样传入一个 error 指针去等待方法填充,而是变为使用 try catch 语句:
do {
try d.writeToFile("Hello", options: [])
} catch let error as NSError {
print ("Error: \(error.domain)")
}
关于 try 和 throws,想再多讲两个小点。首先,try 可以接 ! 表示强制执行,这代表你确定知道这次调用不会抛出异常。如果在调用中出现了异常的话,你的程序将会崩溃,这和我们在对 Optional 值用 ! 进行强制解包时的行为是一致的。另外,我们也可以在 try 后面加上 ? 来进行尝试性的运行。try? 会返回一个 Optional 值:如果运行成功,没有抛出错误的话,它会包含这条语句的返回值,否则将为 nil。和其他返回 Optional 的方法类似,一个典型的 try? 的应用场景是和 if let 这样的语句搭配使用,不过如果你用了 try? 的话,就意味着你无视了错误的具体类型:
func methodThrowsWhenPassingNegative(number: Int) throws -> Int {
if number < 0 {
throw Error.Negative
}
return number
}
if let num = try? methodThrowsWhenPassingNegative(100) {
print(num.dynamicType)
} else {
print("failed")
}
// 输出:
// Int
但是 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中,也就是像是下面这样的方法,我们可以在外层用 rethrows 进行标注:
如果你拿不准要怎么使用的话,就先记住你在要 throws 另一个 throws 时,应该将前者改为 rethrows。
func methodThrows(num: Int) throws {
if num < 0 {
print("Throwing!")
throw Error.Negative
}
print("Executed!")
}
func methodRethrows(num: Int, f: Int throws -> ()) rethrows {
try f(num)
}
do {
try methodRethrows(1, f: methodThrows)
} catch _ {
}
断言
断言 (assertion) 在 Cocoa 开发里一般用来在检查输入参数是否满足一定条件,并对其进行“论断”。
Swift 为我们提供了一系列的 assert 方法来使用断言,其中最常用的一个是:
func assert(@autoclosure condition: () -> Bool,
@autoclosure _ message: () -> String = default,
file: StaticString = default,
line: UInt = default)
在使用时,最常见的情况是给定条件和一个简单的说明。举一个在温度转换时候的例子, 我们想要把摄氏温度转为开尔文温度的时候,因为绝对零度永远不能达到,所以我们不可能接受一个小于 -273.15 摄氏度的温度作为输入:
func convertToKelvin(# celsius: Double) -> Double {
assert(celsius > absoluteZeroInCelsius, "输入的摄氏温度不能低于绝对零度")
return celsius - absoluteZeroInCelsius
}
let roomTemperature = convertToKelvin(celsius: 27)
// roomTemperature = 300.15
let tooCold = convertToKelvin(celsius: -300)
// 运行时错误:
// assertion failed:
// 输入的摄氏温度不能低于绝对零度 : file {YOUR_FILE_PATH}, line {LINE_NUMBER}
断言的另一个优点是它是一个开发时的特性,只有在 Debug 编译的时候有效,而在运行时是不被编译执行的,因此断言并不会消耗运行时的性能。这些特点使得断言成为面向程序员的在调试开发阶段非常合适的调试判断,而在代码发布的时候,我们也不需要刻意去将这些断言手动清理掉,非常方便。
虽然默认情况下只在 Release 的情况下断言才会被禁用,但是有时候我们可能出于某些目的希望断言在调试开发时也暂时停止工作,或者是在发布版本中也继续有效。我们可以通过显式地添加编译标记达到这个目的。在对应 target 的 Build Settings 中,我们在 Swift Compiler - Custom Flags 中的 Other Swift Flags 中添加 -assert-config Debug 来强制启用断言,或者 -assert-config Release 来强制禁用断言。当然,除非有充足的理由,否则并不建议做这样的改动。如果我们需要在 Release 发布时在无法继续时将程序强行终止的话,应该选择使用 fatalError。
PLAYGROUND 延时运行
其中最基础的一个就是异步代码的执行,比如这样的 NSTimer 在默认的 Playground 中是不会执行的:
class MyClass {
@objc func callMe() {
print("Hi")
}
}
let object = MyClass()
NSTimer.scheduledTimerWithTimeInterval(1, target: object,
selector: #selector(MyClass.callMe), userInfo: nil, repeats: true)
在执行完 NSTimer 语句之后,整个 Playground 将停止掉,Hi 永远不会被打印出来。放心,这种异步的操作没有生效并不是因为你写错了什么,而是 Playground 在执行完了所有语句,然后正常退出了而已。
为了使 Playground 具有延时运行的本领,我们需要引入 Playground 的 “扩展包” XCPlayground 框架。现在这个框架中包含了几个与 Playground 的行为交互以及控制 Playground 特性的 API,其中就包括使 Playground 能延时执行的黑魔法,XCPlaygroundPage 和 needsIndefiniteExecution。
我们只需要在刚才的代码上面加上:
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
就可以看到 Hi 以每秒一次的频率被打印出来了。
在实际使用和开发中,我们最经常面临的异步需求可能就是网络请求了,如果我们想要在 Playground 里验证某个 API 是否正确工作的话,使用 XCPlayground 的这个方法开启延时执行也是必要的:
let url = NSURL(string: "http://httpbin.org/get")!
let getTask = NSURLSession.sharedSession().dataTaskWithURL(url) {
(data, response, error) -> Void in
let dictionary = try! NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(dictionary)
}
getTask.resume()
如果你想改变这个时间的话,可以通过 Alt + Cmd + 回车 来打开辅助编辑器。在这里你会看到控制台输出和时间轴,将右下角的 30 改成你想要的数字,就可以对延时运行的最长时间进行设定了。
PLAYGROUND 可视化
在 Playground 中事情就变得简单多了:我们可以使用 XCPlayground 框架的 XCPCaptureValue 方法来将一组数据轻而易举地绘制到时间轴上,从而让我们能看到每一步的结果。这不仅对我们直观且及时地了解算法内部的变化很有帮助,也会是教学或者演示时候的神兵利器。
XCPCaptureValue 的使用方法很简单,在 import XCPlayground 导入框架后,可以找到该方法的定义:
func XCPCaptureValue(identifier: String, value: T)
下面的代码实现了简单的冒泡排序,我们在每一轮排序完成后使用 plot 方法将当前的数组状态用 XCPCaptureValue 的方式进行了输出。通过在时间轴 (通过 “Alt+Cmd+回车” 打开 Assistant Editor) 的输出图,我们就可以非常清楚地了解到整个算法的执行过程了。
import XCPlayground
var arr = [14, 11, 20, 1, 3, 9, 4, 15, 6, 19,
2, 8, 7, 17, 12, 5, 10, 13, 18, 16]
func plot(title: String, array: [T]) {
for value in array {
XCPCaptureValue(title, value: value)
}
}
plot("起始", array: arr)
func swap(inout x: Int, inout y: Int) {
(x, y) = (y, x)
}
func bubbleSort(inout input: [T]) {
for i in 0 ..< input.count - 1 {
let i = input.count - i
var didSwap = false
for j in 0 ..< i - 1 {
if input[j] > input[j + 1] {
didSwap = true
swap(&input[j], &input[j + 1])
}
}
if !didSwap {
break
}
plot("第 \(input.count - (i - 1)) 次迭代", array: input)
}
plot("结果", array: input)
}
bubbleSort(&arr)
因为 XCPCaptureValue 的数据输入是任意类型的,所以不论是传什么进去都是可以表示的。它们将以 QuickLook 预览的方式被表现出来,一些像 UIImage,UIColor 或者 UIBezierPath 这样的类型已经实现了 QuickLook。当然对于那些没有实现快速预览的 NSObject 子类,也可以通过重写
func debugQuickLookObject() -> AnyObject?
来提供一个预览输出。在上面的冒泡排序方法中,我们可以接收任意满足 Comparable 的数组,而绘图方法也可以接受任意类型的输入。作为练习,可以试试看把 arr 的全部数字都换成一些随机的字符串看看时间轴的输出是什么样子吧。
SWIZZLE
Swizzle 是 Objective-C 运行时的黑魔法之一。我们可以通过 Swizzle 的手段,在运行时对某些方法的实现进行替换,这是 Objective-C 甚至说 Cocoa 开发中最为华丽,同时也是最为危险的技巧之一。
一般来说可能不太用得到这样的技术,但是在某些情况下会非常有用,特别是当我们需要触及到一些系统框架的东西的时候。比如我们已经有一个庞大的项目,并使用了很多 UIButton 来让用户交互。某一天,产品汪突然说我们需要统计一下整个 app 中用户点击所有按钮的次数。对于完全不懂技术的选手来说,在他们眼中这似乎不应该是什么难事 -- 只要弄个计数器然后在每次点按钮的时候加一就可以了嘛。但是对于每一个以代码为生的人来说,面临的一个严峻的问题是,这要怎么办。
这种时候就该轮到 Swizzle 大显身手了。我们在全局范围内将所有的 UIButton 的发送事件的方法换掉,就可以一劳永逸地解决这个问题 -- 没有一段段代码的替换查找,不会遗漏任何按钮,之后开发中也不需要对这个计数的功能特别地注意什么。
在 Swift 中,我们也可以利用 Objective-C 运行时来进行 Swizzle。比如上面的例子,我们就可以使用这样的扩展来完成:
extension UIButton {
class func xxx_swizzleSendAction() {
struct xxx_swizzleToken {
static var onceToken : dispatch_once_t = 0
}
dispatch_once(&xxx_swizzleToken.onceToken) {
let cls: AnyClass! = UIButton.self
let originalSelector = #selector(sendAction(_:to:forEvent:))
let swizzledSelector = #selector(xxx_sendAction(_:to:forEvent:))
let originalMethod =
class_getInstanceMethod(cls, originalSelector)
let swizzledMethod =
class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
public func xxx_sendAction(action: Selector,
to: AnyObject!,
forEvent: UIEvent!)
{
struct xxx_buttonTapCounter {
static var count: Int = 0
}
xxx_buttonTapCounter.count += 1
print(xxx_buttonTapCounter.count)
xxx_sendAction(action, to: to, forEvent: forEvent)
}
}
最后我们需要在 app 启动时调用这个 xxx_swizzleSendAction 方法。在 Objective-C 中我们一般在 category 的 +load 中完成,但是 Swift 的 extension 和 Objective-C 的 category 略有不同,extension 并不是运行时加载的,因此也没有加载时候就会被调用的类似 load 的方法。另外,extension 中也不应该做方法重写去覆盖 load (其实重写也是无效的)。事实上,Swift 实现的 load 并不是在 app 运行开始就被调用的。基于这些理由,我们使用另一个类初始化时会被调用的方法来进行交换:
extension UIButton {
override public class func initialize() {
if self != UIButton.self {
return
}
UIButton.xxx_swizzleSendAction()
}
}
和 +load 不同的是,+initialize 会在当前类以及它的子类被初始化时调用。在这里我们对当前类的类型进行了判断,来保证安全性。另外,在 xxx_swizzleSendAction 中,也使用一个 once_token 来保证交换代码仅会被执行一次。
这种方式的 Swizzle 使用了 Objective-C 的动态派发,对于 NSObject 的子类是可以直接使用的,但是对于 Swift 的类,因为默认并没有使用 Objective-C 运行时,因此也没有动态派发的方法列表,所以如果要 Swizzle 的是 Swift 类型的方法的话,我们需要将原方法和替换方法都加上 dynamic 标记,以指明它们需要使用动态派发机制。
那些有用的tips5
LAZY 修饰符和 LAZY 方法
在 Swift 中我们使用在变量属性前加 lazy 关键字的方式来简单地指定延时加载。
class ClassA {
lazy var str: String = {
let str = "Hello"
print("只在首次访问输出")
return str
}()
}
为了简化,我们如果不需要做什么额外工作的话,也可以对这个 lazy 的属性直接写赋值语句:
lazy var str: String = "Hello"
如果我们先进行一次 lazy 操作的话,我们就能得到延时运行版本的容器:
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在处理 \(i)")
return i * 2
}
print("准备访问结果")
for i in result {
print("操作后结果为 \(i)")
}
print("操作完毕")
此时的运行结果:
// 准备访问结果
// 正在处理 1
// 操作后结果为 2
// 正在处理 2
// 操作后结果为 4
// 正在处理 3
// 操作后结果为 6
// 操作完毕
数学和数字
我们可以使用 Int.max 和 Int.min 来取得对应平台的 Int 的最大和最小值。另外在 Double 中,我们还有两个很特殊的值,infinity 和 NaN。
1.797693134862315e+308 < Double.infinity // true
1.797693134862316e+308 < Double.infinity // false
另一个有趣的东西是 NaN,它是 “Not a Number” 的简写,可以用来表示某些未被定义的或者出现了错误的运算,比如下面的操作都会产生 NaN:
let a = 0.0 / 0.0
let b = sqrt(-1.0)
let c = 0.0 * Double.infinity
let num = Double.NaN
if num == num {
print("Num is \(num)")
} else {
print("NaN")
}
// 输出:
// NaN
let num = Double.NaN
if num.isNaN {
print("NaN")
}
if isnan(num) {
print("NaN")
}
// 输出:
// NaN
// NaN
JSON
SwiftyJSON 这样的项目,它就使用了重载下标访问的方式简化了 JSON 操作。使用这个工具,上面的访问可以简化为下面的类型安全的样子:
// 使用 SwiftJSON
if let value = JSON(json)["menu"]["popup"]["menuitem"][0]["value"].string {
print(value)
}
NSNull
// 假设 jsonValue 是从一个 JSON 中取出的 NSNull
let jsonValue: AnyObject = NSNull()
if let string = jsonValue as? String {
print(string.hasPrefix("a"))
} else {
print("不能解析")
}
// 输出:
// 不能解析
调用 C 动态库
Swift 是可以通过 {product-module-name}-Bridging-Header.h 来调用 Objective-C 代码的,于是 C 作为 Objective-C 的子集,自然也一并被解决了。比如对于上面提到的 MD5 的例子,我们就可以通过头文件导入以及添加 extension 来解决:
// TargetName-Bridging-Header.h
#import
// StringMD5.swift
extension String {
var MD5: String {
let cString = self.cStringUsingEncoding(NSUTF8StringEncoding)
let length = CUnsignedInt(
self.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
)
let result = UnsafeMutablePointer.alloc(
Int(CC_MD5_DIGEST_LENGTH)
)
CC_MD5(cString!, length, result)
return String(format:
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15])
}
}
// 测试
print("swifter.tips".MD5)
// 输出
// dff88de99ff03d109de22fed4f71a273
REFLECTION 和 MIRROR
使用 Mirror 类型来做类似反射 (Reflection)的事情:
struct Person {
let name: String
let age: Int
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing) // r 是 MirrorType
print("xiaoMing 是 \(r.displayStyle!)")
print("属性个数:\(r.children.count)")
for i in r.children.startIndex..
使用 dump 方法来通过获取一个对象的镜像并进行标准输出的方式将其输出出来。比如对上面的对象 xiaoMing:
dump(xiaoMing)
// 输出:
// ▿ Person
// - name: XiaoMing
// - age: 16
类似对 Swift 类型的对象做像 Objective-C 中 KVC 那样的 valueForKey: 的取值。通过比较取到的属性的名字和我们想要取得的 key 值就行了,非常简单:
func valueFrom(object: Any, key: String) -> Any? {
let mirror = Mirror(reflecting: object)
for i in mirror.children.startIndex..
输出格式化
在 Swift 里,我们在输出时一般使用的 print 中是支持字符串插值的,而字符串插值时将直接使用类型的 Streamable,Printable 或者 DebugPrintable 接口 (按照先后次序,前面的没有实现的话则使用后面的) 中的方法返回的字符串并进行打印。
let a = 3;
let b = 1.234567 // 我们在这里不去区分 float 和 Double 了
let c = "Hello"
print("int:\(a) double:\(b) string:\(c)")
// 输出:
// int:3 double:1.234567 string:Hello
我们打算只输出上面的 b 中的小数点后两位的话,在 Objective-C 中使用 NSLog 时可以写成下面这样:
NSLog(@"float:%.2f",b);
// 输出:
// float:1.23
swift
let format = String(format:"%.2f",b)
print("double:\(format)")
// 输出:
// double:1.23
当然,每次这么写的话也很麻烦。如果我们需要大量使用类似的字符串格式化功能的话,我们最好为 Double 写一个扩展:
extension Double {
func format(f: String) -> String {
return String(format: "%\(f)f", self)
}
}
这样,在使用字符串插值和 print 的时候就能方便一些了:
let f = ".2"
print("double:\(b.format(f))")
文档注释
注释快捷键:command + alt + /
OPTIONS
我们可以使用 | 或者 & 这样的按位逻辑符对这些选项进行操作,这是因为一般来说在 Objective-C 中的 Options 的定义都是类似这样的按位错开的:
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
UIViewAnimationOptionLayoutSubviews = 1 << 0,
UIViewAnimationOptionAllowUserInteraction = 1 << 1,
UIViewAnimationOptionBeginFromCurrentState = 1 << 2,
//...
UIViewAnimationOptionTransitionFlipFromBottom = 7 << 20,
}
要实现一个 Options 的 struct 的话,可以参照已有的写法建立类并实现 OptionSetType。因为基本上所有的 Options 都是很相似的,所以最好是准备一个 snippet 以快速重用:
struct YourOption: OptionSetType {
let rawValue: UInt
static let None = YourOption(rawValue: 0)
static let Option1 = YourOption(rawValue: 1)
static let Option2 = YourOption(rawValue: 1 << 1)
//...
}
数组 ENUMERATE
var result = 0
for (idx, num) in [1,2,3,4,5].enumerate() {
result += num
if idx == 2 {
break
}
}
print(result)
基本上来说,是时候可以和 enumerateObjectsUsingBlock: 说再见了。
类型编码 @ENCODE
在 Objective-C 中 @encode 使用起来很简单,通过传入一个类型,我们就可以获取代表这个类型的编码 C 字符串:
char *typeChar1 = @encode(int32_t);
char *typeChar2 = @encode(NSArray);
// typeChar1 = "i", typeChar2 = "{NSArray=#}"
我们可以对任意的类型进行这样的操作。这个关键字最常用的地方是在 Objective-C 运行时的消息发送机制中,在传递参数时,由于类型信息的缺失,需要类型编码进行辅助以保证类型信息也能够被传递。在实际的应用开发中,其实使用案例比较少:某些 API 中 Apple 建议使用 NSValue 的 valueWithBytes:objCType: 来获取值 (比如 CIAffineClamp 的文档里) ,这时的 objCType 就需要类型的编码值;另外就是在类型信息丢失时我们可能需要用到这个特性,我们稍后会举一个这方面的例子。
let int: Int = 0
let float: Float = 0.0
let double: Double = 0.0
let intNumber: NSNumber = int
let floatNumber: NSNumber = float
let doubleNumber: NSNumber = double
String.fromCString(intNumber.objCType)
String.fromCString(floatNumber.objCType)
String.fromCString(doubleNumber.objCType)
// 结果分别为:
// {Some "q"}
// {Some "f"}
// {Some "d"}
// 注意,fromCString 返回的是 `String?`
let p = NSValue(CGPoint: CGPointMake(3, 3))
String.fromCString(p.objCType)
// {Some "{CGPoint=dd}"}
let t = NSValue(CGAffineTransform: CGAffineTransformIdentity)
String.fromCString(t.objCType)
// {Some "{CGAffineTransform=dddddd}"}
LOG 输出
在 Swift 中,编译器为我们准备了几个很有用的编译符号,用来处理类似这样的需求,它们分别是:
符号 | 类型 | 描述 |
---|---|---|
#file | String | 包含这个符号的文件的路径 |
#line | Int | 符号出现处的行号 |
#column | Int | 符号出现处的列 |
#function | String | 包含这个符号的方法名字 |
因此,我们可以通过使用这些符号来写一个好一些的 Log 输出方法:
func printLog(message: T,
file: String = #file,
method: String = #function,
line: Int = #line)
{
#if DEBUG
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
#endif
}
// Test.swift
func method() {
//...
printLog("这是一条输出")
//...
}
// 输出:
// Test.swift[62], method(): 这是一条输出
那些有用的tips6
C 代码调用和 @ASMNAME
对于第三方的 C 代码,Swift 也提供了协同使用的方法。我们知道,Swift 中调用 Objective-C 代码非常简单,只需要将合适的头文件暴露在 {product-module-name}-Bridging-Header.h 文件中就行了。而如果我们想要调用非标准库的 C 代码的话,可以遵循同样的方式,将 C 代码的头文件在桥接的头文件中进行导入:
//test.h
int test(int a);
//test.c
int test(int a) {
return a + 1;
}
//Module-Bridging-Header.h
#import "test.h"
//File.swift
func testSwift(input: Int32) {
let result = test(input)
print(result)
}
testSwift(1)
// 输出:2
asmname 可以通过方法名字将某个 C 函数直接映射为 Swift 中的函数。比如上面的例子,我们可以将 test.h 和 Module-Bridging-Header.h 都删掉,然后将 swift 文件中改为下面这样,也是可以正常进行使用的:
//File.swift
//将 C 的 test 方法映射为 Swift 的 c_test 方法
asmname("test") func c_test(a: Int32) -> Int32
func testSwift(input: Int32) {
let result = c_test(input)
print(result)
}
testSwift(1)
// 输出:2
这种导入在第三方 C 方法与系统库重名导致调用发生命名冲突时,可以用来为其中之一的函数重新命名以解决问题。当然我们也可以利用 Module 名字 + 方法名字的方式来解决这个问题。
我们不能简单地用 sizeofValue 来获取长度,而需要进行一些计算。上面的生成 NSData 的方法在 Swift 中书写的话,等效的代码应该是下面这样的:
SIZEOF 和 SIZEOFVALUE
而在 Swift 中,我们如果直接对 bytes 做 sizeofValue 操作的话,将返回 8,这其实是在 64 位系统上一个引用的长度:
// C
char bytes[] = {1, 2, 3};
sizeof(bytes);
// 3
// Swift
var bytes: [CChar] = [1,2,3]
sizeofValue(bytes)
// 8
所以,我们不能简单地用 sizeofValue 来获取长度,而需要进行一些计算。上面的生成 NSData 的方法在 Swift 中书写的话,等效的代码应该是下面这样的:
var bytes: [CChar] = [1,2,3]
let data = NSData(bytes: &bytes, length:sizeof(CChar) * bytes.count)
OPTIONAL MAP
let arr = [1,2,3]
let doubled = arr.map{
$0 * 2
}
print(doubled)
// 输出:
// [2,4,6]
let num: Int? = 3
let result = num.map {
$0 * 2
}
// result 为 {Some 6}
在 Swift 中,我们可以使用以下这五个带有 & 的操作符,这样 Swift 就会忽略掉溢出的错误:
溢出加法 (&+)
溢出减法 (&-)
溢出乘法 (&*)
溢出除法 (&/)
溢出求模 (&%)
var max = Int.max
max = max &+ 1
// 64 位系统下
// max = -9,223,372,036,854,775,808
属性访问控制
对于那些我们只希望在当前文件中使用的属性来说,当然我们可以在声明前面加上 private 使其变为私有:
class MyClass {
private var name: String?
}
但是在开发中所面临的更多的情况是我们希望在类型之外也能够读取到这个类型,同时为了保证类型的封装和安全,只能在类型内部对其进行改变和设置。这时,我们可以通过下面的写法将读取和设置的控制权限分开:
class MyClass {
private(set) var name: String?
}
因为 set 被限制为了 private,我们就可以保证 name 只会在当前文件被更改。
如果我们希望在别的 module 中也能访问这个属性,同时又保持只在当前文件可以设置的话,我们需要将 get 的访问权限提高为 public。
public class MyClass {
public private(set) var name: String?
}
这时我们就可以在 module 之外也访问到 MyClass 的 name 了。
我们在 MyClass 前面也添加的 public,这是编译器所要求的。因为如果只为 name 的 get 添加 public 而不管 MyClass 的话,module 外就连 MyClass 都访问不到了,属性的访问控制级别也就没有任何意义了。
SWIFT 中的测试
在 Swift 2.0 中, Apple 为 app 的测试开了“后门”。现在我们可以通过在测试代码中导入 app 的 target 时,在之前追加 @testable,就可以访问到 app target 中 internal 的内容了。
// 位于 app target 的业务代码
func methodToTest() {
}
// 测试
@testable import MyApp
//...
func testMethodToTest() {
// 配置测试
someObj.methodToTest()
// 断言结果
}
CORE DATA
Core Data 是 Cocoa 的一个重要组成部分,也是非常依赖 @dynamic 特性的部分。Apple 在 Swift 中专门为 Core Data 加入了一个特殊的标注来处理动态代码,那就是 @NSManaged。我们只需要在 NSManagedObject 的子类的成员的字段上加上 @NSManaged 就可以了:
class MyModel: NSManagedObject {
@NSManaged var title: String
}
闭包歧义
3.times { (i: Int)->() in
print(i)
}
3.times { (i: Void)->() in
print(i)
}
3.times { (i: (Int,Int))->() in
print(i)
}
我们想在扩展中实现一个 random 方法来随机地取出 Array 中的一个元素:
extension Array {
var random: Element? {
return self.count != 0 ?
self[Int(arc4random_uniform(UInt32(self.count)))] :
nil
}
}
let languages = ["Swift","ObjC","C++","Java"]
languages.random!
// 随机输出是这四个字符串中的某个
let ranks = [1,2,3,4]
ranks.random!
// 随机输出是这四个数字中的某个
在扩展中是不能添加整个类型可用的新泛型符号的,但是对于某个特定的方法来说,我们可以添加 T 以外的其他泛型符号。比如在刚才的扩展中加上:
func appendRandomDescription
(input: U) -> String {
if let element = self.random {
return "\(element) " + input.description
} else {
return "empty array"
}
}
我们限定了只接受实现了 CustomStringConvertible 的参数作为参数,然后将这个内容附加到自身的某个随机元素的描述上。因为参数 input 实现了 CustomStringConvertible,所以在方法中我们可以使用 description 来获取描述字符串。
let languages = ["Swift","ObjC","C++","Java"]
languages.random!
let ranks = [1,2,3,4]
ranks.random!
languages.appendRandomDescription(ranks.random!)
// 随机组合 languages 和 ranks 中的各一个元素,然后输出
那些有用的tips7
列举 ENUM 类型
扑克牌花色和牌面大小分别由下面两个 enum 来定义:
enum Suit: String {
case Spades = "黑桃"
case Hearts = "红桃"
case Clubs = "草花"
case Diamonds = "方片"
}
enum Rank: Int, Printable {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King
var description: String {
switch self {
case .Ace:
return "A"
case .Jack:
return "J"
case .Queen:
return "Q"
case .King:
return "K"
default:
return String(self.rawValue)
}
}
}
因为在我们这个特定的情况中并没有带有参数的枚举类型,所以我们可以利用 static 的属性来获取一个可以进行循环的数据结构:
protocol EnumeratableEnumType {
static var allValues: [Self] {get}
}
extension Suit: EnumeratableEnumType {
static var allValues: [Suit] {
return [.Spades, .Hearts, .Clubs, .Diamonds]
}
}
extension Rank: EnumeratableEnumType {
static var allValues: [Rank] {
return [.Ace, .Two, .Three,
.Four, .Five, .Six,
.Seven, .Eight, .Nine,
.Ten, .Jack, .Queen, .King]
}
}
在这里我们使用了一个接口来更好地定义适用的接口。关于其中的 class 和 static 的使用情景,可以参看这一篇总结。在实现了 allValues 后,我们就可以按照上面的思路写出:
for suit in Suit.allValues {
for rank in Rank.allValues {
print("\(suit.rawValue)\(rank)")
}
}
// 输出:
// 黑桃A
// 黑桃2
// 黑桃3
// ...
// 方片K
尾递归
一般对于递归,解决栈溢出的一个好方法是采用尾递归的写法。顾名思义,尾递归就是让函数里的最后一个动作是一个函数调用的形式,这个调用的返回值将直接被当前函数返回,从而避免在栈上保存状态。这样一来程序就可以更新最后的栈帧,而不是新建一个,来避免栈溢出的发生。在 Swift 2.0 中,编译器现在支持嵌套方法的递归调用了,因此 sum 函数的尾递归版本可以写为:
func tailSum(n: UInt) -> UInt {
func sumInternal(n: UInt, current: UInt) -> UInt {
if n == 0 {
return current
} else {
return sumInternal(n - 1, current: current + n)
}
}
return sumInternal(n, current: 0)
}
tailSum(1000000)
但是如果你在项目中直接尝试运行这段代码的话还是会报错,因为在 Debug 模式下 Swift 编译器并不会对尾递归进行优化。我们可以在 scheme 设置中将 Run 的配置从 Debug 改为 Release,这段代码就能正确运行了。
ASSOCIATED OBJECT
两个对应的运行时的 get 和 set Associated Object 的 API 是这样的:
func objc_getAssociatedObject(object: AnyObject!,
key: UnsafePointer
) -> AnyObject!
func objc_setAssociatedObject(object: AnyObject!,
key: UnsafePointer,
value: AnyObject!,
policy: objc_AssociationPolicy)
这两个 API 所接受的参数也都 Swift 化了,并且因为 Swift 的安全性,在类型检查上严格了不少,因此我们有必要也进行一些调整。在 Swift 中向某个 extension 里使用 Associated Object 的方式将对象进行关联的写法是:
// MyClass.swift
class MyClass {
}
// MyClassExtension.swift
private var key: Void?
extension MyClass {
var title: String? {
get {
return objc_getAssociatedObject(self, &key) as? String
}
set {
objc_setAssociatedObject(self,
&key, newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
// 测试
func printTitle(input: MyClass) {
if let title = input.title {
print("Title: \(title)")
} else {
print("没有设置")
}
}
let a = MyClass()
printTitle(a)
a.title = "Swifter.tips"
printTitle(a)
// 输出:
// 没有设置
// Title: Swifter.tips
key 的类型在这里声明为了 Void?,并且通过 & 操作符取地址并作为 UnsafePointer
INDIRECT 和嵌套 ENUM
我们用 enum 来重新定义链表结构的话,会是下面这个样子:
indirect enum LinkedList {
case Empty
case Node(Element, LinkedList)
}
let linkedList = LinkedList.Node(1, .Node(2, .Node(3, .Node(4, .Empty))))
// 单项链表:1 -> 2 -> 3 -> 4
实现链表节点的删除方法,在 enum 中添加:
func linkedListByRemovingElement(element: Element)
-> LinkedList
guard case let .Node(value, next) = self else {
return .Empty
}
return value == element ?
next : LinkedList.Node(value, next.linkedListByRemovingElement(element))
}
let result = linkedList.linkedListByRemovingElement(2)
print(result)
// 1 -> 3 -> 4
PROTOCOL EXTENSION
我们定义了这样的一个接口和它的一个扩展:
protocol A1 {
func method1() -> String
}
struct B1: A1 {
func method1() -> String {
return "hello"
}
}
在使用的时候,无论我们将实例的类型为 A1 还是 B1,因为实现只有一个,所以没有任何疑问,调用方法时的输出都是 “hello”:
let b1 = B1() // b1 is B1
b1.method1()
// hello
let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello
但是如果在接口里只定义了一个方法,而在接口扩展中实现了额外的方法的话,事情就变得有趣起来了。考虑下面这组接口和它的扩展:
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}
struct B2: A2 {
func method1() -> String {
return "hello"
}
func method2() -> String {
return "hello"
}
}
let b2 = B2()
b2.method1() // hello
b2.method2() // hello
但是如果我们稍作改变,在上面的代码后面继续添加:
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi
我们可以看到,对 a2 调用 method2 实际上是接口扩展中的方法被调用了,而不是 a2 实例中的方法被调用。我们不妨这样来理解:对于 method1,因为它在 protocol 中被定义了,因此对于一个被声明为遵守接口的类型的实例 (也就是对于 a2) 来说,可以确定实例必然实现了 method1,我们可以放心大胆地用动态派发的方式使用最终的实现 (不论它是在类型中的具体实现,还是在接口扩展中的默认实现);但是对于 method2 来说,我们只是在接口扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2 只是一个符合 A2 接口的实例,编译器对 method2 唯一能确定的只是在接口扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。
整理一下相关的规则的话:
如果类型推断得到的是实际的类型
那么类型中的实现将被调用;如果类型中没有实现的话,那么接口扩展中的默认实现将被使用
如果类型推断得到的是接口,而不是实际类型
并且方法在接口中进行了定义,那么类型中的实现将被调用;如果类型中没有实现,那么接口扩展中的默认实现被使用
否则 (也就是方法没有在接口中定义),扩展中的默认实现将被调用
代码组织和 FRAMEWORK
http://swifter.tips/code-framework/
DELEGATE
protocol MyClassDelegate {
func method()
}
class MyClass {
weak var delegate: MyClassDelegate?
}
class ViewController: UIViewController, MyClassDelegate {
// ...
var someInstance: MyClass!
override func viewDidLoad() {
super.viewDidLoad()
someInstance = MyClass()
someInstance.delegate = self
}
func method() {
print("Do something")
}
//...
}
// weak var delegate: MyClassDelegate? 编译错误
// 'weak' cannot be applied to non-class type 'MyClassDelegate'
在 protocol 声明的名字后面加上 class,这可以为编译器显式地指明这个 protocol 只能由 class 来实现。
protocol MyClassDelegate: class {
func method()
}
LOCK
如果我们喜欢以前的那种形式,甚至可以写一个全局的方法,并接受一个闭包,来将 objc_sync_enter 和 objc_sync_exit 封装起来:
func synchronized(lock: AnyObject, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
再结合 Swift 的尾随闭包的语言特性,这样,使用起来的时候就和 Objective-C 中很像了:
func myMethodLocked(anObj: AnyObject!) {
synchronized(anObj) {
// 在括号内 anObj 不会被其他线程改变
}
}
PLAYGROUND 与项目协作
Playground 其实是可以用在项目里的,通过配置,我们是可以做到让 Playground 使用项目中已有的代码的。直接说结论的话,我们需要满足以下的一些条件:
Playground 必须加入到项目之中,单独的 Playground 是不能使用项目中的已有代码的;
最简单的方式是在项目中使用 File -> New -> File... 然后在里面选择 Playground。注意不要直接选择 File -> New -> Playground...,否则的话你还需要将新建的 Playground 拖到项目中来。
想要使用的代码必须是通过 Cocoa (Touch) Framework 以一个单独的 target 的方式进行组织的;
编译结果的位置需要保持默认位置,即在 Xcode 设置中的 Locations 里 Derived Data 保持默认值;
如果是 iOS 应用,这个框架必须已经针对 iPhone 5s Simulator 这样的** 64 位的模拟器**作为目标进行过编译;
iOS 的 Playground 其实是运行在 64 位模拟器上的,因此为了能找到对应的符号和执行文件,框架代码的位置和编译架构都是有所要求的。
在满足这些条件后,你就可以在 Playground 中通过 import 你的框架 module 名字来导入代码,然后进行使用了。
安全的资源组织方式
假设我们有下面的代码:
enum ImageName: String {
case MyImage = "my_image"
}
enum SegueName: String {
case MySegue = "my_segue"
}
extension UIImage {
convenience init!(imageName: ImageName) {
self.init(named: imageName.rawValue)
}
}
extension UIViewController {
func performSegueWithSegueName(segueName: SegueName, sender: AnyObject?) {
performSegueWithIdentifier(segueName.rawValue, sender: sender)
}
}
在使用时,就可以直接用 extension 中的类型安全的版本了:
let image = UIImage(imageName: .MyImage)
performSegueWithSegueName(.MySegue, sender: self)
但仅仅这样其实还是没有彻底解决名称变更带来的问题。不过在 Swift 中,根据项目内容来自动化生成像是 ImageName 和 SegueName 这样的类型并不是一件难事。Swift 社区中现在也有一些比较成熟的自动化工具了,R.swift 和 SwiftGen 就是其中的佼佼者。它们通过扫描我们的项目文件,来提取出对应的字符串,然后自动生成对应的 enum 或者 struct 文件。当我们之后添加,删除或者改变资源名称的时候,这些工具可以为我们重新生成对应的代表资源名字的类型,从而让我们可以利用编译器的检查来确保代码中所有对该资源的引用都保持正确。这在需要协作的项目中会是非常可靠和值得提倡的做法。
TOLL-FREE BRIDGING 和 UNMANAGED
细心的读者可能会发现在 Objective-C 中类型的名字是 CFURLRef,而到了 Swift 里成了 CFURL。CFURLRef 在 Swift 中是被 typealias 到 CFURL 上的,其实不仅是 URL,其他的各类 CF 类型都进行了类似的处理。这主要是为了减少 API 的迷惑:现在这些 CF 类型的行为更接近于 ARC 管理下的对象,因此去掉 Ref 更能表现出这一特性。
另外在 Objective-C 时代 ARC 不能处理的一个问题是 CF 类型的创建和释放。虽然不能自动化,但是遵循命名规则来处理的话还是比较简单的:对于 CF 系的 API,如果 API 的名字中含有 Create,Copy 或者 Retain 的话,在使用完成后,我们需要调用 CFRelease 来进行释放。
不过 Swift 中这条规则已成明日黄花。既然我们有了明确的规则,那为什么还要一次一次不厌其烦地手动去写 Release 呢?基于这种想法,Swift 中我们不再需要显式地去释放带有这些关键字的内容了 (事实上,含有 CFRelease 的代码甚至无法通过编译)。也就是说,CF 现在也在 ARC 的管辖范围之内了。其实背后的机理一点都不复杂,只不过在合适的地方加上了像 CF_RETURNS_RETAINED 和 CF_RETURNS_NOT_RETAINED 这样的标注。
但是有一点例外,那就是对于非系统的 CF API (比如你自己写的或者是第三方的),因为并没有强制机制要求它们一定遵照 Cocoa 的命名规范,所以贸然进行自动内存管理是不可行的。如果你没有明确地使用上面的标注来指明内存管理的方式的话,将这些返回 CF 对象的 API 导入 Swift 时,它们的类型会被对对应为 Unmanaged