I am beginner and I am trying to build an app. I followed the beginners course here and another one. I am getting stuck when I put a Toggle to true in View1 and View2 should display some changes with “if statement” but the nothing happens. I used the ObservableObject because I am planning to use more views that needs information from View 1. I hope some one can help me out.
Here is a example of my code:
View 1:
import SwiftUI
class ChangeViews: ObservableObject { @Published var showAdd = false
}
struct SettingsConfig: View {
@ObservedObject var changeViews: ChangeViews
var body: some View {
NavigationView {
VStack {
Toggle("Add Guest", isOn: $changeViews.showAdd)
}
View 2:
import SwiftUI
struct AddView: View {
@ObservedObject var changeViews: ChangeViews
@State var name = ""
@State var gender = ""
@State var date = ""
var body: some View {
if (changeViews.showAdd == true) {
VStack {
TextField("name", text: $name)
TextField("gender", text: $gender)
TextField("list date", text: $date)
// TODO: add function to navigate
ZStack {
ButtonView()
.foregroundColor(.blue)
Text("Add")
@ObservedObject var changeViews: ChangeViews
@ObservedObject var data: Data
@State var name = ""
@State var gender = ""
@State var date = ""
var body: some View {
//VStack { // Testting the if statement to set an alert.
if (changeViews.showAdd == true) {
VStack {
TextField("name", text: $name)
TextField("gender", text: $gender)
TextField("list date", text: $date)
// TODO: add function to navigate
ZStack {
ButtonView()
.foregroundColor(.blue)
Text("Add")
.foregroundColor(.white)
.font(.title3)
}
.padding()
Spacer()
Spacer()
Spacer()
NavigationView {
ScrollView {
NavigationLink(destination: ListView(data: Data())) {
ZStack {
ButtonView()
.foregroundColor(.green)
Text("Show List")
.foregroundColor(.white)
.font(.title3)
}
}
}
}
}
.font(.title2)
.padding(30)
}
else {
ZStack {
PopView()
VStack {
Text("Access Denied")
.foregroundColor(.red)
.font(.title)
.bold()
Text("You need to enable 'Add Guest' in Settings")
}
.padding(10)
}
}
}
The initial view is ContentView and as you can see in the code there are 4 tabs. The last tab SettingsConfig I want to use a toggle and when this is set to true I want to grant access to AddView.
Getting back to the original question regarding making a change to an ObservableObject Published property in View1 and then wanting to see that change reflected in View2 which is monitoring the same Published property.
When using a ViewModel (ObservableObject) both of those Views need access to the same instance of the ViewModel. In each of those Views you declared an instance of the ViewModel (ChangeViews) by saying:
@ObservedObject var changeViews: ChangeViews
which is fine.
But, in your Content view you have declared the StateObject as follows:
@StateObject var changeViews = ChangeViews()
then in your tab item AddView you have this:
AddView(changeViews: ChangeViews(), data: Data())
whereas you should be saying this:
AddView(changeViews: changeViews, data: Data())
The subtle difference is that using the variable changeViews rather than a completely different instance of ChangeViews() will ensure that both views are referencing the same ViewModel.
The NewPass is where you enter your pin code to enter SettingsConfig. The pin code is for now hard coded but I will change it when I am at that point. I have to say that the code is not mine, I got it from Google and I change it a bit and it is working fine so far. Still got a bug that I need to fix but that will me be later on.
Data is where the name of the persons that are on the list are stored and are for now hardcoded.
NewPass code is:
import SwiftUI
// import Introspect
public struct NewPass: View {
var maxDigits: Int = 4
var label = "Enter Pin"
@State var pin: String = ""
@State var showPin = false
@State var isDisabled = false
@State var loginSettings = false
var handler:(String, (Bool) -> Void) -> Void
public var body: some View {
NavigationView {
VStack(spacing: 10) {
Text(label).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
// Button to go to next view after correct pin
Button {
if (pin == "1234"){
loginSettings.toggle()
}
} label: {
NavigationLink(destination: SettingsConfig(changeViews: ChangeViews()), isActive: $loginSettings, label: {Text("")})
ZStack {
if (isDisabled == true) {
ButtonView()
.foregroundColor(.blue)
Text("Login")
.foregroundColor(.white)
.font(.title3)
}
else {
//ButtonView()
// .foregroundColor(.gray)
//Text("Login")
// .foregroundColor(.white)
// .font(.title3)
}
}
}
//.sheet(isPresented: $loginSettings) {
// SettingsConfig()
// TODO need to resolve sheet into other view
// }
}
}
.navigationBarBackButtonHidden(true)
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<maxDigits) { index in
Image(systemName: self.getImageName(at: index))
.font(.title)
Spacer()
}
}
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { self.pin }, set: { newValue in
self.pin = newValue
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
// Introspect library can used to make the textField become first resonder on appearing
// if you decide to add the pod 'Introspect' and import it, comment #50 to #53 and uncomment #55 to #61
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
// .disabled(isDisabled)
private var showPinStack: some View {
HStack {
Spacer()
if !pin.isEmpty {
showPinButton
}
}
.frame(height: 20)
.padding([.trailing])
}
private var showPinButton: some View {
Button(action: {
self.showPin.toggle()
}, label: {
self.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.red) :
Image(systemName: "eye.fill").foregroundColor(.green)
})
}
private func submitPin() {
guard !pin.isEmpty else {
showPin = false
return
}
//TODO: Check if next view button will work here
if pin.count == maxDigits {
isDisabled = true
handler(pin) { isSuccess in
if isSuccess {
print("pin matched, go to next page, no action to perfrom here")
} else {
pin = ""
isDisabled = false
print("this has to called after showing toast why is the failure")
}
}
}
// this code is never reached under normal circumstances. If the user pastes a text with count higher than the
// max digits, we remove the additional characters and make a recursive call.
if pin.count > maxDigits {
pin = String(pin.prefix(maxDigits))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= self.pin.count {
return "circle"
}
if self.showPin {
return self.pin.digits[index].numberString + ".circle"
}
return "circle.fill"
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
You might be best advised to set up your ChangeViews to be an EnvironmentObject
In ContentView:
struct ContentView: View {
var body: some View {
TabView {
SearchView()
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
ListView()
.tabItem {
Image(systemName: "list.bullet")
Text("List")
}
AddView()
.tabItem {
Image(systemName: "plus")
Text("Add")
}
SettingsConfig()
.tabItem {
Image(systemName: "gearshape.2")
Text("Settings")
}
}
// This is where the ChangeViews ObservableObject is injected
// the View hierarchy
.environmentObject(ChangeViews())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ChangeViews())
}
}
and in SettingsConfig:
struct SettingsConfig: View {
// This is where the injected ChangeViews is accessed
@EnvironmentObject var changeViews: ChangeViews
var body: some View {
NavigationView {
VStack {
Toggle("Add Guest", isOn: $changeViews.showAdd)
}
}
}
}
struct SettingsConfig_Previews: PreviewProvider {
static var previews: some View {
SettingsConfig()
.environmentObject(ChangeViews())
}
}
and in AddView:
struct AddView: View {
// This is where the injected ChangeViews is accessed
@EnvironmentObject var changeViews: ChangeViews
// Other declarations
// .....
var body: some View {
if (changeViews.showAdd == true) {
VStack {
// The rest of you body code goes here.
// I left it out just to save time and space
....
....
....
....
}
} else {
ZStack {
// Your code goes here......
}
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView()
.environmentObject(ChangeViews())
}
}
This approach should simplify what you are trying to do.
Note that the Previews will be simplified as well since all that you do in them is inject an instance of ChangeViews() as an .environmentObject(). Remember that Previews are not part of the final project that you build. They are only there to allow you to take a look at your view in the Canvas so that as you build it, you can make sure that you are getting the right layout.