Change #State in a #ViewBuilder closure - ios

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")

Related

Updating a #State var in SwiftUI from an async method doesn't work

I have the following View:
struct ContentView: View {
#State var data = [SomeClass]()
var body: some View {
List(data, id: \.self) { item in
Text(item.someText)
}
}
func fetchDataSync() {
Task.detached {
await fetchData()
}
}
#MainActor
func fetchData() async {
let data = await SomeService.getAll()
self.data = data
print(data.first?.someProperty)
// > Optional(115)
print(self.data.first?.someProperty)
// > Optional(101)
}
}
now the method fetchDataSync is a delegate that gets called in a sync context whenever there is new data. I've noticed that the views don't change so I've added the printouts. You can see the printed values, which differ. How is this possible? I'm in a MainActor, and I even tried detaching the task. Didn't help. Is this a bug?
It should be mentioned that the objects returned by getAll are created inside that method and not given to any other part of the code. Since they are class objects, the value might be changed from elsewhere, but if so both references should still be the same and not produce different output.
My theory is that for some reason the state just stays unchanged. Am I doing something wrong?
Okay, wow, luckily I ran into the Duplicate keys of type SomeClass were found in a Dictionary crash. That lead me to realize that SwiftUI is doing some fancy diffing stuff, and using the == operator of my class.
The operator wasn't used for actual equality in my code, but rather for just comparing a single field that I used in a NavigationStack. Lesson learned. Don't ever implement == if it doesn't signify true equality or you might run into really odd bugs later.

Returning a SwiftUI List in a function

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]

What is the function type of the last parameter of the ForEach struct from SwiftUI?

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)
}
}

What does "in" mean in Swift? [duplicate]

This question already has answers here:
Swift closure syntax using { ... in }
(2 answers)
Closed 1 year ago.
I've seen the word "in" multiple times in SwiftUI, but don't quite understand what it means. Because the word is so short and common, I'm having a hard time searching for its meaning and usage.
Most recently, I've seen it in the onChange method for example:
struct ContentView: View {
#State private var name = ""
var body: some View {
TextField("Enter your name:", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: name) { newValue in
print("Name changed to \(name)!")
}
}
As like in your code example using in afther newValue let`s Swift and you know the return value of process is going happen on newValue so it is more like a symbol or flag called: "token" to you and to CPU that work is going happen on newValue because you put it before in, for example you gave name to onChange modifier with using newValue in, you will get updated value for name if get any new update, also note you can use $0 instead of newValue in and works same.
.onChange(of: name) { newValue in
return print("Name changed to \(name)!")
}
Or:
.onChange(of: name) {
print("Name changed to \($0)!")
}
Both are the same it depends how you like it.
Another example for better understanding:
let arrayOfInt: [Int] = [1, 2, 3]
let arrayOfString: [String] = arrayOfInt.map({ value in return value.description })
let arrayOfString: [String] = arrayOfInt.map({ $0.description })
For example we have an arrayOfInt and we want create a String array of it, for this work we can use map, and again like last example we used in in 2 way. in the first one as: value in and in the second on as $0. I recommend you work and learn more about closure in Swift/SwiftUI they are very important, without knowing them well, we cannot have good understanding of codes in iOS.
Also do not forget to attention to word of return right after in you can type and use or not use it, Swift is Smart enough to understand because of inference, but if you use it, it make your code much more easy and simple to read and better in syntax of code.

Swift closure syntax using { ... in }

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.

Resources