Set a #State var inside an If statement in Swift - swift

I am trying to set the value of a #State var in an If statement that is inside of a struct of type :View, like so:
struct Name: View {
#State someVar: Int = 0
var body: some View {
VStack {
if this > that {
someVar = 1
But when I do this I get the error: "Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols". If I use a class method that does what I need, like this:
if this > that {
someClass.doIt()
}
I get the same error.
What is the right way to do this?

You cannot put logic code like that inside your body -- all that you can have there is code that outputs a view.
So, you could do:
var body: some View {
if this > that {
Text("This")
} else {
Text("That")
}
}
because that results in a View (Text) getting rendered. In your example, though, you're just doing assignment.
That has to be done in a separate function or in a closure outside of what gets directly rendered in the view.
So:
func testThisThat() {
if this > that {
someVar = 1
}
}
var body: some View {
Button(action: {
testThisThat()
}) {
Text("Run test")
}
}
In the above, your logic runs in a closure outside the view hierarchy, and a Button gets rendered to the view.
If you give more specifics about what you're trying to do, perhaps the answer can be clarified, but that's the source of the error.
As suggested in the comments, you can also run logic code in onAppear, like this:
var body: some View {
VStack {
//view code
}.onAppear {
//logic
if this > that {
someVar = 1
}
}
}

Related

How can I call a function of a child view from the parent view in swiftUI to change a #state variable?

I'm trying to get into swift/swiftui but I'm really struggling with this one:
I have a MainView containing a ChildView. The ChildView has a function update to fetch the data to display from an external source and assign it to a #State data variable.
I'd like to be able to trigger update from MainView in order to update data.
I've experienced that update is in fact called, however, data is reset to the initial value upon this call.
The summary of what I have:
struct ChildView: View {
#State var data: Int = 0
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
var child = ChildView()
var body: some View {
VStack {
child
Button(action: {
child.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
When googling for a solution, I only came across people suggesting to move update and data to MainView and then pass a binding of data to ChildView.
However, following this logic I'd have to blow up MainView by adding all the data access logic in there. My point of having ChildView at all is to break up code into smaller chunks and to reuse ChildView including the data access methods in other parent views, too.
I just cannot believe there's no way of doing this in SwiftUI.
Is completely understandable to be confused at first with how to deal with state on SwiftUI, but hang on there, you will find your way soon enough.
What you want to do can be achieved in many different ways, depending on the requirements and limitations of your project.
I will mention a few options, but I'm sure there are more, and all of them have pros and cons, but hopefully one can suit your needs.
Binding
Probably the easiest would be to use a #Binding, here a good tutorial/explanation of it.
An example would be to have data declared on your MainView and pass it as a #Binding to your ChildView. When you need to change the data, you change it directly on the MainView and will be reflected on both.
This solutions leads to having the logic on both parts, probably not ideal, but is up to what you need.
Also notice how the initialiser for ChildView is directly on the body of MainView now.
Example
struct ChildView: View {
#Binding var data: Int
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
#State var data: Int = 0
var body: some View {
VStack {
ChildView(data: $data)
Button(action: {
data = 42
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
ObservableObject
Another alternative would be to remove state and logic from your views, using an ObservableObject, here an explanation of it.
Example
class ViewModel: ObservableObject {
#Published var data: Int = 0
func update() {
// fetch data from external source
data = 42
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("\(viewModel.data)")
Button(action: viewModel.update) {
Text("update") // works as expected
}
}
}
struct MainView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ChildView(viewModel: viewModel)
Button(action: {
viewModel.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}

Is there a way to pass mutating closures as arguments to a SwiftUI View?

The Idea
In one of the views of my application I need to mutate some data. To make the code clear, testable and just to test how far I can get without logic in ViewModels, I've moved the mutating logic to the Model layer.
Say this is my Model
struct Model {
var examples: [Example] = []
/* lots of other irrelevant properties and a constructor here */
}
struct Example: Identifiable {
var id = UUID()
var isEnabled: Bool = true
/* other irrelevant properties and a constructor here */
}
And the function that mutates stuff is
// MARK: mutating methods
extension Model {
mutating func disableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
mutating func enableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
}
Now, let's display it in views, shall we?
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples)
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples) {
// TODO: do stuff
}
}
}
}
The attempt
Since I don't want to put the whole Model into the ExampleMutatingView, both because I don't need the whole thing and due to performance reasons, I tried to supply the view with necessary methods.
I've also added the ability to select examples by providing a State variable.
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples,
operationsOnExamples: (enable: model.enableExamples, disable: model.disableExamples))
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
let operationsOnExamples: (enable: ((Set<UUID>) -> Void, disable: (Set<UUID>) -> Void)
#State private var multiSelection = Set<UUID>()
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples, selection: $multiSelection) { example in
Text("\(example.id)")
}
HStack {
Button { operationsOnExamples.enable(with: multiSelection) } label: { Text("Enable selected") }
Button { operationsOnExamples.disable(with: multiSelection) } label: { Text("Disable selected") }
}
}
}
}
The problem
The thing is, with such setup the ContentView greets me with Cannot reference 'mutating' method as function value error. Not good, but mysterious for me for the very reason that fixes it: supplying the actual Model into the view.
The (non ideal) solution
Showing only the parts that changed
// ContentView
1. ExampleMutatingView(model: $model)
// ExampleMutatingView
1. #Binding var model: Model
2. List($model.examples/*...*/)
3. Button { model.enableExamples(with: multiSelection) } /*...*/
4. Button { model.disableExamples(with: multiSelection) } /*...*/
The discussion
Why is it the case? The only difference I see and cannot explain accurately between these two is that supplying the model might give the method access to its self, which is, otherwise, not available. If that's the case, maybe wrapping the methods in some kind of closure with an [unowned self] would help?
I'm fresh to the topic of self in Swift, so I honestly have no idea.
TL;DR: why does it work when I supply the object defining the methods, but does not when I supply only the methods?

How can I make a State wrapper outside of View in SwiftUI?

I know that State wrappers are for View and they designed for this goal, but I wanted to try build and test some code if it is possible, my goal is just for learning purpose,
I have 2 big issues with my code!
Xcode is unable to find T.
How can I initialize my state?
import SwiftUI
var state: State<T> where T: StringProtocol = State(get: { state }, set: { newValue in state = newValue })
struct ContentView: View {
var body: some View {
Text(state)
}
}
Update: I could do samething for Binding here, Now I want do it for State as well with up code
import SwiftUI
var state2: String = String() { didSet { print(state2) } }
var binding: Binding = Binding.init(get: { state2 }, set: { newValue in state2 = newValue })
struct ContentView: View {
var body: some View {
TextField("Enter your text", text: binding)
}
}
If I could find the answer of my issue then, i can define my State and Binding both outside of View, 50% of this work done and it need another 50% for State Wrapper.
New Update:
import SwiftUI
var state: State<String> = State.init(initialValue: "Hello") { didSet { print(state.wrappedValue) } }
var binding: Binding = Binding.init(get: { state.wrappedValue }, set: { newValue in state = State(wrappedValue: newValue) })
struct ContentView: View {
var body: some View {
Text(state) // <<: Here is the issue!
TextField("Enter your text", text: binding)
}
}
Even if you create a State wrapper outside a view, how will the view know when to refresh its body?
Without a way to notify the view, your code will do the same as:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}
What you can do next depends on what you want to achieve.
If all you need is a way to replicate the State behaviour outside the view, I recommend you take a closer look at the Combine framework.
An interesting example is CurrentValueSubject:
var state = CurrentValueSubject<String, Never>("state1")
It stores the current value and also acts as a Publisher.
What will happen if we use it in a view that doesn't observe anything?
struct ContentView: View {
var body: some View {
Text(state.value)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
}
}
The answer is: nothing. The view is drawn once and, even if the state changes, the view won't be re-drawn.
You need a way to notify the view about the changes. In theory you could do something like:
var state = CurrentValueSubject<String, Never>("state1")
struct ContentView: View {
#State var internalState = ""
var body: some View {
Text(internalState)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
.onReceive(state) {
internalState = $0
}
}
}
But this is neither elegant nor clean. In these cases we should probably use #State:
struct ContentView: View {
#State var state = "state1"
var body: some View {
Text(state)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state = "state2"
}
}
}
}
To sum up, if you need a view to be refreshed, just use the native SwiftUI property wrappers (like #State). And if you need to declare state values outside the view, use ObservableObject + #Published.
Otherwise there is a huge Combine framework which does exactly what you want. I recommend you take a look at these links:
Combine: Getting Started
Using Combine

SwiftUI - Button - How to pass a function (with parameters) request to parent from child

I already know how to call a parent function from child but what I should do if my parent function has a parameter? I can't figure it out...
Working code without parameters:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ContentView: View {
var body: some View {
ChildView(function: { self.setViewBackToNil() })
}
func setViewBackToNil() {
print("I am the parent")
}
}
And now I want to add a String parameter to setViewBackToNil(myStringParameter: String)
Ok, I managed to solve it.
You can use #State variable in a parent, and pass it to your child. Then, after changing it in the child view, call function, that was passed from the parent (without parameters), and in the parent get your #State inside the function.

"Function declares an opaque return type [...]" error when declaring a view as a variable inside the body of a View in SwiftUI

Assume I have a View with an Image that has a shadow property:
struct ContentView: View {
var body: some View {
Image("turtlerock").shadow(radius: 10)
}
}
Now imagine I want to access the value of the shadow radius. I assumed I could do this:
struct ContentView: View {
var body: some View {
let myImage = Image("turtlerock").shadow(radius: 10)
print(myImage.modifier.radius)
}
}
However, this returns an error:
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
Is there a way to accomplish this somehow?
When modifying and building views, you can do this without a return statement and a building block one above the other without commas. This is called a multi-statement closure. When you try to create a variable inside a multi-statement closure, the compiler is going to complain because there is a mismatch in types (you can only combine views one after another, nothing more). See this answer for more details: https://stackoverflow.com/a/56435128/7715250
A way to fix this is to explicitly return the views you are combining, so you don't make use of the multi-closure statements:
struct MyView: View {
var body: some View {
let image = Image("Some image").shadow(radius: 10)
let myRadius = image.modifier.radius
// Do something with myRadius
return image // No multi closure statements.
}
}
If your view you want to reference is inside a stack, you should declare it outside the stack like this:
var body: some View {
let myImage = Image("image").shadow(radius: 10)
let stack = HStack {
myImage
Image("image2")
}
return stack
}
You can define the image outside body:
let myImage = Image("turtlerock").shadow(radius: 10)
var body: some View {
myImage
}
To print the radius you can do like so:
var body: some View {
myImage
.tapAction {
print(self.myImage.modifier.radius) // 10.0
}
}
When it happens to me in a testing environment I just nest everything in the body inside a
return ZStack{ ...}
A bit quick and dirty, but it works for my purposes.
I'm using Group {}:
func makeContentView() -> some View {
Group {
if some_condition_here {
Text("Hello World")
.foregroundColor(.red)
.font(.system(size: 13, weight: .bold, design: .monospaced))
} else {
Rectangle()
.fill(Color.gray20)
}
}
}
}