Все мы писали код, в котором какая-то функция создавала и возвращала сконфигурированный объект.
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
}
Заключение
Шаблон функции конфигурирования с передаваемыми параметрами — это удобный фрагмент кода, который должен быть под рукой.
Надеюсь, вы убедитесь в этом еще и самостоятельно. Это все на сегодня. Интересно было бы узнать и о других интересных примерах использования этого фрагмент кода.
Читайте также:
- Новый API форматировщика дат в Swift
- Построение бесконечного списка с помощью SwiftUI и Combine
- Диспетчер загрузки на Swift
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Michael Long: Write Better Swift Code With With()