Как с With() улучшить написание кода на Swift

Все мы писали код, в котором какая-то функция создавала и возвращала сконфигурированный объект.

func makeButton(_ title: String?) -> UIButton {
    let button = UIButton()
    button.translatesAutoresizingMaskIntoConstraints = false
    button.titleLabel?.text = title
    button.titleLabel?.font = .headline
    button.setTitleColor(.red, for: .normal)
    return button
}

Схема одна и та же: создаем переменную с тем, что нам нужно, настраиваем ее, после чего переменную возвращаем.

Но переменная button вместе с оператором return делает приведенный выше код слишком стереотипным. Присутствие button в каждой строке на самом деле не что иное, как просто визуальный шум, обычно скрывающий актуальные параметры конфигурации, которые код фактически задает.

Как сделать этот код лучше?

На помощь приходит Kotlin!

В языке Kotlin имеется конструкция with, и вот что с ее помощью выполняется с лямбдами:

return with(Obj()) {
   objMethod1()
   objMethod2()
   this
}

Обратите внимание: оператор this в последней строке необходим для выполнения того, что нам нужно. В его отсутствие лямбда вернула бы результат, возвращаемый последним выполняемым оператором, в данном случае objMethod2.

Все это здорово и четко. Единственная проблема  —  то, что мы не входим в команду разработчиков компилятора Swift. Так что просто взять и добавить новые типы операторов на Swift не получится.

With()

Добавить собственные операторы нельзя, тогда добавим собственные функции.

А что, если бы мы определили глобальную функцию, принимающую объект, который надо сконфигурировать, изменили бы ее с помощью замыкания конфигурации, а затем вернули результат?

Такая функция позволила бы нам сделать нечто подобное…

func makeButton(_ title: String?) -> UIButton {
    with(UIButton()) {
        $0.translatesAutoresizingMaskIntoConstraints = false
        $0.titleLabel?.text = title
        $0.titleLabel?.font = .headline
        $0.setTitleColor(.red, for: .normal)
    }
}

Уже лучше, хотя нам все еще нужен безымянный параметр $0, чтобы сделать Swift счастливым.

Мы могли бы как-то назвать этот параметр, но по какой-то причине я посчитал вариант параметра без имени более удобным для чтения. Смотрите сами, как вам лучше. Вот вариант с названием:

func makeButton(_ title: String?) -> UIButton {
    with(UIButton()) { b in
        b.translatesAutoresizingMaskIntoConstraints = false
        b.titleLabel?.text = title
        b.titleLabel?.font = .headline
        b.setTitleColor(.red, for: .normal)
    }
}

Имея в качестве названия кнопки префикс, текст слева от точки приходится мысленно отделять от текста справа. В то время как с безымянным параметром, похоже, удается не обращать внимания на этот $0. А он, в свою очередь, делает текстовые названия методов и функций более заметными.

В обоих этих случаях, по-моему, блок конфигурации выделяется, значительно облегчая просмотр и понимание того, какие значения фактически задаются в самом объекте.

Функция

Сама глобальная функция with() достаточно проста:

@discardableResult
@inlinable
func with<V>(_ value: V, _ mutate: ((_ v: inout V) -> Void)) -> V {
    var mutableValue = value
    mutate(&mutableValue)
    return mutableValue
}

В качестве аргумента принимается параметризованное значение, передается в функцию конфигурирования, а затем возвращается конечный результат, с тем чтобы пользователь по своему желанию либо присвоил его, либо вернул.

В нашем варианте использован параметр inout, поэтому он одинаково хорошо работает со структурами. Достаточно просто написать собственную версию, которая была бы несколько более производительной при использовании ее только с классами. Вот версия с классом, который написан с заглавными буквами, чтобы отличаться от версии со значениями:

@discardableResult
@inlinable
public func With<T:AnyObject>(_ value: T, _ mutate: ((_ v: T) -> Void)) -> T {
    mutate(value)
    return value
}

В том случае, когда просто нужен легкий способ использовать with для конфигурирования имеющегося объекта, результат функции отбрасывается:

with(myButtonOutlet) {
   $0.titleLabel?.text = title
   $0.titleLabel?.font = .headline
   $0.setTitleColor(.red, for: .normal)
}

Опять же, есть возможность убрать результат, когда его использование не планируется.

Использование в качестве встроенного аргумента функции

В качестве возвращаемого типа функции мы его уже видели, но with полезен также, в случае когда нужно создать, а затем изменить аргумент или параметр функции:

stackView.addArrangedSubview(with(UIButton()) {
    $0.translatesAutoresizingMaskIntoConstraints = false
    $0.titleLabel?.text = "My Button Title"
    $0.titleLabel?.font = .headline
    $0.setTitleColor(.red, for: .normal)
})

Опять же, нет необходимости создавать временную переменную только для того, чтобы перед передачей в качестве параметра внести в нее изменения:

let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.titleLabel?.text = title
button.titleLabel?.font = .headline
button.setTitleColor(.red, for: .normal)
stackView.addArrangedSubview(button)

Неизменяемые значения

Еще один пример использования with  —  присвоение полностью сконфигурированного значения let, чтобы с этого момента оно оставалось неизменным:

let user = with(User.mock) {
    $0.addPhoneNumber("303-555-8686")
}

Вариации на тему

Чуть выше уже говорилось о написании собственной версии для объектов. Но оказывается, шаблон функции конфигурирования с передаваемыми параметрами удобно применять и в других случаях.

Рассмотрим следующий код, с помощью которого возвращается ячейка в UITableView. (Эти строки выполняются долго, поэтому для большей ясности я переключился на gists):

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    switch section {
    case 0:
        return tableView.dequeueCell(CELLID, for: indexPath) { (_ cell: MyTableViewCell) in
            cell.configure(viewModel.config((for: indexPath.section))
        }
    case 1:
        ...
    }
}

Здесь к UITableView добавили расширение, которое облегчает удаление из очереди ячеек правильного типа, соответствующее их конфигурирование и возвращение по мере необходимости. Для приведения типа удаленной из очереди ячейки к правильному для конфигурирования типу не требуются временные переменные или уродливые преобразования типов.

Единственный нюанс здесь в том, что ячейка должна быть с явно определенным типом MyTableViewCell в функции конфигурирования ячейки. Это соответствует общим ограничениям, необходимым самой этой функции-расширению.

И вот как она выглядит:

extension UITableView {

    func dequeueCell<Cell:UITableViewCell>(_ id: String, for indexPath: IndexPath, configure: (_ cell: Cell) -> Void) -> UITableViewCell {
        let cell = dequeueReusableCell(withIdentifier: id, for: indexPath)
        if let cell = cell as? Cell {
            configure(cell)
        }
        return cell
    }
    
}

Обратите внимание: когда ячейка не соответствует переданному типу, мы просто получаем несконфигурированную ячейку. Когда эта же проблема возникает при удалении из очереди ячейки вручную и при условии самостоятельного приведения UITableViewCell к определенному типу ячейки, по моему мнению здесь фактически одно и то же.

И так и эдак эффект одинаков.

Builder

Тот же функциональный шаблон with применяется и в Builder в тех сценариях, в которых еще предстоит добавить тот или иной модификатор в соответствующее представление. Это альтернатива SwiftUI, использующая набор инструментальных средств для пользовательского интерфейса, и работа по ней продолжается:

Label("Some text")
    .font(.headline)
    .color(.red)
    .with {
        $0.isUserInteractionEnabled = true
    }

Заключение

Шаблон функции конфигурирования с передаваемыми параметрами  —  это удобный фрагмент кода, который должен быть под рукой.

Надеюсь, вы убедитесь в этом еще и самостоятельно. Это все на сегодня. Интересно было бы узнать и о других интересных примерах использования этого фрагмент кода.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Michael Long: Write Better Swift Code With With()

Предыдущая статьяЗапуск тестовых сценариев с Maven
Следующая статьяСоздаем темный режим, используя React и Styled Components