肇鑫的技术博客

业精于勤,荒于嬉

将协议默认实现作为`selector`进行调用

这个是在swift-users邮件列表遇到的。原标题是Calling default implementation of protocol methods as selectors。原本是个很简单的问题,但是经过几轮讨论,得到的收获还是蛮大的。

先来看问题

有如下代码

protocol Foo: class {
    func bar()
}

extension Foo {
    func bar() {
        print("bar")
    }
}

class Baz: Foo {
    init() {
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(bar))  // error: argument of '#selector' refers to instance method 'bar()' that is not exposed to Objective-C
    }
}

编辑器提示无法调用这个#selector,因为它bar()并没有暴露给Objctive-C。而protocol不能插入@objc

如何解决

方法1

不使用协议扩展,而使用基类。

protocol Foo: class {
    func bar()
}

class Base:Foo {
    @objc func bar() {
        print("bar")
    }
}

class Baz: Base {
    override init() {
        super.init()
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(bar))
    }
}

方法2

使用一个代理函数。

protocol Foo: class {
    func bar()
}

extension Foo {
    func bar() {
        print("bar")
    }
}

class Baz: Foo {
    init() {
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(delegate))
    }
    
    @objc func delegate() {
        bar()
    }
}

这两个办法中,1比2好。因为1方便修改。而如果2需要扩展的话,就需要每个都写一遍。太麻烦。

方法3

我也尝试了convenience init(target: Any, closure: @escaping () -> ())的方法,但是我放弃了。因为我发现,我没法调用closure

后续的邮件中,有人提出了可以内嵌一个类,来实现调用closure。这个是他的实现。

public class BlockTapGestureRecognizer: UITapGestureRecognizer {
    private class Target: NSObject {
        private let closure: (UITapGestureRecognizer) -> ()

        init(closure: @escaping (UITapGestureRecognizer) -> ()) {
            self.closure = closure
            super.init()
        }

        @objc func performClosure(_ sender: Any?) {
            guard let recognizer = sender as? UITapGestureRecognizer else {
                print("Unexpected sender (expected UITapGestureRecognizer)")
                return
            }

            self.closure(recognizer)
        }
    }

    private let target: Target

    public init(closure: @escaping (UITapGestureRecognizer) -> ()) {
        self.target = Target(closure: closure)
        super.init(target: self.target, action: #selector(Target.performClosure(_:)))
    }
}

深入探讨NotificationCenter的addObserver(forName:object:queue:using:)中的queque

先看看下面的代码,这两段notiObserver有区别吗?

override func viewDidLoad() {
    super.viewDidLoad()
    
    let noti = Notification(name: MyNotificationName)
    var v = 0
    let center = NotificationCenter.default
    var notiObserver:NSObjectProtocol! = nil
    
    notiObserver = center.addObserver(forName: MyNotificationName, object: nil, queue: .main) {_ in 
        if v == 10 { center.removeObserver(notiObserver) }
        else {
            v += 1
            center.post(noti)
        }
        print(v)
    }
    
    notiObserver = center.addObserver(forName: MyNotificationName, object: nil, queue: nil) { _ in
        DispatchQueue.main.async {
            if v == 10 { center.removeObserver(notiObserver) }
            else {
                v += 1
                center.post(noti)
            }
            print(v)
        }
    }
    
    center.post(noti)
}

无论你觉得有区别,还是没区别,你可以分别注释掉其中一个然后运行。运行结果是这样的。

上面的那段会输出

10
10
10
10
10
10
10
10
10
10
10

下面那段会输出

1
2
3
4
5
6
7
8
9
10
10

这是为什么呢?我们来看文档。苹果的文档在提到queue是这么说的。

queue
The operation queue to which block should be added.
If you pass nil, the block is run synchronously on the posting thread.

也就是说,它默认是在队列中同步执行。即便我们换成了.main,它也不是我们以为的是异步执行的。实际上,我们可以测试出来,这段代码,在运行时,是堆栈式的。新到的Notification,会进行压栈,优先执行。然后才是旧的Notification没执行完的代码。

如图所示,从左到右是时间轴,依次运行的3段代码,绿色的center.post(noti)导致黄色被执行,黄色的center.post(noti)导致蓝色被执行……,一直到v==10,最后的一段代码print(v),才会从上到下执行下来。

stack
结论:如果我们想要代码符合我们的预期的顺序,应该使用第二种方式,通过GCD,将代码加入到顺序执行的queue中去。

iOS SDK出错,导致返回值为空指针问题的处理

正常来讲,苹果的SDK是不会在Swift的非Optional的类型返回空指针的。如果我们遇到了这个情况,就应该立刻向苹果提交错误报告,让苹果修复这个API。文中提到的这个API,我已经向苹果提交了错误报告,期待苹果能尽快修复。下面来谈谈遇到这种情况要如何处理。

今天发现的坑是这个,func calendarItem(withIdentifier identifier: String) -> EKCalendarItem,它是EKEventStore的实例的方法,可以用来同时查询EKEventEKReminder匹配对应id的值。这个id指的是EKCalendarItemcalendarItemIdentifier

实际上如果只是想获得EKEvent,我们应该使用func event(withIdentifier identifier: String) -> EKEvent?,可以看到,这个方法的返回值是Optional<EKEvent>类型的,因此不会有空指针的问题。

代码示例:
在获得了访问日历和提醒事项的权限后,我们先创建一个日历和一个事件,保存这个日历和事件,然后删除事件。最后通过事件id来查询这个事件。由于此时事件已经被删除了,打印这个事件会导致程序崩溃。这个例子的意义在于,由于现在我们的日历、提醒事项都是同步的,很有可能的当前设备的事件,在其它设备被删掉了,而此时如果你还用原来的id查询,程序就有可能崩溃,因此需要额外的处理。

import UIKit
import EventKit

class ViewController: UIViewController {
    var store:EKEventStore!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        store = (UIApplication.shared.delegate as! AppDelegate).store
        
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [unowned self] (timer) in
            let status = EKEventStore.authorizationStatus(for: .event)
            if status == .authorized {
                timer.invalidate()
                // add a new event calendar
                let calendar = EKCalendar(for: .event, eventStore: self.store)
                calendar.title = "issue test"
                calendar.source = self.store.sources.filter({ $0.sourceType == .local || $0.sourceType == .calDAV}).first!
                try! self.store.saveCalendar(calendar, commit: false)
                
                // add a new event to above calendar
                let event = EKEvent(eventStore: self.store)
                event.calendar = calendar
                event.title = "Have a rest"
                event.startDate = Date()
                event.endDate = Date().addingTimeInterval(30 * 60)
                try! self.store.save(event, span: .thisEvent)
                
                // commit
                try! self.store.commit()
                
                // store the id of the event 
                let id = event.calendarItemIdentifier
                
                // remove the event
                try! self.store.remove(event, span: .thisEvent, commit: true)
                
                // get the event
                let item = self.store.calendarItem(withIdentifier: id)
                print(item)
            }
        }
        
    }
}

由于我们添加了事件之后,又对事件进行了删除。此时再用事件的id获取项目时,项目实际应该返回nil,但是由于苹果设计这个API出现了问题,返回的不是Optional,而是确定的类型,这导致该值为一个空指针。此时,你就算想使用try catch,也是不行的,因为它并没有throw。此时你对它做的大部分操作,都会导致程序崩溃。难道就没有路可以走了吗?其实也不是。

虽然它不是Optional类型,但是它是确定类型的空指针,这个是苹果API的错误,是从Objective-c转换到Swift时出现的。我们自己写的Swift不会允许这样的情况出现。

某种角度来说,它其实也是个nil,因此,我们可以使用if let as?来进行匹配

 // get the event
 let item = self.store.calendarItem(withIdentifier: id)
 
 if let event = item as? EKEvent {
     print(event)
 }
 else if let reminder = item as? EKReminder {
     print(reminder)
 }
 else {
     print("item is a null pointer")
 }

这样就可以临时处理掉这个空指针的问题了。