This example implements the view on the gif above
import Foundation
import UIKit
import SnapKit
public final class SimpleView: UIView {
public var appearance = Appearance() {
didSet { appearanceDidChange() }
}
private var titleLabel: UILabel!
private var textLabel: UILabel!
private var imageView: UIImageView!
private var actionButton: UIButton!
private var layout: Layout = .none {
didSet { layoutDidChange(from: oldValue) }
}
private var pendingLayoutChange: (from: Layout, to: Layout)?
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupBindings()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
private func setupSubviews() {
titleLabel = UILabel()
addSubview(titleLabel)
textLabel = UILabel()
addSubview(textLabel)
imageView = UIImageView()
addSubview(imageView)
actionButton = UIButton()
addSubview(actionButton)
appearanceDidChange()
}
private func setupBindings() {
stateDidChange(state: (title: "Title", description: "description", button: "action"))
}
public override func updateConstraints() {
defer {
pendingLayoutChange = nil
super.updateConstraints()
}
guard let pendingLayoutChange = pendingLayoutChange else { return }
switch pendingLayoutChange.to {
case .none:
return
case .full(let offset, true):
titleLabel.snp.remakeConstraints {
$0.top.leading.trailing.equalTo(layoutMarginsGuide)
}
titleLabel.snp.contentHuggingVerticalPriority = 1000
textLabel.snp.remakeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(offset)
$0.leading.trailing.equalTo(layoutMarginsGuide)
}
textLabel.snp.contentHuggingVerticalPriority = 1000
imageView.snp.remakeConstraints {
$0.top.equalTo(textLabel.snp.bottom).offset(offset)
$0.centerX.equalTo(layoutMarginsGuide)
$0.size.equalTo(100)
}
actionButton.snp.remakeConstraints {
$0.top.greaterThanOrEqualTo(imageView.snp.bottom).offset(offset)
$0.leading.bottom.trailing.equalTo(layoutMarginsGuide)
$0.height.equalTo(40)
}
case .full(let offset, false):
titleLabel.snp.remakeConstraints {
$0.top.leading.trailing.equalTo(layoutMarginsGuide)
}
titleLabel.snp.contentHuggingVerticalPriority = 1000
textLabel.snp.remakeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(offset)
$0.leading.trailing.equalTo(layoutMarginsGuide)
}
textLabel.snp.contentHuggingVerticalPriority = 1000
imageView.snp.remakeConstraints {
$0.top.equalTo(textLabel.snp.bottom).offset(offset)
$0.centerX.equalTo(layoutMarginsGuide)
$0.size.equalTo(0)
}
actionButton.snp.remakeConstraints {
$0.top.greaterThanOrEqualTo(textLabel.snp.bottom).offset(offset)
$0.leading.bottom.trailing.equalTo(layoutMarginsGuide)
$0.height.equalTo(40)
}
case .compact(let offset):
titleLabel.snp.remakeConstraints {
$0.top.leading.equalTo(layoutMarginsGuide)
}
titleLabel.snp.contentHuggingVerticalPriority = 1000
textLabel.snp.remakeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(offset)
$0.leading.equalTo(layoutMarginsGuide)
$0.bottom.lessThanOrEqualTo(layoutMarginsGuide)
}
textLabel.snp.contentHuggingVerticalPriority = 1000
imageView.snp.remakeConstraints {
$0.top.equalTo(textLabel.snp.bottom).offset(offset)
$0.leading.equalTo(textLabel.snp.leading)
$0.size.equalTo(0)
}
actionButton.snp.remakeConstraints {
$0.top.bottom.trailing.equalTo(layoutMarginsGuide)
$0.leading.greaterThanOrEqualTo(titleLabel.snp.trailing)
$0.leading.greaterThanOrEqualTo(textLabel.snp.trailing)
}
}
}
private func stateDidChange(state: (title: String, description: String, button: String)) {
titleLabel.text = state.title
textLabel.text = state.description
actionButton.setTitle(state.button, for: .normal)
}
private func appearanceDidChange() {
layout = appearance.layout
applyStyle()
}
private func layoutDidChange(from: Layout) {
guard from != layout else { return }
pendingLayoutChange = (from: from, to: layout)
setNeedsUpdateConstraints()
}
private func applyStyle() {
apply(appearance.viewStyle)
titleLabel.apply(appearance.titleLabelStyle)
textLabel.apply(appearance.textLabelStyle)
imageView.apply(appearance.imageViewStyle)
actionButton.apply(appearance.actionButtonStyle)
}
}
extension SimpleView {
public enum Layout: Equatable {
case none
case full(offset: CGFloat = 20, showImage: Bool = false)
case compact(offset: CGFloat = 10)
}
public struct Appearance {
var layout: Layout = .full()
var backgroundColor = theme.backgroundColor
var labelTextColor = theme.textColor
var labelTextAlignment: NSTextAlignment = .center
var textLabelFontSize: CGFloat = 14
var titleLabelFontSize: CGFloat = 24
var buttonBackgroundColor = theme.accentColor
public static var full: Self { Self() }
public static var fullWithImage: Self {
var appearance = full
appearance.layout = .full(showImage: true)
return appearance
}
public static var compact: Self {
Self(
layout: .compact(),
labelTextAlignment: .left,
textLabelFontSize: 10,
titleLabelFontSize: 16
)
}
}
}
extension SimpleView.Appearance {
var viewStyle: Style<SimpleView> {
return {
$0.backgroundColor = self.backgroundColor
}
+ CommonStyle.debugView(borderColor: labelTextColor)
}
private var labelStyle: Style<UILabel> {
return {
$0.textAlignment = self.labelTextAlignment
$0.textColor = self.labelTextColor
}
+ CommonStyle.debugView(borderColor: labelTextColor)
}
var titleLabelStyle: Style<UILabel> {
labelStyle
+ {
$0.font = UIFont.systemFont(ofSize: self.titleLabelFontSize)
}
}
var textLabelStyle: Style<UILabel> {
labelStyle
+ {
$0.font = UIFont.systemFont(ofSize: self.textLabelFontSize)
}
}
var imageViewStyle: Style<UIImageView> {
CommonStyle.debugView(borderColor: labelTextColor, backgroundColor: .lightGray)
}
var actionButtonStyle: Style<UIButton> {
CommonStyle.actionButton
+ {
$0.backgroundColor = self.buttonBackgroundColor
$0.layer.shadowColor = self.buttonBackgroundColor.cgColor
}
+ CommonStyle.debugView(borderColor: labelTextColor)
}
}
Стили
Для меня идеальная система стилей — БЭМ из CSS. Рекомендую глянуть — там просто и красиво. Давно хочу реализоввать подобное на iOS.
+
) — надо попробовать. Этот оператор удобен в таких ситуациях:Инициализация вьюх
Ты про нейминг и порядок вызовов методов (eg
setupSubiviews
,setupBindings
,applyStyle
)? Я считаю, что здесь достаточно соглашений. Мой опыт говорит, что описывать приватные методы в протоколе или в специальном бейзклассе — порочный путьЛейаут
Ты прав, в примере с
SimpleView
нужно разнести наложение разных типов лейаута на несколько методов, а вupdateConstrainsts
оставить минимум логики. Это в след серии )В примере показал, что вся работа с констреинтами происходит в
updateConstraints
.Итог
Цель примера с
SimpleView
— предложить единообразный подход к созданию вьюх.Наложение стилей, инициализация вьюх и работа с лейаутом не пересекаются и происходят в определенных местах.