preface

Learning is like rowing upstream; not to advance is to drop back. ‘!!!!!

Today’s focus is on improving the Delegate using Protocol and callAsFunction. Firstly, it will briefly introduce how to improve the Delegate Pattern in Swift. The main idea is to use the declaration of shadow variable to ensure that the self variable can always be marked as weak. Then, it proposes some small improvements based on the new features of Swift 5.2. The original | address

Delegate

In short, to avoid tedious old protocol definitions and implementations, we might prefer to provide closures for callbacks. For example, in a custom view that collects user input, provide an externally configurable 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) } }Copy the code

In the Controller of the TextInputView, Delegate = self and textInputView(_:didConfirmText:). Instead of checking for input confirminPut events, you can simply 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) } }Copy the code

Ios learning materials package | address But this introduces a retain cycle! TextInputView onConfirmInput holding the self, and the self through the view hold TextInputView the sub view, memory will be unable to release.

Of course, the solution is very simple, just use [weak self] when setting onConfirmInput to change the closure self to a weak reference:

inputView.onConfirmInput = { [weak self] text in self? .textLabel.text = text }Copy the code

This adds a premise to using closure variables like onConfirmInput: you’ll most likely need to mark self as weak to avoid making an error or you’ll write a memory leak. This leak cannot be located at compile time, there are no warnings or errors at run time, and the problem can easily be carried over into the final product. There’s a truism in the development world:

If a problem can happen, it will 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) } }Copy the code

By making target (usually self) weak when a block is set, and providing a target variable after weak when a block is called, you can ensure that target is not accidentally held on the calling side. For example, the TextInputView above could be rewritten as:

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

When used, subscribe via delegate(on:) :

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

The input arguments to the closure (self, text) and self in the closure body self.textlabel.text are not the original controller self, but the arguments that the Delegate labels self as weak. Therefore, using the shadowing variable self directly in the closure does not create a circular reference.

The original version Delegate up to this point can be found in this Gist, which adds up to 21 lines of code plus blank lines.

Problems and improvements

There are three minor flaws in the above implementation that we will analyze and improve.

1. More natural I calls

Now, calls to the delegate are not as natural as closure variables, and call(_:) or call() needs to be used every time. It’s not a big deal, but it would be easier to just use something like onConfirmInput(inputtextField.text).

The callAsFunction, introduced in Swift 5.2, allows us to call a method directly as an “instance call.” It’s easy 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
Copy the code

This feature is great for simplifying delegate. call by adding the corresponding callAsFunction implementation and calling the 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) } }Copy the code

The onConfirmInput call now looks exactly like a closure.

Methods like callAsFunction, which calls methods directly on instances, have many uses in Python. Adding this feature to Swift makes it easier for Python-accustomed developers to migrate to projects like Swift for TensorFlow. The people who proposed and reviewed this proposal are basically members of Swift for TensorFlow.

2. Double-layer optional values

If Output in the Delegate

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

let onReturnOptional = Delegate<Int, Int? >() let value = onReturnOptional.call(1) // value : Int??Copy the code

This allows us to distinguish between cases where the block is not set and cases where the Delegate does return nil: When onReturnOptional. Delegate (on: block:) not been called (block is nil), the value is a simple nil. But if the delegate is set and the closure returns nil, the value will be.some(nil). This can be confusing in practice. In most cases, we want to flatten returns like.none,.some(.none), and.some(.some(value)) to a single layer of Optional.None or.some(value).

To solve this problem, extend the Delegate to provide overloaded call(_:) implementations for cases where Output is Optional. However Optional is a type with a generic parameter, so there is no way to write conditional extensions like Extension Delegate where Output == Optional. A “trick” would be to customize a new OptionalProtocol and have extension conditional extensions 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 } } }Copy the code

So, even if Output is optional, block? The result of the (input) call can also be unpacked by an if let and return a single layer of result or nil.

3. Masking failure

Because the shadowing variable self is used, self in the closure is the shadowing variable, not self. This requires us to be careful, or it can lead to accidental circular references. Take the following example:

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

The above code compiles and works fine, but since we replaced (self, text) with (_, text), this causes the self inside the closure to refer directly to the real self, which is a strong reference and leaks memory.

Like the [weak self] declaration, there is no way to get a hint from the compiler, so it is hard to avoid completely. Perhaps a possible solution would be to not use an implicit masking like (self, text), but to explicitly write the parameter name in a different form, such as (weakSelf, text), and then use only weakSelf in the closure. However, this is not too far from self masking, which still enforces uniform code rules with “artificial rules”. Of course, you can use Linter and add rules to remind yourself, but these are not ideal either. If you have any good ideas or suggestions, you are welcome to exchange and comment.

Get a wave of likes and attention at the end. I know the good ones have already liked and followed.

IOS learning materials package | address

Recommended reading: Preliminary to Swift Concurrency