Skip to content

Latest commit

 

History

History
130 lines (90 loc) · 3.56 KB

CustomElements.md

File metadata and controls

130 lines (90 loc) · 3.56 KB

Building Custom Elements

ProxyElement (easy mode)

A common motivation for defining a custom element is to create an API boundary; the custom implementation can contain its own initializers, properties, etc.

When it comes to actually displaying content, however, many custom elements simply compose other existing elements together (they don't do custom layout or provide a custom view backing).

ProxyElement is a tool to make this common case easier.

public protocol ProxyElement: Element {
    var elementRepresentation: Element { get }
}

/// `ProxyElement` provides default implementations of the `Element` API that delegate to the element returned by `elementRepresentation`.
extension ProxyElement {
    public var content: ElementContent { get }
    public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription?
}

For example, let's define a custom element that displays a title and subtitle:

struct TitleSubtitleElement: ProxyElement {
    var title: String
    var subtitle: String

    var elementRepresentation: Element {
        Column(
            alignment: .leading,
            minimumSpacing: 8.0
        ) {
            Label(text: title) { label in
                label.font = .boldSystemFont(ofSize: 18.0)
            }

            Label(text: title) { label in
                label.font = .systemFont(ofSize: 14.0)
                label.color = .darkGray
            }     
        }
    }
}

Element (hard mode)

For more information, please see Element reference.

public protocol Element {
    var content: ElementContent { get }
    func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription?
}

To implement the Element protocol, your custom type must implement two methods:

var content: ElementContent

ElementContent represents (surprise!) the content of an element. Elements generally fall into one of two types: containers and leaves.

  • Containers, or elements that have children.
  • Leaves: elements that have no children, but often have some intrinsic size (a label is a good example of this).

func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription?

If a ViewDescription is returned, the element will be view-backed.


Bonus: do you need a custom element?

You may want to define a custom element so that you can reuse it throughout your codebase (great!). In other cases, however, you may simply want to generate an element for display.

For example, let's say we want to show some centered text over an image. This is only used in one place:

final class ImageViewController {

    private let blueprintView = BlueprintView()
    
    var image: UIImage {
        didSet { updateDisplay() }
    }

    var text: String {
        didSet { updateDisplay() }
    }

    private func updateDisplay() {
        let newElement = // We need an element!
        blueprintView.element = newElement
    }
    
}

You might simply define a function to turn input (in this case an image and some text) into an element:

private func makeElement(image: UIImage, text: String) -> Element {
    Overlay {
        Image(image: image)
        
        Label(text: text)
            .centered()   
    }
}

Your updateDisplay() method can then call this method as needed:

final class ImageViewController {

    // ...

    private func updateDisplay() {
        blueprintView.element = makeElement(image: image, text: text)
    }
    
}