Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

MarkupKit: Declarative UI for iOS

DZone's Guide to

MarkupKit: Declarative UI for iOS

This article introduces the MarkupKit framework and provides an overview of some of its key features.

· Mobile Zone
Free Resource

MarkupKit is an open-source framework for simplifying the development of native iOS applications. It allows developers to construct user interfaces declaratively using a human-readable markup language rather than visually using Interface Builder, similar to how applications are built for Android and .NET.

For example, the following markup creates an instance of UILabel  and sets the value of its text  property to "Hello, World!":

<UILabel text="Hello, World!"/>

The output produced by this markup is identical to the output of the following Swift code:

let label = UILabel()
label.text = "Hello, World!"

This article introduces the MarkupKit framework and provides an overview of some of its key features.

Document Structure

MarkupKit uses XML to define the structure of a user interface. The hierarchical nature of an XML document parallels the view hierarchy of an iOS application, making it easy to understand the relationships between views.

Elements

Elements in a MarkupKit document typically represent instances of UIView or its subclasses. As elements are read by the XML parser, the corresponding class instances are dynamically created and added to the view hierarchy.

For example, the following markup declares an instance of LMColumnView  containing a UIImageView  and a UILabel LMColumnView  is a MarkupKit-provided subclass of UIView  that automatically arranges its subviews in a vertical line:

<LMColumnView>
    <UIImageView image="world.png" contentMode="center"/>
    <UILabel text="Hello, World!" textAlignment="center"/>
</LMColumnView>

Elements may not always represent view instances, however. For example, this markup creates an instance of UISegmentedControl , the content of which is defined by a collection of "segment" tags:

<UISegmentedControl>
    <segment title="Small"/>
    <segment title="Medium"/>
    <segment title="Large"/>
    <segment title="Extra-Large"/>
</UISegmentedControl>

Attributes

Attributes in a MarkupKit document typically represent view properties. For example, the following markup declares an instance of a system-style UIButton  and sets its title  property to "Press Me!":

<UIButton style="systemButton" title="Press Me!"/>

Property values are set using key-value coding (KVC). Type conversions for string, number, and boolean properties are handled automatically by KVC. Other types, such as colors, fonts, images, and enumerations, are handled specifically by MarkupKit.

For example, the following markup creates a label whose font is set to 24-point Helvetica and whose text color is set to "#ff0000", or bright red:

<UILabel text="A Red Label" font="Helvetica 24" textColor="#ff0000"/>

A few attributes have special meaning in MarkupKit and do not represent properties. These include "style", "class", and "id". Their respective purposes are explained in more detail later.

Additionally, attributes whose names begin with "on" represent control events, or "actions". The values of these attributes represent the handler methods that are triggered when their associated events are fired. For example, this markup creates a button with an associated action that will be triggered when the button is pressed:

<UIButton style="systemButton" title="Press Me!" onPrimaryActionTriggered="buttonPressed"/>

Actions are also discussed in more detail below.

Template Properties

Often, when constructing a user interface, the same set of property values are applied repeatedly to instances of a given type. For example, an application designer may want all buttons to have a similar appearance. While it is possible to simply duplicate the property definitions across each button instance, this is repetitive and does not allow the design to be easily modified later - every instance must be located and modified individually, which can be time-consuming and error prone.

MarkupKit allows developers to abstract common sets of property definitions into CSS-like "property templates", which can then be applied by name to individual view instances. This makes it much easier to assign common property values as well as modify them later.

Property templates are specified using JavaScript Object Notation (JSON), and may be either external or inline. Inline templates are defined within the markup document itself, and external templates are specified in a separate file.

For example, the following JSON document defines a template named "greeting", which contains definitions for "font" and "textAlignment" properties:

{
  "greeting": {
    "font": "Helvetica 24", 
    "textAlignment": "center"
  }
}

Templates are added to a MarkupKit document using the properties  processing instruction (PI). The following PI adds all properties defined by MyStyles.json to the current document:

<?properties MyStyles?>

Inline templates simply embed the entire template definition within the processing instruction:

<?properties {
  "greeting": {
    "font": "Helvetica 24", 
    "textAlignment": "center"
  }
}?>

Templates are applied to view instances using the reserved "class" attribute. The value of this attribute refers to the name of a template defined within the current document. All property values defined by the template are applied to the view. Nested properties, such as "titleLabel.font", are supported.

For example, given the preceding template definition, the following markup would produce a label reading "Hello, World!" in 24-point Helvetica with horizontally centered text:

<UILabel class="greeting" text="Hello, World!"/>

Multiple templates can be applied to a view using a comma-separated list of template names; for example:

<UILabel class="bold, red" text="Bold Red Label"/>

Outlets

The reserved "id" attribute can be used to assign a name to a view instance. This creates an "outlet" for the view that makes it accessible to calling code. Using KVC, MarkupKit "injects" the named view instance into the document's owner (generally either the view controller for the root view or the root view itself), allowing the application to interact with it.

For example, the following markup declares an instance of UITextField  and assigns it an ID of "textField":

<UITextField id="textField"/>

The owning class might declare an outlet for the text field in Objective-C like this:

@property (nonatomic) IBOutlet UITextField *textField;

or in Swift, like this:

@IBOutlet var textField: UITextField!

In either case, when the document is loaded, the outlet will be populated with the text field instance, and the application can interact with it just as if it was defined in a storyboard or created programmatically.

Actions

Most non-trivial applications need to respond in some way to user interaction. UIKit controls (subclasses of the UIControl  class) fire events that notify an application when such interaction has occurred. For example, the UIButton  class fires the UIControlEventPrimaryActionTriggered  event when a button instance is tapped.

While it would be possible for an application to register for events programmatically using outlets, MarkupKit provides a more convenient alternative. Any attribute whose name begins with "on" (but does not refer to a property) is considered a control event. The value of the attribute represents the name of the action that will be triggered when the event is fired.

For example, the following markup declares an instance of UIButton  that calls the buttonPressed:   method of the document's owner when the button is tapped:

<UIButton style="systemButton" title="Press Me!" onPrimaryActionTriggered="buttonPressed:"/>

For example:

@IBAction func buttonPressed(_ sender: UIButton) {
    // Handle button press
}

Localization

If an attribute's value begins with "@", MarkupKit attempts to look up a localized version of the value before setting the property. For example, if an application has defined a localized greeting in Localizable.strings as follows:

"hello" = "Hello, World!";

The following markup will produce an instance of UILabel  with the value of its text  property set to "Hello, World!":

<UILabel text="@hello"/>

If a localized value is not found, the key will be used instead. For example, if the application does not provide a localized value for "goodbye", the following markup will produce a label containing the literal text "goodbye":

<UILabel text="@goodbye"/>

This allows developers to easily identify missing string resources at runtime.

MarkupKit Classes

MarkupKit includes a number of classes to help simplify application development. Some of the most common are discussed below.

LMViewBuilder

  LMViewBuilder  is the class that is actually responsible for loading a MarkupKit document. It provides the following class method, which, given a document name, owner, and optional root view, deserializes a view hierarchy from markup:

+ (UIView *)viewWithName:(NSString *)name owner:(nullable id)owner root:(nullable UIView *)root;

The name   parameter represents the name of the view to load. It is the file name of the XML document containing the view declaration, minus the .xml extension.

The owner  parameter represents the view's owner. It is often an instance of UIViewController , but this is not strictly required. For example, custom table and collection view cell classes often specify themselves as the owner.

The root  parameter represents the value that will be used as the root view instance when the document is loaded. This value is often nil , meaning that the root view will be specified by the document itself. However, when non- nil , it means that the root view is being provided by the caller.

For example, a view controller that is defined by a storyboard already has an established view instance when viewDidLoad  is called. The controller can pass itself as the view's owner and the value of its view  property as the root  argument. This allows the navigational structure of the application (i.e. segues) to be defined in a storyboard, but the content of individual views to be defined in markup.

Layout Views

Auto layout is an iOS feature that allows developers to create applications that automatically adapt to device size, orientation, or content changes. An application built using auto layout generally has little or no hard-coded view positioning logic, but instead dynamically arranges user interface elements based on their preferred or "intrinsic" content sizes.

Auto layout in iOS is implemented primarily via layout constraints, which, while powerful, are not particularly convenient to work with. To simplify the process, MarkupKit provides the following set of view classes, whose sole responsibility is managing the size and position of their respective subviews:

  • LMRowView – arranges subviews in a horizontal line
  • LMColumnView – arranges subviews in a vertical line
  • LMLayerView – arranges subviews in layers, like a stack of transparencies

These classes use layout constraints internally, allowing developers to easily take advantage of auto layout while eliminating the need to manage constraints directly.For example, the periodic table shown below was constructed using a combination of row views, column views, and labels:Creating this view in Interface Builder would be an arduous task. Creating it programmatically would be even more difficult. However, in markup it is almost trivial. The complete source code for this example can be found here.

LMRowView

The LMRowView  class arranges its subviews in a horizontal line. Subviews are laid out from leading to trailing edge in the order in which they are declared. For example, the following markup creates a row view containing three labels:

<LMRowView layoutMargins="12">
    <UILabel text="One"/>
    <UILabel text="Two"/>
    <UILabel text="Three"/>
    <LMSpacer/>
</LMRowView>

The "layoutMargins" attribute establishes a 12-pixel wide gap around the row view's border, and the trailing spacer view ensures that the labels are left-aligned within the row (or right-aligned in locales that use right-to-left text):Spacer views are discussed in more detail later.

Baseline Alignment

Subviews can be baseline-aligned within a row using the alignToBaseline  property. For example, this markup creates a row view containing three labels, all with different font sizes:

<LMRowView alignToBaseline="true" layoutMargins="12">
    <UILabel text="Ten" font="Helvetica 12"/>
    <UILabel text="Twenty" font="Helvetica 24"/>
    <UILabel text="Thirty" font="Helvetica 48"/>
    <LMSpacer/>
</LMRowView>

Because alignToBaseline  is set to true , the baselines of all three labels will line up:

LMColumnView

The LMColumnView  class arranges its subviews in a vertical line. Subviews are laid out from top to bottom in the order in which they are declared. For example, the following markup creates a column view containing three text fields:

<LMColumnView layoutMargins="12">
    <UITextField placeholder="First" borderStyle="roundedRect"/>
    <UITextField placeholder="Second" borderStyle="roundedRect"/>
    <UITextField placeholder="Third" borderStyle="roundedRect"/>
    <LMSpacer/>
</LMColumnView>

The left and right edges of each subview are automatically pinned to the left and right edges of the column view, ensuring that all of the text fields are the same width:

Grid Alignment

Nested subviews of a column view can be vertically aligned in a spreadsheet-like grid using the alignToGrid   property. When this property is set to true, cells in contiguous rows will be resized to match the width of the widest cell in the column.For example, the following markup would produce a grid containing three rows and two columns:

<LMColumnView alignToGrid="true" layoutMargins="12">
    <LMRowView>
        <UILabel text="One"/>
        <UITextField weight="1" placeholder="First" borderStyle="roundedRect"/>
    </LMRowView>

    <LMRowView>
        <UILabel text="Two"/>
        <UITextField weight="1" placeholder="Second" borderStyle="roundedRect"/>
    </LMRowView>

    <LMRowView>
        <UILabel text="Three"/>
        <UITextField weight="1" placeholder="Third" borderStyle="roundedRect"/>
    </LMRowView>
</LMColumnView>

The weight values ensure that the text fields are allocated all of the remaining space within each row after the size of label has been determined:

Weights are discussed in more detail below.

View Weights

Often, a row or column view will be given more space than it needs to accommodate the intrinsic sizes of its subviews. MarkupKit adds a weight  property to UIView  that is used to determine how the extra space should be allocated. Weight is a numeric value that specifies the amount of excess space the view would like to be given within its superview (once the sizes of all unweighted views have been determined) and is relative to all other weights specified within the superview.

For row views, weight applies to the excess horizontal space, and for column views to the excess vertical space. For example, since both labels in the following example have a weight of 0.5, they will each be allocated 50% of the width of the row view. The labels are given a border to make their bounds more obvious:

<LMRowView layoutMargins="12">
    <UILabel weight="0.5" text="50%" textAlignment="center"
        layer.borderWidth="0.5" layer.borderColor="#ff6666"/>
    <UILabel weight="0.5" text="50%" textAlignment="center"
        layer.borderWidth="0.5" layer.borderColor="#ff6666"/>
</LMRowView>

In this example, the first label will be given one-sixth of the available space, the second one-third (2/6), and the third one-half (3/6):

<LMColumnView layoutMargins="12">
    <UILabel weight="1" text="1/6" textAlignment="center"
        layer.borderWidth="0.5" layer.borderColor="#ff6666"/>
    <UILabel weight="2" text="1/3" textAlignment="center"
        layer.borderWidth="0.5" layer.borderColor="#ff6666"/>
    <UILabel weight="3" text="1/2" textAlignment="center"
        layer.borderWidth="0.5" layer.borderColor="#ff6666"/>
</LMColumnView>

Spacer Views

A common use for weights is to create flexible space around a view. For example, the following markup will center a label horizontally within a row:

<LMRowViewn layoutMargins="12">
    <UIView weight="1"/>
    <UILabel text="Hello, World!"/>
    <UIView weight="1"/>
</LMRowView>

Because such "spacer" views are so common, MarkupKit provides a dedicated UIView  subclass called LMSpacer  for conveniently creating flexible space between other views. LMSpacer  has a default weight of 1, so the previous example could be rewritten as follows, eliminating the "weight" attribute and improving readability:

<LMRowView layoutMargins="12">
    <LMSpacer/>
    <UILabel text="Hello, World!"/>
    <LMSpacer/>
</LMRowView>

Layer Views

The LMLayerView  class simply arranges its subviews in layers, like a stack of transparencies. The subviews are all automatically sized to fill the layer view.For example, the following markup creates a layer view containing an image view and a label:

<LMLayerView>
    <UIImageView image="world.png" contentMode="center"/>
    <UILabel text="Hello, World!" textColor="#ffffff" textAlignment="center"/>
</LMLayerView>

Since it is declared first, the contents of the image view will appear beneath the label text:

More Information

This article introduced the MarkupKit framework and provided an overview of some of its key features, including auto layout, property templates, and localization. For more information, see the project README.

Topics:
ios ,user interface ,auto layout ,xml ,markup ,markupkit ,mobile

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}