Tutorials

Using custom openers to build layouts

Thanks to custom openers you have the freedom to use Unflow data in a completley bespoke design to build the app you want, without the heavy lifting of managing the data source. You could build anything you want with this data, but we've got some examples below.

Unf1ow

For a complete sample of using custom openers, we built Unf1ow. It takes the unique design of the F1 app and re-creates it using the Unflow SDK.

The homepage is entirely powered by Unflow OpenersA sample article created entirely on the dashboardWhen you need it, dark mode screens are available

Examples

Below are some example layouts you can build using custom openers. The samples use Swift but the same functionality exists within all of our SDKs.

Custom items with standard layout

A common pattern is to have a carousel of images, with a little bit of text overlaid. Here's a full example of how you could build that using Unflow and native SwiftUI components.

import SwiftUI
import UnflowUI

/// Full width rows using the opener image as their background.
/// Designed to be used with OpenerView(openerHost:) which does not handle layout or interaction for you.
struct OpenerImageRow: View {
    let opener: Opener

    var body: some View {
        Button(action: {
            try? UnflowSDK.client.openScreen(withID: opener.id)
        }) {
            VStack(alignment: .leading) {
                Text(opener.title)
                    .font(.subheadline.weight(.semibold))
                    .foregroundColor(.white)
            }
            .padding()
            .multilineTextAlignment(.leading)
            .frame(height: 88, alignment: .bottomLeading)
            .frame(maxWidth: .infinity, alignment: .bottomLeading)
            .background(
                image
            )
        }
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
    
    var image: some View {
        ZStack {
            RemoteImage(url: URL(string: opener.imageURL ?? ""))
                .aspectRatio(contentMode: .fill)
            LinearGradient(colors: [.clear, .black.opacity(0.5)], startPoint: .top, endPoint: .bottom)
        }
        .allowsHitTesting(false)
    }
}

Custom layout with custom items

Sometimes you want both a custom item and a custom layout, which we've built here with a long list of full width images. They're a similar design to our previous example, but adjusted for the wider layout.

import SwiftUI
import UnflowUI

/// Full width rows using the opener image as their background.
/// Designed to be used with OpenerView(openerHost:) which does not handle layout or interaction for you.
struct OpenerImageRow: View {
    let opener: Opener

    var body: some View {
        Button(action: {
            try? UnflowSDK.client.openScreen(withID: opener.id)
        }) {
            VStack(alignment: .leading) {
                Text(opener.title)
                    .font(.subheadline.weight(.semibold))
                    .foregroundColor(.white)
            }
            .padding()
            .multilineTextAlignment(.leading)
            .frame(height: 88, alignment: .bottomLeading)
            .frame(maxWidth: .infinity, alignment: .bottomLeading)
            .background(
                image
            )
        }
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
    
    var image: some View {
        ZStack {
            RemoteImage(url: URL(string: opener.imageURL ?? ""))
                .aspectRatio(contentMode: .fill)
            LinearGradient(colors: [.clear, .black.opacity(0.5)], startPoint: .top, endPoint: .bottom)
        }
        .allowsHitTesting(false)
    }
}

Advanced usage

A grid layout with a hero feature and a scrolling list of non-featured content forms a powerful way to pull people into your content. When you use Unflow and its spaces, with a smart layout like this, you can push engagement to the limit.

import SwiftUI
import UnflowUI

/// A simple stack of a rounded image, and the title of an opener. If a subtitle is available, it'll show that too.
struct OpenerTile: View {
    let opener: Opener
    
    var body: some View {
        Button {
            try? UnflowSDK.client.openScreen(withID: opener.id)
        } label: {
            VStack(alignment: .leading, spacing: 8) {
                image
                text
            }
        }
    }
    
    var image: some View {
        Rectangle()
            .foregroundColor(.clear)
            .aspectRatio(1.33, contentMode: .fit)
            .background(
                RemoteImage(url: URL(string: opener.imageURL ?? ""))
                    .aspectRatio(contentMode: .fill)
            )
            .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
    }
    
    var text: some View {
        VStack(alignment: .leading) {
            Text(opener.title)
                .font(.subheadline.weight(.semibold))
                .foregroundColor(.primary)
            if let subtitle = opener.subtitle {
                Text(subtitle)
                    .font(.subheadline.weight(.medium))
                    .foregroundColor(.secondary)
            }
        }
        .multilineTextAlignment(.leading)
    }
}