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 Openers | A sample article created entirely on the dashboard | When 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)
}
}