Why does this SwiftUI code show "Empty" in the destination screen? - swiftui

struct ContentView: View {
#State var showModal = false
#State var text = "Empty"
var body: some View {
Button("show text") {
text = "Filled"
showModal = true
}
.sheet(isPresented: $showModal) {
VStack {
Text(text)
Button("print text") {
print(text)
}
}
}
}
}
I thought that when the "show text" button was tapped, the value of text would be set to "Filled" and showModal would be set to true, so that the screen specified in sheet would be displayed and the word "Filled" would be shown on that screen.
I thought it would show "Filled", but it actually showed "Empty".
Furthermore, when I printed the text using the print text button, the console displayed "Filled".
Why does it work like this?
What am I missing to display the value I set when I tap the button on the destination screen?
using Xcode12.4, Xcode12.5
Add the code for the new pattern.
struct ContentView: View {
#State var number = 0
#State var showModal = false
var body: some View {
VStack {
Button("set number 1") {
number = 1
showModal = true
print("set number = \(number)")
}
Button("set number 2") {
number = 2
showModal = true
print("set number = \(number)")
}
Button("add number") {
number += 1
showModal = true
print("add number = \(number)")
}
}
.sheet(isPresented: $showModal) {
VStack {
let _ = print("number = \(number)")
Text("\(number)")
}
}
}
}
In the above code, when I first tap "set number 1" or "set number 2", the destination screen shows "0". No matter how many times you tap the same button, "0" will be displayed.
However, if you tap "set number 2" after tapping "set number 1", it will work correctly and display "2". If you continue to tap "set number 1", "1" will be displayed and the app will work correctly.
When you tap "add number" for the first time after the app is launched, "0" will still be displayed, but if you tap "add number" again, "2" will be displayed and the app will count up correctly.
This shows that the rendering of the destination screen can be started even when the #State variable is updated, but only when the #State variable is referenced first in the destination screen, it does not seem to be referenced properly.
Can anyone explain why it behaves this way?
Or does this look like a bug in SwiftUI?

Since iOS 14, there are a couple of main ways of presenting a Sheet.
Firstly, for your example, you need to create a separate View and pass your property to a Binding, which will then be correctly updated when the Sheet is presented.
// ContentView
Button { ... }
.sheet(isPresented: $showModal) {
SheetView(text: $text)
}
struct SheetView: View {
#Binding var text: String
var body: some View {
VStack {
Text(text)
Button("print text") {
print(text)
}
}
}
}
The other way of doing it is by using an Optional identifiable object, and when that object has a value the sheet will be presented. Doing that, you do not need to separately manage the state of whether the sheet is showing.
struct Item: Identifiable {
var id = UUID()
var text: String
}
struct ContentView: View {
#State var item: Item? = nil
var body: some View {
Button("show text") {
item = Item(text: "Filled")
}
.sheet(item: $item, content: { item in
VStack {
Text(item.text)
Button("print text") {
print(item.text)
}
}
})
}
}

By adding the following code, I was able to display the word "Filled" in the destination screen.
Declare the updateDetector as a #State variable.
Update the updateDetector onAppear of the View in the sheet.
Reference the updateDetector somewhere in the View in the sheet.
struct ContentView: View {
#State var showModal = false
#State var updateDetector = false
#State var text = "Empty"
var body: some View {
Button("show text") {
text = "Filled"
print("set text to \(text)")
showModal = true
}
.sheet(isPresented: $showModal) {
let _ = print("render text = \(text)")
VStack {
Text(text)
// use EmptyView to access updateDetector
if updateDetector {
EmptyView()
}
}
.onAppear {
// update updateDetector
print("toggle updateDetector")
updateDetector.toggle()
}
}
}
}
Then, after rendering the initial value at the time of preloading, the process of updating the updateDetector onAppear will work, the View will be rendered again, and the updated value of the #State variable will be displayed.
When you tap the "show text" button in the above code, the following will be displayed in the console and "Filled" will be shown on the screen.
set text to Filled
render text = Empty
toggle updateDetector
render text = Filled
I'm still not convinced why I can't get the updated value of the #State variable in the first preloaded sheet. So I will report this to Apple via the Feedback Assistant.

Related

SwiftUI Picker selection not updated unless value shown in view (using Enum for selection)

import SwiftUI
enum TestEnum: String, CaseIterable {
case firstValue = "First Value"
case secondValue = "Second Value"
case thirdValue = "Third Value"
}
struct TestView: View {
#State private var testEnumSelection = TestEnum.allCases.first!
#State private var isShowingSheet = false
var body: some View {
VStack {
Picker("Test Enum Selection", selection: $testEnumSelection) {
ForEach(TestEnum.allCases, id: \.self) { testEnum in
Text(testEnum.rawValue)
}
}
//Text("Enum Selection: \(testEnumSelection.rawValue)") Enum value not updated if this line is not inlcuded
Button("Show Sheet", action: {
isShowingSheet = true
})
}
.padding()
.sheet(isPresented: $isShowingSheet) {
Text(testEnumSelection.rawValue)
.padding()
}
}
}
I am trying to use an enum value selected from a picker in a sheet view but the value from the picker is not being updated for the sheet. The value does get updated if I show the picker selection on screen elsewhere like in a Text object but I don't want to do that.
Could someone explain to me why I need to show the enum selection for it to be updated for the sheet and how to get around doing this?
The sheet content is created once, so it is not updated when state in parent is updated.
The possible solution is to separate sheet content into standalone view and use binding - bound variable will update view internals.
Here is a modified part (tested with Xcode 13.2 / macOS 12.1)
.padding()
.sheet(isPresented: $isShowingSheet) {
SheetContent(value: $testEnumSelection) // << here !!
}
}
}
struct SheetContent: View {
#Binding var value: TestEnum
var body: some View {
Text(value.rawValue)
.padding()
}
}

SwiftUI .fullScreenCover appears as a .sheet

I expected to be able to dismiss a sheet and present a fullScreenCover straight after, however this doesn't seem to work without a delay between the two state modifications.
struct ContentView: View {
#State var sheet = false
#State var cover = false
var body: some View {
Button("Click me for sheet") {
sheet = true
}
.fullScreenCover(isPresented: $cover) {
Text("This is a full screen cover")
}
.sheet(isPresented: $sheet) {
Text("This is a sheet")
Button("This doesn't work") {
sheet = false
cover = true
}
Button("This works") {
sheet = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
cover = true
}
}
}
}
}
Clicking the "This doesn't work" button produces the below image demonstrating a fullScreen cover displaying as a sheet and not covering the entire screen:
Introducing a small delay between the two state modifications which is done by clicking the "This works" button fixes the issue.
What am I not understanding about SwiftUI here that would explain this behaviour?
Tested on:
Xcode 12.5.1
iPhone 12 Pro Max Simulator running iOS 14.5
Adding the modifiers to seperate view like this also doesn't work:
Text("Another view").sheet(isPresented: $sheet) { ...
you could try this:
struct ContentView: View {
#State var sheet = false
#State var cover = false
var body: some View {
Button("Click me for sheet") {
sheet = true
}
.fullScreenCover(isPresented: $cover) {
Text("This is a full screen cover")
}
.sheet(isPresented: $sheet, onDismiss: {cover = true}) {
Text("This is a sheet")
}
}
}

SwiftUI: Switch .sheet on enum, does not work

I have two Modal/Popover .sheet's I would like to show based on which button is pressed by a user. I have setup an enum with the different choices and set a default choice.
Expected behaviour:
When the user selects any choice, the right sheet is displayed. When the user THEN selects the other choice, it also shows the correct sheet.
Observed behaviour:
In the example below, when the user first picks the second choice, the first sheet is shown and will continue to show until the user selects the first sheet, then it will start to switch.
Debug printing shows that the #State variable is changing, however, the sheet presentation does not observe this change and shows the sheets as described above. Any thoughts?
import SwiftUI
//MARK: main view:
struct ContentView: View {
//construct enum to decide which sheet to present:
enum ActiveSheet {
case sheetA, sheetB
}
//setup needed vars and set default sheet to show:
#State var activeSheet = ActiveSheet.sheetA //sets default sheet to Sheet A
#State var showSheet = false
var body: some View {
VStack{
Button(action: {
self.activeSheet = .sheetA //set choice to Sheet A on button press
print(self.activeSheet) //debug print current activeSheet value
self.showSheet.toggle() //trigger sheet
}) {
Text("Show Sheet A")
}
Button(action: {
self.activeSheet = .sheetB //set choice to Sheet B on button press
print(self.activeSheet) //debug print current activeSheet value
self.showSheet.toggle() //trigger sheet
}) {
Text("Show Sheet B")
}
}
//sheet choosing view to display based on selected enum value:
.sheet(isPresented: $showSheet) {
switch self.activeSheet {
case .sheetA:
SheetA() //present sheet A
case .sheetB:
SheetB() //present sheet B
}
}
}
}
//MARK: ancillary sheets:
struct SheetA: View {
var body: some View {
Text("I am sheet A")
.padding()
}
}
struct SheetB: View {
var body: some View {
Text("I am sheet B")
.padding()
}
}
With some very small alterations to your code, you can use sheet(item:) for this, which prevents this problem:
//MARK: main view:
struct ContentView: View {
//construct enum to decide which sheet to present:
enum ActiveSheet : String, Identifiable { // <--- note that it's now Identifiable
case sheetA, sheetB
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil // <--- now an optional property
var body: some View {
VStack{
Button(action: {
self.activeSheet = .sheetA
}) {
Text("Show Sheet A")
}
Button(action: {
self.activeSheet = .sheetB
}) {
Text("Show Sheet B")
}
}
//sheet choosing view to display based on selected enum value:
.sheet(item: $activeSheet) { sheet in // <--- sheet is of type ActiveSheet and lets you present the appropriate sheet based on which is active
switch sheet {
case .sheetA:
SheetA()
case .sheetB:
SheetB()
}
}
}
}
The problem is that without using item:, current versions of SwiftUI render the initial sheet with the first state value (ie sheet A in this case) and don't update properly on the first presentation. Using this item: approach solves the issue.

SwiftUI value in text doesn’t change

I don’t understand why the Text Value doesn’t change. if I remove the TextField, the Text value change :/ is there something about combine or SwiftUI I am missing ?
struct ContentView2: View{
#State private var numTouches: Int = 0
#State private var num: String = ""
var body: some View {
VStack{
Button("Touch me pls"){
self.numTouches += 1
}
Text("\(numTouches)")
TextField("Hello enter a number", text: $num)
}.onReceive(Just(num)) { newValue in
if newValue == "" {
self.numTouches = 0
} else {
self.numTouches = Int.init(newValue)!
}
}
}
}
What happens is that when a button is touched, it increases numTouches, which causes the view's body to re-render. .onReceive subscribes to the Just publisher that immediately publishes the value num, which is empty "" in the beginning, and that sets numTouches back to 0.
It sounds that you have really just a single variable, which is being updated from two places:
via TextField
via Button's action
So, keep it as single #State var numTouches: Int:
struct ContentView2: View{
#State private var numTouches: Int = 0
var body: some View {
VStack{
Button("Touch me pls"){
self.numTouches += 1
}
Text("\(numTouches)")
TextField("Hello enter a number",
text: $numTouches, formatter: NumberFormatter()))
// .keyboardType(.numberPad) // uncomment for number pad keyboard
}
}
}

SwiftUI: .sheet() doesn't go to the previous view with expected data when dismiss current sheet

As minimal, my code is like below. In SinglePersonView When user tap one image of movie in MovieListView(a movie list showing actor attended movies), then it opens the SingleMovieView as sheet mode.
The sheet could be popped up as tapping. But I found after close the sheet and re-select other movie in MovieListView, the sheet always opened as my previous clicked movie info aka the first time chosen one. And I could see in console, the movie id is always the same one as the first time. I get no clues now, do I need some reloading operation on the dismissal or something else?
And is it the correct way to use .sheet() in subView in SwiftUI, or should always keep it in the main body, SinglePersonView in this case.
struct SinglePersonView: View {
var personId = -1
#ObservedObject var model = MovieListViewModel()
var body: some View {
ScrollView() {
VStack() {
...
MovieListView(movies: model.movies)
...
}
}.onAppear {
// json API request
}
}
}
struct MovieListView: View {
var movies: [PersonMovieViewModel]
#State private var showSheet = false
ScrollView() {
HStack() {
ForEach(movies) { movie in
VStack() {
Image(...)
.onTapGesture {
self.showSheet.toggle()
}
.sheet(isPresented: self.$showSheet) {
SingleMovieView(movieId: movie.id)
}
}
}
}
}
}
There should be only one .sheet in view stack, but in provided snapshot there are many which activated all at once - following behaviour is unpredictable, actually.
Here is corrected variant
struct MovieListView: View {
var movies: [PersonMovieViewModel]
#State private var showSheet = false
#State private var selectedID = "" // type of your movie's ID
var body: some View {
ScrollView() {
HStack() {
ForEach(movies) { movie in
VStack() {
Image(...)
.onTapGesture {
self.selectedID = movie.id
self.showSheet.toggle()
}
}
}
}
.sheet(isPresented: $showSheet) {
SingleMovieView(movieId: selectedID)
}
}
}
}