swiftui why can't optional be assigned? - swift

Why can't optional be assigned?
Index has been allocated, but still no value is displayed
Help me, Thank you!
struct TestView: View {
#State private var index: Int? = nil
#State private var show: Bool = false
var body: some View {
VStack {
Text("Hello, world!")
.onTapGesture {
self.index = 1
self.show = true
print(self.index as Any)
}
}
.fullScreenCover(isPresented: $show) {
if let index = self.index {
Text("value:\(index)")
} else {
Text("not value")
}
}
}
}
Xcode Version 12.0 beta 2

SwiftUI relies upon the #State variable causing the body getter to be recalculated when it changes. For this to work, the body getter must depend in certain definite ways on the #State variable. The problem in your code is that it doesn't.
To see this, we can reduce your code to a simpler example:
struct ContentView: View {
#State var message = "Hey"
#State var show: Bool = false
var body: some View {
VStack {
Button("Test") {
message = "Ho"
show = true
}
}
.sheet(isPresented: $show) {Text(message)}
}
}
We change message to Ho, but when the sheet is presented, it still says Hey. This is because nothing happened to make the body recalculate. You might say: What about the phrase Text(message)? Yes, but that's in a closure; it has already been calculated, and message has already been captured.
To see that what I'm saying is right, just add a Text displaying message directly to the main interface:
struct ContentView: View {
#State var message = "Hey"
#State var show: Bool = false
var body: some View {
VStack {
Button("Test") {
message = "Ho"
show = true
}
Text(message)
}
.sheet(isPresented: $show) {Text(message)}
}
}
Now your code works! Of course, we are also displaying an unwanted Text in the interface, but the point is, that plain and simple Text(message), not in a closure, is sufficient to cause the whole body getter to be recalculated when message changes. So we have correctly explained the phenomenon you're asking about.
So what's the real solution? How can we get the content closure to operate as we expect without adding an extra Text to the interface? One way is like this:
struct ContentView: View {
#State var message = "Hey"
#State var show: Bool = false
var body: some View {
VStack {
Button("Test") {
message = "Ho"
show = true
}
}
.sheet(isPresented: $show) {[message] in Text(message)}
}
}
By including message in the capture list for our closure, we make the body getter depend on the message variable, and now the code behaves as desired.

Related

How #State variables in SwiftUI are modified and rendered in different Views?

I was playing with Swift Playground with the following code.
What I was doing is to modify a #State variable in a Button action, and then show its current value in a full-screen sheet.
The problem is that, notice the code line I commented, without this line, the value displayed in the full-screen sheet will be still 1, with this line, the value will be 2 instead, which is what I expected it to be.
I want to know why should this happen. Why the Text matters.
import SwiftUI
struct ContentView: View {
#State private var n = 1
#State private var show = false
var body: some View {
VStack {
// if I comment out this line, the value displayed in
// full-screen cover view will be still 1.
Text("n = \(n)")
Button("Set n = 2") {
n = 2
show = true
}
}
.fullScreenCover(isPresented: $show) {
VStack {
Text("n = \(n)")
Button("Close") {
show = false
// UPDATE(1)
print("n in fullScreenCover is", n)
}
}
}
}
}
Playground Version: Version 4.1 (1676.15)
Update 1:
As Asperi answered, if n in fullScreenCover isn't captured because it's in different contexts (closure?), then why does the print line prints n in fullScreenCover is 2 when not put Text in body?
The fullScreenCover (and similarly sheet) context is different context, created once and capturing current states. It is not visible for view's dynamic properties.
With put Text dependable on state into body just makes all body re-evaluated once state changed, thus re-creating fullScreenCover with current snapshot of context.
A possible solutions:
to capture dependency explicitly, like
.fullScreenCover(isPresented: $show) { [n] in // << here !!
VStack {
Text("n = \(n)")
to make separated view and pass everything there as binding, because binding is actually a reference, so being captured it still remains bound to the same source of truth:
.fullScreenCover(isPresented: $show) {
FullScreenView(n: $n, show: $show)
}
and view
struct FullScreenView: View {
#Binding var n: Int
#Binding var show: Bool
var body: some View {
VStack {
Text("n = \(n)")
Button("Close") {
show = false
}
}
}
}
both give:
Tested with Xcode 13.4 / iOS 15.5
For passing data into fullScreenCover we use a different version which is fullScreenCover(item:onDismiss:content:). There is an example at that link but in your case it would be:
struct FullscreenCoverTest: View {
#State private var n = 1
#State private var coverData: CoverData?
var body: some View {
VStack {
Button("Set n = 2") {
n = 2
coverData = CoverData(n: n)
}
}
.fullScreenCover(item: $coverData) { item in
VStack {
Text("n = \(item.n)")
Button("Close") {
coverData = nil
}
}
}
}
}
struct CoverData: Identifiable {
let id = UUID()
var n: Int
}
Note when it's done this way, if the sheet is open when coverData is changed to one with a different ID, the sheet will animate away and appear again showing the new data.

Issue with setting #State variable dynamically

As in the code below, the choosenKeyboardKnowledge is a #State variable and was initiated as the first object read from the cache. Then in the body, I iterate each object and wrap it into a Button so that when clicked it leads to the corresponding sheet view. But each time after I run the preview and click on whichever button in the list view it always shows the first default view (set in the initializer), and if I dismiss it and click on another line it shows the correct view.
struct KeyboardKnowledgeView: View {
var keyboardKnowledges: [KeyboardKnowledge]
#State private var choosenKeyboardKnowledge: KeyboardKnowledge
#State private var showSheet: Bool = false
init() {
keyboardKnowledges = KeyboardKnowledgeCache.getKeyboardKnowledges()
_choosenKeyboardKnowledge = State(initialValue: keyboardKnowledges[0])
}
var body: some View {
ZStack {
Color.bgGreen.ignoresSafeArea()
List(keyboardKnowledges) { knowledge in
Button(action: {
self.choosenKeyboardKnowledge = knowledge
self.showSheet.toggle()
}) {
Text(knowledge.name)
}
.sheet(isPresented: $showSheet) {
KeyboardKnowledgeDetailsView(keyboardKnowledge: choosenKeyboardKnowledge)
}
}
}
}
}

How to Generate a New Random Number Once Every time Content View Loads?

The intent here is generate a new random number every time MyView loads while keeping that randomly generated number unaffected with any MyView refresh. However, none of the approaches below work. I will explain each approach and its outcome. Any ideas on how to properly accomplish what I am asking?
Approach #1: I assumed a new number is generated every time MyView loads. However, a random number is generated once for the entire app lifetime. I placed a button to force a view refresh, so when I press the button a new random number should not generate, which is what happens. What is wrong with this approach?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView()) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State var randomInt = Int.random(in: 1...100)
#State var myMessage: String = ""
var body: some View {
Text(String(randomInt))
Button("Press Me") {
myMessage = String(randomInt)
}
Text(myMessage)
}
}
Approach #2: I tried to update randomInt variable via let inside the Body but this generates a new random number every time the button is pressed (which is forcing a view refresh). Again, not the intended outcome. What is wrong with this approach?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView()) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State var randomInt = 0
#State var myMessage: String = ""
var body: some View {
let randomInt = Int.random(in: 1...100)
Text(String(randomInt))
Button("Press Me") {
myMessage = String(randomInt)
}
Text(myMessage)
}
}
Approach #3: The idea here to pass a new randomly generated integer every time the "Link to MyView" is pressed. I assumed that Int.Random is ran and passed every time Content View loads. However, a random number is only generated the first-time the entire app runs. What is wrong with this approach?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView(randomInt: Int.random(in: 1...100))) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State var randomInt = 0
#State var myMessage: String = ""
var body: some View {
Text(myMessage)
Button("Press Me") {
myMessage = String(randomInt)
}
}
}
Approach #1:
MyView is created once, when ContentView renders.
Inside MyView, randomInt is set when the view is first created and then never modified. When you press the button, myMessage is set, but randomInt is never changed. If you wanted randomInt to change, you'd want to say something like:
randomInt = Int.random(in: 1...100)
inside your button action:
Approach #2:
You're creating a new randomInt variable in the local scope by declaring let randomInt = inside the view body.
Instead, if I'm reading your initial question correctly, I think you'd want something using onAppear:
struct MyView: View {
#State var randomInt = 0
#State var myMessage: String = ""
var body: some View {
VStack {
Text(String(randomInt))
Button("Press Me") {
myMessage = String(randomInt)
}
Text(myMessage)
}.onAppear {
randomInt = Int.random(in: 1...100)
}
}
}
You'll see that with this, every time you go back in the navigation hierarchy and then revisit MyView, there's a new value (since it appears again). The button triggering a re-render doesn't re-trigger onAppear
Approach #3:
MyView gets created on the first render of the parent view (ContentView). Unless something triggers a refresh of ContentView, you wouldn't generate a new random number here.
In conclusion, I'm a little unclear on what the initial requirement is (what does it mean for a View to 'load'? Does this just mean when it gets shown on the screen?), but hopefully this describes each scenario and maybe introduces the idea of onAppear, which seems like it might be what you're looking for.
Addendum: if you want the random number to be generated only when ContentView loads (as the title says), I'd create it in ContentView instead of your MyView and pass it as a parameter.
struct ContentView: View {
#State var randomInt = Int.random(in: 1...100)
var body: some View {
NavigationView {
NavigationLink(destination: MyView(randomInt: $randomInt)) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#Binding var randomInt : Int
#State var myMessage: String = ""
var body: some View {
Text(String(randomInt))
Button("Press me to refresh only myMessage") {
myMessage = String(randomInt)
}
Button("Press me to change the randomInt as well") {
randomInt = Int.random(in: 1...100)
myMessage = String(randomInt)
}
Text(myMessage)
}
}
I work on a way that always make a new random, your problem was you just used initialized State over and over! Without changing it.
Maybe the most important thing that you miss understood was using a State wrapper and expecting it performance as computed property, as you can see in your 3 example all has the same issue in different name or form! So for your information computed property are not supported in SwiftUI until Now! That means our code or us should make an update to State Wrappers, and accessing them like you tried has not meaning at all to them.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView()) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State private var randomInt: Int?
var body: some View {
if let unwrappedRandomInt: Int = randomInt { Text(unwrappedRandomInt.description) }
Button("Press Me") { randomMakerFunction() }
.onAppear() { randomMakerFunction() }
}
func randomMakerFunction() { randomInt = Int.random(in: 1...100) }
}

Cannot assign value of type 'MenuView' to type 'some View'

I noticed that you can only have a single .popover modifier in SwiftUI. I have to present two possible simple popovers, one of them a MenuView, the other a CreateChannelView.
For that I have:
#State private var popover: some View
#State private var showPopover = false
and then the modifier:
.popover(isPresented: self.$showPopover) {
self.popover
}
The problem is that I don't see how can I assign instances of MenuView or CreateChannelView to popover as I get the error:
Cannot assign value of type 'MenuView' to type 'some View'
This is a little bit different than this question which passes generic views in the init method.
The solution was to use AnyView:
#State private var popover: AnyView
Then it can be assigned as:
self.popover = AnyView(CreateChannelView(showing: self.$showPopover))
You can declare the two views as two different variables and then toggle selection between them with a boolean that determines which is presented. I made an example program with the information you gave me:
struct ContentView: View {
let buttonSize: CGFloat = 30
#State var isPresented = false
#State var usePopover1 = true
var popover1: some View {
Text("Popover 1")
}
var popover2: some View {
Image(systemName: "star")
}
var body: some View {
VStack {
Button(action: {
self.isPresented = true
}) {
Text("Present popover")
}
Button(action: {
self.usePopover1.toggle()
}) {
Text("Switch from \(self.usePopover1 ? "popover1" : "popover2") to \(self.usePopover1 ? "popover2" : "popover1")")
}
}.popover(isPresented: $isPresented) {
if self.usePopover1 {
AnyView(self.popover1)
} else {
AnyView(self.popover2)
}
}
}
}
I just made the two popovers on the spot for demonstration purposes, but you can declare yours as being the two different types you mentioned in your question (let popover1: MenuView = MenuView(...) and let popover2: CreateChannelView = CreateChannelView(...)).

didSet for a #Binding var in Swift

Normally we can use didSet in swift to monitor the updates of a variable. But it didn't work for a #Binding variable. For example, I have the following code:
#Binding var text {
didSet {
......
}
}
But the didSet is never been called.Any idea? Thanks.
Instead of didSet you can always use onReceive (iOS 13+) or onChange (iOS 14+):
import Combine
import SwiftUI
struct ContentView: View {
#State private var counter = 1
var body: some View {
ChildView(counter: $counter)
Button("Increment") {
counter += 1
}
}
}
struct ChildView: View {
#Binding var counter: Int
var body: some View {
Text(String(counter))
.onReceive(Just(counter)) { value in
print("onReceive: \(value)")
}
.onChange(of: counter) { value in
print("onChange: \(value)")
}
}
}
You shouldn’t need a didSet observer on a #Binding.
If you want a didSet because you want to compute something else for display when text changes, just compute it. For example, if you want to display the count of characters in text:
struct ContentView: View {
#Binding var text: String
var count: Int { text.count }
var body: some View {
VStack {
Text(text)
Text(“count: \(count)”)
}
}
}
If you want to observe text because you want to make some other change to your data model, then observing the change from your View is wrong. You should be observing the change from elsewhere in your model, or in a controller object, not from your View. Remember that your View is a value type, not a reference type. SwiftUI creates it when needed, and might store multiple copies of it, or no copies at all.
The best way is to wrap the property in an ObservableObject:
final class TextStore: ObservableObject {
#Published var text: String = "" {
didSet { ... }
}
}
And then use that ObservableObject's property as a binding variable in your view:
struct ContentView: View {
#ObservedObject var store = TextStore()
var body: some View {
TextField("", text: $store.text)
}
}
didSet will now be called whenever text changes.
Alternatively, you could create a sort of makeshift Binding value:
TextField("", text: Binding<String>(
get: {
return self.text
},
set: { newValue in
self.text = newValue
...
}
))
Just note that with this second strategy, the get function will be called every time the view is updated. I wouldn't recommend using this approach, but nevertheless it's good to be aware of it.