# Maintainable code layout in UIKit ###### tags: `iOS@Taipei` `iOS` `concise-swift` `UIStackView` This post is for those who use code layout and feel unhappy about so many constraints and offsets/insets should define and set. It's really the time to reduce code layout process time and take more time on learning `SwiftUI`. > **Warning** > In this post we use `UIStackView` and `layoutMargins` for building concise custom view, that may not suitable for complex animation. ### 📐 Autolayout is not very easy to maintain let's not talk about `frame-layout` maintainability. ![constrain everywhere](https://i.imgur.com/bJv5HRI.jpg) When building custom view, we have to make sure the subviews of the custom view share the size and spaces correctly, also we might want some padding on custom view or on those subviews. If we use pure `Autolayout`, it will be hard to deal with specific design requirement. Says we have to make sure 5 UI element horizontal spaces always equal to `5`, we have to anchor each of them horizontal space `4` times to make it happen. Also when we want to set four direction padding for different value, you have to do four times anchor to make it. ```javascript= cancelButton.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 8.0).active = true cancelButton.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor, constant: -8.0).active = true cancelButton.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: 8.0).active = true cancelButton.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor, constant: -8.0).active = true ``` Even if we using `SnapKit`, if the padding value is not the same, we still have to write code like: ```javascript= cardView.snp.makeConstraints { (make) in make.top.equalToSuperview().inset(16) make.bottom.equalToSuperview() make.leading.trailing.equalToSuperview().inset(20) } ``` First, let's talk about how to make adpative custom view layout easier. ## 🥞 UIStackView In WWDC15 session [Mysteries of Auto Layout, Part 1](https://developer.apple.com/videos/play/wwdc2015/218/) says `UIStackView` is **maintainable layout**. There's many handy properties to set in `UIStackView` and it can really help you to arrange subviews in autolayout. The session also says that since `UIStackView` doesn't need to render background, so it is optimized to render fast than normal view. > **The Session Conclusion** > Start with Stack View, use constraints as needed. Before using UIStackView, you may write code like: ```javascript= let cardView = UIView() addSubview(cardView) cardView.snp.makeConstraints { (make) in make.top.leading.trailing.equalToSuperview().inset(16) make.bottom.equalToSuperview() } cardView.addSubview(imageView) imageView.snp.makeConstraints { (make) in make.top.bottom.equalToSuperview().inset(16) make.trailing.equalToSuperview() } cardView.addSubview(titleLabel) titleLabel.snp.makeConstraints { (make) in make.top.leading.equalToSuperview().inset(16) make.trailing.equalTo(imageView.snp.leading).offset(8) } cardView.addSubview(confirmButton) confirmButton.snp.makeConstraints { (make) in make.top.equalTo(titleLabel.snp.bottom).offset(12) make.leading.bottom.equalToSuperview().inset(16) } cardView.addSubview(skipButton) skipButton.snp.makeConstraints { (make) in make.centerY.equalTo(confirmButton) make.leading.equalTo(confirmButton.snp.trailing).offset(16) } ``` With `UIStackView`, you can make concise and component-style code layout: ```javascript= let cardView = UIView() addSubview(cardView) cardView.snp.makeConstraints { (make) in make.top.leading.trailing.equalToSuperview().inset(16) make.bottom.equalToSuperview() } let buttonStackView = UIStackView(arrangedSubviews: [confirmButton, skipButton]) buttonStackView.axis = .horizontal buttonStackView.spacing = 16 buttonStackView.alignment = .center buttonStackView.distribution = .fill let titleAndButtonStackView = UIStackView(arrangedSubviews: [titleLabel, buttonStackView]) titleAndButtonStackView.axis = .vertical titleAndButtonStackView.spacing = 12 titleAndButtonStackView.alignment = .leading titleAndButtonStackView.distribution = .fill let stackView = UIStackView(arrangedSubviews: [titleAndButtonStackView, imageView]) stackView.axis = .horizontal stackView.spacing = 8 stackView.alignment = .fill stackView.distribution = .fill cardView.addSubview(stackView) stackView.snp.makeConstraints { (make) in make.top.leading.bottom.equalToSuperview().inset(16) make.trailing.equalToSuperview() } ``` Plus, you can use factory method like `makeButtonStackView()` to make your layout logic more readable. ## layoutMargins and layoutMarginsGuide In WWDC18 session [UIKit: Apps for Every Size and Shape](https://developer.apple.com/videos/play/wwdc2018/235) introduce `layoutMargins` and `layoutMarginsGuide`. **layoutMargins are padding** and the **layoutMarginsGuide is the square** that define space inside the padding. We use layoutMarginsGuide to hook autolayout anchor inside the `layoutMargins` just like using safeAreaLayoutGuide to make sure the our view is in the safeAreaInsets. Let me show you how to use `layoutMarginsGuide` to take your layout code readibility to the next level. ```javascript= /// Before: Using pure autolayout concept cardView.snp.makeConstraints { (make) in make.top.leading.trailing.equalToSuperview().inset(16) make.bottom.equalToSuperview() } imageView.snp.makeConstraints { (make) in make.top.bottom.equalToSuperview().inset(16) // top and bottom padding make.trailing.equalToSuperview() // trailing padding } titleLabel.snp.makeConstraints { (make) in make.top.leading.equalToSuperview().inset(16) // leading padding ... } confirmButton.snp.makeConstraints { (make) in ... make.leading.bottom.equalToSuperview().inset(16) // leading and bottom padding } /// After: Using layoutMarginsGuide directionalLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 20, bottom: 8, trailing: 20) cardView.snp.makeConstraints { (make) in make.edges.equalTo(self.layoutMarginsGuide) } cardView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 0) stackView.snp.makeConstraints { (make) in make.edges.equalTo(cardView.layoutMarginsGuide) } ``` ## Combine all concepts If we use `UIStackView`, `layoutMarginsGuide`, factory method and meaningful enum, we can make code really concise and easy to change any layout. ```javascript= directionalLayoutMargins = Constant.Margins.FeatureTips let cardView = makeCardView() addSubview(cardView) cardView.snp.makeConstraints { (make) in make.edges.equalTo(self.layoutMarginsGuide) } let buttonStackView = makeActionButtonsStackView() let titleAndButtonStackView = UIStackView(arrangedSubviews: [titleLabel, buttonStackView]) titleAndButtonStackView.axis = .vertical titleAndButtonStackView.spacing = Constant.Spacing.TitleAndButtons titleAndButtonStackView.alignment = .leading titleAndButtonStackView.distribution = .fill let stackView = UIStackView(arrangedSubviews: [titleAndButtonStackView, imageView]) stackView.axis = .horizontal stackView.spacing = Constant.Spacing.TitleAndImageView stackView.alignment = .fill stackView.distribution = .fill cardView.addSubview(stackView) stackView.snp.makeConstraints { (make) in make.edges.equalTo(cardView.layoutMarginsGuide) } ``` ### preservesSuperviewLayoutMargins Set `.preservesSuperviewLayoutMargins` to true can make superview's `layoutMargins` affect subview, so the entire view can set `layoutMargins` just once. If you subview has animation any may go outside to superview's `layoutMargins`, you should set it to `false`. ## Conclusion