A declarative UI framework for iOS

Related tags

Layout layout
Overview

Travis Coveralls Platform Swift Swift License CocoaPods Compatible Carthage Compatible

Layout

Layout is a native Swift framework for implementing iOS user interfaces using XML template files and runtime-evaluated expressions. It is intended as a more-or-less drop-in replacement for Nibs and Storyboards, but offers a number of advantages such as human-readable templates and live editing.

Screenshot

Introduction

Why?

Layout seeks to solve a number of issues that make Storyboards unsuitable for large, collaborative projects, including:

  • Proprietary, undocumented format
  • Poor composability and reusability
  • Difficult to apply common style elements and metric values without copy-and-paste
  • Hard for humans to read, and consequently hard to resolve merge conflicts
  • Limited WYSIWYG capabilities

Layout also includes a replacement for AutoLayout that aims to be:

  • Simpler to use for basic layouts
  • More intuitive and readable for complex layouts
  • More deterministic and simpler to debug
  • More performant (at least in theory :-))

To find out more about why we built Layout, and the problems it addresses, check out this article.

How?

Layout introduces a new node hierarchy for managing views, similar to the "virtual DOM" used by React Native.

Unlike UIViews (which use NSCoding for serialization), Layout nodes can be deserialized from a lightweight, human-readable XML format, and also offer a concise API for programmatically generating view layouts in code when you don't want to use a separate resource file.

View properties are specified using expressions, which are pure functions that are stored as strings and evaluated at runtime. Now, I know what you're thinking - stringly-typed code is horrible! - but Layout's expressions are strongly-typed, and designed to fail early, with detailed error messages to help you debug.

Layout is designed to work with ordinary UIKit components, not to replace or reinvent them. Layout-based views can be embedded inside Nibs and Storyboards, and Nib and Storyboard-based views can be embedded inside Layout-based views and view controllers, so there is no need to rewrite your entire app if you want to try using Layout.

Usage

Installation

Layout is provided as a standalone Swift framework that you can use in your app. It works with Swift 3.2 and 4.0, and is not tied to any particular package management solution.

To install Layout using CocoaPods, add the following to your Podfile:

pod 'Layout', '~> 0.6'

To install using Carthage, add this to your Cartfile:

github "schibsted/Layout" ~> 0.6

Dependencies

Layout has no external dependencies. It makes use of the Expression and Sprinter frameworks internally, but these have been included inside the Layout module as part of the source distribution, so there is no need to include them separately.

Because Expression and Sprinter are inside the Layout namespace, you can safely use Layout in a project that is already using another copy of either of these frameworks.

Integration

The primary API exposed by Layout is the LayoutNode class. Create a layout node as follows:

let node = LayoutNode(
    view: UIView.self,
    expressions: [
        "width": "100%",
        "height": "100%",
        "backgroundColor": "#fff",
    ],
    children: [
        LayoutNode(
            view: UILabel.self,
            expressions: [
                "width": "100%",
                "top": "50% - height / 2",
                "textAlignment": "center",
                "font": "Courier bold 30",
                "text": "Hello World",
            ]
        )
    ]
)

This example code creates a centered UILabel inside a UIView with a white background that will stretch to fill its superview once mounted.

For simple views, creating the layout in code is a convenient solution that avoids the need for an external file. But the real power of the Layout framework comes from the ability to specify layouts using external XML files, because it allows for live reloading, which can significantly reduce development time.

The equivalent XML markup for the layout above is:

<UIView
    width="100%"
    height="100%"
    backgroundColor="#fff">

    <UILabel
        width="100%"
        top="50% - height / 2"
        textAlignment="center"
        font="Courier bold 30"
        text="Hello World"
    />
</UIView>

Most built-in iOS views should already work when used as a Layout XML element. For custom views, you may need to make a few minor changes for full Layout-compatibility. See the Custom Components section below for details.

To mount a LayoutNode inside a view or view controller, the simplest approach is to create a UIViewController subclass and add the LayoutLoading protocol. You can then use one of the following three options to load your layout:

class MyViewController: UIViewController, LayoutLoading {

    public override func viewDidLoad() {
        super.viewDidLoad()

        // Option 1 - create a layout programmatically
        self.layoutNode = LayoutNode( ... )

        // Option 2 - load a layout synchronously from a bundled XML file
        self.loadLayout(named: ... )

        // Option 3 - load a layout asynchronously from an XML file URL
        self.loadLayout(withContentsOfURL: ... ) { error in
            ...   
        }
    }
}

Use option 1 for layouts generated in code. Use option 2 for XML layout files located inside the application resource bundle.

Option 3 can be used to load a layout from an arbitrary URL, which can be either a local file or remotely-hosted. This is useful if you need to develop directly on a device, because you can host the layout file on your Mac and then connect to it from the device to allow reloading of changes without recompiling the app. It's also potentially useful in production for hosting layouts in some kind of CMS system.

Note: The loadLayout(withContentsOfURL:) method offers limited control over caching, etc. so if you intend to host your layout files remotely, it may be better to download the XML to a local cache location first and then load it from there.

Editor Support

You can edit Layout XML files directly in Xcode, but you will probably miss having autocomplete for view properties. There is currently no way to provide autocomplete support in Xcode, however Layout does now include support for the popular Sublime Text editor.

To install Layout autocompletion in Sublime Text:

  1. Go to Preferences > Browse Packages…, which will open the Packages directory in the Finder
  2. Copy the layout.sublime-completions file from the Layout repository into Packages/User

Autocomplete for standard UIKit views, view controllers and properties will now be available for xml files edited in Sublime Text.

There is currently no way to automatically generate autocomplete suggestions for custom views or properties, but you could manually add these to the layout.sublime-completions file.

We hope to add support for other editors in future. If you are interested in contributing to this effort, please create an issue on Github to discuss it.

Live Reloading

Layout provides a number of helpful features to improve your development productivity, most notably the Red Box debugger and the live reloading feature.

When you load an XML layout file in the iOS Simulator, the Layout framework will attempt to find the original source XML file for the layout and load that instead of the static version bundled into the compiled app.

This means that you can make changes to your XML file and then reload it without recompiling the app or restarting the simulator.

Note: If multiple source files match the bundled file name, you will be asked to choose which one to load. See the Ignore File section below if you need to exclude certain files from the search process.

You can reload your XML files at any time by pressing Cmd-R in the simulator (not in Xcode itself, as that will recompile the app). Layout will detect that key combination and reload the XML.

Note: This only works for changes you make to your layout XML files, or in your Localizable.strings file, not for Swift code changes in your view controller, or other resources such as images.

The live reloading feature, combined with the graceful handling of errors, means that it should be possible to do most of your interface development without needing to recompile the app.

Debugging

If the Layout framework throws an error during XML parsing, mounting, or updating, it will display the Red Box, which is a full-screen overlay that displays the error message along with a reload button.

For non-critical errors (e.g. using a deprecated API) Layout will display a yellow warning bar at the bottom of the screen, which can be dismissed with a tap.

Thanks to the live reloading feature, many bugs (e.g. syntax errors or misnamed properties) can be fixed without recompiling the app. Once you have fixed the bug, pressing reload (or Cmd-R) will dismiss any warnings or errors and reload the layout XML file.

The Red Box interface is managed by the LayoutConsole singleton. This exposes static methods to show and hide the console, along with an isEnabled property to enable or disable the console programmatically. By default, the console is enabled for debug builds and disabled for release, but if you need to override this setting at runtime you can do so.

If the LayoutConsole is disabled, errors and warnings will be printed to the Xcode console instead.

Constants

Static XML is all very well, but most app content is dynamic. Strings, images, and even layouts themselves need to change at runtime based on user-generated content, the current locale, etc.

LayoutNode provides two mechanisms for passing dynamic data to the layout, which can then be referenced inside your expressions: constants and state.

Constants - as the name implies - are values that remain constant for the lifetime of the LayoutNode. These values don't need to be constant for the lifetime of the app, but changing them means re-creating the LayoutNode and its associated view hierarchy from scratch. The constants dictionary is passed into the LayoutNode initializer, and can be referenced by any expression in that node or any of its children.

A good use for constants would be localized strings, or something like colors or fonts used by the app UI theme. These are things that never (or rarely) change during the lifecycle of the app, so it's acceptable that the view hierarchy must be torn down in order to reset them.

Here is how you would pass some constants to your XML-based layout:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "title": NSLocalizedString("homescreen.title", message: ""),
        "titleColor": UIColor.primaryThemeColor,
        "titleFont": UIFont.systemFont(ofSize: 30),
    ]
)

And how you might reference them in the XML:

<UIView ... >
    <UILabel
        width="100%"
        textColor="titleColor"
        font="{titleFont}"
        text="{title}"
    />
</UIView>

You may have noticed that the title and titleFont constants are surrounded by {...} braces, but the titleColor constant isn't. This is explained in the Strings and Fonts subsections below.

You will probably find that some constants are common to every layout in your application, for example if you have constants representing standard spacing metrics, fonts or colors. It would be annoying to have to repeat these everywhere, but the lack of a convenient way to merge dictionaries in Swift (as of version 3.0) makes it painful to use a static dictionary of common constants as well.

For this reason, the constants argument of LayoutNode's initializer is actually variadic, allowing you to pass multiple dictionaries, which will be merged automatically. This makes it much more pleasant to combine a global constants dictionary with a handful of custom values:

let extraConstants: [String: Any] = [
    ...
]

loadLayout(
    named: "MyLayout.xml",
    constants: globalConstants, extraConstants, [
        "title": NSLocalizedString("homescreen.title", message: ""),
        "titleColor": UIColor.primaryThemeColor,
        "titleFont": UIFont.systemFont(ofSize: 30),
    ]
)

State

For more dynamic layouts, you may have properties that need to change frequently (perhaps even during an animation), and recreating the entire view hierarchy to change these is not very efficient. For these properties, you can use state. State works in much the same way as constants, except you can update the state after the LayoutNode has been initialized:

loadLayout(
    named: "MyLayout.xml",
    state: [
        "isSelected": false,
        ...
    ],
    constants: [
        "title": ...
    ]
)

func setSelected() {
    self.layoutNode?.setState([
        "isSelected": true
    ])
}

Note that you can use both constants and state in the same Layout. If a state variable has the same name as a constant, the state variable takes precedence. As with constants, state variables can be passed in at the root node of a hierarchy and accessed by any child node. If children in the hierarchy have their own constants or state variables, these will take priority over values set on their parent.

Although state can be updated dynamically, all state variables referenced in the layout must have been given a value before the LayoutNode is first mounted/updated. It's generally a good idea to set default values for all state variables when you first initialize the node.

Calling setState() on a LayoutNode after it has been created will trigger an update. The update causes all expressions in that node and its children to be re-evaluated. In future it may be possible to detect if parent nodes are indirectly affected by the state changes of their children and update them too, but currently that is not implemented.

In the example above, we've used a dictionary to store the state, but LayoutNode supports the use of arbitrary objects for state. A really good idea for layouts with complex state requirements is to use a struct. When you set the state using a struct or class, Layout uses Swift's introspection features to compare changes and determine if an update is necessary.

Internally the LayoutNode still just treats the struct as a dictionary of key/value pairs, but you get to take advantage of compile-time type validation when manipulating your state programmatically in the rest of your program:

struct LayoutState {
    let isSelected: Bool
}

loadLayout(
    named: "MyLayout.xml",
    state: LayoutState(isSelected: false),
    constants: [
        "title": ...
    ]
)

func setSelected() {
    self.layoutNode?.setState(LayoutState(isSelected: true))
}

When using a state dictionary, you do not have to pass every single property each time you set the state. If you are only updating a subset of properties, it is fine to pass a dictionary with only those key/value pairs. (This is not the case if you are using a struct, but don't worry - this is only a convenience feature, and makes little or no difference to performance.):

loadLayout(
  named: "MyLayout.xml",
  state: [
    "value1": 5,
    "value2": false,
  ]
)

func setSelected() {
    self.layoutNode?.setState(["value1": 10]) // value2 retains its previous value
}

Actions

For any non-trivial view you will need to bind actions from controls in your view hierarchy to your view controller, and communicate user actions back to the view.

You can define actions on any UIControl subclass using actionName="methodName" in your XML, for example:

<UIButton touchUpInside="wasPressed"/>

There is no need to specify a target - the action will be automatically bound to the first matching method encountered in the responder chain. If no matching method is found, Layout will display an error.

Note: The error will be shown when the node is mounted, not deferred until the button is pressed, as it would be for actions bound using Interface Builder.

func wasPressed() {
    ...
}

The action's method name follows the Objective-C selector syntax conventions, so if you wish to pass the button itself as a sender, use a trailing colon in the method name:

<UIButton touchUpInside="wasPressed:"/>

Then the corresponding method can be implemented as:

func wasPressed(_ button: UIButton) {
    ...
}

Action expressions are treated as strings, and like other string expressions they can contain logic to produce a different value depending on the layout constants or state. This is useful if you wish to toggle the action between different methods, e.g.

<UIButton touchUpInside="{isSelected ? 'deselect:' : 'select:'}"/>

In this case, the button will call either the select(_:) or deselect(_:) methods, depending on the value of the isSelected state variable.

Outlets

When creating views inside a Nib or Storyboard, you typically create references to individual views by using properties in your view controller marked with the @IBOutlet attribute, and Layout can utilize the same system to let you reference individual views in your hierarchy from code.

To create an outlet binding for a layout node, declare a property of the correct type on your view controller, and then reference it using the outlet constructor argument for the LayoutNode:

class MyViewController: UIViewController, LayoutLoading {

    @objc var labelNode: LayoutNode? // outlet

    public override func viewDidLoad() {
        super.viewDidLoad()

        self.layoutNode = LayoutNode(
            view: UIView.self,
            children: [
                LayoutNode(
                    view: UILabel.self,
                    outlet: #keyPath(self.labelNode),
                    expressions: [ ... ]
                )
            ]
        )
    }
}

In this example we've bound the LayoutNode containing the UILabel to the labelNode property. A few things to note:

  • There's no need to use the @IBOutlet attribute for your outlet property, but you can do so if you feel it makes the purpose clearer. If you do not use @IBOutlet, you will need to use @objc to ensure the property is visible to Layout at runtime.
  • The type of the outlet property can be either LayoutNode or a UIView subclass that's compatible with the view managed by the node. The syntax is the same in either case - the type will be checked at runtime, and an error will be thrown if it doesn't match up.
  • In the example above we have used Swift's #keyPath syntax to specify the outlet value, for better static validation. This is recommended, but not required.
  • The labelNode outlet in the example has been marked as Optional. It is common to use Implicitly Unwrapped Optionals (IUOs) when defining IBOutlets, and that will work with Layout too, but it will result in a hard crash if you make a mistake in your XML and then try to access the outlet. Using regular Optionals means XML errors can be trapped and fixed without restarting the app.

To specify outlet bindings when using XML templates, use the outlet attribute:

<UIView>
    <UILabel
        outlet="labelNode"
        text="Hello World"
    />
</UIView>

In this case we lose the static validation provided by #keyPath, but Layout still performs a runtime check and will throw a graceful error in the event of a typo or type mismatch, rather than crashing.

Outlets can also be set using an expression instead of a literal value. This is useful if you wish to pass the outlet in to the template via a parameter, for example:

<UIView>
    <param name="labelOutlet" type="String"/>

    <UILabel
        outlet="{labelOutlet}"
        text="Hello World"
    />
</UIView>

The type of the parameter in this case must be String, and not UILabel as you might expect. The reason for this is that the outlet is a keyPath that references a property of the layout's owner (typically a view controller), not a direct reference to the view itself.

Note: Outlet expressions must be set using a constant or literal value, and cannot be changed once set. Attempting to set the outlet using a state variable or other dynamic value will result in an error.

Delegates

Another commonly-used feature in iOS is the delegate pattern. Layout also supports this, but it does so in an implicit way that may be confusing if you aren't expecting it.

When loading a Layout XML file, or a programmatically-created LayoutNode hierarchy into a view controller, the views will be scanned for delegate properties and these will be automatically bound to the controller if it conforms to the specified protocol.

So for example, if your layout contains a UIScrollView, and your view controller conforms to the UIScrollViewDelegate protocol, then the view controller will automatically be attached as the delegate for the view controller:

class MyViewController: UIViewController, LayoutLoading, UITextFieldDelegate {
    var labelNode: LayoutNode!

    public override func viewDidLoad() {
        super.viewDidLoad()

        self.layoutNode = LayoutNode(
            view: UIView.self,
            children: [
                LayoutNode(
                    view: UITextField.self, // delegate is automatically bound to MyViewController
                    expressions: [ ... ]
                )
            ]
        )
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return false
    }
}

There are a few caveats however:

  • This mechanism currently only works for properties called "delegate" or "dataSource", or which are suffixed with "Delegate" or "DataSource" (e.g. "dragDelegate"). This is the standard convention used by UIKit components, but if you have custom controls that use a different naming convention for delegates, they won't be bound automatically and you will need to bind them programmatically.

  • The binding mechanism relies on Objective-C runtime protocol detection, so it won't work for Swift protocols that aren't @objc-compliant.

  • If you have multiple views in your layout that all use the same delegate protocol, e.g. several UIScrollViews or several UITextFields, then they will all be bound to the view controller. If you are only interested in receiving events from some views and not others, you can either add logic inside the delegate methods to determine which view is calling them, or explicitly disable the delegate properties of those views by setting them to nil:

<UITextField delegate="nil"/>

You can also set the delegate to a specific object by passing a reference to it as a state variable or constant and then referencing that in your delegate expression:

self.layoutNode = LayoutNode(
    view: UIView.self,
    constants: [
        "fieldDelegate": someDelegate
    ],
    children: [
        LayoutNode(
            view: UITextField.self,
            expressions: [
                "delegate": "fieldDelegate"
            ]
        )
    ]
)

Note: There is currently no safe way to explicitly bind a delegate to the layoutNode's owner class. Attempting to pass self as a constant or state variable will result in a retain cycle (which is why owner-binding is done implicitly rather than manually).

Animation

UIKit has great support for animation, and naturally you'll want to include animations in your Layout-based interfaces, so how do you handle animation in Layout?

There are three basic types of animation in iOS:

  1. Block-based animations, using UIView.animate(). Normally you would use this in UIKit by setting view properties and/or AutoLayout constraints inside an animation block. In Layout you should call setState() inside an animation block to implicitly animate any changes resulting from the state change:
UIView.animate(withDuration: 0.4) {
    self.layoutNode?.setState([...])
}
  1. Animated setters. Some properties of UIViews have an animated setter variant that automatically applies an animation when called. For example, calling UISwitch.setOn(_:animated:) will animate the state of the switch, whereas setting the on property directly will update it immediately. Layout does not expose the setOn(_:animated:) method in XML, however if you have an expression for <UISwitch isOn="onState"/> then you can cause it to be updated with an animation by calling setState(_:animated:):
self.layoutNode?.setState(["onState": true], animated: true)

Using the animated argument of setState() will implicitly call the animated variant of the setter for any property that is affected by the update. Properties that don't support animation will be set as normal.

  1. User-driven animation. Some animation effects are controlled by the user dragging or scrolling. For example, you might have a parallax effect when scrolling that causes several views to move in various directions or speeds in sync with the scroll. To implement this kind of animation in Layout, call setState() inside the scroll or gesture handler, passing any parameters needed for the expressions that position the animated views. You can either implement the animation logic in Swift and pass the results as a state, or compute the animation state using expressions in your Layout XML - whichever works best for your use-case, e.g.
<UIView alpha="max(0, min(1, (position - 50) / 100))"/>
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.layoutNode?.setState(["position": scrollView.contentOffset.y])
}

Safe Area Insets

iOS 11 introduced the concept of the safe area - a generalization of the top and bottom layout guides that were provided before for insetting content to account for status, navigation and tool/tab bars.

In order to prevent you from needing to include conditional compilation logic in your templates, Layout makes the iOS 11 safeAreaInsets property available across all iOS versions (falling back to using layout guides as the underlying implementation on iOS 10 and earlier).

To position a view inside the safe area of its parent, you could write:

<UIView
    top="parent.safeAreaInsets.top"
    left="parent.safeAreaInsets.left"
    bottom="100% - parent.safeAreaInsets.bottom"
    right="100% - parent.safeAreaInsets.right"
/>

Note: The value for safeAreaInsets exposed by Layout differs slightly from the documented behavior for UIView.safeAreaInsets:

Apple states that the safeAreaInsets value accounts for the status bar and other UI such as navigation or toolbars, but only for the root view of a view controller. For subviews, the insets reflect only the portion of the view that is covered by those bars, so for a small view in the middle of the screen, the insets would always be zero since the toolbars or iPhone X notch would never overlap this view.

For Layout, this approach creates problems, as your view frame may depend on the safeAreaInsets value, which would in turn be affected by the frame, creating a cyclic dependency. Rather than try to resolve this recursively, Layout always returns insets relative to the current view controller, so even for subviews that do not overlap the screen edges, the value of safeAreaInsets will be the same as for the root view.

UIScrollView derives its insets automatically on iOS 11, but this behavior differs from iOS 10. To achieve consistent behavior, you can set the contentInsetAdjustmentBehavior property to never, and then set the contentInset manually:

<UIScrollView
    contentInsetAdjustmentBehavior="never"
    contentInset="parent.safeAreaInsets"
    scrollIndicatorInsets.top="parent.safeAreaInsets.top"
    scrollIndicatorInsets.bottom="parent.safeAreaInsets.bottom"
/>

To simplify backwards compatibility, as with the safeAreaInsets property itself, Layout permits you to set contentInsetAdjustmentBehavior on any iOS version, however the value is ignored on iOS versions earlier than 11.

Legacy Layout Mode

You may have seen references in the code or documentation to the LayoutNode.useLegacyLayoutMode. In the original design of Layout, the right and bottom expressions were specified relative to the top-left corner of the view, rather than relative to their respective edges as you might expect.

Version 0.6.22 of Layout introduces a new layout mode where bottom and right expressions are relative to the bottom and right edges, which is more intuitive for users familiar with CSS or AutoLayout, and is also more consistent with the way that the leading and trailing expressions work.

To avoid breaking compatibility with existing Layout projects, you must explicitly opt-in to the new layout mode by setting LayoutNode.useLegacyLayoutMode = false in your application code. This is a global property so it only needs to be set once. A good place to do this is in the application(_:didFinishLaunchingWithOptions:) method of your AppDelegate:

import UIKit
import Layout

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        // enable Layout's new layout mode
        LayoutNode.useLegacyLayoutMode = false
        
        // other setup code
        ...
    }
}

In a future version of Layout, the new layout mode will become the default and the legacy layout mode will eventually be removed, so it's a good idea to begin migrating your templates now!

Expressions

The most important feature of the LayoutNode class is its built-in support for parsing and evaluating expressions. The implementation of this feature is built on top of the Expression framework, but Layout adds a number of extensions in order to support UIKit types and layout-specific logic.

Expressions can be simple, hard-coded values such as "10", or more complex expressions such as "width / 2 + someConstant". The available operators and functions to use in an expression depend on the name and type of the property being expressed, but all expressions support the standard decimal math and boolean operators and functions that you find in most C-family programming languages. You can also extend Layout with custom functions (see the Functions section below).

Expressions in a LayoutNode can reference constants and state passed in to the node or any of its parents. They can also reference the values of any other expression defined on the node, or any supported property of the view:

5 + width / 3
isSelected ? blue : gray
min(width, height)
a >= b ? a : b
pi / 2

Additionally, a node can reference properties of its parent node using parent.someProperty, or of its immediate sibling nodes using previous.someProperty and next.someProperty:

<UIView>
    <UILabel text="Foo"/>
    
    <!-- this label will be 20pts below its previous sibling -->
    <UILabel
        top="previous.bottom + 20"
        text="Bar"
    />
</UIView>

To reference a node that is not an immediate sibling, you can give the node an id attribute, and then reference that node using # followed by the id:

<UIView>
    <UILabel id="first" left="20" text="Foo"/>
    <UILabel right="20" text="Bar"/>
    
    <!-- this label will be aligned with the first label -->
    <UILabel
        left="#first.left"
        top="previous.bottom + 20"
        text="Bar"
    />
</UIView>

Layout Properties

The set of expressible properties available to a LayoutNode depends on the view type, but every node supports the following properties at a minimum:

top
left
bottom
right
leading
trailing
width
height
center.x
center.y

Note: see the Legacy Layout Mode section for an important note about the right and bottom layout properties.

These are numeric values (measured in screen points) that specify the frame for the view. In addition to the standard operators, all of these properties allow values specified in percentages:

<UIView right="50%"/>

Percentage values are relative to the width or height of the parent LayoutNode (or the superview, if the node has no parent). The expression above is typically equivalent to writing the following (unless the parent is a UIScrollView, in which case the contentInset and safe area are also taken into account):

<UIView right="parent.width / 2">

Additionally, the width and height properties can make use of a virtual variable called auto. The auto variable equates to the content width or height of the node, which is determined by a combination of three things:

  • The intrinsicContentSize property of the native view (if specified)
  • Any AutoLayout constraints applied to the view by its (non-Layout-managed) subviews
  • The enclosing bounds for all the children of the node.

If a node has no children and no intrinsic size, auto is usually equivalent to 100%, depending on the view type.

Though entirely written in Swift, the Layout library makes heavy use of the Objective-C runtime to automatically generate property bindings for any type of view. The available properties therefore depend on the type of view that is passed into the LayoutNode constructor (or the name of the XML node, if you are using XML layouts).

Only types that are visible to the Objective-C runtime can be detected automatically. Fortunately, since UIKit is an Objective-C framework, most view properties work just fine. For ones that don't, it is possible to manually expose these using an extension on the view (this is covered below under Advanced Topics).

Because it is possible to pass in arbitrary values via constants and state, Layout supports referencing almost any type of value inside an expression, even if there is no way to express it as a literal.

Expressions are strongly-typed, so passing the wrong type of value to a function or operator or returning the wrong type from an expression will result in an error. Where possible, these type checks are performed when the node is first mounted, so that the error is surfaced immediately.

The following types of property are given special treatment in order to make it easier to specify them using an expression string:

Geometry

Because Layout manages the view frame automatically, direct manipulation of the view's frame or bounds is not permitted - you should use the top, left, bottom, right, leading, trailing, width, height, center.x and center.y expressions instead. However, there are other geometric properties that do not directly affect the frame, and many of these are available to be set via expressions, for example:

  • contentSize
  • contentInset
  • layer.transform

These properties are not simple numbers, but structs containing several packed values. So how can you manipulate these with Layout expressions?

Firstly, almost any property type can be set using a constant or state variable, even if there is no way to define a literal value for it in an expression. So for example, the following code will set the layer.transform even though Layout has no way to specify a literal CATransform3D struct in an expression:

loadLayout(
    named: "MyLayout.xml",
    state: [
        "flipped": true
    ],
    constants: [
        "identityTransform": CATransform3DIdentity,
        "flipTransform": CATransform3DMakeScale(1, 1, -1)
    ]
)
<UIView layer.transform="flipped ? flipTransform : identityTransform"/>

Secondly, for many geometric struct types, such as CGPoint, CGSize, CGRect, CGAffineTransform and UIEdgeInsets, Layout has built-in support for directly referencing the member properties in expressions. To set the top contentInset value for a UIScrollView, you could use:

<UIScrollView contentInset.top="safeAreaInsets.top + 10"/>

And to explicitly set the contentSize, you could use:

<UIScrollView
    contentSize.width="200%"
    contentSize.height="auto + 20"
/>

Note: % and auto are permitted inside contentSize.width and contentSize.height, just as they are for width and height, but percentages refer to the view's own frame size, not its parent. Percentage sizes inside a UIScrollView also account for the contentInset, so 100% should fill the view's content area without scrolling.

Layout also supports virtual keyPath properties for manipulating CATransform3D (as documented here), and makes equivalent properties available for CGAffineTransform too. That means you can perform operations like rotating or scaling a view directly in your Layout XML without needing to do any matrix math:

<UIView transform.rotation="pi / 2"/>

<UIView transform.scale="0.5"/>

<UIView layer.transform.translation.z="500"/>

Strings

It is often necessary to use literal strings inside an expression, and since expressions themselves are typically wrapped in quotes, it would be annoying to have to used nested quotes every time. For this reason, string expressions are treated as literal strings by default, so in this example...

<UILabel text="title"/>

...the text property of the label has been given the literal value "title", and not the value of a constant named "title", as you might expect.

To use an expression inside a string property, escape the value using { ... } braces. So to use a constant or variable named title instead of the literal value "title", you would write this:

<UILabel text="{title}"/>

You can use arbitrary logic inside the braced expression block, including math and boolean comparisons. The value of the expressions need not be a string, as the result will be stringified. You can use multiple expression blocks inside a single string expression, and mix and match expression blocks with literal segments:

<UILabel text="Hello {name}, you have {n + 1} new messages"/>

If you need to use a string literal inside an expression block, then you can use single quotes to escape it:

<UILabel text="Hello {hasName ? name : 'World'}"/>

If you want to display the literal { or } brace characters, you can escape them as follows:

<UILabel text="Open brace: {'{'}. Close brace: {'}'}."/>

Layout has support for basic manipulation of string literals and variables inside expressions. To concatenate strings, you can either use multiple expression clauses within a single string property, or you can use the + operator within a single expression:

<UILabel text="{'foo'}{'bar'}"/>

<UILabel text="{'foo' + 'bar'}"/>

You can reference individual characters or substrings by using Swift-style subscripting syntax. Ordinarily, Swift requires String subscripts to use values of type String.Index, but for convenience, Layout supports integer indexes and ranges as well. These are zero-based and refer to the Character index (as opposed to bytes or unicode scalars):

<!-- Displays 'e' -->
<UILabel text="{'Hello World'[1]}"/>

<!-- Displays 'foo' -->
<UILabel text="{'foobar'[0..<3]}"/>

<!-- Displays 'bar' -->
<UILabel text="{'foobar'[3...]}"/>

Attempting to reference a substring outside the original string bounds won't crash, but will display a Red Box error. There is currently no way to check the bounds of a string from inside an expression unless you implement a custom count() function, or equivalent (see the functions section below for details).

If your app is localized, you will need to use constants instead of literal strings for virtually all of the strings in your template. Localizing all of these strings and passing them as individual constants would be tedious, so Layout offers some alternatives:

Constants prefixed with strings. are assumed to be localized strings, and will be looked up in the application's Localizable.strings file. So for example, if your Localizable.strings file contains the following entry:

"Signup.NameLabel" = "Name";

Then you can reference this directly in your XML as follows, without creating an explicit constant in code:

<UILabel text="{strings.Signup.NameLabel}"/>

It's common practice on iOS to use the English text as the key for localized strings, which may often contain spaces or punctuation, making it invalid as an identifier. In these cases, you can use backticks to escape the key, as follows:

<UILabel text="{`strings.Some text with spaces and punctuation!`}"/>

Localized strings may contain placeholder tokens for runtime values. On iOS, the convention for this is to use the printf % escape sequences for these placeholders, which are then replaced propgrammatically. Layout supports this mechanism by treating parameterized string constants as functions. For example, for the following localized string:

"Messages.Title" = "Hello %s, you have %i new messages";

You could display the formatted string directly inside your template as follows (assuming that name and messageCount are valid state variables):

<UILabel text="{strings.Messages.Title(name, messageCount)}"/>

Layout checks the placeholders in the format string, and will display an error if you pass the wrong number or types of arguments. Layout's format string processing is powered by the Sprinter framework, and has full support for the IEEE printf spec, so you can use flags such as %1.3f or %3$0x in your localized strings to control parameter order and formatting.

In addition to reducing boilerplate, strings referenced directly from your XML will also take advantage of live reloading, so you can make changes to your Localizable.strings file, and they will be picked up when you type Cmd-R in the simulator, with no need to recompile the app.

Attributed Strings

Attributed strings work much the same way as regular string expressions, except that you can use inline attributed string constants to create styled text:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "styledText": NSAttributedString(string: "styled text", attributes: ...)
    ]
)
<UILabel text="This is some {styledText} embedded in unstyled text" />

A neat extra feature built in to attributed string expressions is support for inline (X)HTML markup:

LayoutNode(
    view: UILabel.self,
    expressions: [
        "text": "I <i>can't believe</i> this <b>actually works!</b>"
    ]
)

Using this feature inside an XML attribute is awkward because the tags have to be escaped using &gt; and &lt;, so Layout lets you use HTML inside a view node, and it will be automatically assigned to the appropriate attributed text property of the view (see the Body Text section for details):

<UILabel>This is a pretty <b>bold</b> solution</UILabel>

HTML support relies on the built-in NSMutableAttributedString HTML parser, which does not recognize inline CSS styles or scripts, and only supports a minimal subset of HTML. The following tags have been verified to work, but others may or may not, depending on the iOS version:

<p>, // paragraph
<h1> ... <h6> // heading
<b>, <strong> // bold
<i>, <em> // italic
<u> // underlined
<strike> // strikethrough
<ol>, <li> // ordered list
<ul>, <li> // unordered list
<br/> // linebreak
<sub> // subscript
<sup> // superscript
<center> // centered text

As with regular text attributes, inline HTML can contain embedded expressions, which can themselves contain either attributed or non-attributed string variables or constants:

<UILabel>Hello <b>{name}</b></UILabel>

URLs

URL expressions are treated as a literal string, so dynamic logic (such as references to constants or variables) must be escaped with { ... }:

<!-- literal url -->
<MyView url="index.html"/>

<!-- url constant or variable -->
<MyView url="{someURL}"/>

URLs that do not contain a scheme are assumed to be local file path references. Paths without a leading / are assumed to be relative to the app resources bundle, and ones beginning with ~/ are relative to the user directory.

<!-- remote url -->
<MyView url="http://example.com"/>

<!-- app resource bundle url -->
<MyView url="images/foo.jpg"/>

<!-- user document url -->
<MyView url="~/Documents/report.pdf"/>

Fonts

Like String and URL expressions, font expressions are treated as literal strings, so references to constants or variables must be escaped with { ... }. A font expression can encode several distinct pieces of data, delimited by spaces.

The UIFont class encapsulates the font family, size, weight and style, so a font expression can contain any or all of the following space-delimited attributes, in any order:

<font-name>
<font-traits>
<font-weight>
<font-style>
<font-size>

Any font attribute that isn't specified will be set to the system default - currently San Francisco 17 point as of iOS 11.

The <font-name> is a string. It is case-insensitive and can represent either an exact font name, or a font family name. The font name is permitted to contain spaces, and can optionally be enclosed in single quotes. Use "System" as the font name if you want to use the system font (although this is the default anyway if no name is specified). You can also use "SystemBold" and "SystemItalic". Here are some examples:

<UILabel font="Courier"/>

<UILabel font="helvetica neue"/>

<UILabel font="'times new roman'"/>

<UILabel font="SystemBold"/>

The <font-traits> are values of type UIFontDescriptorSymbolicTraits. The following traits are supported:

italic
condensed
expanded
monospace

A given font expression may include multiple traits. Note that the bold trait is not supported, because bold is treated as a <font-weight> value instead. If, for some reason, you wish to specify the bold trait instead of the bold weight, you can do so by using the fully-qualified trait name inside braces:

<UILabel text="Font with bold trait" font="{UIFontDescriptorSymbolicTraits.traitBold}"/>

<UILabel text="Font with bold weight" font="bold"/>

The <font-weight> is a UIFont.Weight constant, from the following list:

ultraLight
thin
light
regular
medium
semibold
bold
heavy
black

Examples:

<UILabel font="Courier bold"/>

<UILabel font="System black"/>

<UILabel font="helvetica neue ultraLight"/>

Note: Writing "SystemBold" is not the same as "System bold". The former is equivalent to UIFont.boldSystemFont(ofSize: 17) in Swift, and uses the bold trait (see above), the latter is equivalent to UIFont.systemFont(ofSize: 17, weight: .bold) and uses the bold weight which produces a different result.

The <font-style> is a UIFontTextStyle constant, from the following list:

title1
title2
title3
headline
subheadline
body
callout
footnote
caption1
caption2

Specifying one of these values sets the font size to match the user's font size setting for that style, and enables dynamic text sizing, so that changing the font size setting will automatically update the font.

The <font-size> can be either a number or a percentage. If you use a percentage value it will either be relative to the default font size (17 points) or whatever size has already been specified in the font expression. For example, if the expression includes a font-style constant, the size will be relative to that. Here are some more examples:

<UILabel font="Courier 150%"/>

<UILabel font="Helvetica 30 italic"/>

<UILabel font="helvetica body bold 120%"/>

UIFont constants or variables can also be used via inline expressions. To use a UIFont constant called themeFont, but override its size and weight, you could write:

<UILabel font="{themeFont} 25 bold"/>

You can also define custom named fonts using an extension on UIFont, and Layout will detect them automatically:

extension UIFont {
    @objc static let customFont = UIFont.systemFont(ofSize: 42)
}

Fonts defined in this way can be referenced by name from inside any font expression, either with or without the "Font" suffix, but are not available inside braced sub-expressions {...} unless prefixed with UIFont.:

<UILabel font="customFont bold"/>

<UILabel font="custom italic"/>

<UILabel font="{UIFont.customFont} 120%"/>

Colors

Colors can be specified using CSS-style rgb(a) hex literals. These can be 3, 4, 6 or 8 digits long, and are prefixed with a #:

#fff // opaque white
#fff7 // 50% transparent white
#ff0000 // opaque red
#ff00007f // 50% transparent red

Built-in static UIColor constants are supported as well:

white
red
darkGray
etc.

You can also use CSS-style rgb() and rgba() functions. For consistency with CSS conventions, the red, green and blue values are specified in the range 0-255, and alpha in the range 0-1:

rgb(255,0,0) // red
rgba(255,0,0,0.5) // 50% transparent red

You can use these literals and functions as part of a more complex expression, for example:

<UILabel textColor="isSelected ? #00f : #ccc"/>

<UIView backgroundColor="rgba(255, 255, 255, 1 - transparency)"/>

Note that there is no need to enclose these expressions in braces. Unless the expression clashes with a named color asset (see below), Layout will understand what you meant.

The use of color literals is convenient for development purposes, but you are encouraged to define constants (or XCAssets, if you are targeting iOS 11 and above) for any commonly used colors in your app, as these will be easier to refactor later.

To supply custom named color constants, you can pass colors in the constants dictionary when loading a layout:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "headerColor": UIColor(0.6, 0.5, 0.5, 1),
    ]
)

Color constants are available to use in any expression (although they probably aren't much use outside of a color expression).

You can also define a custom colors using an extension on UIColor, and Layout will detect it automatically:

extension UIColor {
    @objc static let headerColor = UIColor(0.6, 0.5, 0.5, 1)
}

Colors defined in this way can be referenced by name from inside any color expression, either with or without the "Color" suffix, but are not available inside other expression types unless prefixed with UIColor.:

<UIView backgroundColor="headerColor"/>

<UIView backgroundColor="header"/>

<UIView isHidden="backgroundColor == UIColor.header"/>

Finally, in iOS 11 and above, you can define named colors as XCAssets and then reference the color by name in your expressions:

<UIView backgroundColor="MyColor"/>

<UIView backgroundColor="my color"/>

<UIView backgroundColor="color-number-{level}"/>

For color assets defined in a framework or standalone bundle, you can prefix the color name with the bundle name (or fully-qualified bundle identifier) followed by a colon. For example:

<UIView backgroundColor="com.example.MyBundle:MyColor"/>

<UIView backgroundColor="MyBundle:MyColor"/>

You can also reference a Bundle/NSBundle instance stored in a constant or variable:

<UIImageView image="{bundle}:MyColor"/>

Note: There is no need to use quotes around the color asset name, even if it contains spaces or other punctuation. Layout will interpret invalid color asset names as expressions. You can use { ... } braces to disambiguate between asset names and constant or variable names if necessary.

Images

Static images can be specified by name or via a constant or state variable. As with colors, there is no need to use quotes around the name, however you can use { ... } braces to disambiguate if needed:

<UIImageView image="default-avatar"/>

<UIImageView image="{imageConstant}"/>

<UIImageView image="image_{index}.png"/>

As with color assets, image assets defined in a framework or standalone bundle can be referenced by prefixing with a bundle name/identifier or constant followed by a colon:

<UIImageView image="com.example.MyBundle:MyImage"/>

<UIImageView image="MyBundle:MyImage"/>

<UIImageView image="{bundle}:MyImage"/>

Enums

To set a value for an enum-type expression, just use the name of the value. For example:

<UIImageView contentMode="scaleAspectFit"/>

You can use logic directly inside enum expressions - there is no need to escape the logic or use quotes around the names:

<UIImageView contentMode="isSmallImage ? center : scaleAspectFit"/>

Standard UIKit enum values are exposed as constants that may be used only in expressions of that type. There is no need to prefix the enum value name with a . as you would in Swift, but you must prefix with the type to use the enum value inside other expression types:

<!-- will work -->
<UIImageView height="contentMode == UIViewContentMode.scaleAspectFit ? 200 : 300"/>

<!-- won't work -->
<UIImageView height="contentMode == scaleAspectFit ? 200 : 300"/>
<UIImageView height="contentMode == .scaleAspectFit ? 200 : 300"/>

OptionSets

OptionSet expressions work the same way as enums. If you want to set multiple values for an OptionSet, separate them with commas:

<UITextView dataDetectorTypes="phoneNumber, link"/>

There is no need to wrap multiple OptionSet values in square brackets, as you would in Swift. As with enums, OptionSet value names cannot be used outside of the expression that sets them unless they are prefixed with the type name.

Arrays

You can use Swift-style square-bracketed array literals inside any type of expression:

<UISegmentedControl items="['Hello', 'World']"/>

You can use the + operator to concatenate array literals:

<UISegmentedControl items="['Hello'] + ['And', 'Goodbye']"/>

For array-type expressions, the square brackets are optional; you can just pass comma, delimited values and they will be treated as an array:

<UISegmentedControl items="'Hello', 'World'"/>

If you return a single non-array value from an array expression, it will be "boxed" inside an array automatically:

<!-- 'Hello' becomes ["Hello"] -->
<UISegmentedControl items="'Hello'"/>

The , operator automatically flattens nested array constants, so the following code will produce a single, flat array instead of an outer array with another array inside it:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "firstTwoItems": ["First", "Second"],
    ]
)
<UISegmentedControl items="firstTwoItems, 'Third'"/>

You can use the same array literal syntax inside macros, if you need to re-use the values:

<UIView>
    <macro name="ITEMS" items="'First', 'Second'"/>
    <UISegmentedControl items="ITEMS"/>
</UIView>

If you need to access individual elements of an array, you can use the [] subscript operators in an expression:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "colors": [UIColor.green, UIColor.black],
    ],
)
<!-- green label -->
<UILabel textColor="colors[0]"/>

You can also subscript arrays using ranges. All of the standard Swift range operators are supported, including open-ended ranges:

<!-- Only the second and third item -->
<UISegmentedControl items="items[1...2]"/>

<!-- Only the first and second item -->
<UISegmentedControl items="items[..<2]"/>

<!-- All but the first item -->
<UISegmentedControl items="items[1...]"/>

Attempting to access an array with an out-of-bounds index or range won't crash, but will display a Red Box error. There is currently no way to check the bounds of an array from inside an expression unless you implement a custom count() function, or equivalent (see the functions section below for details).

Functions

Layout expressions support a number of built-in math functions such as min(), max(), pow(), etc. But you can also extend Layout with additional custom functions that can be called inside your template.

Custom functions are Swift closures that conform to the signature ([Any]) throws -> Any. Any closure constant conforming to this type that is passed into your LayoutNode can be called inside an expression.

Currently there is no way to specify the number or type of arguments expected by a custom function, so you must be careful to implement type checking within your custom function to prevent crashes. Here are some examples:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "count": { (args: [Any]) throws -> Any in
            guard args.count == 1 else {
                throw LayoutError.message("count() function expects a single argument")  
            }
            switch args[0] {
            case let array as [Any]:
                return array.count
            case let string as String:
                return string.count
            default:
                throw LayoutError.message("count() function expects an Array or String")   
            }
            return array.count
        },
        "uppercased": { (args: [Any]) throws -> Any in
            guard let string = args.first as? String else {
                throw LayoutError.message("uppercased() function expects a String argument")  
            }
            return string.uppercased()
        },
    ],
)
<UILabel text="{uppercased('uppercased text'}"/>

<UILabel text="'foo' contains {count('foo')} characters"/>

Optionals

Layout currently has fairly limited support for optionals in expressions. There is no way to specify that an expression's return value is optional, and so returning nil from an expression is usually an error. There are a few exceptions to this:

  1. Returning nil from a String expression will return an empty string
  2. Returning nil from a UIImage expression will return a blank image with zero width/height
  3. Returning nil for a delegate or other protocol property is permitted to override the default binding behavior

The reason for these specific exceptions is that passing a nil image or text to a component is a common approach in UIKit for indicating that a given element is not needed, and by allowing nil values for these types, we avoid the need to pass additional flags into the component to mark these as unused.

There is slightly more flexibility when handing optional values inside an expression. It is possible to refer to nil in an expression, and to compare values against it. For example:

<UIView backgroundColor="col == nil ? #fff : col"/>

In this example, if the col constant is nil, we return a default color of white instead. This can also be written more simply using the ?? null-coalescing operator:

<UIView backgroundColor="col ?? #fff"/>

Comments

Complicated or obscure code often benefits from documentation in the form of inline comments. You can insert comments into your XML layout files as follows:

<!-- `name` is the user's full name -->
<UILabel text="{name}"/>

Unfortunately, while XML supports comment tags between nodes, there is no way to place comments between attributes within a node, so if a node has multiple attributes this approach is not very satisfactory.

To work around this, Layout allows you to use C-style "//" comments inside the expression attributes themselves:

<UILabel
    text="{name // the user's full name}"
    backgroundColor="colors.primary // the primary color"
/>

This feature is also very convenient during development if you want to temporarily comment-out an expression:

<UIView temporarilyDisabledProperty="// someValue"/>

Comments can be used in any expression, but for string-type expressions there are a few caveats: In a string expression, anything outside of {...} braces is considered to be part of the literal string value, and that includes / characters. For that reason, this won't work as intended:

<UIImage image="MyImage.png // comment"/>

Because the image attribute in the above expression is interpreted as a string, the "// comment" is treated as part of the name. The workaround for this is to put the comment inside {...}. Either of the following will work:

<UIImage image="MyImage.png{// comment}"/>

<UIImage image="{'MyImage.png' // comment}"/>

The exception to this is for when the entire expression is commented out. If you wish to temporarily comment-out an expression, placing "//" at the start of the comment will work regardless of the expression type:

<UIImage image="// MyImage.png"/>

In the unlikely event that you need the literal value of a string expression to begin with "//", you can escape the slashes using {...}:

<UILabel text="// this is a comment"/>

<UILabel text="{// this is also a comment}"/>

<UILabel text="{'// this is not a comment'}"/>

<UILabel text="{'//'} this is also not a comment"/>

Standard Components

Layout has good support for most built-in UIKit views and view controllers. It can automatically create almost any UIView subclass using init(frame:), and can set any property that is compatible with Key Value Coding (KVC), but some views expect extra initializer arguments, or have properties that cannot be set by name at runtime, or which require special treatment for other reasons.

The following views and view controllers have all been tested and are known to work correctly:

  • UIButton
  • UICollectionView
  • UICollectionViewCell
  • UICollectionViewController
  • UIControl
  • UIImageView
  • UILabel
  • UINavigationController
  • UIProgressView
  • UIScrollView
  • UISearchBar
  • UISegmentedControl
  • UISlider
  • UIStackView
  • UIStepper
  • UISwitch
  • UITabBarController
  • UITableView
  • UITableViewCell
  • UITableViewController
  • UITableViewHeaderFooterView
  • UITextField
  • UITextView
  • UIView
  • UIViewController
  • UIVisualEffectView
  • UIWebView
  • WKWebView

If a view is not listed here, it will probably work to some extent, but may need to be partially configured using native Swift code. If you encounter such cases, please report them on Github so we can add better support for them in future.

To configure a view programmatically, create an outlet for it in your XML file:

<SomeView outlet="someView"/>

Then you can perform the configuration in your view controller:

@IBOutlet weak var someView: SomeView? {
    didSet {
        someView?.someProperty = foo
    }
}

In some cases, standard UIKit views and controllers have been extended with additional properties or behaviors to help them interface better with Layout. These cases are listed below:

UIControl

UIControl requires some special treatment because of the way that action binding is performed. Every UIControl has an addTarget(_:action:for:) method used for binding methods to specific events. Since Layout is limited to setting properties, there's no direct way to call this method, so actions are exposed to Layout using the following pseudo-properties:

  • touchDown
  • touchDownRepeat
  • touchDragInside
  • touchDragOutside
  • touchDragEnter
  • touchDragExit
  • touchUpInside
  • touchUpOutside
  • touchCancel
  • valueChanged
  • primaryActionTriggered
  • editingDidBegin
  • editingChanged
  • editingDidEnd
  • editingDidEndOnExit
  • allTouchEvents
  • allEditingEvents
  • allEvents

These properties are of type Selector, and can be set to the name of a method on your view controller. For more details, see the Actions section above.

UIButton

UIButton has the ability to change various appearance properties based on its current UIControlState, but the API for specifying these properties is method-based rather than property-based, so cannot be exposed directly to Layout. Instead, Layout provides pseudo-properties for each state:

To set for all states:

  • title
  • attributedTitle
  • titleColor
  • titleShadowColor
  • image
  • backgroundImage

To set for specific states, where [state] can be one of normal, highlighted, disabled, selected or focused:

  • [state]Title
  • [state]AttributedTitle
  • [state]TitleColor
  • [state]TitleShadowColor
  • [state]Image
  • [state]BackgroundImage

UISegmentedControl

UISegmentedControl contains a number of segments, each of which can display either an image or title. This is set up using the init(items:) constructor, which accepts an array of String or UIImage elements.

Layout exposes this using the items expression. You can set this to an array of titles as follows:

<UISegmentedControl items="'First', 'Second', 'Third'"/>

This works for strings, however there is no way to specify image literals inside an array in a Layout expression currently, so to use images for your segement items you will need to create them programmatically in Swift and pass them to the layout as constants or state variables:

<UISegmentedControl items="hello, world"/>
loadLayout(
    named: "MyLayout.xml",
    constants: [
        "hello": UIImage(named: "HelloIcon"),
        "world": UIImage(named: "WorldIcon"),
    ]
)

UISegmentedControl also has methods for inserting, removing or updating the segment titles and images, but this API is not suitable for use with Layout, so instead the items array is exposed as a pseudo property that can be updated at any time. In the example below, changing the segmentItems state in Swift updates the displayed segments:

<UISegmentedControl items="segmentItems"/>
loadLayout(
    named: "MyLayout.xml",
    state: [
        "segmentItems": ["Hello", UIImage(named: "HelloIcon")],
    ]
)

...

layoutNode?.setState(["segmentItems": ["Goodbye", UIImage(named: "GoodbyeIcon")]], animated: true)

Like UIButton, UISegmentedControl has style properties that can vary based on the UIControlState, and these are supported in the same way, using pseudo-properties.

To set for all states:

  • backgroundImage
  • dividerImage
  • titleColor
  • titleFont

To set for specific states, where [state] can be one of normal, highlighted, disabled, selected or focused:

  • [state]BackgroundImage
  • [state]TitleColor
  • [state]TitleFont

Note: Setting dividerImage for different states is not currently supported due to limitations of the naming convention. It is also not currently possible to set different images for different UIBarMetrics values.

You can set the content offset for all segments using:

  • contentPositionAdjustment
  • contentPositionAdjustment.horizontal
  • contentPositionAdjustment.vertical

Or for specific segments, where [segment] can be one of any, left, center, right, alone:

  • [segment]ContentPositionAdjustment
  • [segment]ContentPositionAdjustment.horizontal
  • [segment]ContentPositionAdjustment.vertical

UIStepper

Like UIButton and UISegmentedControl, UIStepper also has state-based pseudo-properties:

To set for all states:

  • backgroundImage
  • dividerImage
  • incrementImage
  • decrementImage

To set for specific states, where [state] can be one of normal, highlighted, disabled, selected or focused:

  • [state]BackgroundImage
  • [state]IncrementImage
  • [state]DecrementImage

UIStackView

You can use Layout's expressions to create arbitrarily complex view arrangements, but sometimes the expressions required to describe relationships between siblings can be quite verbose, and it would be nice to be able to use something more like flexbox to describe the overall arrangement for a collection of views.

Layout supports UIKit's UIStackView class, which you can use for flexbox-like collections in situations where UITableView or UICollectionView would be overkill. Here is an example of a simple vertical stack:

<UIStackView
    alignment="center"
    axis="vertical"
    spacing="10">
    
    <UILabel text="First row"/>
    <UILabel text="Second row"/>
</UIStackView>

Subview nodes nested inside a UIStackView are automatically added to the arrangedSubviews array. The width and height properties are respected for children of a UIStackView, but the top, left, bottom, right, center.x and center.y expressions are ignored.

Since UIStackView is a non-drawing view, only its position and layout attributes can be configured. Inherited UIView properties such as backgroundColor or borderWidth are unavailable.

UITableView

You can use a UITableView inside a Layout template in much the same way as you would use any other view:

<UITableView
    backgroundColor="#fff"
    outlet="tableView"
    style="plain"
/>

The tableView's delegate and dataSource will automatically be bound to the file's owner, which is typically either your UIViewController subclass, or the first nested view controller that conforms to one or both of the UITableViewDelegate/DataSource protocols. If you don't want that behavior, you can explicitly set them (see the Delegates section above).

You would define the view controller logic for a Layout-managed table in pretty much the same way as you would if not using Layout:

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet var tableView: UITableView? {
        didSet {

            // Register your cells after the tableView has been created
            // the `didSet` handler for the tableView property is a good place
            tableView?.register(MyCellClass.self, forCellReuseIdentifier: "cell")
        }
    }

    var rowData: [MyModel]

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return rowData.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell =  tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCellClass
        cell.textLabel.text = rowData.title
        return cell
    }
}

Using a Layout-based UITableViewCell is also possible. There are two ways to define a UITableViewCell in XML - either directly inside your table XML, or in a standalone file. A cell template defined inside the table XML might look something like this:

<UITableView
    backgroundColor="#fff"
    outlet="tableView"
    style="plain">

    <UITableViewCell
        reuseIdentifier="cell"
        textLabel.text="{title}">

        <UIImageView
            top="50% - height / 2"
            right="100% - 20"
            width="auto"
            height="auto"
            image="{image}"
            tintColor="#999"
        />
    </UITableViewCell>

</UITableView>

Then the logic in your table view controller would be:

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var rowData: [MyModel]

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return rowData.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        // Use special Layout extension method to dequeue the node rather than the view itself
        let node = tableView.dequeueReusableCellNode(withIdentifier: "cell", for: indexPath)

        // Set the node state to update the cell
        node.setState(rowData[indexPath.row])

        // Cast the node view to a table cell and return it
        return node.view as! UITableViewCell
    }
}

Alternatively, you can define the cell in its own XML file. If you do that, the dequeuing process is the same, but you will need to register it manually:

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet var tableView: UITableView? {
        didSet {
            // Use special Layout extension method to register the layout xml file for the cell
            tableView?.registerLayout(named: "MyCell.xml", forCellReuseIdentifier: "cell")
        }
    }

    ...
}

Layout supports dynamic table cell height calculation. To enable this, just set a height expression for your cell. Dynamic table cell sizing also requires that the table view's rowHeight is set to UITableViewAutomaticDimension and a nonzero value is provided for estimatedRowHeight, but Layout sets these for you by default.

Note: If your cells all have the same height, it is significantly more efficient to set an explicit rowHeight property on the UITableView instead of setting the height for each cell.

Layout also supports using XML layouts for UITableViewHeaderFooterView, and there are equivalent methods for registering and dequeuing UITableViewHeaderFooterView layout nodes.

Note: To use a custom section header or footer you will need to set the estimatedSectionHeaderHeight or estimatedSectionFooterHeight to a nonzero value in your XML:

<UITableView estimatedSectionHeaderHeight="20">

    <UITableViewHeaderFooterView
        backgroundView.backgroundColor="#fff"
        height="auto + 10"
        reuseIdentifier="templateHeader"
        textLabel.text="Section Header"
    />
    
    ...

</UITableView>

If you prefer, you can create a <UITableViewController/> in your XML instead of subclassing UIViewController and implementing the table data source and delegate. Note that if you do this, there is no need to explicitly create the UITableView yourself, as the UITableViewController already includes one. To configure the table, you can set properties of the table view directly on the controller using a tableView. prefix, e.g.

<UITableViewController
    backgroundColor="#fff"
    tableView.separatorStyle="none"
    tableView.contentInset.top="20"
    style="plain">

    <UITableViewCell
        reuseIdentifier="cell"
        textLabel.text="{title}"
    />
</UITableViewController>

UICollectionView

Layout supports UICollectionView in a similar way to UITableView. If you do not specify a custom UICollectionViewLayout, Layout assumes that you want to use a UICollectionViewFlowLayout, and creates one for you automatically. When using a UICollectionViewFlowLayout, you can configure its properties using expressions on the collection view, prefixed with collectionViewLayout.:

<UICollectionView
    backgroundColor="#fff"
    collectionViewLayout.itemSize.height="100"
    collectionViewLayout.itemSize.width="100"
    collectionViewLayout.minimumInteritemSpacing="10"
    collectionViewLayout.scrollDirection="horizontal"
/>

As with UITableView the collection view's delegate and dataSource will automatically be bound to the file's owner. Using a Layout-based UICollectionViewCell, either directly inside your collection view XML or in a standalone file, also works the same. A cell template defined inside the collection view XML might look something like this:

<UICollectionView
    backgroundColor="#fff"
    collectionViewLayout.itemSize.height="100"
    collectionViewLayout.itemSize.width="100">

    <UICollectionViewCell
        clipsToBounds="true"
        reuseIdentifier="cell">

        <UIImageView
            contentMode="scaleAspectFit"
            height="100%"
            width="100%"
            image="{image}"
            tintColor="#999"
        />
    </UICollectionViewCell>

</UICollectionView>

Then the logic in your collection view controller would be:

class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    var itemData: [MyModel]

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return itemData.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        // Use special Layout extension method to dequeue the node rather than the view itself
        let node = collectionView.dequeueReusableCellNode(withIdentifier: "cell", for: indexPath)

        // Set the node state to update the cell
        node.setState(itemData[indexPath.row])

        // Cast the node view to a table cell and return it
        return node.view as! UICollectionViewCell
    }
}

Alternatively, you can define the cell in its own XML file. If you do that, the dequeuing process is the same, but you will need to register it manually:

class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    var itemData: [MyModel]

    @IBOutlet var collectionView: UICollectionView? {
        didSet {
            // Use special Layout extension method to register the layout xml file for the cell
            collectionView?.registerLayout(named: "MyCell.xml", forCellReuseIdentifier: "cell")
        }
    }

    ...
}

Dynamic collection cell size calculation is also supported. To enable this, just set a width and height expression for your cell. If your cells all have the same size, it is more efficient to set an explicit collectionViewLayout.itemSize on the UICollectionView instead.

Layout does not currently support using XML to define supplementary UICollectionReusableView instances, but this will be added in future.

Layout supports the use of UICollectionViewController, with the same caveats as for UITableViewController.

UIVisualEffectView

UIVisualEffectView has an effect property of type UIVisualEffect. UIVisualEffect is an abstract base class that is not used directly - instead you would typically set the effect to be either a UIBlurEffect or a UIVibrancyEffect (which itself contains a UIBlurEffect).

The effect property can be set programmatically, or by passing a UIVisualEffect instance into your LayoutNode as a constant or state variable:

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "blurEffect": UIBlurEffect(style: .regular),
    ]
)
<UICollectionView
    effect="blurEffect"
>

For convenience, Layout also allows you to configure the effect directly using expressions. To configure the effect use the UIBlurEffect(style) or UIVibrancyEffect(style) constructor functions inside the effect expression as follows:

<UICollectionView
    effect="UIVibrancyEffect(light)"
>

The style argument is of type UIBlurEffectStyle, and is supported for both UIBlurEffect and UIVibrancyEffect. You can set the style using a constant or state variable, or it can be set to one of the following built-in values:

  • extraLight
  • light
  • dark
  • extraDark
  • regular
  • prominent

Note: You can also use this solution for setting the UITableView.separatorEffect property, or any other property of type UIVisualEffect that is exposed in a custom view or controller.

UIWebView

The API for UIWebView uses methods for loading content, which isn't directly usable from XML, so Layout exposes these methods as properties instead. To load a URL, you can use the request property, as follows:

<UIWebView request="http://apple.com"/>

<UIWebView request="{urlRequestConstant}"/>

You can use a literal URL string or a constant or state variable containing a URL or URLRequest value. The request parameter can also be used for local file content. Paths without a scheme or leading / are assumed to be relative to the app resources bundle, and ones beginning with ~/ are relative to the user directory:

<!-- bundled resource -->
<UIWebView request="pages/index.html"/>

<!-- user document -->
<UIWebView request="~/Documents/homepage.html"/>

To load a literal HTML string you can use the htmlString property:

<UIWebView htmlString="&lt;p&gt;Hello World&lt;/p&gt;"/>

<UIWebView htmlString="{htmlConstant}"/>

Note: If you specify a literal htmlString attribute in your Layout XML then you will have to encode the tags using &lt;, &gt; and &quot;. A better alternative is to use Layout's inline HTML feature (as described in the Attributed Strings section):

<UIWebView>
    <p>Hello World</p>
</UIWebView>

Unlike labels, webviews can display arbitrary HTML including CSS styles and JavaScript. Defining CSS or JavaScript blocks inline in your XML is likely to be awkward however due to the need to escape <, & and { characters. It is probably easier to put complex scripts or stylesheets in a separate local file (although currently Layout does not support live reloading for such files).

The UIWebView.loadHTMLString() method also accepts a baseURL parameter for relative URLs inside the HTML. Layout exposes this as a separate baseURL property:

<UIWebView
    baseURL="http://example.com"
    htmlString="&lt;img href=&quot;/someImage.jpg&quot;&gt;"
/>

If you need to adjust the content insets for the web view, you can do this via the scrollView property:

<UIWebView
    scrollView.contentInsetAdjustmentBehavior="never"
    scrollView.contentInset.bottom="safeAreaInsets.bottom"
    scrollView.scrollIndicatorInsets="scrollView.contentInset"
    request="..."
/>

WKWebView

Layout supports WKWebView in the same way as UIWebView, by converting the various load methods into properties. In addition to the aforementioned scrollView, request, htmlString and baseURL properties, for WKWebView Layout also adds fileURL and readAccessURL properties, which are used for secure access to local web content:

<WKWebView
    readAccessURL="~/Documents"
    fileURL="~/Documents/homepage.html"
/>

Layout also exposes the configuration property of WKWebView. This is a read-only property, but you can set it with a constant value when constructing your view, or configure the properties individually using expressions:

<WKWebView
    configuration="baseConfiguration"
    request="..."
/>

<WKWebView
    configuration.allowsAirPlayForMediaPlayback="true"
    configuration.allowsInlineMediaPlayback="false"
    request="..."
/>

UITabBarController

For the most part, Layout works best when implemented on a per-screen basis, with one LayoutLoading view controller for each screen. There is basic support for defining collection view controllers such as UITabBarController however, as demonstrated in the SampleApp.

To define a UITabBarController-based layout in XML, nest one or more UIViewController nodes inside a UITabBarController node, as follows:

<UITabBarController>
    <UIViewController ... />
    <UIViewController ... />
    ... etc
</UITabBarController>

Every UIViewController has a tabBarItem property that can be used to configure the tab appearance when that view controller is nested inside a UITabBarController, and Layout exposes this object and its properties for configuration via expressions:

<UITabBarController>
    <UIViewController
        tabBarItem.title="Foo"
        tabBarItem.image="Bar.png"
    />
    ...
</UITabBarController>

The tabBarItem has the following sub-properties that may be set:

  • title
  • image
  • selectedImage
  • systemItem
  • badgeValue
  • badgeColor (iOS 10+ only)
  • titlePositionAdjustment

The systemItem property overrides the title and image. It can be set to any of the following constant values:

  • more
  • favorites
  • featured
  • topRated
  • recents
  • contacts
  • history
  • bookmarks
  • search
  • downloads
  • mostRecent
  • mostViewed

It is not possible to replace the UITabBar of a UITabBarController without subclassing it and overriding the tabBar property, however, you can customize the tab bar in Layout by adding a <UITabBar/> node to your <UITabBarController/>:

<UITabBarController>
    <UITabBar
        backgroundImage="Background.png"
        barStyle="default"
        isTranslucent="false"
    />
    ...
</UITabBarController>

The following property and pseudo-property expressions are available for <UITabBar/>:

  • barStyle
  • barPosition
  • barTintColor
  • isTranslucent
  • tintColor
  • unselectedItemTintColor (iOS 10+ only)
  • backgroundImage
  • selectionIndicatorImage
  • shadowImage
  • itemPositioning
  • itemWidth
  • itemSpacing

UINavigationController

UINavigationController is not a particularly good fit for the Layout paradigm because it represents a mutable stack of view controllers, and Layout's XML files can only describe a static hierarchy.

You can use Layout to specify the initial stack of view controllers in a navigation controller, however, which can then be updated programmatically:

<UINavigationController>
    <UIViewController
        title="Root View"
    />
    <UIViewController
        title="Middle View"
    />
    <UIViewController
        title="Topmost View"
    />
</UINavigationController>

As with the tab bar, the navigation bar is not configured directly, but indirectly via the navigationItem property of each UIViewController. Layout exposes this object and its properties as follows:

<UINavigationController>
    <UIViewController
        navigationItem.title="Form"
        navigationItem.leftBarButtonItem.title="Submit"
        navigationItem.leftBarButtonItem.action="submit:"
    />
    ...
</UINavigationController>

The navigationItem has the following sub-properties that may be set:

  • title
  • prompt
  • titleView
  • hidesBackButton
  • leftBarButtonItem
  • leftBarButtonItems
  • rightBarButtonItem
  • rightBarButtonItems
  • leftItemsSupplementBackButton

Many of these properties can only be usefully configured via constants or state variables, since there is no way to create literal values for them in an expression, however the leftBarButtonItem and rightBarButtonItem can also be manipulated directly using the following sub-properties:

  • title
  • image
  • systemItem
  • style
  • action
  • width
  • tintColor

The action property is a selector that should match a method on the owning view controller. As with UIControl, there is no way to set the target explicitly at present.

The style property is an enum that accepts either plain (the default), or done as its value. The systemItem property overrides the title and image, and can be set to any of the following constant values:

  • done
  • cancel
  • edit
  • save
  • add
  • flexibleSpace
  • fixedSpace
  • compose
  • reply
  • action
  • organize
  • bookmarks
  • search
  • refresh
  • stop
  • camera
  • trash
  • play
  • pause
  • rewind
  • fastForward
  • undo
  • redo
  • pageCurl

It is also possible to customize the navigation bar and toolbar of a UINavigationController at construction time by supplying custom subclasses. This feature is exposed in Layout using constructor expressions:

<UINavigationController
    navigationBarClass="MyNavigationBar"
    toolbarClass="MyToolbar">
    ...
</UINavigationController>

Alternatively, to customize properties of the navigation bar or toolbar, you can include a <UINavigationBar/> or <UIToolbar/> node directly inside the UINavigationController, as follows:

<UINavigationController>
    <UINavigationBar
        backgroundImage="Background.png"
        barStyle="default"
        isTranslucent="false"
    />
    ...
</UINavigationController>

The following property and pseudo-property expressions are available for <UINavigationBar/> and <UIToolbar/>:

  • barStyle
  • barPosition
  • barTintColor
  • isTranslucent
  • tintColor
  • backgroundImage
  • shadowImage

And the following for <UINavigationBar/> only:

  • titleColor
  • titleFont
  • titleVerticalPositionAdjustment
  • backIndicatorImage
  • backIndicatorTransitionMaskImage

Custom Components

As covered in the Standard Components section above, Layout can create and configure most built-in UIKit views and view controllers automatically without needing any special support, but some require special treatment to conform to the Layout paradigm.

The same applies to custom UI components that you create yourself. If you follow standard conventions for your view interfaces, then for the most part these should just work, however you may need to take some extra steps for full compatibility:

Namespacing

As you are probably aware, Swift classes are scoped to a particular module. If you have an app called "MyApp" and it declares a custom UIView subclass called FooView, then the fully-qualified class name of the view would be MyApp.FooView, not just FooView, as it would have been in Objective-C.

Layout deals with the common case for you by using the main module's namespace automatically if you don't include it yourself. Either of these will work for referencing a custom view in your XML:

<MyApp.FooView/>

<FooView/>

In the interests of avoiding boilerplate, you should generally use the latter form. However, if you package custom components into a separate module then you will need to refer to them using their fully-qualified name in your XML.

Custom Property Types

As mentioned above, Layout uses the Objective-C runtime to automatically detect property names and types for use with expressions. If you are using Swift 4.0 or above, you will need to explicitly annotate your properties with @objc for them to work in Layout, as the default behavior is now for properties to not be exposed to the Objective-C runtime.

Even if you mark your properties with @objc, the Objective-C runtime only supports a subset of possible Swift types, and even for Objective-C types, some runtime information is lost. For example, it's currently impossible to automatically detect the valid set of raw values and case names for enum types at runtime.

There are also some situations where otherwise compatible property types may be implemented in a way that doesn't show up as an Objective-C property at runtime, or the property setter may not be compatible with KVC (Key-Value Coding), resulting in a crash when it is accessed using setValue(forKey:).

To solve this, it is possible to manually expose additional properties and custom setters/getters for views by using an extension. The Layout framework already uses this feature to expose constants for many standard UIKit properties, but if you are using a 3rd party component, or creating your own, you may need to write an extension to properly support configuration via Layout expressions.

To generate a Layout-compatible property type definition and setter/getter for a custom view, create an extension as follows:

extension MyView {

    open override class var expressionTypes: [String: RuntimeType] {
        var types = super.expressionTypes
        types["myProperty"] = RuntimeType(...)
        return types
    }

    open override func setValue(_ value: Any, forExpression name: String) throws {
        switch name {
        case "myProperty":
            self.myProperty = values as! ...
        default:
            try super.setValue(value, forExpression: name)
        }
    }
    
    open override func value(_ value: Any, forSymbol name: String) throws -> Any {
        switch name {
        case "myProperty":
            return self.myProperty
        default:
            return try super.value(value, forSymbol: name)
        }
    }
}

These overrides add "myProperty" to the list of known expressions for that view, and provide static setter and getter methods for the property.

Note: The setter uses setValue(_:forExpression:) but the getter uses value(_:forSymbol:). That's because not every symbol that can be read inside an expression can be set using an expression - for example, you might have read-only properties such as safeAreaInsets that are read-only, and therefore do not require a setter. Read-only properties should not be included in the expressionTypes dictionary.

The RuntimeType class shown in the example is a type wrapper used by Layout to work around the limitations of the Swift type system. It can encapsulate information such as the list of possible values for a given enum, which it is not possible to determine automatically at runtime.

RuntimeType can be used to wrap any Swift type, for example:

RuntimeType(MyStructType.self)

The preferred way to define custom runtime types is as static vars on the RuntimeType class, added via an extension:

extension RuntimeType {
    
    @objc static let myStructType = RuntimeType(MyStructType.self)
}

extension MyView {

    open override class var expressionTypes: [String: RuntimeType] {
        var types = super.expressionTypes
        types["myProperty"] = .myStructType
        return types
    }
    
    ...
}

Exposing your runtime type in this way makes it available for use in parameters, and for enum types it makes the cases available for use in any expression via the type's namespace. Note the name of the myStructType property matches the type name, but with a lowercase prefix - this is required, as is the @objc attribute.

Layout's RuntimeType wrapper can also be used to specify a set of enum values:

extension RuntimeType {

    @objc static let nsTextAlignment = RuntimeType([
        "left": .left,
        "right": .right,
        "center": .center,
        "justified": .justified,
        "natural": .natural,
    ] as [String: NSTextAlignment])
}

Swift enum values cannot be set automatically using the Objective-C runtime, but if the underlying type of the property matches the rawValue (as is the case for most Objective-C APIs) then it's typically not necessary to also provide a custom setValue(forExpression:) implementation. You'll have to determine this by testing it on a case-by-case basis.

OptionSets can be specified in the same way as enums:

extension RuntimeType {

    @objc static let uiDataDetectorTypes = RuntimeType([
        "phoneNumber": .phoneNumber,
        "link": .link,
        "address": .address,
        "calendarEvent": .calendarEvent,
        "all": .all,
    ] as [String: UIDataDetectorTypes])
}

Again, for Objective-C APIs it is typically not necessary to provide a custom setValue(forExpression:) implementation for an OptionSet value, but if the type of the property is defined in Swift as the OptionSet type itself rather than the rawValue type, then you may need to do so.

Custom Constructor Arguments

By default, Layout automatically instantiates views using the init(frame:) designated initializer, with a size of zero. But sometimes views have an alternative constructor that accepts one or more arguments that can't be changed later. In these cases it is necessary to manually expose this constructor to Layout.

To expose a custom view constructor, create an extension as follows:

extension MyView {

    open override class var parameterTypes: [String: RuntimeType] {
        return [
            "myArgument": RuntimeType(SomeType.self)
        ]
    }
    
    open override class func create(with node: LayoutNode) throws -> MyView {
        if let myArgument = try node.value(forExpression: "myArgument") as? SomeType {
            self.init(myArgument: myArgument)
            return
        }
        self.init(frame: .zero)
    }
}

Note: We are overriding the parameterTypes variable here instead of the expressionTypes variable we used earlier for implementing custom properties. The difference is that parameterTypes are for expressions that are only used for constructing the view, and can't be changed later. Parameter expressions will not be re-evaluated when state is updated.

The create(with:) method calls value(forExpression:) to get the value for the expression. This will return nil if the expression has not been set, so there is no need to check that separately.

In the example above we fall back to the default constructor if the argument isn't set, but if we want to make the argument mandatory, we could throw an error instead:

open override class func create(with node: LayoutNode) throws -> MyView {
    guard let myArgument = try node.value(forExpression: "myArgument") as? SomeType else {
        throw LayoutError("myArgument is required")
    }
    self.init(myArgument: myArgument)
}

Body Text

Layout supports the use of inline (X)HTML within an XML file as a convenient way to specify attributed string values (see the Attributed Strings section for details). In order to enable this feature for a custom view, you will need to tell Layout which property the HTML should be used to set.

This is done using the bodyExpression class property:

extension MyView {

    open override class var bodyExpression: String? {
        return "heading"
    }
}

The value of this property must be the name of an existing property defined in the expressionTypes or parameterTypes dictionaries. The type of the property must be either String or NSAttributedString.

For convenience, Layout will detect if the view has a property called "text", "attributedText", "title" or "attributedTitle", and automatically map the body text to that. If your view has a text property matching one of those names, there is no need to override bodyExpression.

Returning nil from the bodyExpression property will disable the inline HTML feature for that view.

Default Expressions

Layout tries to determine sensible defaults for the width and height expressions if unspecified. To do this, it looks at a variety of properties, such as the intrinsicContentSize and whether the view uses AutoLayout constraints. This mechanism doesn't work 100% of the time, however.

For custom components, you can provide explicit default expressions to be used instead. These are not limited to "width" and "height" expressions - you can provide defaults for any expression type.

To set the default expressions for your view, create an extension as follows:

extension MyView {

    open override class var defaultExpressions: [String: String] {
        return [
            "width": "100%",
            "height": "auto",
            "backgroundColor": "white",
        ]
    }
}

Note: The defaults for "width" and "height" should almost always be set to either "100%" or "auto". For views that have a fixed size, you might be tempted to set a specific numerical default width or height, but it's generally better to do that by overriding the intrinsicContentSize property instead, so that the view also works when used with regular AutoLayout instead of Layout:

extension MyView {

    open override class var intrinsicContentSize: CGSize {
        return CGSize(
            width: UIViewNoIntrinsicMetric,
            height: 40
        )
    }
}

Advanced Topics

Layout-based Components

If you are creating a library of views that use Layout internally, it's probably overkill wrap each one in its own UIViewController subclass.

If the consumers of your component library are using Layout, then you could expose all your components as xml files and allow them to be composed directly using Layout templates or code, but if you want the library to work well with an ordinary UIKit app then it's better if each component is exposed as a regular UIView subclass.

To implement this, subclass UIView (or UIControl, UIButton, etc) and add the LayoutLoading protocol. You can then use the loadLayout(...) methods just as you would with a view controller:

class MyView: UIView, LayoutLoading {

    override init(frame: CGRect) {
        super.init(frame: frame)

        loadLayout(
            named: "MyView.xml",
            state: ...,
            constants: ...,
        )
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()

        // Ensure layout is updated after screen rotation, etc
        self.layoutNode?.view.frame = self.bounds
    }
}

Note: In the above example, the root view defined in the xml will be loaded as a subview of MyView, and will be automatically set to the same size. It would therefore not make sense for the root view in the xml to also be an instance of MyView, unless you want your view structure to be:

<MyView>
    <MyView>
        ...
    </MyView>
</MyView>

Attempting to load a view inside itself like this will throw a runtime error, because otherwise there's a danger of creating an infinite loading loop.

If the layout has a dynamic size, you may wish to update the container view's frame whenever the layout frame changes. To implement that, add the following code:

class MyView: UIView, LayoutLoading {

    ...
    
    override func layoutSubviews() {
        super.layoutSubviews()

        // Ensure layout is updated after screen rotation, etc
        self.layoutNode?.view.frame = self.bounds
        
        // Update frame to match layout
        self.frame.size = self.intrinsicContentSize
    }
    
    public override var intrinsicContentSize: CGSize {
        return layoutNode?.frame.size ?? .zero
    }
}

The default implementation of LayoutLoading will bubble errors up the responder chain to the first view or view controller that handles them. If no responder in the chain intercepts the error, it will be displayed in the Red Box console.

Manual Integration

If you would prefer not to use the LayoutLoading protocol, you can mount a LayoutNode into a view or view controller manually by using the mount(in:) method:

class MyViewController: UIViewController {
    var layoutNode: LayoutNode?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create a layout node from and XML file or data object
        self.layoutNode = try? LayoutNode.with(xmlData: ...)

        // Mount it
        try? self.layoutNode?.mount(in: self)
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        // Ensure layout is updated after screen rotation, etc
        self.layoutNode?.view.frame = self.bounds
    }
}

If you are using some fancy architecture like Viper that splits up view controllers into sub-components, you may find that you need to bind a LayoutNode to something other than a UIView or UIViewController subclass. In that case you can use the bind(to:) method, which will connect the node's outlets, actions and delegates to the specified owner object, but won't attempt to mount the view or view controllers.

The mount(in:) and bind(to:) methods may each throw an error if there is a problem with your XML markup, or in an expression's syntax or symbols.

These errors are not expected to occur in a correctly implemented layout - they typically only happen if you have made a mistake in your code - so for release builds it should be OK to suppress them with try! or try? (assuming you've tested your app properly before releasing it).

If you are loading XML templates from an external source, you might prefer to catch and log these errors instead of allowing them to crash or fail silently, as there is a greater likelihood of an error making it into production if templates and native code are updated independently.

Composition

For large or complex layouts, you may wish to split your layout into multiple files. This can be done easily when creating a LayoutNode programmatically, by assigning subtrees of LayoutNodes to temporary variables, but what about layouts defined in XML?

Fortunately, Layout has a nice solution for this: any layout node in your XML file can contain an xml attribute that references an external XML file. This reference can point to a local file, or even a remote URL:

<UIView xml="MyView.xml"/>

The referenced XML is just an ordinary layout file, and can be loaded and used normally, but when loaded using the composition feature it replaces the node that loads it. The attributes of the original node will be merged with the external node once it has loaded.

Loading is synchronous for local xml files, but for remote URLs loading is performed asynchronously, so the original node will be displayed first and will be updated once the XML for the external node has loaded.

Any children of the original node will be replaced by the contents of the loaded node, so you can insert a placeholder view to be displayed while the real content is loading:

<UIView backgroundColor="#fff" xml="MyView.xml">
    <UILabel text="Loading..."/>
</UIView>

The root node of the referenced XML file must be the same class as (or a subclass of) the node that loads it. You can replace a <UIView/> node with a <UIImageView/> for example, or a <UIViewController/> with a <UITableViewController/>, but you cannot replace a <UILabel/> with a <UIButton/>, or a <UIView/> with a <UIViewController/>.

Templates

Templates are sort of the opposite of composition, and work more like class inheritance in OOP. As with the composition feature, a template is a standalone XML file that you import into your node. But when a layout node imports a template, the node's children are appended to those of the inherited layout, instead of the template node replacing them. This is useful if you have a bunch of nodes with common attributes or elements:

<UIView template="MyTemplate.xml">
    <UILabel>Some unique content</UILabel>
</UIView>

As with composition, the template itself is just an ordinary layout file, and can be loaded and used normally:

<!-- MyTemplate.xml -->
<UIView backgroundColor="#fff">
    <UILabel>Shared Heading</UILabel>

    <!-- children of the importing node will be inserted here -->
</UIView>

The imported template's root node class must be either the same class or a superclass of the importing node (unlike with composition, where it must be the same class or a subclass).

If your template has a complex internal structure, you may wish to specify where children will be inserted, instead of just having them appended to the existing top-level sub-nodes. To do that, you can use the <children/> tag.

The <children/> tag can be placed anywhere inside the template (including inside sub-nodes of the template node) and it will be replaced by the children of the importing node:

<!-- MyTemplate.xml -->
<UIView backgroundColor="#fff">
    <UILabel>Shared Heading</UILabel>
    <UIView>
        <children/> <!-- children of the importing node will be inserted here -->
    </UIView>
    <UILabel>Shared Footer</UILabel>
</UIView>

Parameters

When using templates, you can configure the root node of the template by setting expressions on the importing node, but this offers rather limited control over customization. Ideally, you want to be able to configure properties of nodes inside the template as well, and that's where parameters come in.

You define parameters by adding <param/> nodes inside an ordinary Layout node:

<!-- MyTemplate.xml -->
<UIView>
    <param name="text" type="String"/>
    <param name="image" type="UIImage"/>

    <UIImageView image="{image}"/>
    <UILabel text="{text}"/>
</UIView>

Each parameter has a name and type attribute. The parameter defines a symbol that can be referenced by any expression defined on the containing node or any of its children.

Parameters can be set using expressions on the importing node:

<UIView
    template="MyTemplate.xml"
    text="Lorem ipsum sit dolor "
    image="Rocket.png"
/>

You can set default values for parameters by defining a matching expression on the containing node. It will be overridden if the same expression is defined on the importing node:

<!-- MyTemplate.xml -->
<UIView title="Default text">
    <param name="title" type="String"/>
    ...
</UIView>

Macros

Sometimes you will find yourself repeating the same expression multiple times in a given layout. For example, all the views may have the same width or height, or the same spacing relative to their siblings. For example:

<UIView>
    <UILabel left="20" right="100% - 20" top="20" text="Foo"/>
    <UILabel left="20" right="100% - 20" top="previous.bottom + 20" text="Bar"/>
    <UILabel left="20" right="100% - 20" top="previous.bottom + 20" text="Baz"/>
</UIView>

Although you can pass numeric values into your layout as constants, this doesn't work for expressions like "100%" or "previous.bottom", where the symbols being referenced are relative to the position of the node in the hierarchy, so the actual value will vary in each instance.

Layout has a solution for this, in the form of macros. A macro is a reusable expression that you define inside your Layout template. Macros can be referenced by expressions on the node containing them, or any child of that node, but unlike parameters they cannot be set or overridden externally, and their value is determined at the point of use, rather than relative to the node where they are defined.

Using macros, we can change the example above to:

<UIView>
   <macro name="SPACING" value="20"/>
   <macro name="LEFT" value="SPACING"/>
   <macro name="RIGHT" value="100% - SPACING"/>
   <macro name="TOP" value="previous.bottom + SPACING"/>
   
   <UILabel left="LEFT" right="RIGHT" top="TOP" text="Foo"/>
   <UILabel left="LEFT" right="RIGHT" top="TOP" text="Bar"/>
   <UILabel left="LEFT" right="RIGHT" top="TOP" text="Baz"/>
</UIView>

This eliminates the repetition, making the layout more DRY, and easier to refactor.

Note the use of UPPERCASE names for the macros - this isn't required, but it's a good way to visually distinguish between macros and ordinary constants, parameters or state variables. It also avoids namespace collisions with existing view properties.

Ignore File

Every time you load a layout XML file when running in the iOS Simulator, Layout scans your project directory to locate the file. This is usually pretty fast, but if your project has a lot of subfolders then it can take a noticeable time to locate an XML file the first time.

To speed up this scan, you can add a .layout-ignore file to your project directory that tells Layout to ignore certain subdirectories. The format of the .layout-ignore file is a simple list of file paths (one per line) that should be ignored. You can use # to denote a comment, e.g. for grouping purposes:

# Ignore these
Tests
Pods

File paths are relative to the folder in which the .layout-ignore file is placed. Wildcards like *.xml are not supported, and the use of relative paths like ../ is not recommended.

Searching begins from the directory containing your .xcodeproj, but you can place the .layout-ignore file in any subdirectory of your project, and you can include multiple ignore files in different directories.

Layout already ignores invisible files/folders, along with the following directories, so there is no need to include these:

build
*.build
*.app
*.framework
*.xcodeproj
*.xcassets

The paths listed in .layout-ignore will also be ignored by LayoutTool.

Example Projects

There are several example projects included with the Layout library:

SampleApp

The SampleApp project demonstrates a range of Layout features. It is split into four tabs, and the entire project, including the UITabBarController, is specified using Layout XML files. The tabs are as follows:

  • Boxes - demonstrates use of state to manage an animated layout
  • Pages - demonstrates using a UIScrollView to create paged content
  • Text - demonstrates Layout's text features, include the use of HTML and attributed string constants
  • Table - demonstrates Layout's support for UITableView and UITableViewCell

UIDesigner

The UIDesigner project is an experimental WYSIWYG tool for constructing layouts. It's written as an iPad app which you can run in the simulator or on a device.

UIDesigner is currently in a very early stage of development. It supports most of the features exposed by the Layout XML format, but lacks import/export, and the ability to specify constants, parameters or outlet bindings.

Sandbox

The Sandbox app is a simple playground for experimenting with XML layouts. It runs on iPhone or iPad.

Like UIDesigner, the Sandbox app currently lacks any load/save or import/export capability, but you can copy and paste XML to and from the edit screen.

LayoutTool

The Layout project includes the source code for a command-line app called LayoutTool, which provides some useful functions to help with development using Layout. You do not need to install LayoutTool to use Layout, but you may find it helpful.

Installation

The latest built binary of LayoutTool is included in the project inside the LayoutTool directory, and you can just drag-and-drop it to install.

To ensure compatibility, always update LayoutTool at the same time as updating the Layout framework, because using an old version of LayoutTool to process XML files containing newer features may result in data loss or corruption.

Note: The LayoutTool binary is only updated when there are changes that affect its behavior, so don't worry if the version doesn't match exactly.

To automatically install LayoutTool into your project using CocoaPods, add the following to your Podfile:

pod 'Layout/CLI'

This will install the LayoutTool binary inside the Pods/Layout/LayoutTool directory inside your project folder. You can then reference this using other scripts in your project.

Formatting

The main function provided by LayoutTool is automatic formatting of Layout XML files. The LayoutTool format command will find any Layout XML files at the specified path(s) and apply standard formatting. You can use the tool as follows:

> LayoutTool format /path/to/xml/file(s) [/another/path]

For more information, use LayoutTool help.

To automatically apply LayoutTool format to your project every time it is built, you can add a Run Script build phase that applies the tool. Assuming you've installed the LayoutTool CLI using CocoaPods, that script will look something like:

"${PODS_ROOT}/Layout/LayoutTool/LayoutTool" format "${SRCROOT}/path/to/your/layout/xml/"

The formatting applied by LayoutTool is specifically designed for Layout files. It is better to use LayoutTool for formatting these files rather than a generic XML-formatting tool.

Conversely, LayoutTool is only appropriate for formatting Layout XML files. It is not a general-purpose XML formatting tool, and may not behave as expected when applied to arbitrary XML.

LayoutTool ignores XML files that do not appear to belong to Layout, but if your project contains non-Layout XML files then it is a good idea to exclude these paths from the LayoutTool format command, to improve formatting performance and avoid accidental false positives.

To safely determine which files the formatting will be applied to, without overwriting anything, you can use LayoutTool list to display all the Layout XML files that LayoutTool can find in your project.

Renaming

LayoutTool also provides a function for renaming classes or expression variables inside one or more Layout XML templates. Use it as follows:

"${PODS_ROOT}/Layout/LayoutTool/LayoutTool" rename "${SRCROOT}/path/to/your/layout/xml/" oldName newName

Only class names and values inside expressions will be affected. Attributes (i.e. expression names) are ignored, along with HTML elements and literal string fragments.

Note: Performing a rename also applies standard formatting to the file. There is currently no way to disable this.

Strings

LayoutTool's strings command prints a list of all Localizable.strings constants referenced in your Layout XML templates. Use it as follows:

"${PODS_ROOT}/Layout/LayoutTool/LayoutTool" strings "${SRCROOT}/path/to/your/layout/xml/"

Xcode Extension

If you are writing Layout XML inside Xcode, you may wish to install the Layout Xcode Editor Extension, which provides a subset of [LayoutTool]'s functionality directly inside the Xcode IDE.

Installation

The latest built binary of Layout for Xcode is included in the project inside the EditorExtension directory, and you can just drag-and-drop it to your Applications folder to install.

Once installed, run the Layout for Xcode app and follow the on-screen instructions.

To ensure compatibility, always update the Layout for Xcode app at the same time as updating the Layout framework, because using an old version of Layout for Xcode to format XML files containing newer features may result in data loss or corruption.

Note: The Layout for Xcode app is only updated when there are changes that affect its behavior, so don't worry if the version doesn't match exactly.

Formatting

When you have a Layout XML file open in Xcode, select the Editor > Layout > Format XML menu to reformat the file.

FAQ

Q. How is this different from frameworks like React Native?

React Native is a complete x-platform replacement for native iOS and Android development, whereas Layout is a way to build ordinary iOS UIKit apps more easily. In particular, Layout has much tighter integration with native UIKit controls, requires less boilerplate to use custom controls, and works directly with your existing native Swift code.

Q. How is this different from frameworks like Render?

The programming model is very similar, but Layout's runtime expression language means that you can do a larger proportion of your UI development without needing to restart the Simulator.

Q. Does Layout use Flexbox?

No. Layout requires you to position each view explicitly using top/left/width/height properties, but its percentage-based units and auto-sizing feature make it easy to create complex layouts with minimal code. You can also use iOS's native flexbox-style UIStackView within your Layout templates.

Q. Why does Layout use XML instead of a more modern format like JSON?

XML is better suited to representing document-like structures such as view hierarchies. JSON does not distinguish between node types, attributes, and children in its syntax, which leads to a lot of extra verbosity when representing hierarchical structures because each node must include keys for "type" and "children", or equivalent. JSON also doesn't support comments, which are useful in complex layouts. While XML isn't perfect, it is the most appropriate of the formats that iOS has built-in support for.

Q. Do I really have to write my layouts in XML?

You can create LayoutNodes manually in code, but XML is the recommended approach for now since it makes it possible to use the live reloading feature.

Q. Is Layout App Store-safe? Has it been used in production?

Yes, we have submitted apps using Layout to the App Store, and they have been approved without issue.

Q. Which platforms are supported?

Layout works on iOS 9.0 and above. There is currently no support for other Apple OSes (tvOS, watchOS, macOS), nor competing platforms such as Android or Windows.

Q. Will Layout ever support watchOS/tvOS?

There are no plans at the moment, but it should be fairly simple to add support for iOS-derivative platforms. If you need this, please create a pull request with whatever changes are required to make Layout build on those platforms.

Q. Will Layout ever support macOS/AppKit?

There are no plans at the moment, but this would make sense in future given the shared language and similar frameworks. If you are interested in implementing such a feature, please create an issue on GitHub to discuss the approach.

Q. Will Layout ever support Android/Windows?

There are no plans to port Layout to other platforms at the moment. Android and Windows in particular already use a human-readable XML format for their view templates, which eliminates some of the need for a Layout-like replacement.

Q. Why isn't Cmd-R reloading my XML file in the simulator?

Make sure that the Hardware > Keyboard > Connect Hardware Keyboard option is enabled in the simulator.

Q. Why do I get an error saying my custom view class isn't recognized?

Read the Namespacing section above.

Q. Why do I get an error when trying to set a property of my custom component?

Read the Custom Property Types section above.

Q. Do I have to use a UIViewController subclass load my layout?

No. See the Advanced Topics section above.

Q. When I launched my app, Layout asked me to select a source file and I chose the wrong one, now my app isn't working correctly. What do I do?

If the app runs OK, or displays a Red Box, you can reset it with Cmd-Alt-R. If it's actually crashing, the best option is to delete the app from the Simulator then re-install it.

Issues
  • [Question] better way to reference outlet inside UITableViewCell?

    [Question] better way to reference outlet inside UITableViewCell?

    Hi there,

    I have a cell within a table defined in XML like this:

    <UITableViewCell reuseIdentifier="cell">
                <UITextField
                    autocapitalizationType="words"
                    height="100%"
                    isSecureTextEntry="isPassword ? true : false"
                    left="15"
                    placeholder="{placeholder}"
                    text="{value}"
                    textAlignment="left"
                    width="100%"
                />
    </UITableViewCell>
    

    I wanted to reference the UITextField inside the cell in my view controller so that whenever text chnaged within a textField, I get notify and do something about it. Here is my current solution to get the reference and add target to the text field:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let node = tableView.dequeueReusableCellNode(withIdentifier: "cell", for: indexPath)
        
        node.setState(viewModel.formValues[indexPath.row])
        
        let textField = node.children.first?.view as! UITextField
        textField.tag = indexPath.row
        textField.removeTarget(nil, action: nil, for: .allEvents)
        textField.addTarget(self, action: #selector(SignUpFormViewController.textFieldDidChanged(textField:)), for: .editingChanged)
    
        return node.view as! UITableViewCell
      }
    

    Is there a more elegant way to reference outlet without a cell?

    opened by hermanccw 15
  • Outlets are searched in view controller doing the layout node loading

    Outlets are searched in view controller doing the layout node loading

    Version: 0.48 Xcode: 9b5 iOS Sim: 11.0

    I am duplicating the approach to loading layouts provided by SampleApp. I have a MainViewController that loads Main.xml which provides UITabBarViewController with a couple of tabs.

    The tabs though contain subclasses of UIViewController and LayoutViewController and the view that is nested under them contain elements with outlet attributes. I would expect the outlets to be connected to the instances of the tabbed view controllers, however I get an error that the outlet could not be found in MainViewController.

    Is this intended behaviour or a bug?

    opened by eimantas 15
  • Font not respecting specified weight

    Font not respecting specified weight

    I appreciate font weights were added/fixed in 0.5.8, but it still appears as though the specified weights aren't being respected.

    This XML...

    <UIView
      backgroundColor="#1998D5"
      width="100%"
      height="100%">
      <UILabel
        top="safeAreaInsets.top"
        font="Roboto 21.0"
        numberOfLines="0"
        textColor="white"
        text="This should be Roboto @ 21.0" />
      <UILabel
        top="previous.bottom + 20.0"
        font="Roboto Light 21.0"
        numberOfLines="0"
        textColor="white"
        text="This should be Roboto Light @ 21.0" />
      <UILabel
        top="previous.bottom + 20.0"
        font="Roboto Thin 21.0"
        numberOfLines="0"
        textColor="white"
        text="This should be Roboto Thin @ 21.0" />
    </UIView>
    

    Renders as the following...

    layout

    All three are rendering as Roboto, but the thin and light weights appear to be ignored.

    This is how it renders when the exact same layout is created using a storyboard...

    storyboard

    As a sidenote, those two views are supposed to be the same blue—I just copied the hex value out of the XML file and pasted it into the colour picker in IB. Any ideas why they'd be different?

    opened by ghost 13
  • LayoutNode are never released

    LayoutNode are never released

    I'm not sure what I am doing wrong, I have a simple view controller with a UICollectionView, and I am using Layout for both the view controller and the cells in the collection view, however, it looks like all the LayoutNodes that are created are never released... Just going back and forth to that view controller quick leads to an out of memory crash.

    I've peaked at the memory graph but didn't find anything useful (other than seeing a ton of LayoutNodes all over the place, that retain other LayoutNodes, etc.)

    I decided to simplify (a lot) and just went with a blank project that just has 2 view controllers, one (non-layout backed) that has a button that displays the 2nd view controller modally (a simple layout-backed view controller that has a label, an image view, and a button to dismiss it).

    After tapping the button a couple of times, I peaked at the memory graph. Surely enough, all the layout nodes are still there ( even though the layout-backed view controller itself isn't in memory anymore ). I figured maybe this was on purpose, but apparently all these layout nodes are also retaining all the views they contain, so showing and dismissing the layout view controller 10 times for example, I end up with 10 image view instances, 10 buttons, 10 labels, etc. that are leaked.

    Am I doing something wrong? I can put together a simple test project to show the issue, but it looks like pretty much any LayoutNode I create, no matter how, are just never released so it should be fairly easy to repro...

    opened by SilverTab 12
  • Introducing Layout XML-defined cells into legacy TableView

    Introducing Layout XML-defined cells into legacy TableView

    I am very impressed by the work and ingenuity behind Layout. So much I decided to try introducing it into one of projects.

    Unfortunately, I can't get UITableViewCell laid out properly: the content of XML is loaded, but everything is crammed in top-left corner. I started learning by modifying example app and tinkering with TableCell.xml, and got that:

    screen shot 2017-10-31 at 13 47 20

    Then, using the same XML in my legacy project (existing UITableView, existing reusable cells defined in storyboard) result with this:

    screen shot 2017-10-31 at 13 36 09

    There's ScrollView wrapping UIDatePicker, but it has zero size in my project.

    I do register cell ("node"):

    @IBOutlet var tview: UITableView! {
            didSet {
                tview.registerLayout(named: "DelayedTimePickerCell.xml", forCellReuseIdentifier: CellIdentifiers.TimePicker)
            }
        }
    

    and dequeue in cellForRow:

    let node = tableView.dequeueReusableCellNode(withIdentifier: CellIdentifiers.TimePicker, for: indexPath)
                    node.setState([])
                    return node.view as! UITableViewCell
    

    The only part I am missing is not using LayoutViewController - do I have to, even if I only wanted to use cells? I will appreciate any pointers to what I am doing wrong.

    opened by tomekc 10
  • Feature proposal: make  LayoutTool return unix exit codes

    Feature proposal: make LayoutTool return unix exit codes

    I'm currently working on a small thing when I'd like the LayoutTool to return unix exit codes: 0 for success, 1 for error. If LayoutTool is hooked in Build Phases, with proper exit codes Xcode will fail the build early if formatting fails. @nicklockwood do you think it's an addition worth doing?

    opened by mkarp 10
  • Crashing after updating to 0.6.32

    Crashing after updating to 0.6.32

    Hi,

    I am getting the following error after updating to 0.6.32. I ran my project on iOS 12 simulator. Our app supports down to iOS 9. Could it be our min supported version giving us this error?

    [_UIButtonBarStackView layout_intrinsicContentSize]: unrecognized selector sent to instance 0x7ffed57036f0
    2018-10-17 19:27:05.576194+0800 photobook[45429:5278405] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_UIButtonBarStackView layout_intrinsicContentSize]: unrecognized selector sent to instance 0x7ffed57036f0'
    *** First throw call stack:
    (
    	0   CoreFoundation                      0x0000000108df229b __exceptionPreprocess + 331
    	1   libobjc.A.dylib                     0x000000010838e735 objc_exception_throw + 48
    	2   CoreFoundation                      0x0000000108e10fa4 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    	3   UIKitCore                           0x000000010f945163 -[UIResponder doesNotRecognizeSelector:] + 287
    	4   CoreFoundation                      0x0000000108df6fb6 ___forwarding___ + 1446
    	5   CoreFoundation                      0x0000000108df8e88 _CF_forwarding_prep_0 + 120
    	6   Layout                              0x0000000106ba99df $SSo20UICollectionViewCellC6LayoutE27layout_intrinsicContentSize33_E820540BA7B1A5B68DC346B3C080A81DLLSo6CGSizeVvg + 287
    	7   Layout                              0x0000000106ba988e $SSo16UICollectionViewC6LayoutE27layout_intrinsicContentSize33_E820540BA7B1A5B68DC346B3C080A81DLLSo6CGSizeVvgToTm + 30
    	8   UIKitCore                           0x000000010f43b9c9 -[UIView(UIConstraintBasedLayout) _generateContentSizeConstraints] + 35
    	9   UIKitCore                           0x000000010f43b61a -[UIView(UIConstraintBasedLayout) _updateContentSizeConstraints] + 257
    	10  UIKitCore                           0x000000010f446902 -[UIView(AdditionalLayoutSupport) _updateSystemConstraints] + 99
    	11  UIKitCore                           0x000000010fa12fb5 __32-[UIStackView updateConstraints]_block_invoke + 46
    	12  UIKitCore                           0x000000010f444876 -[UIView(AdditionalLayoutSupport) _withUnsatisfiableConstraintsLoggingSuspendedIfEngineDelegateExists:] + 104
    	13  UIKitCore                           0x000000010fa12f81 -[UIStackView updateConstraints] + 70
    	14  UIKitCore                           0x000000010f5cf23f -[_UIButtonBarStackView updateConstraints] + 87
    	15  UIKitCore                           0x000000010f445254 -[UIView(AdditionalLayoutSupport) _sendUpdateConstraintsIfNecessaryForSecondPass:] + 161
    	16  UIKitCore                           0x000000010f445829 -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededCollectingViews:forSecondPass:] + 1245
    	17  UIKitCore                           0x000000010f4456ad -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededCollectingViews:forSecondPass:] + 865
    	18  Foundation                          0x0000000107f9c55a -[NSISEngine withBehaviors:performModifications:] + 110
    	19  UIKitCore                           0x000000010f445a83 -[UIView(AdditionalLayoutSupport) _recursiveUpdateConstraintsIfNeededCollectingViews:forSecondPass:] + 112
    	20  UIKitCore                           0x000000010f4456ad -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededCollectingViews:forSecondPass:] + 865
    	21  UIKitCore                           0x000000010f446046 __100-[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededWithViewForVariableChangeNotifications:]_block_invoke + 85
    	22  UIKitCore                           0x000000010f445bce -[UIView(AdditionalLayoutSupport) _updateConstraintsIfNeededWithViewForVariableChangeNotifications:] + 155
    	23  UIKitCore                           0x000000010f446c82 -[UIView(AdditionalLayoutSupport) _updateConstraintsAtEngineLevelIfNeededWithViewForVariableChangeNotifications:] + 374
    	24  UIKitCore                           0x000000010fc621eb -[UIView(Hierarchy) _updateConstraintsAsNecessaryAndApplyLayoutFromEngine] + 242
    	25  UIKitCore                           0x000000010fc77069 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1525
    	26  UIKitCore                           0x000000010f823474 -[UINavigationBar layoutSublayersOfLayer:] + 248
    	27  QuartzCore                          0x000000010a755d3d -[CALayer layoutSublayers] + 175
    	28  QuartzCore                          0x000000010a75abf7 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 395
    	29  UIKitCore                           0x000000010fc618f4 -[UIView(Hierarchy) layoutBelowIfNeeded] + 1429
    	30  UIKitCore                           0x000000010f836153 -[UINavigationController _positionNavigationBarHidden:edge:initialOffset:] + 800
    	31  UIKitCore                           0x000000010f8363f0 -[UINavigationController _positionNavigationBarHidden:edge:] + 388
    	32  UIKitCore                           0x000000010f84abfd -[UINavigationController __viewWillLayoutSubviews] + 231
    	33  UIKitCore                           0x000000010f7d663f -[UILayoutContainerView layoutSubviews] + 217
    	34  UIKitCore                           0x000000010fc77015 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1441
    	35  QuartzCore                          0x000000010a755d3d -[CALayer layoutSublayers] + 175
    	36  QuartzCore                          0x000000010a75abf7 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 395
    	37  QuartzCore                          0x000000010a6d3aa6 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 342
    	38  QuartzCore                          0x000000010a70ac2a _ZN2CA11Transaction6commitEv + 576
    	39  UIKitCore                           0x000000010f578d4c __34-[UIApplication _firstCommitBlock]_block_invoke_2 + 139
    	40  CoreFoundation                      0x0000000108d55a3c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    	41  CoreFoundation                      0x0000000108d551f0 __CFRunLoopDoBlocks + 336
    	42  CoreFoundation                      0x0000000108d4fa64 __CFRunLoopRun + 1284
    	43  CoreFoundation                      0x0000000108d4f221 CFRunLoopRunSpecific + 625
    	44  GraphicsServices                    0x0000000112e051dd GSEventRunModal + 62
    	45  UIKitCore                           0x000000010f55e115 UIApplicationMain + 140
    	46  photobook                           0x0000000103d25fd4 main + 68
    	47  libdyld.dylib                       0x000000010c2e9551 start + 1
    	48  ???                                 0x0000000000000001 0x0 + 1
    
    opened by rachel-13 9
  • LayoutLoading with support for loading node from Data

    LayoutLoading with support for loading node from Data

    Hi,

    What do you think about adding support for loading xml data from a Data instance in the LayoutLoading extension?. Currently there's only support for loading from a local file or URL.

    Suggested function signature (which pretty much mirrors the LayoutNode's init):

    public func loadLayout(
            xmlData: Data,
            url: URL? = nil,
            state: Any = (),
            constants: [String :Any] = [:]
            ) 
    

    This could be used, amongst other things, to load xml blobs from CoreData or having xml data as strings in the code.

    I know that it's already possible to do this if you create the LayoutNode yourself using the init(xmlData: Data) initialiser and set it as the layoutNode instance on the LayoutViewController.

    However, I think it would be convenient to have the option available in the LayoutLoading extension and use it inside viewDidLoad. I guess the API found in LayoutViewController is what most developer will use to setup the view, right?

    Could probably do a PR for this if it's of any interest?

    Some neat this that could be done is showing a spinner while loading content from a remote server.

    let data = """
    <UIView backgroundColor="#767676" text="Hello" 
        xml="https://raw.githubusercontent.com/schibsted/layout/master/SampleApp/PageTemplate.xml">
    
        <param name="text" type="String"/> <!-- This is required when loading async -->
        <UIActivityIndicatorView top="50% - height/2" left="50% - width/2"
            activityIndicatorViewStyle="whiteLarge" isAnimating="true" color="gray" />
        
    </UIView>
    """
    
    loadLayout(xmlData: data.data(using: .utf8)!)
    

    Actually, I think I found an issue while trying this out. If I point to a remote resource, the loading fails with an "unknown property 'text'" error. If I point to the same file locally, it works.

    Adding <param name="text" type="String"/> to the body fixes the issue but I don't think it's the correct behaviour, right? Should I file another issue for this?

    As a side note... In a post 1.0 world, I think it would be cool to support custom url protocol providers. A protocol provider would be responsible for fetching and providing xml data based on the protocol in the url. For example, I could register a protocol provider, myresources://. When the layout engine finds any xml resources to load with that protocol, it hands over the loading to the provider. A typical provider could be one that fetches a signed resources and before it's handed over to the Layout engine the signature is verified :)

    opened by mariob 8
  • Anyone have trouble using outlets within a UITableViewCell.xml file?

    Anyone have trouble using outlets within a UITableViewCell.xml file?

    This is the following code I'm having trouble with which produces the following error.

    UITableViewCell does not have an outlet named journalTextView.....

    The setState value for ""journal" works as well as other variables... so sure the issue. Scratching my head for 2 days off and on.

    class TableScreen: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextViewDelegate
    {
        @IBOutlet var journalTextView: UITextView?
        @IBOutlet var TableView: UITableView? {
            didSet {
                TableView?.registerLayout(
                    named: "Cell.xml",
                    forCellReuseIdentifier: "Cell"
                )
            }
        }
    ....
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
            let cellNode = tableView.dequeueReusableCellNode(withIdentifier: "Cell", for: indexPath)
    
            cellNode.setState([
                "journal": journalTextView?.text as Any,
                ])
            return cellNode.view as! UITableViewCell
        }
    

    TableScreen.xml

    <TableScreen>
        <UITableView
            outlet="TableView"
            ...
            >
        </UITableView>
    </TableScreen>
    

    Cell.xml

    <UITableViewCell
        reuseIdentifier="Cell"
        ....
        >
        ....
        <UIView>
            <UITextView
                outlet="journalTextView"
                text="{journal}"
               ...
            />
        </UIView>
    </UITableViewCell>
    
    opened by sidhenn 8
  • Weird Bug when using two different custom cellview in different tableviews

    Weird Bug when using two different custom cellview in different tableviews

    I am facing a strange bug where the content overlaps like a glitch in tableview. I am using tabbar with two view controllers and each viewcontroller has a tableview with different single custom cell. The first viewcontroller seems to have no problem initially but if i switch tab the second viewcontroller would be messed up and then i come back to the first it would get messed up too as i scroll. If I use same custom cell in both the tableview it doesn't happen but if i use different one in different tableview with different reuseIdentifier the glitch happens

    opened by TwunTee 8
  • Live Reloading as an Option

    Live Reloading as an Option

    This is to enable the live reloading feature as an option that can be enabled/disabled through project scheme argument. Having this flexibility allows us to run the app in simulator much faster when we don't need the feature.

    The argument:

    -LayoutLiveReloadEnabled
    
    opened by tengfoung 0
  • Bump activesupport from 5.2.1 to 6.0.3.1

    Bump activesupport from 5.2.1 to 6.0.3.1

    Bumps activesupport from 5.2.1 to 6.0.3.1.

    Release notes

    Sourced from activesupport's releases.

    6.0.3

    In this version, we fixed warnings when used with Ruby 2.7 across the entire framework.

    Following are the list of other changes, per-framework.

    Active Support

    • Array#to_sentence no longer returns a frozen string.

      Before:

      ['one', 'two'].to_sentence.frozen?
      # => true
      

      After:

      ['one', 'two'].to_sentence.frozen?
      # => false
      

      Nicolas Dular

    • Update ActiveSupport::Messages::Metadata#fresh? to work for cookies with expiry set when ActiveSupport.parse_json_times = true.

      Christian Gregg

    Active Model

    • No changes.

    Active Record

    • Recommend applications don't use the database kwarg in connected_to

      The database kwarg in connected_to was meant to be used for one-off scripts but is often used in requests. This is really dangerous because it re-establishes a connection every time. It's deprecated in 6.1 and will be removed in 6.2 without replacement. This change soft deprecates it in 6.0 by removing documentation.

      Eileen M. Uchitelle

    • Fix support for PostgreSQL 11+ partitioned indexes.

      Sebastián Palma

    • Add support for beginless ranges, introduced in Ruby 2.7.

      Josh Goodall

    ... (truncated)
    Changelog

    Sourced from activesupport's changelog.

    Rails 6.0.3.1 (May 18, 2020)

    • [CVE-2020-8165] Deprecate Marshal.load on raw cache read in RedisCacheStore

    • [CVE-2020-8165] Avoid Marshal.load on raw cache value in MemCacheStore

    Rails 6.0.3 (May 06, 2020)

    • Array#to_sentence no longer returns a frozen string.

      Before:

      ['one', 'two'].to_sentence.frozen?
      # => true
      

      After:

      ['one', 'two'].to_sentence.frozen?
      # => false
      

      Nicolas Dular

    • Update ActiveSupport::Messages::Metadata#fresh? to work for cookies with expiry set when ActiveSupport.parse_json_times = true.

      Christian Gregg

    Rails 6.0.2.2 (March 19, 2020)

    • No changes.

    Rails 6.0.2.1 (December 18, 2019)

    • No changes.

    Rails 6.0.2 (December 13, 2019)

    • Eager load translations during initialization.

      Diego Plentz

    • Use per-thread CPU time clock on ActiveSupport::Notifications.

      George Claghorn

    Rails 6.0.1 (November 5, 2019)

    ... (truncated)
    Commits
    • 34991a6 Preparing for 6.0.3.1 release
    • 2c8fe2a bumping version, updating changelog
    • 0ad524a update changelog
    • bd39a13 activesupport: Deprecate Marshal.load on raw cache read in RedisCacheStore
    • 0a7ce52 activesupport: Avoid Marshal.load on raw cache value in MemCacheStore
    • b738f19 Preparing for 6.0.3 release
    • 509b9da Preparing for 6.0.3.rc1 release
    • 02d07cc adds missing require [Fixes #39042]
    • f2f7bcc Fix Builder::XmlMarkup lazy load in Array#to_xml
    • 320734e Merge pull request #36941 from ts-3156/master
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • After upgrading to macOS Catalina application gets stuck on the Launch screen

    After upgrading to macOS Catalina application gets stuck on the Launch screen

    Hi! After upgrading my operating system to Catalina (macOS 10.15), my application that is using Layout gets stuck on the launch screen. I don't have any compilation errors or warnings and I don't get any crash at runtime. There is nothing new shown in debug logs as well. It worked fine in the previous macOS version (High Sierra 10.14). My other applications that are not using this Pod, don't get stuck on the launch screen. Has anyone a similar problem?

    • Layout version: 0.7
    • Xcode 11.1
    • Swift 5
    opened by nadrka 1
  • Add setStringValue:forExpression method on UIView

    Add setStringValue:forExpression method on UIView

    The setStringValue:forExpression method on UIView can set any value from a string, just like Layout does when looking at the XML attributes for a View. This expands the possibilities of the setValue:forExpression, which needs the value to be of the right type.

    opened by cadrega 2
  • Setting any UIView property at runtime from string property name and string property value

    Setting any UIView property at runtime from string property name and string property value

    Hi @nicklockwood ! Layout is really great at creating completely custom and dynamically server-side created GUIs.

    However, I would like to alter the GUI appearance at runtime, based on a server-side implementation. To do so, in my ViewController which hosts a mounted LayoutNode, I created a method that will be called after receiving a message from the network:

    func setViewValue(tag: Int, valueName: String, newValue: String) {
    	guard let baseView = self.view else {
    		return
    	}
    
    	if let v = baseView.viewWithTag(tag) {
    		try? v.setValue(newValue, forExpression:valueName)
    	}
    }
    

    This already works flawlessly for String properties, like "title". However, I would like to set any property type just as Layout does in XML. I don't need to handle complicated expressions, just be able to specify a UIColor, false/true, geometry or number values...

    It seems to me that the steps would be:

    • search in the layoutNode "expressionTypes" dictionary for the right type
    • create a LayoutExpression object with the expression name, value, type, and the layoutNode
    • evaluate() the expression
    • call setValueForExpression() with the result of the evaluation

    However, many of these methods or even entire structs (LayoutExpression) are declared internal. Is there a ready-made method for doing this? Am I missing something? Or a major rework would be needed to do this? If yes, can you point me in the right direction? I can come back with a pull request once I have this finished. Thanks!

    opened by cadrega 1
  • iOS 13, Xcode 11 infinite loop related to the swizzled layoutSubviews()

    iOS 13, Xcode 11 infinite loop related to the swizzled layoutSubviews()

    We have a custom view and we overrode layoutSubviews() as recommended:

    open override func layoutSubviews() {
            super.layoutSubviews()
            // Ensure layout is updated after screen rotation, etc
            self.layoutNode?.view.frame = self.bounds
            // Update frame to match layout
            self.frame.size = self.intrinsicContentSize
        }
    

    and it would appear the app is stuck in some kind of infinite loop, our custom view layoutSubviews () and layout_layoutSubviews() being called forever and blocking the main thread

    extension UIView {
        fileprivate static func _swizzle() {
            guard !viewSwizzled else { return }
            replace(#selector(layoutSubviews), of: self, with: #selector(layout_layoutSubviews))
            viewSwizzled = true
        }
    
        // Swizzled layoutSubviews implementation
        @objc private func layout_layoutSubviews() {
            layout_layoutSubviews()
            _layoutNode?.updateLayout()
        }
    }
    
    opened by sebastienwindal 3
  • Trouble with UIStackView.addArrangedSubView

    Trouble with UIStackView.addArrangedSubView

    When adding arranged subviews to a Layout stack view programmatically, it doesn't update its height from the initial 0, so all the subviews get placed at (0, 0). However, when I add the subviews to the stack view XML, they stack up correctly — they're probably calculating their children's heights in that case.

    So does anyone know what I'm missing? I guess I want to confirm (before I spend more time banging my head against the rock) whether or not there's an obvious method to solve this problem, or if I need to look into calculating the heights myself.

    Edit: I do need to mention that setting the stack view's frame to a new CGRect doesn't have any effect. Reading out its .frame.height value confirms that it's still at 0.0 after something like stackView?.frame = CGRect(x:0, y:0, width:300, height:600). All of this is happening in my UIViewController's viewDidLoad() method.

    opened by neptunus 0
  • LayoutLoading: load XML from Data

    LayoutLoading: load XML from Data

    Add capabiilty to load XML from Data in LayoutLoading protocol. New pull request, tested against version 0.6.38

    opened by cadrega 0
  • UITextField causing unresponsive in Xcode 11 Beta 5 iOS 13

    UITextField causing unresponsive in Xcode 11 Beta 5 iOS 13

    Everything else seems to work fine except when tapping away on focused UITextField, the app become unresponsive. It is reproducible using SampleApp project provided in the repository.

    Details:

    Xcode 11 Beta 5 (11M382q) iOS Simulator iPhone XR (iOS 13)

    Steps to reproduce:

    1. Simply add the UI component as such in Text.xml:
    <UITextField
        top="SPACING"
        width="100%"
        height="40"
        backgroundColor="white"
    />
    
    1. Run the SampleApp scheme.
    2. Go to Text tab.
    3. Tap on the textfield, type something and tap somewhere else to defocus.

    Result:

    App become unresponsive.

    In console, it shows:

    atos[30314]: atos cannot examine process 30302 (SampleApp) for unknown reasons, even though it appears to exist; try running with sudo. ==30302==WARNING: Can't read from symbolizer at fd 11

    However, in my own project it showed different error:

    [] nw_connection_receive_internal_block_invoke [C36] Receive reply failed with error "Operation canceled"

    Thread info:

    image

    opened by tengfoung 1
  • UITableViewCell subclass using an xml layout

    UITableViewCell subclass using an xml layout

    Is there a way to achieve that?

    When I'm trying something like this:

    class MyTableViewCell: UITableViewCell, LayoutLoading {
    

    I'm getting the following error: Type 'MyTableViewCell' does not conform to protocol 'LayoutLoading'.

    My original problem is that I want to use a library like Kingfisher or SDWebImage to load an image inside my cell. layoutNode.setState doesn't allow that, so it looks like I need an outlet, but I don't know how to create an outlet for a view in a Layout-based UITableViewCell.

    I can create a custom view for my cell contents and then manually put it into a UITableViewCell subclass, but is there a better way to do it?

    opened by algrid 0
Owner
Nick Lockwood
Nick Lockwood
A declarative Auto Layout DSL for Swift :iphone::triangular_ruler:

Cartography ?? ?? Using Cartography, you can set up your Auto Layout constraints in declarative code and without any stringly typing! In short, it all

Robert Böhnke 7.3k Jan 8, 2022
⚓️ Declarative, extensible, powerful Auto Layout

EasyAnchor ❤️ Support my apps ❤️ Push Hero - pure Swift native macOS application to test push notifications PastePal - Pasteboard, note and shortcut m

Khoa 443 Dec 22, 2021
A powerful Swift programmatic UI layout framework.

Build dynamic and beautiful user interfaces like a boss, with Swift. Neon is built around how user interfaces are naturally and intuitively designed.

Mike 4.6k Jan 10, 2022
A Swift binding framework

Bond, Swift Bond Update: Bond 7 has been released! Check out the migration guide to learn more about the update. Bond is a Swift binding framework tha

Declarative Hub 4.2k Jan 13, 2022
Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Not Apple Autolayout wrapper. Provides placeholders. Linux support.

CGLayout Powerful autolayout framework, that can manage UIView(NSView), CALayer and not rendered views. Has cross-hierarchy coordinate space. Implemen

Koryttsev Denis 45 Dec 17, 2021
An alternative layout framework, a balanced medium between manual layout and auto layout. Great for calculating frames with FlightAnimator!!

FlightLayout Introduction FlightLayout is a light weight, and easy to learn layout framework as an extension of the UIView. Functionally, it lives som

Anton 23 Nov 20, 2020
Lightweight Swift framework for Apple's Auto-Layout

I am glad to share with you a lightweight Swift framework for Apple's Auto-Layout. It helps you write readable and compact UI code using simple API. A

null 348 Jan 4, 2022
Harness the power of AutoLayout NSLayoutConstraints with a simplified, chainable and expressive syntax. Supports iOS and OSX Auto Layout

Masonry Masonry is still actively maintained, we are committed to fixing bugs and merging good quality PRs from the wider community. However if you're

null 18k Jan 2, 2022
The ultimate API for iOS & OS X Auto Layout — impressively simple, immensely powerful. Objective-C and Swift compatible.

The ultimate API for iOS & OS X Auto Layout — impressively simple, immensely powerful. PureLayout extends UIView/NSView, NSArray, and NSLayoutConstrai

PureLayout 7.5k Dec 26, 2021
A Swift Autolayout DSL for iOS & OS X

SnapKit is a DSL to make Auto Layout easy on both iOS and OS X. ⚠️ To use with Swift 4.x please ensure you are using >= 4.0.0 ⚠️ ⚠️ To use with Swift

null 18.2k Jan 14, 2022
✂ Easy to use and flexible library for manually laying out views and layers for iOS and tvOS. Supports AsyncDisplayKit.

ManualLayout Table of Contents Installation Usage API Cheat Sheet Installation Carthage Add the following line to your Cartfile. github "isair/ManualL

Baris Sencan 284 Dec 17, 2021
LayoutKit is a fast view layout library for iOS, macOS, and tvOS.

?? UNMAINTAINED ?? This project is no longer used by LinkedIn and is currently unmaintained. LayoutKit is a fast view layout library for iOS, macOS, a

LinkedIn's Attic 3.2k Jan 10, 2022
A collection of operators and utilities that simplify iOS layout code.

Anchorage A lightweight collection of intuitive operators and utilities that simplify Auto Layout code. Anchorage is built directly on top of the NSLa

Rightpoint 612 Jan 4, 2022
DEPRECATED - BrickKit For IOS

BrickKit is a delightful layout library for iOS and tvOS. It is written entirely in Swift! Deprecated BrickKit is being phased out at Wayfair, and the

Wayfair Tech – Archive 616 Jan 4, 2022
Fast Swift Views layouting without auto layout. No magic, pure code, full control and blazing fast. Concise syntax, intuitive, readable & chainable. [iOS/macOS/tvOS/CALayer]

Extremely Fast views layouting without auto layout. No magic, pure code, full control and blazing fast. Concise syntax, intuitive, readable & chainabl

layoutBox 1.9k Jan 3, 2022
An easy way to create and layout UI components for iOS (Swift version).

Introduction Cupcake is a framework that allow you to easily create and layout UI components for iOS 8.0+. It use chaining syntax and provides some fr

nerdycat 283 Dec 15, 2021
DEPRECATED - BrickKit For IOS

BrickKit is a delightful layout library for iOS and tvOS. It is written entirely in Swift! Deprecated BrickKit is being phased out at Wayfair, and the

Wayfair Tech – Archive 616 Jan 4, 2022
LayoutKit is a fast view layout library for iOS, macOS, and tvOS.

?? UNMAINTAINED ?? This project is no longer used by LinkedIn and is currently unmaintained. LayoutKit is a fast view layout library for iOS, macOS, a

LinkedIn's Attic 3.2k Jan 10, 2022