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.
Related
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)
}
}
}
}
}
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
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) }
}
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.
Im trying to implement a view that can change the amount of displaying items (created by a ForEach loop) if the content array's size changes, just like how a shopping app might change its number of items available after the user pull to refresh
Here are some code I have tried so far. If I remember correctly, these worked with Xcode beta 4, but with beta 5:
If the array's size increases, the loop will still display the originial number of elements
Array's size decrease will cause an index out of range error
Code:
import SwiftUI
struct test : View {
#State var array:[String] = []
#State var label = "not pressed"
var body: some View {
VStack{
Text(label).onTapGesture {
self.array.append("ForEach refreshed")
self.label = "pressed"
}
ForEach(0..<array.count){number in
Text(self.array[number])
}
}
}
}
#if DEBUG
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
#endif
I'm new to SwiftUI and GUI programming in general, and just feels like every content is defined at launch time and really hard to make changes afterwards (For example: reset a view after the user navigate away then return to it). Solutions to the loop problem or any tips for making views more dynamic would be greatly appreciated!
Beta 5 Release Notes say:
The retroactive conformance of Int to the Identifiable protocol is
removed. Change any code that relies on this conformance to pass
.self to the id parameter of the relevant initializer. Constant
ranges of Int continue to be accepted:
List(0..<5) {
Text("Rooms")
}
However, you shouldn’t pass a range that changes at runtime. If you
use a variable that changes at runtime to define the range, the list
displays views according to the initial range and ignores any
subsequent updates to the range.
You should change your ForEach to receive an array, instead of range. Ideally an Identifiable array, to avoid using \.self. But depending on your goal, this can still work:
import SwiftUI
struct ContentView : View {
#State var array:[String] = []
#State var label = "not pressed"
var body: some View {
VStack{
Text(label).onTapGesture {
self.array.append("ForEach refreshed")
self.label = "pressed"
}
ForEach(array, id: \.self) { item in
Text(item)
}
}
}
}
Or as rob mayoff suggested, if you need the index:
struct ContentView : View {
#State var array:[String] = []
#State var label = "not pressed"
var body: some View {
VStack{
Text(label).onTapGesture {
self.array.append("ForEach refreshed")
self.label = "pressed"
}
ForEach(array.indices, id: \.self) { index in
Text(self.array[index])
}
}
}
}```