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



Книга Как с 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
}

Заключение


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


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


655   0  

Comments

    Ничего не найдено.