As a developer who relies heavily on SwiftUI, working with views is nothing new. Ever since I first encountered SwiftUI’s declarative approach to programming, I’ve loved the feeling of writing code. But the more you touch it, the more problems you run into. At first, I simply referred to many of the problems as paranormal phenomena and thought it was most likely due to the immaturity of SwiftUI. With continuous learning and exploration, I found that a considerable part of the problems are caused by my own insufficient cognition, which can be completely improved or avoided.

I’ll explore the ViewBuilder for building SwiftUI views in the next two posts. The previous article introduced the implementers behind ViewBuilder — Result Builders; In the next part, we will explore the secrets of SwiftUI view further by copying the ViewBuilder.

The original post was posted on my blog wwww.fatbobman.com

Welcome to subscribe my public account: [Elbow’s Swift Notepad]

This paper hopes to achieve the goal

I hope that after reading the two articles, you can eliminate or alleviate your confusion about the following questions:

  • How to support ViewBuilder for custom views and methods
  • Why do complex SwiftUI views tend to freeze or have compile timeouts on Xcode
  • Why “Extra arguments” error (only a limited number of views can be placed at the same level)
  • Why be careful with AnyView
  • How to avoid using AnyView
  • Why does the view contain all selected branch type information whether it is displayed or not
  • Why is the body of most official view types Never
  • The difference between a ViewModifier and a modifier for a particular view type
  • The difference between SwiftUI’s implicit and explicit identities

What are Result builders

introduce

Result Builders allow functions to implicitly build result values from a series of components, arranging them according to build rules set by the developer. By translating function statement application builders, Result Builders provide the ability to create new domain-specific languages (DSLS) in Swift (the ability of these builders is intentionally limited by Swift to preserve the dynamic semantics of the original code).

DSLS created using Result Builders use simpler, less invalid content, and easier code to understand (especially when expressing logical content with choices, loops, and so on) than common DSLS implemented using point syntax, such as:

Use point syntax (Plot) :

.div(
    .div(
        .forEach(archiveItems.keys.sorted(by: >)) { absoluteMonth in
            .group(
                .ul(
                    .forEach(archiveItems[absoluteMonth]) { item in
                        .li(
                            .a(
                                .href(item.path),
                                .text(item.title)
                            )
                        )
                    }
                ),
                .if( show, 
                    .text("hello"), 
                  else: .text("wrold"),)}))Copy the code

Builders created by Result Builders (swift-html) :

Div {
    Div {
        for i in 0..<100 {
            Ul {
                for item in archiveItems[i] {
                    li {
                        A(item.title)
                            .href(item.path)
                    }
                }
            }
            if show {
                Text("hello")}else {
                Text("world")}}}}Copy the code

History and Development

Since Swift 5.1, Result Builders have been hidden in the Swift language (then called Function Builder) with the introduction of SwiftUI. As Swift and SwiftUI continue to evolve, they are officially included in Swift 5.4. Apple makes extensive use of this feature in the SwiftUI framework. In addition to the most common ViewBuilder, other features include: AccessibilityRotorContentBuilder, CommandsBuilder, LibraryContentBuilder, SceneBuilder, TableColumnBuilder, TableRowBuilder, T OolbarContentBuilder, WidgetBundleBuilder, etc. In addition, the Regex Builder DSL has appeared in the latest Swift proposal. Other developers have taken advantage of this feature to create a number of third-party libraries.

Basic usage

Define the builder type

A result builder type must meet two basic requirements.

  • It must be annotated with @resultbuilder, which indicates that it is intended to be used as a resultBuilder type, and allows it to be used as a custom property.

  • It must implement at least one type method called buildBlock

Such as:

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ parts: String...). -> String {
        parts.map{"⭐ ️" + $0 + "🌈"}.joined(separator: "")}}Copy the code

With the above code, we have created a result builder with minimal functionality. The usage method is as follows:

@StringBuilder
func getStrings(a) -> String {
    Pleasant Goat
    "Beautiful Goat."
    "Big Big Wolf"
}

// ⭐️ 🌈 ⭐ 🌈 ⭐ Beauty sheep 🌈 ⭐ Wolf 🌈
Copy the code

Provide a sufficient subset of the resulting build methods for the builder type

  • buildBlock(_ components: Component...) -> Component

    The combined result used to build a block of statements. Each result builder should provide at least one concrete implementation of it.

  • buildOptional(_ component: Component?) -> Component

    Used to deal with partial results that may or may not occur in a particular execution. When a result builder provides buildOptional(_:), translated functions can use if statements without else, and support for iflets is also provided.

  • BuildEither (first: Component) -> Component and buildEither(second: Component) -> Component

    Used to set up partial results under different paths of select statements. When a result builder provides an implementation of both methods, the translated function can use an if statement with else as well as a switch statement.

  • buildArray(_ components: [Component]) -> Component

    Partial results used to collect from all iterations of a loop. After a result builder provides an implementation of buildArray(_:), the translated function can use for… In statement.

  • buildExpression(_ expression: Expression) -> Component

    It allows the result builder to distinguish between expression types and component types, providing context type information for statement expressions.

  • buildFinalResult(_ component: Component) -> FinalResult

    Used to rewrap the outermost buildBlock result. For example, let the result builder hide some types that it doesn’t want to expose (cast to be exposed).

  • buildLimitedAvailability(_ component: Component) -> Component

    Used to convert partial results of buildBlock produced in a restricted environment (for example, if #available) into results that can be adapted to any environment to improve API compatibility.

As a result, the builder uses the AD Hoc protocol, which means we can override the above methods more flexibly. In some cases, however, the result builder’s translation process can change its behavior depending on whether the result builder type implements a method.

Each of these methods will be described in detail with examples. The term “result builder” will be shortened to “Builder” below.

Example 1: AttributedStringBuilder

In this case, we’ll create a builder that declares AttributedString. For those of you who aren’t familiar with AttributedString, you can read my other blog on AttributedString — it’s not just about making text prettier.

The full code for example 1 can be obtained here (Demo1)

After this example, we can declare AttributedString as follows:

@AttributedStringBuilder
var text: Text {
    "_*Hello*_"
        .localized()
        .color(.green)

    if case .male = gender {
        " Boy!"
            .color(.blue)
            .bold()

    } else {
        " Girl!"
            .color(.pink)
            .bold()
    }
}
Copy the code

Create the builder type

@resultBuilder
public enum AttributedStringBuilder {
    // The corresponding block does not move component
    public static func buildBlock(a) -> AttributedString{.init("")}// Block with n components (n is a positive integer)
    public static func buildBlock(_ components: AttributedString...). -> AttributedString {
        components.reduce(into: AttributedString("")) { result, next in
            result.append(next)
        }
    }
}
Copy the code

We first created a builder called AttributedStringBuilder and implemented two buildBlock methods for it. The builder automatically selects the corresponding method when translating.

Now, any number of components (AttributedString) can be supplied to a block, which buildBlock will convert to the specified result (AttributedString).

When implementing the buildBlock method, the return data types of components and block should be defined according to the actual requirements, not necessarily the same.

Translate blocks using builders

The builder can be invoked explicitly, for example:

@AttributedStringBuilder // Make it clear
var myFirstText: AttributedString {
    AttributedString("Hello")
    AttributedString("World")}// "HelloWorld"

@AttributedStringBuilder
func mySecondText(a) -> AttributedString {} BuildBlock () -> AttributedString
/ / ""
Copy the code

The builder can also be called implicitly:

// On the API side
func generateText(@AttributedStringBuilder _ content: () - >AttributedString) -> Text {
    Text(content())
}

// call implicitly on the client side
VStack {
    generateText {
        AttributedString("Hello")
        AttributedString(" World")}}struct MyTest {
    var content: AttributedString
    // Annotate in the constructor
    init(@AttributedStringBuilder _ content: () - >AttributedString) {
        self.content = content()
    }
}

// implicit call
let attributedString = MyTest {
    AttributedString("ABC")
    AttributedString("BBC")
}.content
Copy the code

Either way, if the return keyword is used at the end of the block to return the result, the builder will automatically ignore the translation process. Such as:

@AttributedStringBuilder 
var myFirstText: AttributedString {
    AttributedString("Hello") // This statement will be ignored
    return AttributedString("World") // Just return World
}
// "World"
Copy the code

To use a syntax not supported by the builder in a block, the developer will try to use return to return the resulting value, which should be avoided. As a result, the developer will lose the flexibility afforded by translation through the builder.

The following code has a completely different state with and without a builder translation:

// The builder interprets automatically, the block returns only the final composite result, and the code executes normally
@ViewBuilder
func blockTest(a) -> some View {
    if name.isEmpty {
        Text("Hello anonymous!")}else {
        Rectangle()
            .overlay(Text("Hello \(name)"))}}// The constructor's translation behavior is ignored for return. The two branches of the block return two different types, which cannot satisfy the requirement that the same type must be returned (some View), and the compilation fails.
@ViewBuilder
func blockTest(a) -> some View {
    if name.isEmpty {
        return Text("Hello anonymous!")}else {
        return Rectangle()
            .overlay(Text("Hello \(name)"))}}Copy the code

Calling code in a block in the following way can do other work without affecting the builder translation process:

@AttributedStringBuilder 
var myFirstText: AttributedString {
    let _ = print("update") // Declare that the statement does not affect the builder's translation
    AttributedString("Hello") 
    AttributedString("World")}Copy the code

Adding modifier

Before moving on to the rest of the constructor’s methods, let’s add some ViewModifier to AttributedStringBuilder to make it as easy to style AttributedString as SwiftUI does. Add the following code:

public extension AttributedString {
    func color(_ color: Color) -> AttributedString {
        then {
            $0.foregroundColor = color
        }
    }

    func bold(a) -> AttributedString {
        return then {
            if var inlinePresentationIntent = $0.inlinePresentationIntent {
                var container = AttributeContainer()
                inlinePresentationIntent.insert(.stronglyEmphasized)
                container.inlinePresentationIntent = inlinePresentationIntent
                let _ = $0.mergeAttributes(container)
            } else {
                $0.inlinePresentationIntent = .stronglyEmphasized
            }
        }
    }

    func italic(a) -> AttributedString {
        return then {
            if var inlinePresentationIntent = $0.inlinePresentationIntent {
                var container = AttributeContainer()
                inlinePresentationIntent.insert(.emphasized)
                container.inlinePresentationIntent = inlinePresentationIntent
                let _ = $0.mergeAttributes(container)
            } else {
                $0.inlinePresentationIntent = .emphasized
            }
        }
    }

    func then(_ perform: (inout Self) - >Void) -> Self {
        var result = self
        perform(&result)
        return result
    }
}
Copy the code

Because AttributedString is a value type, we need to create a new copy and modify the attributes on it. Use the modifier as follows:

@AttributedStringBuilder
var myFirstText: AttributedString {
    AttributedString("Hello")
         .color(.red)
    AttributedString("World")
         .color(.blue)
         .bold()
}
Copy the code

I’ve written very little code, but it’s starting to feel like a DSL.

Simplified representation

Since blocks can only accept a specific type of Component (AttributedString), each line of code needs to be prefixed with an AttributedString type, which can be a lot of work and a bad reading experience. The process can be simplified by using buildExpression.

Add the following code:

public static func buildExpression(_ string: String) -> AttributedString {
    AttributedString(string)
}
Copy the code

The builder first converts String to AttributedString and then passes it into buildBlock. After adding the code above, we replace AttributedString directly with String:

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    "World"
}
Copy the code

Now, however, we have a new problem — we can’t mix String and AttributedString in blocks. This is because if we don’t provide a custom buildExpression implementation, the builder will use buildBlock to infer that the component type is AttributedString. Once we provide custom buildExpression, the builder will no longer use automatic inference. The solution is to create a buildExpression for AttributedString as well:

public static func buildExpression(_ attributedString: AttributedString) -> AttributedString {
    attributedString
}
Copy the code

Now you can mix the two in a block.

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    AttributedString("World")}Copy the code

Another problem is that we can’t directly use the modifier we created earlier under String. Because the modifier was for AttributedString, the dot syntax will only use the String method. You can either extend the String to convert it to AttributedString, or add a modifier to the String. Let’s take the second, more tedious approach for now:

public extension String {
    func color(_ color: Color) -> AttributedString {
        AttributedString(self)
            .color(color)
    }

    func bold(a) -> AttributedString {
        AttributedString(self)
            .bold()
    }
    
    func italic(a) -> AttributedString {
        AttributedString(self)
            .italic()
    }
}
Copy the code

Now we can declare quickly and clearly.

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
         .color(.red)
    "World"
         .color(.blue)
         .bold()
}
Copy the code

AttributedString provides support for localized strings and some Markdown syntax, but only for AttributedString constructed from the String.LocalizationValue type, You can solve this problem by:

public extension String {
    func localized(a) -> AttributedString{.init(localized: LocalizationValue(self))}}Copy the code

Convert a String to an AttributedString constructed with string.localizationValue, and then you can use the modifier written for AttributedString directly. (You can do the same for Strings, To avoid repeating the modifier for String.

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
         .color(.red)
    "~**World**~"
         .localized()
         .color(.blue)
         //.bold() uses Markdown syntax to describe bold. Currently, with the Markdown syntax, setting inlinePresentationIntent directly conflicts.
}
Copy the code

Logic for builder translation

Understanding how the builder translates will help you learn later.

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    AttributedString("World")
         .color(.red)
}
Copy the code

As the builder processes the above code, it will translate into the following code:

var myFirstText: AttributedString {
    let _a = AttributedStringBuilder.buildExpression("Hello")  // Calls buildExpression for String
    let _b = AttributedStringBuilder.buildExpression(AttributedString("World").color(.red)) // Calls buildExpression against AtributedString
    return AttributedStringBuilder.buildBlock(_a,_b) // Call the multi-parameter buildBloack
}
Copy the code

The top and bottom pieces of code are completely equivalent, and Swift automatically does this for us behind the scenes.

When learning to create a builder, it helps to have a better sense of when to call each method by adding print commands inside the implementation of the builder method.

Added select statement support (if without else)

Result Builders use a completely different internal processing mechanism for selecting statements that contain and do not contain else. For ifs that do not contain else, we simply implement the following method:

public static func buildOptional(_ component: AttributedString?). -> AttributedString {
    component ?? .init("")}Copy the code

When the builder calls this method, it passes in different parameters depending on whether the condition is reached. If the condition is not reached, nil is passed in. Use method:

var show = true
@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    if show {
        "World"}}Copy the code

After adding the buildOptional implementation, the builder will also support the if let syntax, such as:

var name:String? = "fat"
@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    if let name = name {
        " \(name)"}}Copy the code

BuildOptional corresponds to the translation code:

// The logic corresponding to the if code above
var myFirstText: AttributedString {
    let _a = AttributedStringBuilder.buildExpression("Hello")
    var vCase0: AttributedString?
    if show = = true {
        vCase0 = AttributedStringBuilder.buildExpression("World")}let _b = AttributedStringBuilder.buildOptional(vCase0)
    return AttributedStringBuilder.buildBlock(_a, _b)
}

// The logic corresponding to the iflet code above
var myFirstText: AttributedString {
    let _a = AttributedStringBuilder.buildExpression("Hello")
    var vCase0:AttributedString?
    if let name = name {
        vCase0 = AttributedStringBuilder.buildExpression(name)
    }
    let _b = AttributedStringBuilder.buildOptional(vCase0)
    return AttributedStringBuilder.buildBlock(_a,_b)
}
Copy the code

That’s why we only need to implement buildOptional to support both if (without else) and if lets.

Added support for multi-branch selection

For the if else and switch syntax, you implement buildEither(first:) and buildEither(second:) :

// Call the branch whose condition is true (left branch)
public static func buildEither(first component: AttributedString) -> AttributedString {
    component
}

// Call the branch whose condition is no (right branch)
public static func buildEither(second component: AttributedString) -> AttributedString {
    component
}
Copy the code

The usage method is as follows:

var show = true
@AttributedStringBuilder
var myFirstText: AttributedString {
    if show {
        "Hello"
    } else {
        "World"}}Copy the code

The corresponding translation code is:

var myFirstText: AttributedString {
    let vMerged: AttributedString
    if show {
        vMerged = AttributedStringBuilder.buildEither(first: AttributedStringBuilder.buildExpression("Hello"))}else {
        vMerged = AttributedStringBuilder.buildEither(second: AttributedStringBuilder.buildExpression("World"))}return AttributedStringBuilder.buildBlock(vMerged)
}
Copy the code

When else statements are included, the builder generates a binary tree at translation time, with each result assigned to one of the leaves. The builder will still handle branches that do not use else in if else, for example:

var show = true
var name = "fatbobman"
@AttributedStringBuilder
var myFirstText: Text {
    if show {
        "Hello"
    } else if name.count > 5 {
        name
    }
}
Copy the code

The translated code is:

// The translated code
var myFirstText: AttributedString {
    let vMerged: AttributedString
    if show {
        vMerged = AttributedStringBuilder.buildEither(first: AttributedStringBuilder.buildExpression("Hello"))}else {
        // First use buildOptional to handle cases that do not include else
        var vCase0: AttributedString?
        if name.count > 5 {
            vCase0 = AttributedStringBuilder.buildExpression(name)
        }
        let _a = AttributedStringBuilder.buildOptional(vCase0)
        // The right branch is finally merged to vMerged
        vMerged = AttributedStringBuilder.buildEither(second: _a)
    }
    return AttributedStringBuilder.buildBlock(vMerged)
}
Copy the code

Support for the Switch works the same way. The builder applies the above rules recursively as it translates.

You may wonder whether buildEither’s implementation is so simple that it doesn’t make much sense. This was a question many people had during the Result Builders proposal. The Swift design has its own niche. In the next article, We’ll see how A ViewBuilder can store type information for all branches through buildEither.

Support for… In circulation

for… The IN statement collects the results of all iterations together into an array and passes it to buildArray. Providing buildArray implementations enables the builder to support loop statements.

// In this case, we concatenate all the iteration results directly to generate an AttributedString
public static func buildArray(_ components: [AttributedString]) -> AttributedString {
    components.reduce(into: AttributedString("")) { result, next in
        result.append(next)
    }
}
Copy the code

Usage:

@AttributedStringBuilder
func test(count: Int) -> Text {
    for i in 0..<count {
        " \(i) "}}Copy the code

Corresponding translation code:

func test(count: Int) -> AttributedString {
    var vArray = [AttributedString] ()for i in 0..<count {
        vArray.append(AttributedStringBuilder.buildExpression(" \(i)"))}let _a = AttributedStringBuilder.buildArray(vArray)
    return AttributedStringBuilder.buildBlock(_a)
}
Copy the code

Improving version compatibility

If an implementation of buildLimitedAvailability is provided, the builder provides a check for API availability (such as if #available(..)). ). This is very common with SwiftUI, for example some Views or modifiers only support newer platforms and we need to provide additional content for platforms that are not supported.

public static func buildLimitedAvailability(_ component: AttributedString) -> AttributedString{.init("")}Copy the code

The logic is very simple, use buildLimitedAvailablility does not support the information such as the types, methods for removing.

Usage:

// Create a method not supported by the current platform
@available(macOS 13.0.iOS 16.0.*)
public extension AttributedString {
    func futureMethod(a) -> AttributedString {
        self}}@AttributedStringBuilder
var text: AttributedString {
    if #available(macOS 13.0.iOS 16.0.*) {
        AttributedString("Hello macOS 13")
            .futureMethod()
    } else {
        AttributedString("Hi Monterey")}}Copy the code

The corresponding translation logic is:

var text: AttributedString {
    let vMerge: AttributedString
    if #available(macOS 13.0.iOS 16.0.*) {
        let _temp = AttributedStringBuilder
            .buildLimitedAvailability( // Erase the type or method
                AttributedStringBuilder.buildExpression(AttributedString("Hello macOS 13").futureMethod())
            )
        vMerge = AttributedStringBuilder.buildEither(first: _temp)
    } else {
        let _temp = AttributedStringBuilder.buildExpression(AttributedString("Hi Monterey"))
        vMerge = AttributedStringBuilder.buildEither(second: _temp)
    }
    return = AttributedStringBuilder.buildBlock(vMerge)
}
Copy the code

Repackage the results

If we provide an implementation of buildFinalResult, the builder will use buildFinalResult to re-convert the result at the end of the translation, with the value returned by buildFinalResult as the final result.

In most cases, we don’t need to implement buildFinalResult, and the builder will take buildBlock’s return as the final result.

public static func buildFinalResult(_ component: AttributedString) -> Text {
    Text(component)
}
Copy the code

To demonstrate, in this example we convert AttributedString to Text with buildFinalResult:

@AttributedStringBuilder
var text: Text {  // The final result type has been translated to Text
    "Hello world"
}
Copy the code

Corresponding translation logic:

var text: Text {
    let _a = AttributedStringBuilder.buildExpression("Hello world")
    let _blockResult = AttributedStringBuilder.buildBlock(_a)
    return AttributedStringBuilder.buildFinalResult(_blockResult)
}
Copy the code

At this point, we have achieved what we set out to do at the beginning of this section. However, the current implementation does not give us the possibility to create containers such as SwiftUI, which will be addressed in Example 2.

Example 2: AttributedTextBuilder

The full code for example 2 is available here (Demo2)

The deficiencies of version one

  • You can only add the modifier to Component (AttributedString, String) one by one
  • Unable to dynamically layout,buildBlockTo concatenate all contents, line breaks can only be added separately\nTo implement the

Use protocols instead of types

The main reason for this problem is that the component of buildBlock above is an AttributedString specific type, limiting our ability to create containers (other components). The SwiftUI View solution is to replace a specific type with a protocol and make AttributedString conform to that protocol.

First, we’ll create a new protocol — AttributedText:

public protocol AttributedText {
    var content: AttributedString { get }
    init(_ attributed: AttributedString)
}

extension AttributedString: AttributedText {
    public var content: AttributedString {
        self
    }

    public init(_ attributed: AttributedString) {
        self = attributed
    }
}
Copy the code

Make AttributedString conform to this protocol:

extension AttributedString: AttributedText {
    public var content: AttributedString {
        self
    }

    public init(_ attributed: AttributedString) {
        self = attributed
    }
}
Copy the code

Create a new builder, AttributedTextBuilder. The big change is to change all component types to AttributedText.

@resultBuilder
public enum AttributedTextBuilder {
    public static func buildBlock(a) -> AttributedString {
        AttributedString("")}public static func buildBlock(_ components: AttributedText...). -> AttributedString {
        let result = components.map { $0.content }.reduce(into: AttributedString("")) { result, next in
            result.append(next)
        }
        return result.content
    }

    public static func buildExpression(_ attributedString: AttributedText) -> AttributedString {
        attributedString.content
    }

    public static func buildExpression(_ string: String) -> AttributedString {
        AttributedString(string)
    }

    public static func buildOptional(_ component: AttributedText?). -> AttributedString {
        component?.content ?? .init("")}public static func buildEither(first component: AttributedText) -> AttributedString {
        component.content
    }

    public static func buildEither(second component: AttributedText) -> AttributedString {
        component.content
    }

    public static func buildArray(_ components: [AttributedText]) -> AttributedString {
        let result = components.map { $0.content }.reduce(into: AttributedString("")) { result, next in
            result.append(next)
        }
        return result.content
    }

    public static func buildLimitedAvailability(_ component: AttributedText) -> AttributedString{.init("")}}Copy the code

Create modifier for AttributedText:

public extension AttributedText {
    func transform(_ perform: (inout AttributedString) - >Void) -> Self {
        var attributedString = self.content
        perform(&attributedString)
        return Self(attributedString)
    }

    func color(_ color: Color) -> AttributedText {
        transform {
            $0 = $0.color(color)
        }
    }

    func bold(a) -> AttributedText {
        transform {
            $0 = $0.bold()
        }
    }

    func italic(a) -> AttributedText {
        transform {
            $0 = $0.italic()
        }
    }
}
Copy the code

At this point we have the ability to create custom view controls like in SwiftUI.

Create the Container

Container is similar to Group in SwiftUI. You can use the modifier for all elements in a Container without changing the layout.

public struct Container: AttributedText {
    public var content: AttributedString

    public init(_ attributed: AttributedString) {
        content = attributed
    }

    public init(@AttributedTextBuilder _ attributedText: () - >AttributedText) {
        self.content = attributedText().content
    }
}
Copy the code

Because the Container is also compliant with the AttributedText protocol, it is treated as a Component and can be applied with a modifier. Usage:

@AttributedTextBuilder
var attributedText: AttributedText {
    Container {
        "Hello "
            .localized()
            .color(.red)
            .bold()

        "~World~"
            .localized()
    }
    .color(.green)
    .italic()
}
Copy the code

When you execute the code above, you’ll notice that the red Hello is now green, which is not what we expected. In SwiftUI, the inner Settings should take precedence over the outer Settings. To fix this, we need to make some changes to AttributedString’s modifier.

public extension AttributedString {
    func color(_ color: Color) -> AttributedString {
        var container = AttributeContainer()
        container.foregroundColor = color
        return then {
            for run in $0.runs {
                $0[run.range].mergeAttributes(container, mergePolicy: .keepCurrent)
            }
        }
    }

    func bold(a) -> AttributedString {
        return then {
            for run in $0.runs {
                if var inlinePresentationIntent = run.inlinePresentationIntent {
                    var container = AttributeContainer()
                    inlinePresentationIntent.insert(.stronglyEmphasized)
                    container.inlinePresentationIntent = inlinePresentationIntent
                    let _ = $0[run.range].mergeAttributes(container)
                } else {
                    $0[run.range].inlinePresentationIntent = .stronglyEmphasized
                }
            }
        }
    }

    func italic(a) -> AttributedString {
        return then {
            for run in $0.runs {
                if var inlinePresentationIntent = run.inlinePresentationIntent {
                    var container = AttributeContainer()
                    inlinePresentationIntent.insert(.emphasized)
                    container.inlinePresentationIntent = inlinePresentationIntent
                    let _ = $0[run.range].mergeAttributes(container)
                } else {
                    $0[run.range].inlinePresentationIntent = .emphasized
                }
            }
        }
    }

    func then(_ perform: (inout Self) - >Void) -> Self {
        var result = self
        perform(&result)
        return result
    }
}
Copy the code

By iterating through the Run view of AttributedString, we implement the inner setting of the same attribute taking precedence over the outer setting.

Create com.lowagie.text.paragraph

A Paragraph creates line breaks at the beginning and end of its content.

public struct Paragraph: AttributedText {
    public var content: AttributedString
    public init(_ attributed: AttributedString) {
        content = attributed
    }

    public init(@AttributedTextBuilder _ attributedText: () - >AttributedText) {
        self.content = "\n" + attributedText().content + "\n"}}Copy the code

By making the protocol a component, you open up more possibilities for the builder.

Improvements and deficiencies of Result Builders

Improvements that have been made

Since Swift 5.1, Result Builders have gone through several versions of improvements that add some functionality and address some performance issues:

  • addedbuildOptionalAnd cancel thebuildeIf, in keeping with the rightif(do not containelse) support was added at the same timeif letThe support of
  • Supported as of SwiftUI 2.0switchThe keyword
  • Modified Swift 5.1buildBlockThe grammatical translation mechanism of the. Disables “backward” propagation of parameter types. This is the primary cause of the “expression too complex to be solved in a reasonable time” compilation error in the early SwiftUI view code

Current deficiencies

  • Lack of partial selection and control, such as guard, break, continue

  • Lack of ability to restrict naming to the builder context

    It is common for DSLS to introduce shorthand words, and currently creating a component for a builder can only take the form of creating new data types (such as Container, Paragraph, above) or global functions. Hopefully, in the future, you’ll be able to restrict these names to context only and not introduce them into the global scope.

subsequent

The basic functionality of Result Builders is very simple, and we have only a small amount of code for builder methods in the previous article. But creating a good, easy-to-use DSL is a lot of work, and developers should weigh the benefits of using Result Builders against their actual needs.

In the next article, we will try to copy a builder that is consistent with the basic shape of ViewBuilder. We believe that the process of copying will give you a deeper understanding of ViewBuilder and SwiftUI views.

I hope this article has been helpful to you.

The original post was posted on my blog wwww.fatbobman.com

Welcome to subscribe my public account: [Elbow’s Swift Notepad]