SwiftLayout

3.0.0

A swifty way to use UIKit
ioskrew/SwiftLayout

What's New

3.0.0

2023-11-06T01:12:42Z

SwiftLayout 3.0.0 update

The SwiftLayout 3.0.0 update has finally been released.
Let's talk about some of the important changes in 3.0.0

1. Introduction of Name space

Methods such as sublayout(_:), anchors(_:), etc. that are called directly from UIView could potentially cause naming conflicts with methods defined by other packages and code. We introduced a namespace called "sl" so that the first time you call a SwiftLayout-related method from UIView, you can access it through the namespace.

// before
someUIView.anchors {
	...
}.sublayout {
	...
}

// after
someUIView.sl.anchors {
	...
}.sublayout {
	...
}

2. Changed Anchors syntax

As suggested by [#274](#274), we unified the syntax of Anchors, which was divided into property interfaces and method interfaces, into the form of property interfaces.

// before
Anchors.allSides()
Anchors.cap()
Anchors.horizontal(offset: 13)
Anchors.center(offsetX: offsetX, offsetY: offsetY)

// after
Anchors.allSides.equalToSuper()
Anchors.cap // As before, you can omit equalToSuper().
Anchors.horizontal.equalToSuper(inwardOffset: 13)
Anchors.center.equalToSuper(xOffset: offsetX, yOffset: offsetY)

3. onActivate(_:) replaces the config(_:) method

The config(_:) method is a method that runs on call and executes the passed in closure regardless of the layout's activation. This was problematic as it could cause the closure to be executed at an unintended time, which led to the replacement of config(_:) with the onActivate(_:) method.

The onActivate(_:) method does not immediately execute the passed in closure, but instead executes the passed in closure at the time the layout is activated when methods like active, finalActive, etc. are called.

  • This was changed to the @escaping closure to delay the execution of the closure to the time of activation.
  • Because of the change to @escaping closures, it is essential to capture variables in order to access variables outside of the closure.
  • You should consider weakly capturing external variables to avoid strong reference cycles.
  • When the Layout is released from memory, both the closure and the captured variables are released.
    This means that if you don't store Layout in a stored property, a strong reference cycle will not occur.

What's Changed

New Contributors

Full Changelog: 2.8.0...3.0.0

SwiftLayout Logo

Yesterday never dies

A swifty way to use UIKit

@LayoutBuilder var layout: some Layout {
  self.sl.sublayout {
    leftParenthesis.sl.anchors {
      Anchors.leading.equalToSuper(constant: 16)
      Anchors.centerY
    }
    viewLogo.sl.anchors {
      Anchors.leading.equalTo(leftParenthesis, attribute: .trailing, constant: 20)
      Anchors.centerY.equalToSuper(constant: 30)
      Anchors.size.equalTo(width: 200, height: 200)
    }
    UIImageView().sl.identifying("plus").sl.onActivate { imageView in
      imageView.image = UIImage(systemName: "plus")
      imageView.tintColor = .SLColor
    }.anchors {
      Anchors.center.equalToSuper(yOffset: 30)
      Anchors.size.equalTo(width: 150, height: 150)
    }
    constraintLogo.sl.anchors {
      Anchors.trailing.equalTo(rightParenthesis.leadingAnchor)
      Anchors.centerY.equalTo("plus")
      Anchors.size.equalTo(width: 200, height: 150)
    }
    rightParenthesis.sl.anchors {
      Anchors.trailing.equalToSuper(constant: -16)
      Anchors.centerY
    }
  }
}

Translation

Requirements

  • iOS 13+

    Swift version SwiftLayout version
    Swift 5.7+ 2.8.0
    Swift 5.5 2.7.0
    Swift 5.4 2.5.4

Installation

SwiftLayout only supports deployments via SPM(Swift Package Manager).

dependencies: [
  .package(url: "https://github.com/ioskrew/SwiftLayout", from: "3.0.0"),
],

Features

  • DSL features for addSubview and removeFromSuperview
  • DSL features for NSLayoutConstraint, NSLayoutAnchor and activation
  • can updates only required in view states.
  • using conditional and loop statements like if else, swift case, for in view hierarhcy and autolayout constraints.
  • offer propertyWrapper for automatically updating of layout
  • offering varierty features for relations of constraints.

Usage

LayoutBuilder

LayoutBuilder is a DSL builder for setting up the UIView hierarchy; this allows subviews to be added to the parent view in a simple and visible way.

@LayoutBuilder var layout: some Layout {
  view.sl.sublayout {
    subview.sl.sublayout {
      subsubview
      subsub2view
    }
  }
}

this is like below:

view.addSubview(subview)
subview.addSubview(subsubview)
subview.addSubview(subsub2view)

AnchorsBuilder

AnchorsBuilder is a DSL builder for Anchors types that aids in the creation of autolayout constraints between views. It is mainly used within anchors, a method of Layout.

Anchors

Anchors have attributes for NSLayoutConstraint and can creates.

summary of NSLayoutConstraint

  • first: Item1 and attribute1
  • second: item2 and attribute2
  • relation: relation(=, >=, <=), constant, multiplier

equation of constraint has following format: Item1.attribute1 [= | >= | <= ] multiplier x item2.attribute2 + constant

Detailed information about NSLayoutConstraint can be found here.

  • It starts by getting the required properties using static values ​​defined in Anchors.

    Anchors.top.bottom
  • You can set up a second item (NSLayoutConstraint.secondItem, secondAttribute) through a relationship method such as equalTo.

    superview.sl.sublayout {
      selfview.sl.anchors {
        Anchors.top.equalTo(superview, attribute: .top, constant: 10)
      }
    }

    this is same as following constraint format:

    selfview.top = superview.top + 10
    
  • Attributes in Anchors that do not have a relation function in the child can be configured to match the parent item

    superview.sl.sublayout {
      selfview.sl.anchors {
        Anchors.sl.top.bottom
      }
    }

    this can be expressed by the following expression:

    selfview.top = superview.top
    selfview.bottom = superview.bottom
    ...
    

    also, the multiplier can be set as follows.

    Anchors.top.multiplier(10)
  • Width and height become the item itself if you do not set the second item.

    superview.sl.sublayout {
      selfview.sl.anchors {
        Anchors.width.height.equalTo(constant: 10) // only for selfview
      }
    }

    this represents the following expression.

    selfview.width = 10
    selfview.height = 10
    

LayoutBuilder + AnchorsBuilder

ah, finally

LayoutBuilder and AnchorsBuilder can now be used together to add subviews, create autolayouts, and apply them to views.

  • A sublayout method is required to add subviews after invoking an anchors method.

    @LayoutBuilder func layout() -> some Layout {
      superview.sl.sublayout {
        selfview.sl.anchors {
          Anchors.allSides
        }.sublayout {
          subview.sl.anchors {
            Anchors.allSides
          }
        }
      } 
    }
  • Is your hierarchy too complex? Just separates it.

    @LayoutBuilder func layout() -> some Layout {
      superview.sl.sublayout {
        selfview.sl.anchors {
          Anchors.allSides
        }
      }
      selfview.sl.sublayout {
        subview.sl.anchors {
          Anchors.allSides
        }
      }
    }

active and finalActive

The Layout types created with LayoutBuilder and AnchorsBuilder only contain information to actually work.
For the application of addSubview and constraint, the method below must be called:

  • you can call finalActive of Layout for instantly do all stuff in case of no needs to updates.

  • finalActive return nothing after addSubview and active constraints instantly.

    @LayoutBuilder func layout() -> some Layout {
      superview.sl.sublayout {
        selfview.sl.anchors {
          Anchors.top
        }
      }
    }
    
    init() {
      layout().finalActive()
    }
  • you can call active of Layout if needs using some features for updates.
    Returns Activation, an object containing information needed for update.

    @LayoutBuilder func layout() -> some Layout {
      superview.sl.sublayout {
        selfview.sl.anchors {
          if someCondition {
            Anchors.bottom
          } else {
            Anchors.top
          }
        }
      }
    }
    
    var activation: Activation
    
    init() {
      activation = layout().active()
    }
    
    func someUpdate() {
      activation = layout().update(fromActivation: activation)
    }

Layoutable

In SwiftLayout, Layoutable plays a role similar to that of View in SwiftUI.

For implementing Layoutable, you needs be write following codes

  • var activation: Activation?

  • @LayoutBuilder var layout: some Layout { ... }: @LayoutBuilder may not required.

    class SomeView: UIView, Layoutable {
      var activation: Activation?
      @LayoutBuilder var layout: some Layout {
        self.sl.sublayout {
          ...
        }
      }
    
      init(frame: CGRect) {
        super.init(frame: frame)
        self.sl.updateLayout() // call active or update of Layout
      }
    }

LayoutProperty

Builders of SwiftLayout is DSL languages, so you can perform if, switch case, for etc.

However, in order to reflect the state change in the layout of the view, you must directly call the updateLayout method of the sl property provided by Layoutable when necessary.

var showMiddleName: Bool = false {
  didSet {
    self.sl.updateLayout()
  }
}

var layout: some Layout {
  self.sl.sublayout {
    firstNameLabel
    if showMiddleName {
      middleNameLabel
    }
    lastNameLabel
  }
}

If showMiddleName is false, middleNameLabel is not added to the super view, and if it is already added, it is removed from the super view.

In this case, you can update automatically by using LayoutProperty:

@LayoutProeprty var showMiddleName: Bool = false // change value call updateLayout of Layoutable

var layout: some Layout {
  self.sl.sublayout {
    firstNameLabel
    if showMiddleName {
      middleNameLabel
    }
    lastNameLabel
  }
}

Animations

You can start animation by updating constraint in Layoutable, And the method is as easy as the following

  • in the animation block of UIView, call updateLayout with forceLayout parameter set to true.
final class PreviewView: UIView, Layoutable {
  var capTop = true {
    didSet {
      // start animation for change constraints
      UIView.animate(withDuration: 1.0) {
        self.sl.updateLayout(forceLayout: true)
      }
    }
  }
  // or just use the convenient propertyWrapper like below
  // @AnimatableLayoutProperty(duration: 1.0) var capTop = true
  
  let capButton = UIButton()
  let shoeButton = UIButton()
  let titleLabel = UILabel()
  
  var topView: UIButton { capTop ? capButton : shoeButton }
  var bottomView: UIButton { capTop ? shoeButton : capButton }
  
  var activation: Activation?
  
  var layout: some Layout {
    self.sl.sublayout {
      topView.sl.anchors {
        Anchors.cap
      }
      bottomView.sl.anchors {
        Anchors.top.equalTo(topView.bottomAnchor)
        Anchors.height.equalTo(topView)
        Anchors.shoe
      }
      titleLabel.sl.onActivate { label in
        label.text = "Top Title"
        UIView.transition(with: label, duration: 1.0, options: [.beginFromCurrentState, .transitionCrossDissolve]) {
          label.textColor = self.capTop ? .black : .yellow
        }
      }.anchors {
        Anchors.center.equalTo(topView)
      }
      UILabel().sl.onActivate { label in
        label.text = "Bottom Title"
        label.textColor = self.capTop ? .yellow : .black
      }.identifying("title.bottom").anchors {
        Anchors.center.equalTo(bottomView)
      }
    }
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    initViews()
  }
  
  required init?(coder: NSCoder) {
    super.init(coder: coder)
    initViews()
  }
  
  func initViews() {
    capButton.backgroundColor = .yellow
    shoeButton.backgroundColor = .black
    capButton.addAction(.init(handler: { [weak self] _ in
      self?.capTop.toggle()
    }), for: .touchUpInside)
    shoeButton.addAction(.init(handler: { [weak self] _ in
      self?.capTop.toggle()
    }), for: .touchUpInside)
    self.sl.updateLayout()
  }
}
animation.mp4

Other useful features

onActivate(_:) of UIView

You can use the onActivate function in a layout to decorate and modify the view. The closure passed to the onActivate function is called during the activation process.

contentView.sl.sublayout {
  nameLabel.sl.onActivate { label in 
    label.text = "Hello"
    label.textColor = .black
  }.anchors {
    Anchors.allSides
  }
}

identifying of UIView and Layout

You can set accessibilityIdentifier and use that instead of the view reference.

contentView.sl.sublayout {
  nameLabel.sl.identifying("name").sl.anchors {
    Anchors.cap
  }
  ageLabel.sl.anchors {
    Anchors.top.equalTo("name", attribute: .bottom)
    Anchors.shoe
  }
}
  • from a debugging point, if you set identifier, the corresponding string is output together in the description of NSLayoutConstraint.

Using in SwiftUI

implement Layoutable on UIView or UIViewController you can easily using it in SwiftUI.

class ViewUIView: UIView, Layoutable {
  var layout: some Layout { 
    ...
  }
}

...

struct SomeView: View {
  var body: some View {
    VStack {
      ...
	    ViewUIView().sl.swiftUI
      ...
    }
  }
}

struct ViewUIView_Previews: PreviewProvider {
  static var previews: some Previews {
    ViewUIView().sl.swiftUI
  }
}

SwiftLayoutUtil

LayoutPrinter

This can be useful when you want to make sure the current layout is configured the way you want it to.

  • prints the tree created by the hierarchy of layouts and anchors.

    var layout: some Layout {
      root.sl.sublayout {
        child.sl.anchors {
          Anchors.top
          Anchors.leading.trailing
        }
        friend.sl.anchors {
          Anchors.top.equalTo(child, attribute: .bottom)
          Anchors.bottom
          Anchors.leading.trailing
        }
      }
    }
  • you can use LayoutPrinter in source or debug console.

    (lldb)  po import SwiftLayoutUtil; LayoutPrinter(layout).print()

    ViewLayout - view: root
    └─ TupleLayout
       ├─ ViewLayout - view: child
       └─ ViewLayout - view: friend
    
  • if necessary, you can also print Anchors applied to the layout.

    (lldb)  po import SwiftLayoutUtil; LayoutPrinter(layout, withAnchors: true).print()

    ViewLayout - view: root
    └─ TupleLayout
       ├─ ViewLayout - view: child
       │        .top == superview.top
       │        .leading == superview.leading
       │        .trailing == superview.trailing
       └─ ViewLayout - view: friend
                .top == child.bottom
                .bottom == superview.bottom
                .leading == superview.leading
                .trailing == superview.trailing
    

ViewPrinter

This can be useful when you want to migrate your current view to SwiftLayout for several reasons.

  • printing UIView hierarchy and autolayout constraint relationship to SwiftLayout syntax

    let contentView: UIView
    let firstNameLabel: UILabel
    contentView.addSubview(firstNameLabel)
  • you can use ViewPrinter in source or debug console.

    (lldb) po import SwiftLayoutUtil; ViewPrinter(contentView).print()

    // If there is no separate identification setting, the view is displayed in the form of addressValue:View type.
    0x01234567890:UIView {
      0x01234567891:UILabel
    }
  • printing labels for view by name of view property is very convenient.

    class SomeView {
      let root: UIView // subview of SomeView
      let child: UIView // subview of root
      let friend: UIView // subview of root
    }
    let someView = SomeView()

    (lldb) po import SwiftLayoutUtil; ViewPrinter(someView, tags: [someView: "SomeView"]).updateIdentifiers().print()

    SomeView {
      root.sl.sublayout {
        child.sl.anchors {
          Anchors.top
          Anchors.leading.trailing
        }
        friend.sl.anchors {
          Anchors.top.equalTo(child, attribute: .bottom)
          Anchors.bottom
          Anchors.leading.trailing
        }
      }
    }

Credits

Description

  • Swift Tools 5.7.0
View More Packages from this Author

Dependencies

  • None
Last updated: Fri Oct 18 2024 19:15:01 GMT-0900 (Hawaii-Aleutian Daylight Time)