On the Apple SwiftUI Tutorial: Drawing Paths and Shapes, the following syntax is shown:
struct Badge: View {
var body: some View {
GeometryReader { geometry in
Path { path in
...
}
.fill(Color.black)
}
}
}
I did not see the syntax in the Swift docs on structs and feel very confused.
Additionally, how might one know that geometry is "in" the GeometryReader statement, whatever that means?
That has nothing to do with structs. That is closure syntax, where the variable name before in is the input argument to the closure.
In the context of SwiftUI, that closure is actually a ViewBuilder, which is a specific type of a function builder. You can read more about function readers and the other syntactic sugars that made SwiftUI possible here.
You can learn the type of the closure input argument(s) by option clicking on them in Xcode, just like any other variables. Or you can check the documentation of GeometryReader to learn what input arguments its closure accepts.
This is simplified syntax of trailing closure, so
Path { path in
...
}
is just the same as
Path( { (path) in
...
})
of used Path constructor
/// Initializes to an empty path then calls `callback` to add
/// the initial elements.
public init(_ callback: (inout Path) -> ())
By the way, this syntax is widely used in SwiftUI, so you will see the same in VStack { .. }, GeometryReader { g in ... }, ScrollView { ... } and almost all others UI elements of SwfitUI framework.
Related
When using SwiftUI, I like to create functions that return Views, it's just an easy way to separate and clean up the code. Like this:
func deleteButton() -> some View { return (Button...) }
This works like a charm, but when I try to return a List like this:
func itemsList() -> List<Never, some DynamicViewContent> { ... }
I get the error "'some' types are only implemented for the declared type of properties and subscripts and the return type of functions".
I tried it without "some", and just "DynamicViewContent" without List, but neither has worked.
Can someone help me with this?
Option #1:
Return some View:
func myList() -> some View {
List(items, id:\.self) { item in
Text("Hello")
}
}
Option #2:
Use the specific type. The easiest way I can think of to get this is to temporarily set a variable to the List. For example:
var myList = List(items, id:\.self) { item in
Text("Hello")
}
If I then Option-Click on myList, Xcode will display a popup showing the exact type that myList is inferred to be. You can use that as the return type for your function. In the case of my contrived example, it is List<Never, ForEach<[GameModel], GameModel, Text>> where items is [GameModel]
Context: I'm following the tutorial on https://developer.apple.com/tutorials/swiftui/ to get an introduction to Swift and iOS.
In learning about closure notation in Swift, I wanted to write the more expanded version of a closure I came across.
ForEach(filteredLandmarks) {
landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
The first code snippet works great.
From what I understand about trailing closures, is that the type of the function needed as the last argument to the ForEach struct is implies the type of the function defined by the closure, hence we don't need to explicitly say the type of the function defined by the closure and refer only to it's one and only argument landmark implicitly. But what is the type if I wanted to explicitly define it?
The following is my attempt, which produces the error in Xcode
value of protocol type 'View' cannot conform to 'View'; only struct/enum/class types can conform to protocols
ForEach(filteredLandmarks) {
(landmark: Landmark) -> View in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
I know landmark has a type of Landmark, because removing the -> View works perfectly. So really, all I need to know is what is the return type?
You are calling this initialiser:
init(_ data: Data, content: #escaping (Data.Element) -> Content)
(Available when Data conforms to RandomAccessCollection, ID is Data.Element.ID, Content conforms to View, and Data.Element conforms to Identifiable.)
As you can see, the closure is supposed to return Content. What is Content?
ForEach is actually a generic type, and Data and Content are actually the generic type parameters of ForEach:
struct ForEach<Data, ID, Content>
where Data : RandomAccessCollection, ID : Hashable
This means that you get to decide what Content is.
Using -> View doesn't work, because for that initialiser to be available, Content must conform to View. View does not conform to View, so you can't do that.
In your code, you are returning a NavigationLink, and that is indeed the type of Content for ForEach. You should do:
(landmark: Landmark) -> NavigationLink in
However, the catch is, NavigationLink is also generic! You must specify its generic parameters!
The type of the destination parameter is the second generic parameter, and the view you return in the trailing closure is its first generic parameter. Assuming LandmarkDetail and LandmarkRow are not generic, you'd need to do:
(landmark: Landmark) -> NavigationLink<LandmarkRow, LandmarkDetail> in
Note that most of the time when writing SwiftUI, such analysis of what type the closure returns is not possible. Suppose you did:
ForEach(filteredLandmarks) {
landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark).padding()
}
}
instead, then we would not be able to know the exact type of the first generic parameter of NavigationLink, as padding returns an opaque type of some View. Note that you can do this in your closure return type too:
(landmark: Landmark) -> some View in
Figured it out soon after posting, funny how that happens.
The question has the answer, because the closure is returning something, so we just need to know what type is being returned.
The closure in question is returning a NavigationLink
NavigationLink<Label, Destination> where Label: View, Destination: View
from the docs.
The Destination parameter of NavigationLink is explicitly identified as a LandmarkDetail object. The Label parameter is defined by the trailing closure. Here's the answer.
ForEach(filteredLandmarks) {
(landmark: Landmark) -> NavigationLink<LandmarkRow, LandmarkDetail> in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
I'm a Java developer trying to learn Swift/SwiftUI. I am going through the Apple tutorial for SwiftUI (https://developer.apple.com/tutorials/swiftui/creating-and-combining-views) In it they have the following code snippet:
import SwiftUI
#main
struct LandmarksApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I am trying to understand this portion:
var body: some Scene {
WindowGroup {
ContentView()
}
}
I understand that it creates a new variable called body of type Scene. I know that you can instantiate objects like this:
var x: String = "test"
but I'm not familiar with the syntax. Does that create a new object of type Scene?
I have gone through this
but I couldn't find this syntax on there.
This syntax is indeed hard nut to crack for Swift novices. It involves implicit getters, constructors, several kind of closure shortcuts, implicit return types, generic type inference and opaque types. Let's go step by step.
Getter syntax for computed properties
LandmarksApp conforms to the App protocol. This requires the definition of a body property of a type complying with the Scene protocol. Moreover, we are told in the documentation that the body is expected to be a read-only computed property.
The general syntax of a computed property's getter is:
var X : T {
get { ... }
...
}
But if a it's a read-only propery, a shortcut may be used: put the getter code in a block directly after the type and without the get:
var CX : T { ... } // that's a computed property with its getter
Moreover, you do not need a return if the getter code is made of a single expression: the return is implicit.
WindowGroup constructor using a closure
WindowsGroup is the type we are going to use. It complies with Scene. It is a generic type based on the type parameter Content that conforms to the View protocol, and we have a ContentView for that. So here the first try:
struct LandmarksApp: App {
var body : WindowGroup<ContentView> { // read-only getter
WindowGroup<ContentView>(content: myhelper) // return a newly constructed object
}
}
The constructor for WindowGroup<T> requires as argument a function that takes no argument and returns a T. To start simple, an ordinary function was used. It's defined as follows:
func myhelper() -> ContentView {
return ContentView()
}
Simplification based on closures and generics
We can simplify with a closure to replace the helper function:
var body : WindowGroup<ContentView> {
WindowGroup<ContentView>(content: { ()->ContentView in return ContentView() })
}
Swift can infer the types used in the closure from the context and it also allows an implicit return for closures made of a single expression. In absence of parameters, we remove in. All those simplification lead to:
var body : WindowGroup<ContentView> {
WindowGroup<ContentView>(content: { ContentView() })
}
Swift is also able to infer the generic type from the context. So this expression could be simplified to:
var body : WindowGroup<ContentView> {
WindowGroup (content: { ContentView() })
}
Last but not least, the most tricky part: Swift allows a special syntax if the closure is the last argument of a function. It's called trailing closures. This leads to:
var body : WindowGroup<ContentView> {
WindowGroup () { ContentView() }
}
This syntax also allows to get rid of the empty parenthesis:
var body : WindowGroup<ContentView> {
WindowGroup { ContentView() }
}
Opaque types
Until now, we used a body of a concrete well-known type. But couldn't the type of this variable be inferred? It would thus be tempting to write:
var body { // OUCH !!!
WindowGroup { ContentView() }
}
But Swift requires computed properties to have an explicit type. To keep it as general as possible but ensure compliance with the required protocol, an opaque type may be used:
var body : some Scene {
WindowGroup { ContentView() }
}
More on opaque types in this question.
Wrap up
I'm working on an iOS application adopting the MVVM pattern, using SwiftUI for designing the Views and Swift Combine in order to glue together my Views with their respective ViewModels.
In one of my ViewModels I've created a Publisher (type Void) for a button press and another one for the content of a TextField (type String).
I want to be able to combine both Publishers within my ViewModel in a way that the combined Publisher only emits events when the button Publisher emits an event while taking the latest event from the String publisher, so I can do some kind of evaluation on the TextField data, every time the user pressed the button. So my VM looks like this:
import Combine
import Foundation
public class MyViewModel: ObservableObject {
#Published var textFieldContent: String? = nil
#Published var buttonPressed: ()
init() {
// Combine `$textFieldContent` and `$buttonPressed` for evaulation of textFieldContent upon every button press...
}
}
Both publishers are being pupulated with data by SwiftUI, so i will omit that part and let's just assume both publishers receive some data over time.
Coming from the RxSwift Framework, my goto solution would have been the withLatestFrom operator to combine both observables.
Diving into the Apple Documentation of Publisher in the section "Combining Elements from Multiple Publishers" however, I cannot find something similar, so I expect this kind of operator to be missing currently.
So my question: Is it possible to use the existing operator-API of the Combine Framework to get the same behavior in the end like withLatestFrom?
It sounds great to have a built-in operator for this, but you can construct the same behavior out of the operators you've got, and if this is something you do often, it's easy to make a custom operator out of existing operators.
The idea in this situation would be to use combineLatest along with an operator such as removeDuplicates that prevents a value from passing down the pipeline unless the button has emitted a new value. For example (this is just a test in the playground):
var storage = Set<AnyCancellable>()
var button = PassthroughSubject<Void, Never>()
func pressTheButton() { button.send() }
var text = PassthroughSubject<String, Never>()
var textValue = ""
let letters = (97...122).map({String(UnicodeScalar($0))})
func typeSomeText() { textValue += letters.randomElement()!; text.send(textValue)}
button.map {_ in Date()}.combineLatest(text)
.removeDuplicates {
$0.0 == $1.0
}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
typeSomeText()
typeSomeText()
typeSomeText()
pressTheButton()
typeSomeText()
typeSomeText()
pressTheButton()
The output is two random strings such as "zed" and "zedaf". The point is that text is being sent down the pipeline every time we call typeSomeText, but we don't receive the text at the end of the pipeline unless we call pressTheButton.
That seems to be the sort of thing you're after.
You'll notice that I'm completely ignoring what the value sent by the button is. (In my example it's just a void anyway.) If that value is important, then change the initial map to include that value as part of a tuple, and strip out the Date part of the tuple afterward:
button.map {value in (value:value, date:Date())}.combineLatest(text)
.removeDuplicates {
$0.0.date == $1.0.date
}
.map {($0.value, $1)}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
The point here is that what arrives after the line .map {($0.value, $1)} is exactly like what withLatestFrom would produce: a tuple of both publishers' most recent values.
As improvement of #matt answer this is more convenient withLatestFrom, that fires on same event in original stream
Updated: Fix issue with combineLatest in iOS versions prior to 14.5
extension Publisher {
func withLatestFrom<P>(
_ other: P
) -> AnyPublisher<(Self.Output, P.Output), Failure> where P: Publisher, Self.Failure == P.Failure {
let other = other
// Note: Do not use `.map(Optional.some)` and `.prepend(nil)`.
// There is a bug in iOS versions prior 14.5 in `.combineLatest`. If P.Output itself is Optional.
// In this case prepended `Optional.some(nil)` will become just `nil` after `combineLatest`.
.map { (value: $0, ()) }
.prepend((value: nil, ()))
return map { (value: $0, token: UUID()) }
.combineLatest(other)
.removeDuplicates(by: { (old, new) in
let lhs = old.0, rhs = new.0
return lhs.token == rhs.token
})
.map { ($0.value, $1.value) }
.compactMap { (left, right) in
right.map { (left, $0) }
}
.eraseToAnyPublisher()
}
}
Kind-of a non-answer, but you could do this instead:
buttonTapped.sink { [unowned self] in
print(textFieldContent)
}
This code is fairly obvious, no need to know what withLatestFrom means, albeit has the problem of having to capture self.
I wonder if this is the reason Apple engineers didn't add withLatestFrom to the core Combine framework.
I'm trying to understand how to change #State variables in #ViewBuilder closures. The following is just a simple example:
struct ContentView: View {
#State var width = CGFloat(0)
var body: some View { //Error 1
GeometryReader { geometry in //Error 2
self.width = geometry.size.width
return Text("Hello world!")
}
}
}
I'm getting some errors:
Error 1:
Function declares an opaque return type, but has no return statements
in its body from which to infer an underlying type
But the return is redundant because there's only one line of code inside the View computed property.
Error 2:
Cannot convert return expression of type 'GeometryReader<_>' to return
type 'some View'
Since I explicitly write return Text("...") shouldn't the type be clear?
What's the problem here?
First, you can't make arbitrary swift statements in a functionBuilder. There is a specific syntax allowed. In SwiftUI's case, it's a ViewBuilder, which under the hood is composing your type. When you make consecutive statements in SwiftUI, you are actually relying on the compiler to compose a new type underneath according to the rules of that DSL.
2nd, SwiftUI is a recipe, it's not an event system. You don't change state variables in the body function, you set up how things should react when state variables change externally. If you want another view to react in some way to that width, you need to define that content with respect to the width of the component you want. Without knowing what your ultimate goal is here, it's hard to answer how to relate the content to each other.
EDIT:
I was asked to elaborate on what exactly is allowed. Each functionBuilder has a different allowable syntax which is defined in the functionBuilder itself. This has a good overview on function builders in Swift 5.1: https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api
As for what SwiftUI is specifically looking for, it's essentially looking for each statement to return an instance of View.
// works fine!
VStack {
Text("Does this")
Text("Work?")
}
// doesn't work!
VStack {
Text("Updating work status...")
self.workStatus = .ok // this doesn't return an instance of `View`!
}
// roundabout, but ok...
VStack {
Text("Being clever")
gimmeView()
}
// fine to execute arbitrary code now, as long as we return a `View`
private func gimmeView() -> some View {
self.workingStatus = .roundabout
return Text("Yes, but it does work.")
}
This is why you got the obtuse error you got:
Cannot convert return expression of type 'GeometryReader<_>' to return type 'some View'
The type system can't construct any View out of View and essentially Void when you execute:
self.width = geometry.size.width
When you do consecutive View statements, underneath, it's still being converted into a new type of View:
// the result of this is TupleView<Text, Text>
Text("left")
Text("right")