SwiftUI - Why won't my #Published variable update within my #ObservedObject call? - swift

In essence, I'm learning by creating a simple app that takes an ISBN and returns the book information. I've created a view file to display this information along with a text entry for capturing the ISBN. The logic side of things as pertaining to getting the information from the internet is fine, however, when I try to update the view with the modified variables, it only prints them as declared, and NOT redrawing them after modification. Any advice would be appreciated. Thanks!
This specific example references #Published var test = "Testing 1 2 3 as the default, and after the search, the var is modified to test = modified text which is then printed in the text view of TestView.swift. The print statement correctly displays the modified text, however it does not update in the view in which it is passed. The goal is to pass the final dictionary, but for testing purposes I'm using a simple string variable.
TestFile.swift
import Foundation
import SwiftyXMLParser
import SwiftUI
class getdata: ObservableObject {
#Published var outputDict: [String:String] = [:]
#Published var test = "Testing 1 2 3"
.
.
//code to grab xml from url
.
.
func parseData(input: String) {
var title: String?
var author: String?
let xml = try! XML.parse(input)
if let text = xml["GoodreadsResponse", "book", "title"].text {
title = text
outputDict["title"] = title
}
if let text = xml["GoodreadsResponse", "book", "authors", "author", "name"].text {
author = text
outputDict["author"] = author
}
print("Title: \(outputDict["title"]!), Author: \(outputDict["author"]!)")
test = "modified text"
print(test)
}
}
TestView.swift
import SwiftUI
struct testView: View {
#State var textEntry: String = ""
#ObservedObject var viewModel: getdata
var body: some View {
let try1 = getdata()
VStack {
TextField("Enter ISBN", text: $textEntry)
.padding()
Button(action: {try1.getData(bookID: textEntry)}, label: {
Text("Search")
})
Text("Title: \(self.viewModel.test)")
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView(viewModel: getdata())
}
}

Although I don't think your example compiles, and you haven't provided the definitions of some of the functions, I can point you at one error:
Inside the body of the view you are creating a new Observable object:
let try1 = getdata()
And your Button calls messages on this:
Button(action: {try1.getData(bookID: textEntry)}...
But your TextView reads from the original viewModel:
Text("Title: \(self.viewModel.test)")
You are interacting with try1, but deriving your view from viewModel. They are separate objects.

Related

How can I save what I'm writing in a TextField to CoreData?

i'm trying to save what i'm typing into core data. So, if somone accidentaly closed the app, the text need to remain in the TextField. This is how i'm trying to do it, but i'm getting this error message:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Cart deliveryAddress]: unrecognized selector sent to instance 0x600002276a00'
This is my View :
struct LivrareView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [])
var carts: FetchedResults<Cart>
#State var adresaTextField : String = ""
#State var telefonTextField : String = ""
#State var tacamuriSwitch: Bool = false
#State var oraLivrare: String = ""
#State var specificatiiTextEditor: String = "Ex : Nu merge interfonu"
#ObservedObject var cart : Cart
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(Texts.livrareViewText1)
.foregroundColor(.orange)
TextField("Ex: str. 16 Decembrie 1989, nr, 23, ap 1, et 1", text: $cart.wrappedDeliveryAddress, onEditingChanged: { _ in
let newCart = Cart(context: viewContext)
newCart.deliveryAddress = cart.wrappedDeliveryAddress
print(newCart.deliveryAddress)
do {
try viewContext.save()
} catch {
let error = error as NSError
fatalError("Unresolved error\(error)")
}
})
}
}
.padding(34)
}
public init(model: Cart? = nil) {
self.cart = model ?? Cart()
}
}
}
Here is my Cart. I'm using codegen : " Manually / none "
#objc(Cart)
public class Cart: NSManagedObject, Identifiable {
}
extension Cart {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Cart> {
return NSFetchRequest<Cart>(entityName: "Cart")
}
#NSManaged public var grams: Double
#NSManaged public var name: String?
#NSManaged public var price: Int32
#NSManaged public var deliveryAddress: String?
public var wrappedDeliveryAddress : String {
get { deliveryAddress ?? ""}
set { deliveryAddress = newValue}
}
}
This is a photo of my Core Data Model:
How can i make this work ?
This is where i'm calling the View :
TabView(selection: self.$index){
LivrareView()
.tag(0)
RidicareView()
.tag(1)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
The issue you are having is trying to create a Cart in the public init. Essentially, you are saying if the prior view gives me a Cart use it, otherwise, I am going to make my own Cart that doesn't exist in the managed object context. Since you never pass a Cart in to this view, you only get a Cart that is not in context. The first rule of Core Data is you have to operate in the same context. (Rule 2 is sometimes you can violate this, but not as a beginner.) When you try to use it, you get the error. And, the way you have things set up, you don't need it.
I condensed LivrareView down to the bare minimum, and then added some Text() views so you can see what is going on. First, you already have a #State variable for adresa. You don't need the #ObservedObject var cart for that. Then, when the time comes, you put the adresaTextField in to newCart.wrappedDeliveryAddress (you made it, you just as well use it exclusively and make deliveryAddress private) and then save your entity. I moved the save out of the .onEditingChange into an .onSubmit() to save a little sanity, but the bug will become apparent when you use this code. You are creating a new entity every time you hit return (instead of every keystroke as your code had). I think this is what you were trying to avoid when you created the #ObservedObject var cart, but that won't work. Your better plan is simply to use all #State variables, collect your data and then create and save your managed object with a "save" button. I would also validate your entries first, before you allow a save.
struct LivrareView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: []) var carts: FetchedResults<Cart>
#State var adresaTextField : String = ""
var body: some View {
ScrollView {
VStack(alignment: .leading) {
TextField("Ex: str. 16 Decembrie 1989, nr, 23, ap 1, et 1", text: $adresaTextField)
.onSubmit {
let newCart = Cart(context: viewContext)
newCart.wrappedDeliveryAddress = adresaTextField
print(newCart.wrappedDeliveryAddress)
do {
try viewContext.save()
} catch {
let error = error as NSError
fatalError("Unresolved error\(error)")
}
}
Text("adresaTextField is \(adresaTextField)")
Text("Managed Objects:")
ForEach(carts) { cart in
Text(cart.wrappedDeliveryAddress)
}
}
}
}
}
edit:
Also, a suggestion to save you some heartache when using Core Data with manual codegen. Create a third extension file to put your custom code in. If you change your attributes and need to regenerate the extensions, you will erase your custom code. I call mine "Cart+CoreDataWrappedProperties". You can use what naming extension you want, but it will not be replaced if you regenerate the code. Ask me how I know.

Binding Array of Structs with Strings in an Array of Structs

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}

SwiftUI + Combine, using Models and ViewModels together

I'm very new to Swift and I am currently trying to learn by building a rent splitting app with SwiftUI + Combine. I want to follow the MVVM pattern and am trying to implement this. At the moment I have the following Model, ViewModel and View files:
Model:
import Foundation
import Combine
struct InputAmounts {
var myMonthlyIncome : Double
var housemateMonthlyIncome : Double
var totalRent : Double
}
ViewModel (where I have attempted to use the data from the Model to conform to the MVVM pattern, but I am not sure I have done this in the cleanest way/correct way so please correct me if wrong)
import Foundation
import Combine
class FairRentViewModel : ObservableObject {
private var inputAmounts: InputAmounts
init(inputAmounts: InputAmounts) {
self.inputAmounts = inputAmounts
}
var yourShare: Double {
inputAmounts.totalRent = Double(inputAmounts.totalRent)
inputAmounts.myMonthlyIncome = Double(inputAmounts.myMonthlyIncome)
inputAmounts.housemateMonthlyIncome = Double(inputAmounts.housemateMonthlyIncome)
let totalIncome = Double(inputAmounts.myMonthlyIncome + inputAmounts.housemateMonthlyIncome)
let percentage = Double(inputAmounts.myMonthlyIncome / totalIncome)
let value = Double(inputAmounts.totalRent * percentage)
return Double(round(100*value)/100)
}
}
And then am trying to pass this all to the View:
import SwiftUI
import Combine
struct FairRentView: View {
#ObservedObject private var viewModel: FairRentViewModel
init(viewModel: FairRentViewModel){
self.viewModel = viewModel
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter the total monthly rent:")) {
TextField("Total rent", text: $viewModel.totalRent)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter your monthly income:")) {
TextField("Your monthly wage", text: $viewModel.myMonthlyIncome)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter your housemate's monthly income:")) {
TextField("Housemate's monthly income", text: $viewModel.housemateMonthlyIncome)
.keyboardType(.decimalPad)
}
Section {
Text("Your share: £\(viewModel.yourShare, specifier: "%.2f")")
}
}
.navigationBarTitle("FairRent")
}
}
}
struct FairRentView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = FairRentViewModel(inputAmounts: <#InputAmounts#>)
FairRentView(viewModel: viewModel)
}
}
I am getting the build errors with the View:
"Value of type 'ObservedObject.Wrapper' has no dynamic member 'totalRent' using key path from root type 'FairRentViewModel'"
"Value of type 'ObservedObject.Wrapper' has no dynamic member 'myMonthlyIncome' using key path from root type 'FairRentViewModel'"
"Value of type 'ObservedObject.Wrapper' has no dynamic member 'housemateMonthlyIncome' using key path from root type 'FairRentViewModel'"
My questions are:
What does this error mean and please point me in the right direction to solve?
Have I gone completely the wrong way at trying to implement the MVVM pattern here?
As I said I am a Swift beginner just trying to learn so any advice would be appreciated.
UPDATE IN RESPONSE TO ANSWER
var yourShare: String {
inputAmounts.totalRent = (inputAmounts.totalRent)
inputAmounts.myMonthlyIncome = (inputAmounts.myMonthlyIncome)
inputAmounts.housemateMonthlyIncome = (inputAmounts.housemateMonthlyIncome)
var totalIncome = Double(inputAmounts.myMonthlyIncome) 0.00 + Double(inputAmounts.housemateMonthlyIncome) ?? 0.00
var percentage = Double(myMonthlyIncome) ?? 0.0 / Double(totalIncome) ?? 0.0
var value = (totalRent * percentage)
return FairRentViewModel.formatter.string(for: value) ?? ""
}
I am getting errors here that "Value of optional type 'Double?' must be unwrapped to a value of type 'Double'" which I thought I was achieving with the ?? operands?
Your view model should have properties for each of the model properties you want to work with in the view and the view model should also be responsible for converting them to a format suitable for the view. The properties should be marked as #Published to so that the view gets updated if they are changed.
For example
#Published var myMonthlyIncome: String
and this is as you see a String and we can convert it in the init
myMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.myMonthlyIncome) ?? ""
Here is my complete version of the view model
final class FairRentViewModel : ObservableObject {
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
private var inputAmounts: InputAmounts
#Published var myMonthlyIncome: String
#Published var housemateMonthlyIncome: String
#Published var totalRent: String
init(inputAmounts: InputAmounts) {
self.inputAmounts = inputAmounts
myMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.myMonthlyIncome) ?? ""
housemateMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.housemateMonthlyIncome) ?? ""
totalRent = FairRentViewModel.formatter.string(for: inputAmounts.totalRent) ?? ""
}
var yourShare: String {
let value = inputAmounts.totalRent * inputAmounts.myMonthlyIncome / (inputAmounts.myMonthlyIncome + inputAmounts.housemateMonthlyIncome)
return FairRentViewModel.formatter.string(for: Double(round(100*value)/100)) ?? ""
}
func save() {
inputAmounts.myMonthlyIncome = FairRentViewModel.formatter.number(from: myMonthlyIncome)?.doubleValue ?? 0
//...
}
}
You should do something similar for yourShare and I the save method is just a simple example of how to update the model from the view if you tie the function to a Button action or similar.
Also note that the number style I used for the formatter is just a guess, you might need to change that. And it is recommended to work with Decimal instead of Double when dealing with money.

How to modify a user input inside a SwiftUI form loop

I'm developing a simple SwiftUI app in Xcode 11. I want to have a form that loops through multiple user input strings and displays a form with a button. When the user presses the button it modifies the input value - specifically increment or decrement it.
However when passing an array of references like UserInput().foo where UserInput is a published observable object I cannot modify the value inside a ForEach because the ForEach is passed a copy as oppose to the original reference (at least that's my basic understanding). How do I then try to achieve it? I read about inout and everybody says to avoid it but surely this must be a relatively common issue.
I've made an simple example of what I'm trying to do but I can't quite work it out:
import SwiftUI
class UserInput: ObservableObject {
#Published var foo: String = ""
#Published var bar: String = ""
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView?{
var userinputs = [
[UserInput().foo, "Foo"],
[UserInput().bar, "Bar"]
]
var inputs: some View{
VStack(){
ForEach(userinputs, id: \.self){userinput in
Text("\(userinput[1]): \(String(userinput[0]))")
Button(action: {
increment(input: String(userinput[0]))
}){
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String){
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
As I understood, when adding a value to userinputs, the ForEach values doesn't change.
Well, if that's the case, first of all, you could try creating a struct and in it, you declare foo and bar, then just declare a variable of type the struct. It'll look like this:
struct Input: Identifiable {
var id = UUID()
var foo: String
var bar: String
}
class UserInput: ObservableObject {
#Published var inputs: [Input] = [Input]()
}
//ContentView
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView? {
var inputs: some View {
VStack {
ForEach(input.inputs) { userinput in
Text("\(userinput.bar): \(String(userinput.foo))")
Button(action: {
increment(input: String(userinput.foo))
}) {
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String) {
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
Wouldn't this be easier and more elegant?

Why does picker binding not update when using SwiftUI?

I have just begun learning Swift (and even newer at Swift UI!) so apologies if this is a newbie error.
I am trying to write a very simple programme where a user chooses someone's name from a picker and then sees text below that displays a greeting for that person.
But, the bound var chosenPerson does not update when a new value is picked using the picker. This means that instead of showing a greeting like "Hello Harry", "Hello no-one" is shown even when I've picked a person.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson: String? = nil
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach ((0..<people.count), id: \.self) { person in
Text(self.people[person])
}
}
}
Section{
Text("Hello \(chosenPerson ?? "no-one")")
}
}
}
}
}
(I have included one or two pieces of the original formatting in case this is making a difference)
I've had a look at this question, it seemed like it might be a similar problem but adding .tag(person) to Text(self.people[person])did not solve my issue.
How can I get the greeting to show the picked person's name?
Bind to the index, not to the string. Using the picker, you are not doing anything that would ever change the string! What changes when a picker changes is the selected index.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson = 0
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach(0..<people.count) { person in
Text(self.people[person])
}
}
}
Section {
Text("Hello \(people[chosenPerson])")
}
}
}
}
}
The accepted answer is right if you are using simple arrays, but It was not working for me because I was using an array of custom model structs with and id defined as string, and in this situation the selection must be of the same type as this id.
Example:
struct CustomModel: Codable, Identifiable, Hashable{
var id: String // <- ID of type string
var name: String
var imageUrl: String
And then, when you are going to use the picker:
struct UsingView: View {
#State private var chosenCustomModel: String = "" //<- String as ID
#State private var models: [CustomModel] = []
var body: some View {
VStack{
Picker("Picker", selection: $chosenCustomModel){
ForEach(models){ model in
Text(model.name)
.foregroundColor(.blue)
}
}
}
Hope it helps somebody.