Use protocol and callAsFunction to improve Delegate pointer

Posted May 27, 20206 min read

In March 2018, I wrote an article on how to Improve Delegate Pattern in Swift, The main idea is to use shadow variable declarations to ensure that self variables can be marked as weak from time to time. In this article, in order to ensure that readers who have not read the original text can be on the same channel, I will first(again) briefly introduce this method. Then, combined with the new features of Swift 5.2, some small improvements are proposed.


Simply put, in order to avoid the tedious and old definition and implementation of protocol, we may prefer to provide closures to complete the callback. For example, in a custom view that collects user input, provide an external function type variable onConfirmInput, and call it when appropriate:

class TextInputView:UIView {@IBOutlet weak var inputTextField:UITextField! var onConfirmInput:((String?)-> Void)? @IBAction func confirmButtonPressed(\ _ sender:Any) {onConfirmInput?(inputTextField.text)}}

In the controller of TextInputView, you do n t need a bunch of textInputView.delegate = self and textInputView(_:didConfirmText:) to detect input determination events, you can directly set onConfirmInput:

class ViewController:UIViewController {@IBOutlet weak var textLabel:UILabel! override func viewDidLoad() {super.viewDidLoad() let inputView = TextInputView(frame:/*...*/) inputView.onConfirmInput = {text in self. textLabel.text = text} view.addSubview(inputView)}}

But this introduces a retain cycle! TextInputView.onConfirmInput holds self, and self holds TextInputView sub view through view, the memory will not be released.

Of course, the solution is also very simple, we only need to use [weak self] when setting onConfirmInput to replace self in the closure with a weak reference:

inputView.onConfirmInput = {\ [weak self ]text in self? .textLabel.text = text}

This adds a premise to the use of closure variables like onConfirmInput:you will most likely need to mark self as weak to avoid making mistakes, otherwise you will write a memory leak. This leak cannot be located during compilation, and there will be no warnings or errors at runtime, and such problems are also easily brought to the final product. There is a truth in the development world:

If a problem may occur, then it must happen.

A simple Delegate type can solve this problem:

class Delegate <Input, Output> {private var block:((Input)-> Output?)? func delegate <T:AnyObject>(on target:T, block:((T, Input)-> Output)?) { self.block = {\ [weak target ]input in guard let target = target else {return nil} return block?(target, input)}} func call(\ _ input:Input)-> Output? {return block?(input)}}

By setting block to weak target(usually self), and providing a variable of target after weak when calling block, you can ensure that the call side does not Will accidentally hold target. For example, the above TextInputView can be rewritten as:

class TextInputView:UIView {//... let onConfirmInput = Delegate <String ?, Void>() @IBAction func confirmButtonPressed(_ sender:Any) {}}

When using, complete the subscription through delegate(on:):

inputView.onConfirmInput.delegate(on:self) {(self, text) in self.textLabel.text = text}

The input parameter (self, text) of the closure and self in the body self.textLabel.text of the closure, ** is not the original self that represents the controller, but the Delegate Selfis marked as the parameter afterweak. Therefore, using this masking variable self` directly in the closure will not cause a circular reference.

The original version "Delegate" up to here can be found in this Gist , plus There are 21 lines of code in the upper line.

Problems and improvements

There are three small flaws in the above implementation, we do some analysis and improvement on them.

1 . More natural i call

Now, the call to the delegate is not as natural as the closure variable, you need to use call(_ :) or call() every time. Although it is not a big deal, it would be simpler if you could directly use a form like onConfirmInput(inputTextField.text).

[CallAsFunction]introduced in Swift 5.2( , It allows us to call a method directly by" calling an instance ". It is very simple to use, just create an instance method called callAsFunction:

struct Adder {let value:Int func callAsFunction(\ _ input:Int)-> Int {return input + value}} let add2 = Adder(value:2) add2(1) //3

This feature is very suitable for simplifying, just add the corresponding callAsFunction implementation and call block:

public class Delegate <Input, Output> {//... func callAsFunction(_ input:Input)-> Output? {return block?(input)}} class TextInputView:UIView {@IBAction func confirmButtonPressed(_ sender:Any) {onConfirmInput(inputTextField.text)}}

Now, the onConfirmInput call looks exactly like a closure.

Similar to callAsFunction, the method of calling a method directly on an instance has many applications in Python. Adding this feature to the Swift language will make it easier for developers accustomed to Python to migrate to projects like Swift for TensorFlow. The people involved in the proposal and review of this proposal are basically members of Swift for TensorFlow.

2 . Double layer optional value

If Output inDelegate <Input, Output>is an optional value, then the result after call will be a double optional Output ??.

let onReturnOptional = Delegate <Int, Int?>() let value = //value:Int ??

This allows us to distinguish between the case where block is not set and the case where Delegate does return nil:whenonReturnOptional.delegate(on:block:)has not been called( block is nil), Value is simply nil. But if delegate is set, but when the closure returns nil, the value of value will be.some(nil). In actual use, it is easy to cause confusion. In most cases, we hope to flatten the return values of .none,.some(.none)and.some(.some(value)) Go to the single layer of .none or.some(value)of Optional.

To solve this problem, Delegate can be extended to provide overloadedcall(_ :)implementations where Output is Optional. But Optional is a type with generic parameters, so we ca n t write conditional extensions like extension Delegate where Output == Optional. A "trick" way is to customize a new OptionalProtocol and let extension do conditional expansion based on where Output:OptionalProtocol:

public protocol OptionalProtocol {static var createNil:Self {get}} extension Optional:OptionalProtocol {public static var createNil:Optional <Wrapped> {return nil}} extension Delegate where Output:OptionalProtocol {public func call(_ input:Input)-> Output {if let result = block?(Input) {return result} else {return .createNil}}}

In this way, even if Output is an optional value, the result of theblock?(Input)call can be unpacked by if let and return a single layer of result or nil.

3 . Shading failure

Since the masking variable self is used, self in the closure is actually the masking variable, not the original self. This requires us to be more careful, otherwise it may cause unexpected circular references. For example, the following example:

inputView.onConfirmInput.delegate(on:self) {(\ _, text) in self.textLabel.text = text}

The above code is compiled and used without problems, but because we replaced (self, text) with (_, text), this leads to self in self.textLabel.text inside the closure. With reference to the real self, this is a strong reference and the memory leaks.

This error is the same as the [weak self] statement. There is no way to get the compiler's prompt, so it is difficult to avoid it completely. Perhaps a feasible solution is not to use implicit masking like (self, text), but to explicitly write the parameter name in a different form, such as (weakSelf, text), and then just use weakSelf in the closure . But in fact, this is not far from the shadow of self, and it still ca n t get rid of using artificial regulations to enforce uniform code rules. Of course, you can also rely on linter and add corresponding rules to remind yourself, but these methods are not very ideal. If you have any good ideas or suggestions, exchanges and suggestions are very welcome.

As a developer, it is especially important to have a learning atmosphere and a communication circle. This is my iOS communication group:there are BAT, Ari test questions, interview experience, discussing technology, and everyone is learning and growing together!


Click into group Password:111

Enter the group and receive the 2020 interview questions directly

å ¨è¿   é    æ ??  å  ¥ å  ¾ç     æ ???? è¿ °