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.
Related
I am new to Swift and want to solve this. I want, that the currency from the picker is saved when I close and come back into the app. (via #AppStorage) But I can't use anything I have in mind to define the $currency to $cur in #AppStorage. Thanks for your help! Here the code:
//Settings
struct SettingsView: View {
#State private var currency = "$"
var currencies = ["$", "€", "¥", "£"]
#AppStorage("name") var name = ""
#AppStorage("cur") var cur = "\($currency)"
//Cannot use instance member '$currency' within property initializer; property initializers run before 'self' is available
var body: some View {
NavigationView {
Form {
Section(header: Text("Your name")) {
TextField("Tim", text: $name)
}
Section(header: Text("Your currency")) {
Picker(selection: $currency, label: Text("Your Currency")) {
ForEach(currencies, id: \.self) {
Text($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
.navigationTitle("Settings")
}
}
You can't refer to $currency when you define cur. This is a limitation of Swift. It obvious to you that currency has a value already, but Swift won't let you use it until after the initializer is done setting up self. This has nothing to do with AppStorage -- it's a rule that property initializers cannot refer to other (non-static) properties.
So, this
#State private var currency = "$"
#AppStorage("cur") var cur = "\($currency)"
Could be
#State private var currency = "$"
#AppStorage("cur") var cur = "$" // set it to the same value
or
private static let dollar = "$"
#State private var currency = SettingsView.dollar
#AppStorage("cur") var cur = SettingsView.dollar
I am making an app that requires me to use an ml model and is performing its calculation to get the output from the ml model inside a function, now I need to display that constant which stores the final output of that ml model inside the body property of my swift UI view so that I can present it inside Text() maybe and modify the way it looks Lil bit
all of this code is inside a single swift file
here is all the code
after making the changes it's still showing some error
struct newView: View {
let model = AudiCar()
#ObservedObject var values: impData
#State var price = ""
var body: some View {
Text("calculated price is " + price)
.onAppear {
calculatePrice()
}
func calculatePrice() { <- this is where it showing error, saying " Closure containing a declaration cannot be used with function builder 'ViewBuilder' "
do {
let AudiCarOutput = try model.prediction(model: String(values.nameSelection), year: Double(values.yearSelection), transmission: String(values.transmisssionSelec), mileage: Double(values.mileage), fuelType: String(values.fuelSelection))
let price = String(AudiCarOutput.price)
}
catch {
}
}
}
struct newView_Previews: PreviewProvider {
static var previews: some View {
newView(values: impData())
}
}
try something like this:
struct NewView: View {
let model = AudiCar()
#ObservevedObject var values: impData
#State var price = ""
var body: some View {
Text("calculated price is " + price)
.onAppear {
calculatePrice()
}
}
func calculatePrice() {
do {
...
price = String(AudiCarOutput.price)
}
catch {
...
}
}
}
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.
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?
I'm trying to recreate the SwiftUI demo with a difference being I want to use my own object Item.
Item:
class Item {
var company: String = ""
var item_class: String = ""
var name: String = ""
var stock: Int = 0
var average_cost: Decimal = 0.00
var otc_price: Decimal = 0.00
var dealer_price: Decimal = 0.00
var ctc_price: Decimal = 0.00
class var _API_LIST_EP: String {return "api/inventory/items/"}
// Init and Funcs
// JToken is an extended typealias for [String : Any] that makes parsing easier
required init(_ jt: JToken) {
company = jt.string(forKey: "company")
item_class = jt.string(forKey: "item_class")
name = jt.string(forKey: "name")
stock = jt.int(forKey: "stock")
average_cost = jt.decimal(forKey: "average_cost")
otc_price = jt.decimal(forKey: "otc_price")
dealer_price = jt.decimal(forKey: "dealer_price")
ctc_price = jt.decimal(forKey: "ctc_price")
}
}
In a similar question, it was noted that the issue stemmed from one of the object's variables being initialized in an ambiguous state however after filling up all of my object's variables, it still comes up
Problematic code:
struct ContentView : View {
var itemList: [Item] = []
var body: some View {
List(itemList) { item in
Image(systemName: "photo")
VStack(alignment: .leading) {
Text(item.name)
Text(item.company)
.font(.subheadline)
.color(.gray)
}
}
}
}
Screenshot of the error:
The error message is misleading.
List(itemList) { item in ... }
requires that the element type of itemList conforms to the Identifiable protocol. For classes (as in your case) it is sufficient to declare protocol conformance
class Item: Identifiable {
// ...
}
because there is a default implementation (based on the object identifier).