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)
}
}
Я тоже думал и пробовал накатить конкатенацию стилей, но мне по итогу не понравились два момента
style1 + style2
, то надо переключаться между ними и держать в голове какие параметры выставляются и где. Может получится сложно. Или на уровне code style вводить ограничения на это.Эти два момента не являются критичными по мне. Предлагаю вынести на общее обсуждение. Если зайдет, я не против сложения стилей.
Наверное мне нравится создание вьюх не лениво в объявлении, а в спец месте. Но тогда я предлагаю сделать либо
BaseView
либоBaseViewProtocol
для всех наших вьюх, что бы структура вызовов сборщиков представления была унифицирована.Все, что касается layout уже "эээ сложнааа". Т.е., со второго раза, понятнее, но думаю сложно будет на уровень проекта это занести. Пример хороший, но таких вьюх, имеющих несколько представлений у нас мало. На вскидку только тикеты в чате и может что-то еще. Без учета вышесказанного, хотелось бы наверное, что бы констрейнты применялись как и стиль в отдельном блоке, что бы switch был коротким и емким. Но, мне кажется, ты тоже так хотел, просто лень писать уже было.
Вынесение применение стилей в отдельный экстеншн-неймспейс поддерживаю, сам так начал делать в следущей ветке, уже больно много места в основном классе это съедает.
Итого, лойс за доведение моего накида до такого состояния, предлагаю на общее выкинуть.