SwiftUI sheet never updated after first launch - swiftui

I use a modal sheet whose content is updated for each call. However, when the content is marked as #State, the view body is never updated.
Is anyone seeing this as well? Is a workaround available?
This is the calling view:
struct ContentView: View {
#State var isPresented = false
#State var i = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}
.sheet(
isPresented: $isPresented,
content: {
SheetViewNOK(i: self.i)
}
)
}
}
This does work:
struct SheetViewOK: View {
var i: Int
var body: some View {
Text("Hello \(i)") // String is always updated
}
}
This does not work. But obviously, in a real app, I need to use #State because changes made by the user need to be reflected in the sheet's content:
struct SheetViewNOK: View {
#State var i: Int
var body: some View {
Text("Hello \(i)") // String is never updated after creation
}
}

In your .sheet you are passing the value of your ContentView #State to a new #State. So it will be independent from the ContentView.
To create a connection or a binding of your ContentView #State value, you should define your SheetView var as #Binding. With this edit you will pass the binding of your state value to your sheet view.
struct SheetView: View {
#Binding var i: Int
var body: some View {
Text("Hello \(i)")
}
}
struct ContentView: View {
#State var isPresented = false
#State var i: Int = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}.sheet(
isPresented: $isPresented,
content: {
SheetView(i: self.$i)
})
}
}

There are 3 different ways
use a binding
use multiple .sheets
use $item
This is fully explained in this video.
Multiple Sheets in a SwiftUI View

Related

sheet does not dismiss on swiftUI

#Environment(\.presentationMode) var presentationMode
#Environment(\.dismiss) var dismiss
I know just toggle State variable of 'fullScreenCover' or 'sheet' but I found another approach and both above doesn't work.
Here's simple code.
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.dismiss) var dismiss
#State var sheetState = false
#State private var value = ""
var body: some View {
VStack {
Text(value)
Button("Present Sheet") {
sheetState.toggle()
}
}
.sheet(isPresented: $sheetState) {
Button("set1") {
value = "1"
//presentationMode.wrappedValue.dismiss()
dismiss()
}
}
}
}
When I press the button of sheet named 'set1' it change the 'value' set '1' and Text of VStack set "1" correctly but sheet doesn't dismiss.
It absolutely same both approach.
Am I using these things wrong?
here's my result.
https://imgur.com/a/5cjExyv
Technically you are trying to dismiss ContentView which makes no sense.
You have to create a separate View struct for the sheet.
struct ContentView: View {
#State private var sheetState = false
#State private var value = ""
var body: some View {
VStack {
Text(value)
Button("Present Sheet") {
sheetState.toggle()
}
}
.sheet(isPresented: $sheetState) {
SheetView(value: $value)
}
}
}
struct SheetView : View {
#Environment(\.dismiss) var dismiss
#Binding var value : String
var body: some View {
Button("set1") {
value = "1"
dismiss()
}
}
}

Swiftui how to update variable once view is closed

I am using SwiftUi 3.0 and I am new to it . I am learning about ObservedObjects . What I am trying to do is update the count of a variable every time that I close a view . This is the entire small app . The screen starts at DataUpdateView view when I click Next View I go to DataUpdateView2 view . Once I close DataUpdateView2 and go back to the original view I want to have the
Text("Score Count \(progress.score)")
score number increase by 1 since in the second view I do a +1 every time that I close that view . Any suggestions would be great
import SwiftUI
class UserProgress: ObservableObject {
#Published var score = 0
}
struct DataUpdateView: View {
#State var nextView = false
#StateObject var progress = UserProgress()
var body: some View {
VStack {
Text("Score Count \(progress.score)")
Text("Next View")
.onTapGesture {
nextView = true
}.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2()
})
}
}
}
struct DataUpdateView_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView()
}
}
struct DataUpdateView2: View {
#ObservedObject var progress = UserProgress()
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Back")
.onTapGesture {
progress.score += 1
self.presentationMode.wrappedValue.dismiss()
}
}
}
struct DataUpdateView2_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView2()
}
}
You're probably not seeing the first view update since both views are instantiating their own UserProgress(). You need to pass the object you already created in the first view along to the second in the initializer
So In DataUpdateView:
.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2(progress: progress)
})
}
And then in DataUpdateView2:
struct DataUpdateView2: View {
#ObservedObject var progress: UserProgress
#Environment(\.presentationMode) var presentationMode
// ...
}
So now the second view is receiving the object from the first rather than creating its own.
Note: If you are not using an ObservableObject, then take a look at the second part.
In this specific situation, you don't even need a Binding variable, you can just use the .onDisappear method. .onDisappear Documentation.
import SwiftUI
class UserProgress: ObservableObject {
#Published var score = 0
}
struct DataUpdateView: View {
#State var nextView = false
#StateObject var progress = UserProgress()
var body: some View {
VStack {
Text("Score Count \(progress.score)")
Text("Next View")
.onTapGesture {
nextView = true
}.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2()
})
}
}
}
struct DataUpdateView_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView()
}
}
struct DataUpdateView2: View {
#ObservedObject var progress = UserProgress()
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Back")
.onTapGesture{
presentationMode.wrappedValue.dismiss()
print("Dismissed!")
}
.onDisappear{
//This is called when the view disappears.
progress.score += 1
}
}
}
struct DataUpdateView2_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView2()
}
}
Second Part
If you want the variable to update when the view closes, you could use the .onDisappear method and a Binding value. An example implementation of this is below:
struct ViewOne: View{
#State var number = 0
var body: some View{
VStack{
Text("Number: \(number)")
NavigationLink(destination: ViewTwo(variable: $number)){
Text("Go To View Two")
}
}
}
}
struct ViewTwo: View{
#Binding var variable: Int
var body: some View{
//Content of view 2 here
Text("View Two")
.onDisappear{
//This is called when the view disappears
variable += 1
}
}
}
In short you need to use same view model in both views. A possible and seems simplest approach in your code is to inject view model from first view to second via environment object, like
#StateObject var progress = UserProgress()
var body: some View {
VStack {
Text("Score Count \(progress.score)")
Text("Next View")
.onTapGesture {
nextView = true
}.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2()
.environmentObject(progress) // << here !!
})
and use it internally, like
struct DataUpdateView2: View {
#EnvironmentObject var progress: UserProgress // << injected automatically !!

Calling Method on Child View in SwiftUI

I have an ImageEditView that contains an ImageCanvasView and an ImageCaptureButton.
Ideally, I want ImageCaptureButton to call a method on ImageCanvasView called takeScreenshot.
How do I achieve this in SwiftUI? I've been thinking of trying to save ImageCanvasView into a variable in ImageEditView so that my ImageCaptureButton can then call its method, but SwiftUI's declarative nature means this isn't possible.
----- EDIT below -----
The flow is as follows:
ImageSelectView (user selects an image)
ImageEditView (user edits an image) - this view contains ImageCanvasView and ImageCaptureButton
ImageShareView (user shares the image)
The following is ImageEditView
import SwiftUI
struct ImageEditView: View {
#State var selectedImage: Image
#State private var isNavLinkPresented = false
#State private var imageSnapshot: UIImage = UIImage()
var canvasView: ImageCanvasView = ImageCanvasView(selectedImage: $selectedImage)
// this won't work: Cannot use instance member '$selectedImage' within property initializer; property initializers run before 'self' is available
var body: some View {
VStack {
canvasView
Spacer()
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: ImageShareView(imageToShare: $imageSnapshot), isActive: $isNavLinkPresented) {
Text("Next")
.onTapGesture {
imageSnapshot = canvasView.takeScreenshot()
isNavLinkPresented = true
}
}
}
}
.navigationBarTitle(Text("Edit image"))
}
}
You are in the right direction. By creating var of the view, you can call the function.
Here is the example demo
struct ImageEditView: View {
var canvasView: ImageCanvasView = ImageCanvasView()
var body: some View {
VStack {
Text("ImageEditView")
canvasView
Button("ImageCaptureButton") {
canvasView.takeScreenshot()
}
}
}
}
struct ImageCanvasView: View {
var body: some View {
Text("ImageCanvasView")
}
func takeScreenshot() {
print(#function + " Tapped ")
}
}
You can also use computed property to pass data
struct ImageEditView: View {
var data: String = ""
var canvasView: ImageCanvasView {
ImageCanvasView(data: data)
}
// Body code
}
struct ImageCanvasView: View {
var data: String
var body: some View {
Text("ImageCanvasView")
}
func takeScreenshot() {
print(#function + " Tapped ")
}
}
EDIT
Use init to use the same instance of ImageCanvasView.
struct ImageEditView: View {
#State var selectedImage: Image
#State private var isNavLinkPresented = false
#State private var imageSnapshot: UIImage = UIImage()
var canvasView: ImageCanvasView!
init(selectedImage: Image) {
self.selectedImage = selectedImage
canvasView = ImageCanvasView(selectedImage: $selectedImage)
}
// Other code

#State var doesn't store value

My goal is to pass values between views, from Chooser to ThemeEditor. When the user presses an icon, I'm saving the object that I want to pass and later, using sheet and passing the newly created view with the content of the #State var.
The assignment is done successfully to the #State var themeToEdit, however it is nil when the ThemeEditor view is created in the sheet
What am I doing wrong?
struct Chooser: View {
#EnvironmentObject var store: Store
#State private var showThemeEditor = false
#State private var themeToEdit: ThemeContent?
#State private var editMode: EditMode = .inactive
#State private var isValid = false
var body: some View {
NavigationView {
List {
ForEach(self.store.games) { game in
NavigationLink(destination: gameView(game))
{
Image(systemName: "wrench.fill")
.imageScale(.large)
.opacity(editMode.isEditing ? 1 : 0)
.onTapGesture {
self.showThemeEditor = true
/* themeInfo is of type struct ThemeContent: Codable, Hashable, Identifiable */
self.themeToEdit = game.themeInfo
}
VStack (alignment: .leading) {
Text(self.store.name(for: something))
HStack{
/* some stuff */
Text(" of: ")
Text("Interesting info")
}
}
}
}
.sheet(isPresented: $showThemeEditor) {
if self.themeToEdit != nil { /* << themeToEdit is nil here - always */
ThemeEditor(forTheme: self.themeToEdit!, $isValid)
}
}
}
.environment(\.editMode, $editMode)
}
}
}
struct ThemeEditor: View {
#State private var newTheme: ThemeContent
#Binding var isValid: Bool
#State private var themeName = ""
init(forTheme theme: ThemeContent, isValid: Binding<Bool>) {
self._newTheme = State(wrappedValue: theme)
self._validThemeEdited = isValid
}
var body: some View {
....
}
}
struct ThemeContent: Codable, Hashable, Identifiable {
/* stores simple typed variables of information */
}
The .sheet content view is captured at the moment of creation, so if you want to check something inside, you need to use .sheet(item:) variant, like
.sheet(item: self.$themeToEdit) { item in
if item != nil {
ThemeEditor(forTheme: item!, $isValid)
}
}
Note: it is not clear what is ThemeContent, but it might be needed to conform it to additional protocols.
Use Binding. Change your ThemeEditor view with this.
struct ThemeEditor: View {
#Binding private var newTheme: ThemeContent?
#Binding var isValid: Bool
#State private var themeName = ""
init(forTheme theme: Binding<ThemeContent?>, isValid: Binding<Bool>) {
self._newTheme = theme
self._isValid = isValid
}
var body: some View {
....
}
}
And for sheet code
.sheet(isPresented: $showThemeEditor) {
ThemeEditor(forTheme: $themeToEdit, isValid: $isValid)
}
On Action
.onTapGesture {
/* themeInfo is of type struct ThemeContent: Codable, Hashable, Identifiable */
self.themeToEdit = game.themeInfo
self.showThemeEditor = true
}

Presenting a Sheet modally, #State value is not changing

I have tried much time on it but I couldn't figure out why it is not working.
The problem is when I tap the button, the new value inside the sheet is not updated. It always show the same value which is set up in start.
#State var value:String = "empty"
#State var explorePageIsEnabled:Bool = false
VStack{
Button("tap me"){
value = "the new one"
exploreStatusIsEnabled.toggle()
}
.sheet(isPresented: $exploreStatusIsEnabled, content: {
Text(value)
})
}
Deployment target is IOS 14+
Create a separate struct view for text and use Binding.
struct SheetView: View {
#Binding var value: String
var body: some View {
Text(value)
}
}
struct ContentView: View {
#State private var value: String = "empty"
#State private var explorePageIsEnabled: Bool = false
var body: some View {
VStack{
Button("tap me"){
value = "the new one"
explorePageIsEnabled.toggle()
}
.sheet(isPresented: $explorePageIsEnabled, content: {
SheetView(value: $value)
})
}
}
}