Issue with setting #State variable dynamically - swift

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)
}
}
}
}
}

Related

How to switch between views using #State and fullScreenCover in SwiftUI

I'm trying to figure out how to switch between views using #State and fullScreenCover in SwiftUI
I have a main view and 2 simple views (View1 and View2). View1 must open on the tap of the "Show view 1" button, and View2 when typing on the "Show view 2" button. I also created 2 #State variables for this to work and initialized it with a .View1 (ActiveFullscreenview enum):
#State private var showFullscreenView = false
#State private var activeFullscreenView: ActiveFullscreenView = .View1
The problem is that it constantly showing the View1 even when I tap on the Show view 2 Button.
I thought that when tapping on the button and giving the activeFullscreenView variable a value of .View2 , it should change the variable and show the fullScreenView.
I supposed that it was because I called the showFullscreenView and it changed the variable and after that the default value was .View1 so it didn't change it.
tried this too:
self.showFullscreenView = true
self.activeFullscreenView = .View2
The second assumption was that I shouldn't give this variable the initial value. But in this case it adds some additional initialisation code that is not convenient:
#State private var activeFullscreenView: ActiveFullscreenView = .View1
Currently I have the following code that has the described issue:
import SwiftUI
struct MainView: View {
enum ActiveFullscreenView {
case View1, View2
}
#State private var showFullscreenView = false
#State private var activeFullscreenView: ActiveFullscreenView = .View1
var body: some View {
ZStack {
VStack {
// Button to show View 1 in FullScreen
Button("Show view 1", action: {
self.activeFullscreenView = .View1
self.showFullscreenView = true
})
// Button to show View 2 in FullScreen
Button("Show view 2", action: {
self.activeFullscreenView = .View2
self.showFullscreenView = true
})
}
}
.fullScreenCover(isPresented: $showFullscreenView) {
switch self.activeFullscreenView {
case .View1:
Text("View1")
case .View2:
Text("View2")
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
Any help related to solving the problem appreciated. Convenient ways ways about how to achieve the same results of showing separate views using fullscreenCover appreciated.
Working in Xcode 12.4, latest iOS 14
With the help of #jnpdx who provided
This Answer that is Similar to My problem the problem is solved. Here is the code. As long as fullScreenCover and sheet are both modals, I renamed enum to ModalView:
struct MainView: View {
// enum to decide which view to present:
enum ModalView: String, Identifiable { // Identifiable
case View1, View2
var id: String {
return self.rawValue
}
}
#State var activeModalView : ModalView? = nil
var body: some View {
ZStack {
VStack {
// Button to show View 1
Button("Show view 1", action: {
self.activeModalView = .View1
})
// Button to show View 2
Button("Show view 2", action: {
self.activeModalView = .View2
})
}
} // use fullScreenCover or sheet depending on your needs
.fullScreenCover(item: $activeModalView) { activeModalValue in
switch activeModalValue {
case .View1:
Text("View1")
case .View2:
Text("View2")
}
}
}
}

How to switch to another view programmatically in SwiftUI (without a button press)

I have a problem to switch to another view without pressing a button in my SwiftUI app.
I can take many examples of what I want to do :
1- Imagine a login form where we need to fill 2 textfields : username and password. When there's a perfect match we are automatically authenticated.
2- Take my example now. I have this view :
What I want to do : when I select a row in the list (ex. Lyon-Perrache TGV) AND if the two textfields are not empty => I'm redirected to another view.
There's many examples to know how to move to a view from another one, but always with a button click .. (with NavigationLink)
I need to do this programatically exactly when the two textfields are filled AND when I select the destination textfield from the list.
that's a part of my code :
import SwiftUI
struct Search: View {
#ObservedObject var webservice = Webservice()
#State var Depart: String = ""
#State var Destination: String = ""
#State var listStation = [Station]()
#State var predictedValues: Array<String> = []
#State var isBeingEditedDeparture: Bool = false
#State var isBeingEditedDestination: Bool = false
#State var hey: Bool = false
#State var activate: Bool = false
init() {
UITableView.appearance().showsVerticalScrollIndicator = false
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
//UITableView.appearance().separatorColor = .clear
}
var body: some View {
VStack(alignment: .leading){
PredictingTextField(listStation: self.$listStation , predictedValues: self.$predictedValues,
Depart: self.$Depart, Destination: self.$Destination, isBeingEditedDeparture:
self.$isBeingEditedDeparture, isBeingEditedDestination: self.$isBeingEditedDestination )
.textFieldStyle(RoundedBorderTextFieldStyle())
List() {
ForEach(self.predictedValues, id: \.self){ value in
Text(value)
.onTapGesture {
if(self.isBeingEditedDeparture == true)
{
self.Depart = value
}
else{
self.Destination = value
self.activate = true
}
}
}
}.padding(.top, 10)
}
}
I have declared a variable named "activate" to be the trigger point of moving to the new View.
It's equal to true when we select a row in the list for "destination" textfield.
How can I do to go further ?
Thank you in advance !
iOS 16+
Note: Below is a simplified example of how to programatically present a new link. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath that allow us to push views programmatically.
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("ContentView")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("NewView")
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
path.append("NewView")
}
}
}
}
In the above example you can trigger the NewView to appear just by executing path.append("NewView") from any place.
iOS 13-15
You can use a NavigationLink with the isActive parameter. When you set this parameter to true it will present a NewView:
#State var activated: Bool = false
NavigationLink(destination: NewView(), isActive: $activated) {
EmptyView()
}
Then you need to place your NavigationLink in the NavigationView to make it work.

Fire some code when dismissing SwiftUI modal

I have a modal built with SwitftUI which has a TextField with onCommit: code which saves user input from #State variable to file when user taps "return" on keyboard.
However, if user types something inside TextField and then dismisses the modal without pressing "return", the onCommit: code doesn't fire and user input stays unsaved. How do I fire some code accessing inner variable of my modal View when it is dismissed?
Try the following:
Instead having a private #State var on your modal, make it an internal #Binding that you pass into the modal from the call site. This way the modified bound variable is available on both the caller and the modal view.
import SwiftUI
struct ContentView: View {
#State var dismiss = false
#State var txt = ""
#State var store = ""
var body: some View {
VStack {
Text("modal").sheet(isPresented: $dismiss, onDismiss: {
self.store = self.txt
}) {
TextField("txt", text: self.$txt) {
self.store = self.txt
}.padding().border(Color.red)
}.onTapGesture {
self.dismiss.toggle()
}
Text(store)
}
}
}
struct ContetView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Value from #State variable does not change

I have created a View that provides a convinient save button and a save method. Both can then be used inside a parent view.
The idea is to provide these so that the navigation bar items can be customized, but keep the original implementation.
Inside the view there is one Textfield which is bound to a #State variable. If the save method is called from within the same view everthing works as expected. If the parent view calls the save method on the child view, the changes to the #State variable are not applied.
Is this a bug in SwiftUI, or am I am missing something? I've created a simple playbook implementation that demonstrates the issue.
Thank you for your help.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
// Create the child view to make the save button available inside this view
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
// Set the trailing button to the one from the child view.
// This is required as this view might be inside a modal
// sheet, and we need to add the cancel button as a leading
// button:
// leading: self.cancelButton
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
I think a more SwiftUi way of doing it:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
return NavigationView {
// tell the child view where to render it's navigation item
// Instead of configuring navigation items.
NavigationLink(destination: Child(navigationSide: .left)) {
Text("Open")
}
}
}
}
struct Child: View {
enum NavigationSide { case left, right }
// If you really want to encapsulate all state in this view then #State
// is a good choice.
// If the parent view needs to read it, too, #Binding would be your friend here
#State private var value: String = "default"
// no need for #State as it's never changed from here.
var navigationSide = NavigationSide.right
// wrap in AnyView here to make ternary in ui code easier readable.
var saveButton: AnyView {
AnyView(Button(action: save) {
Text("Save")
})
}
var emptyAnyView: AnyView { AnyView(EmptyView()) }
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(leading: navigationSide == .left ? saveButton : emptyAnyView,
trailing: navigationSide == .right ? saveButton : emptyAnyView)
}
func save() {
print("\(value)")
}
}
TextField will only update your value binding when the return button is pressed. To get text changes that occur during editing, set up an observed object on Child with didSet. This was the playground I altered used from your example.
struct ContentView: View {
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
class TextChanges: ObservableObject {
var completion: (() -> ())?
#Published var text = "default" {
didSet {
print(text)
}
}
}
struct Child: View {
#ObservedObject var textChanges = TextChanges()
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $textChanges.text).multilineTextAlignment(.center)
Button(action: {
print(self.textChanges.text)
}) {
Text("Update")
}
}
}
func save() {
print("\(textChanges.text)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Inside Child: value is mutable because it's wrapped with #State.
Inside ContentView: child is immutable because it's not wrapped with #State.
Your issue can be fixed with this line: #State var child = Child()
Good luck.
Child view needs to keep its state as a #Binding. This works:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var v = "default"
var body: some View {
let child = Child(value: $v)
return NavigationView {
NavigationLink(
destination: child.navigationBarItems(trailing: child.saveButton)
) {
Text("Open")
}
}
}
}
struct Child: View {
#Binding var value: String
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Based on this commend from #nine-stones (thank you!) I implemented a more SwiftUI way so solve my problem. It does not allow the customization of the navigation items as I planned, but that was not the problem that needed to be solved. I wanted to use the Child view in a navigation link, as well as inside a modal sheet. The problem was how to perform custom cancel actions. This is why I removed the button implementation and replaced it with a cancelAction closure. Now I can display the child view wherever and however I want.
One thing I still do not know why SwiftUI is not applying the child context to the button inside the saveButton method.
Still, here is the code, maybe it helps someone in the future.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
NavigationLink(
destination: Child(
// Instead of defining the buttons here, I send an optional
// cancel action to the child. This will make it possible
// to use the child view on navigation links, as well as in
// modal dialogs.
cancelAction: {
self.presentationMode.wrappedValue.dismiss()
}
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
#Environment(\.presentationMode) var presentationMode
var cancelAction: (() -> Void)?
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(
leading: self.cancelAction != nil ? Button(action: self.cancelAction!, label: {
Text("Cancel")
}) : nil,
trailing: self.saveButton
)
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())

Modifying a #State var from a #Binding var isn't refreshing the view in SwiftUI

So I have a ParentView which contains a FilterBar and a List. It looks something like this:
struct ParentView: View {
#State var listCellModels: [ListCellModel]
// Both these vars are passed to the FilterBar and adjusted by the FilterBar
#State var isEditing: Bool = false
#State var selectedType: FilterType = .none
// When selected type is changed, I need to reload the models in the list with the new filter
private var filteredModels: [ListCellModel] {
return listCellModels.filter{
(selectedType.rawValue == 0 || $0.approved.rawValue == selectedType.rawValue)
}
}
var body: some View {
VStack {
FilterBar(isEditing: $isEditing, selectedType: $selectedType)
// All the items in the list are buttons that display a custom view I had
// this works fine, and when isEditing is changed the view DOES update
List(filteredModels) { model in
Button(action: {
// Does a thing
}, label: {
ListViewCell(model: model, isEditing: self.$isEditing)
})
}
}
}
}
My Filter bar is just a simple HStack with a couple buttons that modify the variables
struct FilterBar: View {
#Binding var isEditing: Bool
#Binding var selectedType: FilterType
var body: some View {
HStack(alignment: .center) {
Button(action: {
self.selectedType = FilterType.init(rawValue: (self.selectedType.rawValue + 1) % 4)!
}, label: {
Text("Filter: \(selectedType.name)")
}).padding(.top).padding(.leading)
Spacer()
Button(action: {
self.isEditing = !self.isEditing
}, label: {
Text(!isEditing ? "Edit" : "Done")
}).padding(.top).padding(.trailing)
}
}
}
When I tap the button that changes isEditing, all of the cells in the list update to show their "Editing" states, but when i tap the button to change selectedType, the variable in the parent view does get updated, as I've observed in the debugger - however the view does not reload. So it appears as if the old filter is still being applied.
Is there any reason why updating this #State var is not reloading the view?
Are there any workarounds?
Well, it is like... workaround... but for testing, try
FilterBar(isEditing: $isEditing, selectedType: $selectedType)
if selectedType != .none {
EmptyView()
}
In general, it would be correct to introduce view model as ObservableObject and have filteredModels in it as #Published, so your FilterBar changed that property, which will automatically refreshed the ParentView.