What Enables Swiftui'S Dsl

What enables SwiftUI's DSL?

As Martin says, if you look at the documentation for VStack's init(alignment:spacing:content:), you can see that the content: parameter has the attribute @ViewBuilder:

init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
@ViewBuilder content: () -> Content)

This attribute refers to the ViewBuilder type, which if you look at the generated interface, looks like:

@_functionBuilder public struct ViewBuilder {

/// Builds an empty view from an block containing no statements, `{ }`.
public static func buildBlock() -> EmptyView

/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
/// through unmodified.
public static func buildBlock(_ content: Content) -> Content
where Content : View
}

The @_functionBuilder attribute is a part of an unofficial feature called "function builders", which has been pitched on Swift evolution here, and implemented specially for the version of Swift that ships with Xcode 11, allowing it to be used in SwiftUI.

Marking a type @_functionBuilder allows it to be used as a custom attribute on various declarations such as functions, computed properties and, in this case, parameters of function type. Such annotated declarations use the function builder to transform blocks of code:

  • For annotated functions, the block of code that gets transformed is the implementation.
  • For annotated computed properties, the block of code that gets transformed is the getter.
  • For annotated parameters of function type, the block of code that gets transformed is any closure expression that is passed to it (if any).

The way in which a function builder transforms code is defined by its implementation of builder methods such as buildBlock, which takes a set of expressions and consolidates them into a single value.

For example, ViewBuilder implements buildBlock for 1 to 10 View conforming parameters, consolidating multiple views into a single TupleView:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
/// through unmodified.
public static func buildBlock<Content>(_ content: Content)
-> Content where Content : View

public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1)
-> TupleView<(C0, C1)> where C0 : View, C1 : View

public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
-> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View

// ...
}

This allows a set of view expressions within a closure passed to VStack's initialiser to be transformed into a call to buildBlock that takes the same number of arguments. For example:

struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
}
}

gets transformed into a call to buildBlock(_:_:):

struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
}
}
}

resulting in the opaque result type some View being satisfied by TupleView<(Text, Text)>.

You'll note that ViewBuilder only defines buildBlock up to 10 parameters, so if we attempt to define 11 subviews:

  var body: some View {
// error: Static member 'leading' cannot be used on instance of
// type 'HorizontalAlignment'
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}
}

we get a compiler error, as there's no builder method to handle this block of code (note that because this feature is still a work-in-progress, the error messages around it won't be that helpful).

In reality, I don't believe people will run into this restriction all that often, for example the above example would be better served using the ForEach view instead:

  var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
Text("Hello world \(i)")
}
}
}

If however you do need more than 10 statically defined views, you can easily workaround this restriction using the Group view:

  var body: some View {
VStack(alignment: .leading) {
Group {
Text("Hello world")
// ...
// up to 10 views
}
Group {
Text("Hello world")
// ...
// up to 10 more views
}
// ...
}

ViewBuilder also implements other function builder methods such:

extension ViewBuilder {
/// Provides support for "if" statements in multi-statement closures, producing
/// ConditionalContent for the "then" branch.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View

/// Provides support for "if-else" statements in multi-statement closures,
/// producing ConditionalContent for the "else" branch.
public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
}

This gives it the ability to handle if statements:

  var body: some View {
VStack(alignment: .leading) {
if .random() {
Text("Hello World!")
} else {
Text("Goodbye World!")
}
Text("Something else")
}
}

which gets transformed into:

  var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(
.random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
: ViewBuilder.buildEither(second: Text("Goodbye World!")),
Text("Something else")
)
}
}

(emitting redundant 1-argument calls to ViewBuilder.buildBlock for clarity).

What exactly does creating a Form in swiftUI does mean?

It is calling of Form constructor

/// A container for grouping controls used for data entry, such as in settings
/// or inspectors.
///
/// - SeeAlso: `Section`, which can be used to add sections between groups of
/// content.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct Form<Content> : View where Content : View {

public init(@ViewBuilder content: () -> Content)

so

Form{ Text("Hello, World") }

is equivalent of

Form.init(content: { () -> Text in
return Text("Hello, World")
})

How to interpret VStack{ .. } from SwiftUI's official tutorial code?

If you look at the docs for VStack.init, you'll see that the last argument it accepts is indeed a closure. The magic here is that the closure is marked with @ViewBuilder.

@ViewBuilder is a kind of function builder. The idea is that you pass in a closure containing a bunch of expressions, and then the function builder will combine those expressions into a single value. It's kind of like returning an array, but in an arguably better-looking syntax. (it's not really an array though. The return type of the closure is decided by the function builder.)

In your code, you are returning an "array" of 4 views.

  1. MapView
  2. CircleImage
  3. another VStack
  4. Spacer

These will get passed to the ViewBuilder, and it combines all of them into a single View object.

And if you are wondering what the methods called on at the end of each view are doing, they are just methods that return slight modifications of the objects on which they are called. For example padding returns the same view, but with some padding applied.

What does List var in NavigationLink mean in SwiftUI

List is a struct which is a View, and it takes a closure as a parameter when initialising itself.

You can image this closure as a function , so that this function is kind of like this.

func imagineFunc(item: YourItem) -> YourRowContent {
//some code goes here
return YourRowContent
}

Note: above function is only for explanation. there are no such types.

so , when ListView wants to create one of its rows , then List call this given closure(imagineFunc) and get made a RowContent/row view.

According to your code when List view call the given closure(for each row) you have return a NavigationLink(You can imagine this as a button which can navigate to another view when it is inside navigation view). So that each row becomes a NavigationLink in your List.

What is the some keyword in Swift(UI)?

some View is an opaque result type as introduced by SE-0244 and is available in Swift 5.1 with Xcode 11. You can think of this as being a "reverse" generic placeholder.

Unlike a regular generic placeholder which is satisfied by the caller:

protocol P {}
struct S1 : P {}
struct S2 : P {}

func foo<T : P>(_ x: T) {}
foo(S1()) // Caller chooses T == S1.
foo(S2()) // Caller chooses T == S2.

An opaque result type is an implicit generic placeholder satisfied by the implementation, so you can think of this:

func bar() -> some P {
return S1() // Implementation chooses S1 for the opaque result.
}

as looking like this:

func bar() -> <Output : P> Output {
return S1() // Implementation chooses Output == S1.
}

In fact, the eventual goal with this feature is to allow reverse generics in this more explicit form, which would also let you add constraints, e.g -> <T : Collection> T where T.Element == Int. See this post for more info.

The main thing to take away from this is that a function returning some P is one that returns a value of a specific single concrete type that conforms to P. Attempting to return different conforming types within the function yields a compiler error:

// error: Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types.
func bar(_ x: Int) -> some P {
if x > 10 {
return S1()
} else {
return S2()
}
}

As the implicit generic placeholder cannot be satisfied by multiple types.

This is in contrast to a function returning P, which can be used to represent both S1 and S2 because it represents an arbitrary P conforming value:

func baz(_ x: Int) -> P {
if x > 10 {
return S1()
} else {
return S2()
}
}

Okay, so what benefits do opaque result types -> some P have over protocol return types -> P?


1. Opaque result types can be used with PATs

A major current limitation of protocols is that PATs (protocols with associated types) cannot be used as actual types. Although this is a restriction that will likely be lifted in a future version of the language, because opaque result types are effectively just generic placeholders, they can be used with PATs today.

This means you can do things like:

func giveMeACollection() -> some Collection {
return [1, 2, 3]
}

let collection = giveMeACollection()
print(collection.count) // 3

2. Opaque result types have identity

Because opaque result types enforce a single concrete type is returned, the compiler knows that two calls to the same function must return two values of the same type.

This means you can do things like:

//   foo() -> <Output : Equatable> Output {
func foo() -> some Equatable {
return 5 // The opaque result type is inferred to be Int.
}

let x = foo()
let y = foo()
print(x == y) // Legal both x and y have the return type of foo.

This is legal because the compiler knows that both x and y have the same concrete type. This is an important requirement for ==, where both parameters of type Self.

protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}

This means that it expects two values that are both the same type as the concrete conforming type. Even if Equatable were usable as a type, you wouldn't be able to compare two arbitrary Equatable conforming values with each other, for example:

func foo(_ x: Int) -> Equatable { // Assume this is legal.
if x > 10 {
return 0
} else {
return "hello world"
}
}

let x = foo(20)
let y = foo(5)
print(x == y) // Illegal.

As the compiler cannot prove that two arbitrary Equatable values have the same underlying concrete type.

In a similar manner, if we introduced another opaque type returning function:

//   foo() -> <Output1 : Equatable> Output1 {
func foo() -> some Equatable {
return 5 // The opaque result type is inferred to be Int.
}

// bar() -> <Output2 : Equatable> Output2 {
func bar() -> some Equatable {
return "" // The opaque result type is inferred to be String.
}

let x = foo()
let y = bar()
print(x == y) // Illegal, the return type of foo != return type of bar.

The example becomes illegal because although both foo and bar return some Equatable, their "reverse" generic placeholders Output1 and Output2 could be satisfied by different types.


3. Opaque result types compose with generic placeholders

Unlike regular protocol-typed values, opaque result types compose well with regular generic placeholders, for example:

protocol P {
var i: Int { get }
}
struct S : P {
var i: Int
}

func makeP() -> some P { // Opaque result type inferred to be S.
return S(i: .random(in: 0 ..< 10))
}

func bar<T : P>(_ x: T, _ y: T) -> T {
return x.i < y.i ? x : y
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Legal, T is inferred to be the return type of makeP.

This wouldn't have worked if makeP had just returned P, as two P values may have different underlying concrete types, for example:

struct T : P {
var i: Int
}

func makeP() -> P {
if .random() { // 50:50 chance of picking each branch.
return S(i: 0)
} else {
return T(i: 1)
}
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Illegal.

Why use an opaque result type over the concrete type?

At this point you may be thinking to yourself, why not just write the code as:

func makeP() -> S {
return S(i: 0)
}

Well, the use of an opaque result type allows you to make the type S an implementation detail by exposing only the interface provided by P, giving you flexibility of changing the concrete type later down the line without breaking any code that depends on the function.

For example, you could replace:

func makeP() -> some P {
return S(i: 0)
}

with:

func makeP() -> some P { 
return T(i: 1)
}

without breaking any code that calls makeP().

See the Opaque Types section of the language guide and the Swift evolution proposal for further information on this feature.

Displaying an empty view in SwiftUI

You can use the @ViewBuilder. Then you don't even need an EmptyView:

@ViewBuilder
var body: some View {
if let text = text {
Text(text)
}
}

Note than you don't return anything, with a @ViewBuilder you just build your view.

Conditionally use view in SwiftUI

The simplest way to avoid using an extra container like HStack is to annotate your body property as @ViewBuilder, like this:

@ViewBuilder
var body: some View {
if user.isLoggedIn {
MainView()
} else {
LoginView()
}
}


Related Topics



Leave a reply



Submit