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,
buildBlock
To concatenate all contents, line breaks can only be added separately\n
To 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:
- added
buildOptional
And cancel thebuildeIf
, in keeping with the rightif
(do not containelse
) support was added at the same timeif let
The support of - Supported as of SwiftUI 2.0
switch
The keyword - Modified Swift 5.1
buildBlock
The 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]