肇鑫的技术博客

肇鑫 / Owen Zhao

独立开发者,主要开发 iOS、watchOS、macOS 应用。

目前在维护 SleepTapRooster Time,以及 Markdown Writer 相关工具。

最新文章

WKAudioFilePlayer的播放问题

watchOS

假设手表扩展中存在xishuai.mp3文件。

let url = Bundle.main.url(forResource: "xishuai", withExtension: "mp3")!
let playAsset = WKAudioFileAsset(url: url)
let playItem = WKAudioFilePlayerItem(asset: playAsset)
player = WKAudioFilePlayer(playerItem: playItem)

WKAudioFilePlayer目前在真机下,如果真机没有连接蓝牙耳机,则播放无声音。因此,不能保证作为通知声音的替代。

`==`与`hashValue`

Swift

Hashable是最常见的协议之一。也许是由于它太常见了。以至于好多人都没法正确的实现它。下面总结一下实现它需要的原则。

基本原则

基本原则只有两条:

  1. Hashable继承了Equatable。因此,要实现Hashable必须同时实现Equatable
  2. 当相同类型的ab相等时,它们的hashValue相等。反之不成立,即hashValue相等的两个相同类型的ab,不一定相等。
class Fruit:Hashable {
    let name:String
    
    var hashValue: Int {
        return name.hashValue
    }
    
    init(name:String) {
        self.name = name
    }
    
    static func ==(lhs: Fruit, rhs: Fruit) -> Bool {
        return lhs.name == rhs.name
    }
}

注意

var hashValue: Int只要求返回值,因此如何实现都可以,设置只返回固定的值,比如var hashValue: Int { return 10 }也是可以的。
不能使用hashValue来判断是否相等。

不同的解读

上面的代码在涉及到子类时,不同的人会有不同的理解。比如:

class Apple:Fruit {
    override var hashValue: Int {
        return super.hashValue - "apple".characters.count
    }
}

class Banana:Fruit {
    override var hashValue: Int {
        return super.hashValue - "banana".characters.count
    }
}

let a = Apple(name: "")
let b = Banana(name: "")
print(a == b) // true
print(a.hashValue) // prints 4799450059485596700
print(b.hashValue) // prints 4799450059485596699

上面的代码,print(a == b) // true,但是hashValue却不相等。这违背了上面提到的基本原则2。这段代码需要修改。但是如何修改,不同的人有不同的看法。

判断类型

我认为最好的办法就是优先判断类型。因为,类型不同的类,就不应该相等。比如一个苹果就不应该和一个香蕉相等。

class Fruit:Hashable {
    let name:String
    
    var hashValue: Int {
        return name.hashValue
    }
    
    init(name:String) {
        self.name = name
    }
    
    static func ==(lhs: Fruit, rhs: Fruit) -> Bool {
        return type(of:lhs) == type(of:rhs) && lhs.name == rhs.name
    }
}

class Apple:Fruit {
    override var hashValue: Int {
        return super.hashValue - "apple".characters.count
    }
}

class Banana:Fruit {
    override var hashValue: Int {
        return super.hashValue - "banana".characters.count
    }
}

let a = Apple(name: "")
let b = Banana(name: "")
print(a == b) // false
print(a.hashValue) // prints 4799450059485596700
print(b.hashValue) // prints 4799450059485596699

即在非final的类的==里,总是先比较类型,类型相同的,再毕竟其它。这么做之后,就永远不会发生一个苹果等于一个香蕉的笑话了。但是这里还有一个问题,我们没有编写子类的==。这里暂时没有问题,但是如果遇到有其它属性的子类,就可能出现问题。

class Orange:Fruit {
    let weight:Double
    
    override var hashValue: Int {
        return super.hashValue - "orange".characters.count + Int(weight)
    }
    
    init(name: String, weight: Double) {
        self.weight = weight
        super.init(name: name)
    }
}

let o1 = Orange(name: "", weight: 0.4)
let o2 = Orange(name: "", weight: 1.2)
print(o1 == o2) // true
print(o1.hashValue) // prints 4799450059485596699
print(o2.hashValue) // prints 4799450059485596700

由于Orange没有重新定义==,比较时,直接调用了Fruit==,导致了不符合基本原则2。补救办法就是在Orange重新定义==

class Orange:Fruit {
    let weight:Double
    
    override var hashValue: Int {
        return super.hashValue - "orange".characters.count + Int(weight)
    }
    
    static func ==(lhs: Orange, rhs: Orange) -> Bool {
        return type(of:lhs) == type(of:rhs) && lhs.name == rhs.name && lhs.weight == rhs.weight
    }
    
    init(name: String, weight: Double) {
        self.weight = weight
        super.init(name: name)
    }
}

let o1 = Orange(name: "", weight: 0.4)
let o2 = Orange(name: "", weight: 1.2)
print(o1 == o2) // false
print(o1.hashValue) // prints 4799450059485596699
print(o2.hashValue) // prints 4799450059485596700

不重写hashValue,仅重新定义==

一些特殊的情况下,也许你可以不重写hashValue,而仅重新定义==

import Foundation
import Cocoa

class Fruit:Hashable {
    let name:String
    
    var hashValue: Int {
        return name.hashValue
    }
    
    init(name:String) {
        self.name = name
    }
    
    static func ==(lhs: Fruit, rhs: Fruit) -> Bool {
        return lhs.name == rhs.name
    }
}

class Apple:Fruit {

}

class ColoredApple:Apple {
    let color:NSColor
    
    init(name:String, color:NSColor) {
        self.color = color
        super.init(name: name)
    }
    
    static func ==(lhs: ColoredApple, rhs: ColoredApple) -> Bool {
        return lhs.name == rhs.name && lhs.color == rhs.color
    }
}

let greenApple = ColoredApple(name: "green apple", color: .green)
let apple = Apple(name: "green apple")
print(greenApple == apple) // true
print(greenApple.hashValue) // -2580839601755588497
print(apple.hashValue) //-2580839601755588497

var set:Set<Apple> = [greenApple, apple]
print(set.count) // 1

var d:Dictionary<Apple, Int> = [greenApple:10]
// eat one
d.updateValue(d[greenApple]! - 1, forKey: apple)
print(d[greenApple]!) // 9

这种做法不常见,一旦选择了不重写hashValue,就必须在该基类的所有子类都保持一致。

注意

由于Set和Dictionary使用hashValue进行判断,放入其中的元素的hashValue在没有从其中移出时,不能改变,否则会出现问题。
可以看到applegreenApple因为hashValue相同,它们在Set和Dictionary里被当做是同一元素/键值。

`Self`和`type(of:)`的用法

Swift

在讲解Self之前,需要先简短介绍一下type(of:)的用法。

type(of:)

从Xcode 8 beta 6开始,dynamicType被替换为了type(of:)。(SE-0096)

type(of:)的功能是获得实例在运行时(runtime)的元类型(meta type)。这个在之前的Swift里,被称作是dynamicType,与静态的类型进行区别。

Self

Self是一个特殊的类型。它是self对应的类型。而self是动态类型。比如:

class Foo {
    func printType() {
        print(type(of:self))
    }
    
    func printSelf() {
        print(self)
    }
}

class Bar:Foo {
    
}

let foo = Foo()
foo.printType() // Foo
foo.printSelf() // Foo

let bar = Bar()
bar.printType() // Bar
bar.printSelf() // Bar

var test = Foo()
test.printType() // Foo
test.printSelf() // Foo

test = Bar()
test.printType() // Bar
test.printSelf() // Bar

可以看到,self始终对应的是实际类型,无论变量的类型是Foo还是Bar。

由于这个特性的存在,这使得Self无法从静态类型进行转换,而只能通过self来生成。即:

class Foo {
    func bar() -> Self {
        return self
    }
}

class Foo1 {
    func bar1() -> Self {
        return type(of:self).init()
    }
    
    required init() { }
}

注意

Foo1中,有一个required init(),这是type(of:self).init()必须的。

Self的使用有以下方式:

1. 在协议的返回值中使用

这是最常见的使用方式。

protocol Foo {
    func instance() -> Self
}

class Bar: Foo {
    func instance() -> Self {
        return self // Declaration: let `self`: Self
    }
    func other() {
        let i = self // Declaration: let `self`: Bar
    }
}

class otherBar:Foo {
    func instance() -> Self {
        return type(of:self).init()
    }
    
    required init() { }
}

####注意
Bar中的实现协议的函数里,self是Self类型。而非Self返回值的函数里,self是所在类的类型。
可以使用type(of:self).init(),获得一个新的Self实例。此时,类中必须要有一个required init()
Self作为函数的返回值的类型时,可以直接写在类中,而不必非要有协议。
下面2和3中的情形,使用associateType的效果,要好于Self。因此,1是Self的最常用的方式。


2. 在协议的函数的参数类型中使用

protocol Foo {
    func bar(b:Self)
}

class Bar:Foo {
    func bar(b:Bar) {
        print(b)
    }
}

####注意
Bar中的func bar(b:Bar)是Bar而不是Self。但是这满足协议。这也符合类的多态,即Bar的子类可以调用这个函数。


3. 在协议的属性里使用

protocol Foo {
    var bar:Self { get set }
}

final class Bar:Foo {
    var bar: Bar
    
    init(b:Bar) {
        self.bar = b
    }
}

####注意
在协议的属性里使用Self时,类必须时final类。


###参考文献:
以下参考文献的一些代码针对的是3.0版本之前的Swift。部分内容需要修正才能运行。但基本原理是相同的。

Types and meta types in swift
Swift中你应该知道的一些有用的tips
Generic Protocols & Their Shortcomings 这篇难度较高,慎看。
Self in Protocol and class method

Realm的坑(三)

RealmSwift

上一个坑里,Realm的坑(二),我们使用非受管对象避开必须使用写入交易的问题。但是,每次使用时都要设置属性还是挺麻烦的。我们寻求一种可以一劳永逸的方式。

最初的想法

我最初想到的是利用NSCopying协议,然后利用Object对象的properties属性给Object实例的属性赋值。如:

extension Object:NSCopying {
    public func copy(with zone: NSZone? = nil) -> Any {
        let o = Object()
        for p in objectSchema.properties {
            let value = self.value(forKey: p.name)
            switch p.type {
            case .linkingObjects:
                break
            default:
                o.setValue(value, forKey: p.name)
            }
        }
        
        return o
    }
}

由于NSCopying的返回值是Any,因此,在使用时需要转换,像这样:

let anotherBar = bar.copy() as! Foo
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

但是这里发生了一个问题。当前版本的Realm中有一个错误,不能在swift中直接创建Object(),程序会崩溃。因此我提交了一个issue

进阶

既然不能直接使用Object(),我决定使用通用类型的函数,像这样:

func unmanagedCopy<T>(of i:T) -> T where T:Object {
    let o = T()
    for p in i.objectSchema.properties {
        let value = i.value(forKey: p.name)
        switch p.type {
        case .linkingObjects:
            break
        default:
            o.setValue(value, forKey: p.name)
        }
    }
    
    return o
}

let anotherBar = unmanagedCopy(of: bar)
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

这么做,带来一个好处,就是我在具体使用的时候不用进行类型转换了。缺点就是这是一个全局函数,也许放到一个struct里会更好一些?

最终的解决方案

在上面提单的issue里,**JadenGeller**给了我两个很好的建议。下面的代码是最终的解决方案。

// MARK: - UnmanagedCopy Protocol
protocol UnmanagedCopy {
    func unmanagedCopy() -> Self
}

extension Object:UnmanagedCopy{
    func unmanagedCopy() -> Self {
        let o = type(of:self).init()
        for p in objectSchema.properties {
            let value = self.value(forKey: p.name)
            switch p.type {
            case .linkingObjects:
                break
            default:
                o.setValue(value, forKey: p.name)
            }
        }
        
        return o
    }
}

首先建立一个UnmanagedCopy的协议。虽然直接写这个函数也可以,但是建立协议可以使函数的目的更加明确。

然后在Object对象的扩展里实现了这个协议。这里用let o = type(of:self).init()避开了不能使用Object()的问题,变量o的类型是Self。函数返回值类型Self可以确保最终的类型与self的实际类型相同,这就保证了使用时不必再进行二次转换。具体使用:

let anotherBar = bar.unmanagedCopy()
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

这个函数已经基本够用了,它包含你的实例里所有持久性数据的属性,但是不包括ignore函数里包含的属性public class func ignoredProperties() -> [String]。所以,如果你有额外的需求,就应该在Object的子类里重写这个函数。如:

class Foo:Object {
    dynamic var id:Int = 0
    dynamic var name:String = ""
    dynamic var age:Int = 8
    
    var temp = "temp value"
    
    override class func ignoredProperties() -> [String] {
        return ["age", "temp"]
    }
    
    override class func primaryKey() -> String? {
        return "id"
    }
    
    override func unmanagedCopy() -> Self {
        let o = type(of:self).init()
        for p in objectSchema.properties {
            let value = self.value(forKey: p.name)
            switch p.type {
            case .linkingObjects:
                break
            default:
                o.setValue(value, forKey: p.name)
            }
        }
        
        o.age = age
        o.temp = temp
        
        return o
    }
}

有关Self的更多用法,看Self的用法

相关

Realm的坑(一)
Realm的坑(二)
Realm的坑(三)
Realm的坑(四)

Realm的坑(二)

RealmSwift

managed与unmanaged

我们都知道,变量都有其生命周期。当存在变量的强引用时,变量会一直存在。比如,有

struct Foo {
   var name:String
}
let bar = Foo(name:"John")

bar在赋值后一直存在。我们始终都可以使用bar.name来获得name属性的值John。但是在Realm中一切都变得不一样了。在Realm中,你通过子类继承Object对象来构建你的Model。然后通过Realm.add来添加这个子类的实例到Realm中。

class Foo:Object {
    dynamic var name:String = ""
}
let bar = Foo()

let realm = try! Realm()
try! realm.write {
	realm.add(bar)
}

你新建出来的bar是unmanaged的实例。realm.add(bar)之后,bar就是managed的实例了。一旦受管,该实例的用法就不能再那么随心所欲了,之后对它的操作就必须在Realm的交易里完成。如:

let bar = Foo()
bar.name = "Marry" // ok

let realm = try! Realm()
try! realm.write {
	realm.add(bar)
	bar.name = "Kelly" // ok
}

bar.name = "Jimmy" // *** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'

为什么name被设置成Marry和Kelly的时候没问题,但是一设置成Jimmy就不行了呢?

  1. 这是因为bar的状态发生了改变。在realm.add()之前,bar是非受管的状态,此时bar在Realm数据库中没有记录。它的行为和一般的类的实例类似,因此可以直接设置name
  2. realm.add()之后,bar在数据库中有了记录,变成了受管的状态,此时再更改它,就必须在realm的写入交易中完成。就像Kelly那样。
  3. Jimmy的部分,由于没有在写入交易中完成,程序会抛出异常并终止运行。

如果不想添加额外的写入交易,就需要添加新建一个非托管对象,然后在添加它到Realm,如:

let anotherBar = Foo()
anotherBar.name = bar.name
  
try! realm.write {
    realm.add(anotherBar)
}

这样的代码有一点儿问题。anotherBarbar对应的是两个不同的对象。现在数据库中同时存在baranotherBar对应的对象了。因此我们需要删除bar

let anotherBar = Foo()
anotherBar.name = bar.name
  
try! realm.write {
    realm.add(anotherBar)
    realm.delete(bar)
}

这很不环保。对于有主键的类的实例,可以使用add(object: Object, update: true),比如:

class Foo:Object {
    dynamic var id:Int = 0
    dynamic var name:String = ""
    
    override class func primaryKey() -> String? {
        return "id"
    }
}

let bar = Foo()
bar.id = 1
bar.name = "Marry" // ok

let realm = try! Realm() 
try! realm.write {
    realm.add(bar)
    bar.name = "Kelly" // ok
}
  
let anotherBar = Foo()
anotherBar.id = 1
anotherBar.name = bar.name
  
try! realm.write {
    realm.add(anotherBar, update:true)
}

每次还要重新设置非受管对象还是挺麻烦的,如果能直接生成一个就好了。这就是第三个坑。Realm的坑(三)

相关

Realm的坑(一)
Realm的坑(二)
Realm的坑(三)
Realm的坑(四)

Realm的坑(一)

RealmSwift

Realm中常见的“Realm accessed from incorrect thread”问题的分析总结

Realm的Results中,有一个很有用的特性,叫做自动更新。顾名思义,就是这个结果会在使用时自动重新查询,已保证最新。当然,这也带来了新的问题。

一旦在创建该变量的线程之外使用该变量,就会导致程序崩溃。

比如,今天我就是在请求通知权限的代码中使用了tasks变量。结果导致程序崩溃。因为这个请求是调用了系统的请求,不是程序本身的线程。

解决办法

  1. 新建realm,重新查询
  2. 使用临时变量,而不使用Results的变量。
  3. 使用DispatchQueue.main.async,更改执行的thread。

备注

print(Thread.current) //可以调试当前的thread

相关

Realm的坑(一)
Realm的坑(二)
Realm的坑(三)
Realm的坑(四)

命令行下使用Swift 2.3 工具链(Toolchain)的方法

Swift

我们知道Xcode 8 beta 同时提供了Swift 3.0 和Swift 2.3来供我们选择。并且我们还知道,可以在Xcode的偏好里,选择对应的工具,如图1-1所示。

图1-1 在Xcode中选择命令行工具

在Xcode中选择命令行工具w750

此时,在终端中查看swift的版本,会是3.0的,如图1-2所示。

代码 1-2 终端查看Swift版本

~ zhaoxin$ swiftc -v
Apple Swift version 3.0 (swiftlang-800.0.42.1 clang-800.0.36.1)
Target: x86_64-apple-macosx10.9
~ zhaoxin$ xcrun swiftc -v
Apple Swift version 3.0 (swiftlang-800.0.42.1 clang-800.0.36.1)
Target: x86_64-apple-macosx10.9

Swift.org官方给出的建议是使用添加环境变量的方式。

$ export PATH=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin:"${PATH}"

但是这样做只会影响swiftc,不会影响xcrun swiftc。

~ zhaoxin$ export PATH=/Applications/Xcode-beta.app/Contents/Developer/Toolchains/Swift_2.3.xctoolchain/usr/bin:"${PATH}"
$ swiftc -v
Apple Swift version 2.3 (swiftlang-800.10.11 clang-800.0.36)
Target: x86_64-apple-macosx10.9
$ xcrun swiftc -v
Apple Swift version 3.0 (swiftlang-800.0.42.1 clang-800.0.36.1)
Target: x86_64-apple-macosx10.9

阅读xcrun的帮助,我们可以发现:

--toolchain <name> 
		Specifies which toolchain to use to perform the  lookup.  If  no
		--toolchain argument is provided, then the toolchain to use will
		be taken from the TOOLCHAINS environment variable, if present.

OK