#State with a #Appstorage property does not update a SwiftUI View - swift

I organized some settings to be stored in UserDefauls in a struct like this, because I want to have them in one place and to have getters and Setters.
enum PrefKeys : String {
case KEY1
case KEY2
var key: String { return self.rawValue.lowercased()}
}
struct Preferences {
#AppStorage(PrefKeys.KEY1.key) private var _pref_string_1 = ""
#AppStorage(PrefKeys.KEY1.key) var pref_string_2 = ""
var pref_string_1: String {
set { _pref_string_1 = newValue.lowercased() }
get { return _pref_string_1.lowercased() }
}
}
using it like this works fine:
struct ContentView: View {
var p = Preferences()
var body: some View {
NavigationView{
VStack(alignment: .leading){
Text("pref_string_1: \(p.pref_string_1)")
Text("pref_string_2: \(p.pref_string_2)")
NavigationLink("Sub",destination: SubView())
}
}
.padding()
}
}
If I use p as a #State var, it does not update the view, when the #State var is changed:
struct SubView: View {
#State var psub = Preferences()
#AppStorage("standalone pref") private var standalonePref = ""
var body: some View {
VStack(alignment: .leading){
Text("Preference1 in struct: \(psub.pref_string_1)")
TextField("Preference1 in struct:", text: $psub.pref_string_1)
Text("standalonePref \(standalonePref)")
TextField("standalonePref:", text: $standalonePref)
}
}
}
How can I fix this?

Related

Passing a state variable to parent view

I have the following code:
struct BookView: View {
#State var title = ""
#State var author = ""
var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}
struct MainView: View {
#State private var presentNewBook: Bool = false
var body: some View {
NavigationView {
// ... some button that toggles presentNewBook
}.sheet(isPresented: $presentNewBook) {
let view = BookView()
view.toolbar {
ToolbarItem(placement: principal) {
TextField("Title", text: view.$title)
}
}
}
}
}
This compiles but is giving me the following error on runtime:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
How do I pass a state variable to some other outside view? I can't use ObservableObject on BookView since that would require me to change it from struct to class
In general, your state should always be owned higher up the view hierarchy. Trying to access the child state from a parent is an anti-pattern.
One option is to use #Bindings to pass the values down to child views:
struct BookView: View {
#Binding var title : String
#Binding var author : String
var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}
struct ContentView: View {
#State private var presentNewBook: Bool = false
#State private var title = ""
#State private var author = ""
var body: some View {
NavigationView {
VStack {
Text("Title: \(title)")
Text("Author: \(author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(title: $title, author: $author)
}
}
}
Another possibility is using an ObservableObject:
class BookState : ObservableObject {
#Published var title = ""
#Published var author = ""
}
struct BookView: View {
#ObservedObject var bookState : BookState
var body: some View {
TextField("Title", text: $bookState.title)
TextField("Author", text: $bookState.author)
}
}
struct ContentView: View {
#State private var presentNewBook: Bool = false
#StateObject private var bookState = BookState()
var body: some View {
NavigationView {
VStack {
Text("Title: \(bookState.title)")
Text("Author: \(bookState.author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(bookState: bookState)
}
}
}
I've altered your example views a bit because to me the structure was unclear, but the concept of owning the state at the parent level is the important element.
You can also pass a state variable among views as such:
let view = BookView(title: "foobar")
view.toolbar {
ToolbarItem(placement: principal) {
TextField("Title", text: view.$title)
}
}
Then, inside of BookView:
#State var title: String
init(title: String) {
_title = State(initialValue: title)
}
Source: How could I initialize the #State variable in the init function in SwiftUI?

How do I validate dynamically added textFields on a button click in SwiftUI?

I have the following InputView struct and add those InputViews dynamically within a foreach loop in another view:
struct InputView: View {
#State private var input: String = ""
var correct_input: Int
var body: some View {
TextField("?", text: $input)
.foregroundColor(setColor())
}
func setColor() -> Color {
if (Int(input) == correct_input) {
return Color.green
}
return Color.red
}
}
Up to now it is shown immediately whether the input is correct. However, I would like to add a button so that the input of all InputViews is only validated when it is clicked. How can I achieve this in SwiftUI?
You can be done this by making a model of text fields and use one isValid flag for each InputView for the track.
Here, is the possible demo solution.
struct TextFieldModel: Identifiable {
var id = UUID()
var input: String
var correctInput: Int
var isValidate: Bool = true
}
struct InputView: View {
#Binding var input: TextFieldModel
var body: some View {
TextField("?", text: $input.input)
.foregroundColor(input.isValidate ? Color.blue : Color.red)
}
}
struct ContentViewTextFields: View {
#State var arrTextFields: [TextFieldModel] = [
.init(input: "", correctInput: 5),
.init(input: "", correctInput: 10),
.init(input: "", correctInput: 1)
]
#State var isValidate: Bool = true
var body: some View {
VStack{
ForEach(arrTextFields.indices) { index in
InputView(input: $arrTextFields[index])
.background(Color.gray.opacity(0.2))
.padding()
}
Spacer()
Button("Validate") {
// Here validate all text
arrTextFields.indices.forEach({arrTextFields[$0].isValidate = (Int(arrTextFields[$0].input) == arrTextFields[$0].correctInput) })
}
}
}
}
You can have a button to check the input, setting some #State variable like correct to true if it is correct.
Example:
struct ContentView: View {
var body: some View {
InputView(correctInput: 5)
}
}
struct InputView: View {
#State private var input = ""
#State private var correct = false
let correctInput: Int
var body: some View {
VStack {
TextField("?", text: $input)
.foregroundColor(correct ? .green : .red)
Button("Check answer") {
correct = Int(input) == correctInput
}
}
}
}

Use object property as binding variable [duplicate]

I have the following InputView struct and add those InputViews dynamically within a foreach loop in another view:
struct InputView: View {
#State private var input: String = ""
var correct_input: Int
var body: some View {
TextField("?", text: $input)
.foregroundColor(setColor())
}
func setColor() -> Color {
if (Int(input) == correct_input) {
return Color.green
}
return Color.red
}
}
Up to now it is shown immediately whether the input is correct. However, I would like to add a button so that the input of all InputViews is only validated when it is clicked. How can I achieve this in SwiftUI?
You can be done this by making a model of text fields and use one isValid flag for each InputView for the track.
Here, is the possible demo solution.
struct TextFieldModel: Identifiable {
var id = UUID()
var input: String
var correctInput: Int
var isValidate: Bool = true
}
struct InputView: View {
#Binding var input: TextFieldModel
var body: some View {
TextField("?", text: $input.input)
.foregroundColor(input.isValidate ? Color.blue : Color.red)
}
}
struct ContentViewTextFields: View {
#State var arrTextFields: [TextFieldModel] = [
.init(input: "", correctInput: 5),
.init(input: "", correctInput: 10),
.init(input: "", correctInput: 1)
]
#State var isValidate: Bool = true
var body: some View {
VStack{
ForEach(arrTextFields.indices) { index in
InputView(input: $arrTextFields[index])
.background(Color.gray.opacity(0.2))
.padding()
}
Spacer()
Button("Validate") {
// Here validate all text
arrTextFields.indices.forEach({arrTextFields[$0].isValidate = (Int(arrTextFields[$0].input) == arrTextFields[$0].correctInput) })
}
}
}
}
You can have a button to check the input, setting some #State variable like correct to true if it is correct.
Example:
struct ContentView: View {
var body: some View {
InputView(correctInput: 5)
}
}
struct InputView: View {
#State private var input = ""
#State private var correct = false
let correctInput: Int
var body: some View {
VStack {
TextField("?", text: $input)
.foregroundColor(correct ? .green : .red)
Button("Check answer") {
correct = Int(input) == correctInput
}
}
}
}

SwiftUI SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}

SwiftUI: assign binding variable from json parsed object

I am trying to assign a value I fetch and parse from JSON to another view.
struct ContentView: View {
#State private var showAlert = false
#State private var showAbout = false
#State private var showModal = false
#State private var title = "hi"
#State private var isCodeSelectorPresented = false
#ObservedObject var fetch = FetchNovitads()
var body: some View {
VStack {
NavigationView {
List(fetch.Novitadss) { Novitads in
VStack(alignment: .leading) {
// 3.
Text(Novitads.name!.de)
.platformFont()
.fontWeight(.black)
Text(Novitads.textTeaser.de)
.platformFont()
.fontWeight(.medium)
.onTapGesture {
self.showModal.toggle()
// 3.
}.sheet(isPresented: self.$showModal) {
ModalView(showModal: self.$showModal,
title: self.$title)
}
In this sample code the title (defined as "hi") is passed correctly.
What I want to do however is to assign the value of Novitads.name!.de to the title variable so that I can use it in the modal view.
I just display self.$title in the ModalView Text("(String(title))")
Then you don't need binding here and pass value directly, like
ModalView(showModal: self.$showModal, title: Novitads.name!.de)
and your ModalView declaration be as
struct ModalView: View {
#Binding showModal: Bool
let title: String
/// .. all other your code
}
Note: #State private var title = "hi" can be removed at all
try assigning the title like this:
struct ContentView: View {
struct ContentView: View {
#State private var showAlert = false
#State private var showAbout = false
#State private var showModal = false
#State private var title = "hi"
#State private var isCodeSelectorPresented = false
#ObservedObject var fetch = FetchNovitads()
var body: some View {
VStack {
NavigationView {
List(fetch.Novitadss) { Novitads in
VStack(alignment: .leading) {
// 3.
Text(Novitads.name!.de)
.platformFont()
.fontWeight(.black)
Text(Novitads.textTeaser.de)
.platformFont()
.fontWeight(.medium)
.onTapGesture {
self.showModal.toggle()
// 3.
}.sheet(isPresented: self.$showModal) {
ModalView(showModal: self.$showModal, title: self.$title)
}
}
}
}
}.onAppear(perform:{ self.title = self.fetch.Novitads.name!.de })
}