Краткий рассказ о «композиции над наследованием» (включая дженерики, ура!)

Отказ от ответственности : этот метод актуален только в том случае, если вы создаете свои макеты программно (без Interface Builder и раскадровок). Если вы хотите глубже понять, почему вам следует отказаться от Interface Builder, обязательно прочтите эту замечательную статью команды Zeplin. С другой стороны, если вы хотите укрепить свою любовь к Interface Builder, я настоятельно рекомендую эту вдохновляющую статью от Скотта Берревоетса из Lyft.

Что случилось?

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

Полный разочарований по поводу того, что UITableViewCell и UIView похожи, но на самом деле разные (технически вы можете использовать UITableViewCell как UIView, но такая практика довольно сомнительна), вы решаете просто скопировать и вставить код во вновь созданный подкласс UIView. И здесь все начинает идти в корне неправильно - потому что в тот момент, когда вам нужно внести изменение, вы должны сохранить это изменение в нескольких местах своей кодовой базы.

Так что я могу с этим поделать?

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

protocol ViewLayout {
    
    func layout(on view: UIView)
    
    init()
    
}

Затем мы создаем макет, используя этот протокол.

final class TwoLabelsLayout : ViewLayout {
    
    let leftLabel = UILabel()
    let rightLabel = UILabel()
    
    init() { }
    
    func layout(on view: UIView) {
        let stackView = UIStackView(arrangedSubviews: [leftLabel, rightLabel])
        stackView.axis = .horizontal
        
        stackView.translatesAutoresizingMaskIntoConstraints = false // don't forget this one to enable Auto Layout in code
        
        view.addSubview(stackView)
        stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
        stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true
        stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true
        stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 10).isActive = true
    }
    
}

Посмотрите, как вы не имеете дело с какими-либо деталями жизненного цикла представления и - вы просто пишете код для конкретного макета.

Следующим шагом будет создание вспомогательных универсальных базовых классов:

class View<Layout : ViewLayout> : UIView {
    
    public let layout: Layout
    
    public override init(frame: CGRect) {
        self.layout = Layout()
        super.init(frame: frame)
        layout.layout(on: self)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}
class TableViewCell<Layout : ViewLayout> : UITableViewCell {
    
    public let layout: Layout
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        self.layout = Layout()
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        layout.layout(on: contentView) // important!
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

И теперь мы можем безболезненно использовать наши макеты для создания представлений

let twoLabels = View<TwoLabelsLayout>(frame: .zero)

или зарегистрируйте классы ячеек табличного представления

tableView.register(TableViewCell<TwoLabelsLayout>.self, forCellReuseIdentifier: "two-labels")

Это было довольно просто, да?

Конечно, чтобы получить доступ к содержанию представления и изменить его, вы должны использовать синтаксис .layout

twoLabels.layout.leftLabel.text = "AnySuggestion"

Хорошо, но что мне делать с существующей кодовой базой?

Принять этот подход в уже существующих представлениях и ячейках представления таблиц очень просто (хотя и требует немного времени).

Во-первых, конечно, вы должны извлечь весь код макета в эти ViewLayout объекты. Затем, если, например, у вас есть TextFieldAndImageView, вы просто меняете его суперкласс и оборачиваете эти .layout вызовы, чтобы ваш уже существующий код компилировался:

class TextFieldAndImageView : View<TextFieldAndImageLayout> {
    
    var textField: UITextField {
        return layout.textField
    }
    
    var imageView: UIImageView {
        return layout.imageView
    }
    
}

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

Я пробовал это, но не работает. Что случилось?

При адаптации принципа «макета просмотра» есть некоторые моменты, в которых следует проявлять осторожность:

  1. В вашем layout(on:) методе ViewLayout вам нужно не забыть вызвать view.addSubview, чтобы ваши данные действительно отображались на экране.
  2. При создании базового TableViewCell<Layout : ViewLayout> класса очень важно писать layout.layout(on: contentView) вместо layout.layout(on: self).
  3. translatesAutoresizingMaskIntoConstraints = false. Всегда актуально.

Звучит здорово, но что, если я использую Interface Builder?

Интерфейсный разработчик, безусловно, отличный инструмент, но он является огромным, огромным врагом возможности повторного использования кода. Широкое использование Interface Builder и Storyboards требует много размышлений и усилий, чтобы все делать «правильно», поэтому полный отказ от Interface Builder может фактически улучшить архитектуру вашего приложения. Просмотрите ссылки в заявлении об отказе от ответственности или ниже, чтобы лучше понять проблему.

В ViewLayout подходе нет ничего нового, но он применяет некоторые общие архитектурные принципы (такие как возможность повторного использования, разделение задач и «композиция вместо наследования») к специфическому для UIKit коду. Для меня использование этого подхода оказалось гораздо более гибким, масштабируемым и поддерживаемым решением, чем просто наличие UIView подклассов повсюду.

Спасибо, что прочитали сообщение! Не стесняйтесь спрашивать или предлагать что-либо в разделе Ответы ниже. Вы также можете связаться со мной в Твиттере или найти меня на GitHub. Если вы написали статью (или наткнулись на нее), посвященную аналогичной теме, обязательно разместите ссылку на нее в ответах, чтобы я мог включить ее прямо ниже.

Дальнейшее обучение:

Привет! Я Олег, автор книги Женский футбол 2017 и независимый разработчик iOS / watchOS, страстно увлеченный Swift. Пока я работаю над своим следующим приложением, вы можете проверить мой последний проект под названием The Cleaning App - небольшую утилиту, которая поможет вам отслеживать ваши процедуры очистки. Спасибо за поддержку!