Swift 中系统 API delegate 参数一定要在当前类实现传 self 吗

52 天前
 jiangzm

因为要写原生插件,swift 刚接触,语法比 objc 好看多了。

在开发相机组件时遇到一个问题,AVCapturePhotoCaptureDelegate 契约用自定义 UIView/UIViewController 实现没问题, 换成独立的类实现就回调不了,不是什么原因。

上代码:

TakePhotoDelegate.swift

import Foundation
import AVFoundation
import UIKit

public typealias TakePhotoCallback = ((String, String) -> Void)?;

class TakePhotoWithCompletion : NSObject, AVCapturePhotoCaptureDelegate {

    // ...
    var completion: TakePhotoCallback;
    
    init(callback: TakePhotoCallback) {

        self.completion = callback;
        super.init();
    }

    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    
        // todo sth
    
        self.completion!("{file path}", "{error}");
        
    }
}


CameraxView.swift

import AVFoundation
import UIKit

class CameraxView: UIView {

    // ...

    @objc
    func takePhoto(options: Dictionary<String, String>, completion: TakePhotoCallback) {
        
        let photoSettings = AVCapturePhotoSettings.init(format:[AVVideoCodecKey:AVVideoCodecType.jpeg])
        let delegate = TakePhotoWithCompletion.init(callback: completion)
        
        imageOutPut?.capturePhoto(with:photoSettings, delegate: delegate)

    }

  // ...
}

点击拍照按钮能听到快门声,但是 photoOutput 没有回调

如果换成如下自定义 UIView 实现 AVCapturePhotoCaptureDelegate 没有问题,能正常回调

import AVFoundation
import UIKit

class CameraxView: UIView, AVCapturePhotoCaptureDelegate {

    // ...
    var completion: TakePhotoCallback = nil;

    @objc
    func takePhoto(options: Dictionary<String, String>) {
        
        let photoSettings = AVCapturePhotoSettings.init(format:[AVVideoCodecKey:AVVideoCodecType.jpeg])

        self.completion = completion
        
        imageOutPut?.capturePhoto(with:photoSettings, delegate: self)

    }

    @objc
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    
        // todo sth
    
        self.completion!("{file path}", "{error}");
        
    }

    // ...

}

有这个差异的原因是因为系统 API 的限制吗还是 swift 语言层面问题, 如果 takePhoto 方法希望传闭包参数回调拍照结果,正确应该怎么写。

(回调参数赋值给实例对象, 个人感觉是个很奇怪的做法)

985 次点击
所在节点    Swift
6 条回复
hguandl
52 天前
我推测这与弱引用有关。capturePhoto 不会保持对 delegate 的引用,导致其在 takePhoto 结尾被直接释放掉了。可以试一试给 TakePhotoWithCompletion 加个自定义 deinit ,打一下日志或断点看一下释放时机。
jiangzm
52 天前
@hguandl #1 是弱引用问题, 加了 deinit 有打印出来。 那除了强引用设置成实例成员还有其他方式解决么。
一般用闭包封装委托是怎么做的。感觉我的写法和这个例子差不多 https://stackoverflow.com/questions/24173210/implement-delegate-with-closure-in-swift
hguandl
52 天前
@jiangzm delegate 这种设计模式 + Swift 的 ARC 特性决定了几乎只能这样写,不然就会造成内存泄漏。如果是纯 Swift 应用的话可以封装成 async 函数来解决,但是看你是要和桥接 objc ,那就没有太好的方法。
louiswang002
52 天前
let delegate = TakePhotoWithCompletion.init(callback: completion)
创建的对象没有引用,imageOutPut 对 delegate 是弱引用,当前栈弹出时,delegate 对象就被释放了,肯定不会有输出;你在第二种实现中,对 delegate 对象创建了一个强引用,imageOutPut 对 delegate 的弱引用对象也就存在了,也能触发 delegate 了
jiangzm
52 天前
不知道为啥 swift 要搞一个 delegate 概念出来,也不是语法关键字 就是普通的 class 实现接口(契约) 。既然方法参数和属性能传函数(闭包),没有委托好像也行。发现有些系统 api 原来是传函数的,新版搞个新 class 新 api 就要传委托。^_^

这个和 C#的 delegate 完全不同,委托是函数引用/指针,匿名函数(swift 闭包)、lambda 表达式、事件都是基于委托的。

Swift 应该是差一个事件的概念, 但是呢没有一步到位整了个中间的 delegate 概念,再加上内存管理不是那么智能,使用起来有点束手束脚的感觉。

C#中的委托和事件都是可以多播的,大部分组件对外暴露的都是事件,拿上面委托例子来说就是这样:

``` imageOutPut.onPhotoOutput = () => { xxx }; ```

或者
```imageOutPut.onPhotoOutput += () => { xxx }; ```
Sricecake
43 天前
因为这些 API 是 OC 时代定义的,OC 当时没有闭包,所以只能以这种类似 JAVA 的 Interface 方式实现回调,你虽然使用的是 Swift ,但是其实还是在调用原 OC 的库。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1076938

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX