Codementor Events

Using result builders for action sheets in SwiftUI

Published May 03, 2021Last updated Oct 29, 2021
Using result builders for action sheets in SwiftUI

One of the key features of SwiftUI is a declarative syntax for layout. It is available thanks to result builders , previously called function builders. With result builders, we can implicitly build up a final value from a sequence of components. The final revision of this feature is released in Swift 5.4, and Xcode 12.5 suggests code completions and Fix-Its for it. I guess it's a good sign for exploring it and making action sheets more declarative!

Preparation

We create a simple SwiftUI app, where we can select ingredients for sandwich.

struct ContentView: View {
    
    @State private var ingredients: [String] = []
    @State private var isActionSheetPresented = false
    
    var body: some View {
        VStack {
            Text(ingredients.joined())
                .font(.system(.title))
            Button("Make a sandwich") {
                isActionSheetPresented = true
            }
        }
        .padding()
        .actionSheet(isPresented: $isActionSheetPresented) {
            let buttons = [ActionSheet.Button.default(Text("🍞")) {
                ingredients.append("🍞")
            },
            ActionSheet.Button.cancel()]
            return ActionSheet(title: Text("Select an ingredient"), message: nil, buttons: buttons)
        }
    }
}

When we tap on the button, ActionSheet is presented with buttons from the array in the initializer. The syntax for action buttons, especially with defined actions, looks a bit complicated. Let's improve it with a custom result builder.

Basics

We create a ButtonsBuilder struct with @resultBuilder attribute:

@resultBuilder
struct ButtonsBuilder {}

To start using it, we must implement at least one static buildBlock function:

@resultBuilder
struct ButtonsBuilder {
    
    static func buildBlock(_ components: ActionSheet.Button...) -> [ActionSheet.Button] {
        components
    }
}

Here we have a variadic parameter with ActionSheet.Button and just return it as is.

Because ActionSheet knows nothing about our builder, we create a new initializer with title, message, and the builder:

extension ActionSheet {
    
    init(title: Text, message: Text? = nil, @ButtonsBuilder buttons: () -> [ActionSheet.Button]) {
        self.init(title: title, message: message, buttons: buttons())
    }
}

Now we're ready to refactor ActionSheet configuration:

.actionSheet(isPresented: $isActionSheetPresented) {
    ActionSheet(title: Text("Select an ingredient"), message: nil) {
        ActionSheet.Button.default(Text("🍞")) {
            ingredients.append("🍞")
        }
        ActionSheet.Button.cancel()
    }
}

Looks great!

What if?.. Working with conditions

Result builders may build a partial result depending on some conditions. In our app, we add a new State and Toggle . If it is enabled, we add cucumbers and tomatos otherwise.

// In States section
@State private var likeCucumbers = true

// Below Text in ContentView
Toggle("I love cucumbers", isOn: $likeCucumbers)

To support if-else conditions in our builder, we must implement buildEither(first:) and buildEither(second:) functions:

@resultBuilder
struct ButtonsBuilder {
    
    ...
    
    static func buildEither(first components: [ActionSheet.Button]) -> [ActionSheet.Button] {
        components
    }
    
    static func buildEither(second components: [ActionSheet.Button]) -> [ActionSheet.Button] {
        components
    }
}

If we try to add if-else statement like this:

if likeCucumbers {
  ActionSheet.Button.default(Text("πŸ₯’")) {
    ingredients.append("πŸ₯’")
  }
}
else {
  ActionSheet.Button.default(Text("πŸ…")) {
    ingredients.append("πŸ…")
  }
}

We have an erro:

Cannot pass array of type '[ActionSheet.Button]' (aka 'Array<Alert.Button>') as variadic arguments of type 'ActionSheet.Button' (aka 'Alert.Button')

We can solve the error by defining a new protocol and implementing it by both a single ActionSheet.Button and a collection of ButtonsConvertible:

protocol ButtonsConvertible {
    
    var buttons: [ActionSheet.Button] { get }
}

extension ActionSheet.Button: ButtonsConvertible {
    
    var buttons: [ActionSheet.Button] {
        [self]
    }
}

extension Array: ButtonsConvertible where Element == ButtonsConvertible {

    var buttons: [ActionSheet.Button] { self.flatMap(\.buttons) }
}

In ButtonsBuilder we replace all ActionSheet.Button with ButtonsConvertible. And finally, we implement buildFinalResult function that gets all ButtonsConvertible and maps it to buttons:

@resultBuilder
struct ButtonsBuilder {
    
    static func buildBlock(_ components: ButtonsConvertible...) -> [ButtonsConvertible] {
        components
    }
    
    ...
    
    static func buildFinalResult(_ components: [ButtonsConvertible]) -> [ActionSheet.Button] {
        components.flatMap(\.buttons)
    }
}

Using ForEach for Actions

SwiftUI has an awesome ForEach element. It gets different data collections and converts them to views via @ViewBuilder. I was wondering if there is any chance to use it for buttons πŸ€”. Of course, let's start with an extension:

extension ForEach: ButtonsConvertible where Content == ActionSheet.Button {

    var buttons: [ActionSheet.Button] {
        data.map(content)
    }
}

Here we declare that Content generic must be ActionSheet.Button and map data to buttons via content closure.

ActionSheet.Button is a simple typealias for Alert.Button, and Alert.Button is just a struct that doesn't conform View protocol. To solve it, we implement it and return Never for the body:

extension ActionSheet.Button: View {

    public var body: Never {
        fatalError()
    }
}

Because we don't use ForEach for rendering, the body will never be called. And it works now!

ForEach(["πŸ§…", "πŸ§„"], id: \.self) { string in
  ActionSheet.Button.default(Text(string)) {
    ingredients.append(string)
  }
}

We can explicitly add ids like in the example, use Identifiable array or even ranges inside ForEach. The downside of this trick is that we can accidentally use ActionSheet.Button inside any body and get fatalError in runtime.

Conclusion

Result builder is a great enhancement in Swift language. In certain cases, it improves code readability dramatically. If you want to play with the example, check ResultBuilderExample repo.

Thanks for reading πŸ™

References

Twitter Β· Telegram Β· Github

Discover and read more posts from Artem Novichkov
get started
post commentsBe the first to share your opinion
hypee lixir
7 months ago

Legal and Ethical Considerations: It’s important to note that the legality of HDO Box can be a complex issue. Depending on the specific version or source of the app, it may involve copyrighted content that is distributed without proper authorization. Engaging with such content may infringe upon copyright laws and ethical guidelines, potentially leading to legal consequences. https://hdobox.app/hdo-box-firestick/

quest craft
a year ago

Result builder is a great enhancement in Swift language. In certain cases, it improves code readability dramatically. Filmplus App

Anupam
a year ago

Here is an example of how to create an action sheet with a Result Builder:

struct ContentView: View {
@State private var showActionSheet = false

var body: some View {
    Button(action: {
        self.showActionSheet = true
    }) {
        Text("Show Action Sheet")
    }
    .actionSheet(isPresented: $showActionSheet) {
        ActionSheet(title: Text("Action Sheet"), message: Text("Choose an option"), buttons: [
            .default(Text("Option 1"), action: {
                // handle option 1
            }),
            .default(Text("Option 2"), action: {
                // handle option 2
            }),
            .cancel()
        ])
    }
}

}

<a href=β€œhttps://cloudstream.ws/”>cloudstream</a> provides best quality of Movies, TV Shows, Anime at just one click on your android devices
It allows you to wach your favourite Movies and Shows in all languages along with subtitle with hd quality
It has amazing features of smooth downloading to watch offline without getting disturbed by your internet

Show more replies