How can I merge two Doubles together? - double

SwiftUI ... Having troubles with how to use variables...
I have two Binding Variables as Doubles.
struct DisplayCalc : View {
#Binding var mhzValue : Double
#Binding var mhzValueStep : Double
}
Example:
mzhValue is 7.00
mhzValueStep is say 9.40
I would like to set a new variable say newVariable = 7.940
Any suggestions ?
Thank you..
Craig

You can do it easily by right shifting the value(assuming it will be positive) until it is less than 1. For this purpose you can use an extension of Double. Here is a code that will serve your purpose.
struct ContentView : View {
var mhzValue : Double = 7.000
var mhzValueStep : Double = 102.200
private var mySum: Double { mhzValue + mhzValueStep.getMinimalDouble() }
var body: some View {
Text("My sum is \(self.mySum)")
}
}
extension Double {
func getMinimalDouble() -> Double {
var value = self
while(value >= 1.0) {
value /= 10.0
}
return value
}
}

You could do it like this :
struct DisplayCalc : View {
#Binding var mhzValue : Double
#Binding var mhzValueStep : Double
private var mySum: Double { mhzValue + mhzValueStep }
var body: some View {
Text("My sum is \(self.mySum)")
}
}

Related

Update upcoming values in sink if needed

I'm currently trying to modify an upcoming value from a textField which is using a Binding<Double>, but haven't found any working solution yet. It's only been infinite loops (Like the example below) and other solutions which didn't work in the end anyway. So, for example, if an user inputs an amount which is too low, I would want to change the upcoming value to the minimum and vice verse if the value is higher than the maximum value.
I also want to present the modified value (if needed) for the user, so I can't just store it in another variable.
Any ideas on how to solve this?
Example
class ViewModel: ObservableObject {
#Published var amount: Double
private var subscriptions: Set<AnyCancellable> = []
private let minimum: Double = 10_000
private let maximum: Double = 100_000
init() {
$amount
.sink {
if $0 < self.minimum {
// Set minimum value
self.amount = self.minimum
} else if $0 > self.maximum {
// Set maximum value
self.amount = self.maximum
}
// If `Else` is implemented it will just be an infinite loop...
else {
self.amount = $0
}
}
.store(in: &subscriptions)
}
func prepareStuff() {
// Start preparing
let chosenAmount = amount
}
}
One way is to use a property wrapper to clamp the values.
Here is a very basic example of the issue, where we have an amount, that we can change to any value. The Stepper just makes it easy for input/testing:
struct ContentView: View {
#State private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount)
Text(String(amount))
}
}
}
The problem with this example is that amount isn't limited to a range. To fix this, create a Clamping property wrapper (partially from here):
#propertyWrapper
struct Clamping<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
var clampedValue: Value {
get { wrappedValue }
set { wrappedValue = newValue }
}
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
}
And then we can chain property wrappers, and get a working example where amount is limited:
struct ContentView: View {
#State #Clamping(-5 ... 5) private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount.clampedValue)
Text(String(amount))
}
}
}
I know, this isn't the proper way to limit a Stepper's range. Instead you should use Stepper(_:value:in:). However, this is to instead demonstrate clamping a value - not how to clamp a Stepper.
What does this mean you need to do?
Well, first off change your #Published property to this:
#Published #Clamping(10_000 ... 100_000) var amount: Double
And now you can just access amount like normal to get the clamped value. Use $amount.clampedValue like I did in my solution to get your Binding<Double> binding.
If having troubles sometimes with compiling chained property wrappers (probably a bug), here is my example recreated using a Model object and #Published:
struct ContentView: View {
#StateObject private var model = Model(amount: 0)
var body: some View {
Form {
Stepper("Amount", value: $model.amount.clampedValue)
Text(String(model.amount.clampedValue))
}
}
}
class Model: ObservableObject {
#Published var amount: Clamping<Int>
init(amount: Int) {
_amount = Published(wrappedValue: Clamping(wrappedValue: amount, -5 ... 5))
}
}

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?

SwiftUI / Combine : Listening array items value change

I want to display multiple text fields, representing scores of each part of a match.
Example : For a volleyball match, we have 25/20, 25/22, 25/23. The global score is 3/0.
The global components architecture :
>> ParentComponent
>> MainComponent
>> X TextFieldsComponent (2 text fields, home/visitor score)
The lowest component, TextFieldsComponent, contains basic bindings :
struct TextFieldsComponent: View {
#ObservedObject var model: Model
class Model: ObservableObject, Identifiable, CustomStringConvertible {
let id: String
#Published var firstScore: String
#Published var secondScore: String
var description: String {
"\(firstScore) \(secondScore)"
}
init(id: String, firstScore: String = .empty, secondScore: String = .empty) {
self.id = id
self.firstScore = firstScore
self.secondScore = secondScore
}
}
var body: some View {
HStack {
TextField("Dom.", text: $model.firstScore)
.keyboardType(.numberPad)
TextField("Ext.", text: $model.secondScore)
.keyboardType(.numberPad)
}
}
}
The parent component needs to show the total score of all parts of the match. And I wanted to try a Combine binding/stream to get the total score.
I tried multiple solutions and I ended up with this non-working code (the reduce seems to not be take all the elements of the array but internally stores a previous result) :
struct MainComponent: View {
#ObservedObject var model: Model
#ObservedObject private var totalScoreModel: TotalScoreModel
class Model: ObservableObject {
#Published var scores: [TextFieldsComponent.Model]
init(scores: [TextFieldsComponent.Model] = [TextFieldsComponent.Model(id: "main")]) {
self.scores = scores
}
}
private final class TotalScoreModel: ObservableObject {
#Published var totalScore: String = ""
private var cancellable: AnyCancellable?
init(publisher: AnyPublisher<String, Never>) {
cancellable = publisher.print().sink {
self.totalScore = $0
}
}
}
init(model: Model) {
self.model = model
totalScoreModel = TotalScoreModel(
publisher: Publishers.MergeMany(
model.scores.map {
Publishers.CombineLatest($0.$firstScore, $0.$secondScore)
.map { ($0.0, $0.1) }
.eraseToAnyPublisher()
}
)
.reduce((0, 0), { previous, next in
guard let first = Int(next.0), let second = Int(next.1) else { return previous }
return (
previous.0 + (first == second ? 0 : (first > second ? 1 : 0)),
previous.1 + (first == second ? 0 : (first > second ? 0 : 1))
)
})
.map { "[\($0.0)] - [\($0.1)]" }
.eraseToAnyPublisher()
)
}
var body: some View {
VStack {
Text(totalScoreModel.totalScore)
ForEach(model.scores) { score in
TextFieldsComponent(model: score)
}
}
}
}
I'm searching for a solution to get an event on each binding change, and merge it in a single stream, to display it in MainComponent.
N/B: The TextFieldsComponent needs to be usable in standalone too.
MergeMany is the correct approach here, as you started out yourself, though I think you overcomplicated things.
If you want to display the total score in the View (and let's say the total score is "owned" by Model instead of TotalScoreModel, which makes sense since it owns the underlying scores), you'd then need to signal that this model will change when any of the underlying scores will change.
Then you can provide the total score as a computed property, and SwiftUI will read the updated value when it recreates the view.
class Model: ObservableObject {
#Published var scores: [TextFieldsComponent.Model]
var totalScore: (Int, Int) {
scores.map { ($0.firstScore, $0.secondScore) }
.reduce((0,0)) { $1.0 > $1.1 ? ( $0.0 + 1, $0.1 ) : ($0.0, $0.1 + 1) }
}
private var cancellables = Set<AnyCancellable>()
init(scores: [TextFieldsComponent.Model] = [.init(id: "main")]) {
self.scores = scores
// get the ObservableObjectPublisher publishers
let observables = scores.map { $0.objectWillChange }
// notify that this object will change when any of the scores change
Publishers.MergeMany(observables)
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
Then, in the View, you can just use the Model.totalScore as usual:
#ObservedObject var model: Model
var body: some View {
Text(model.totalScore)
}

Delegate #State value to a child component

A Swift new guy here. Trying to understand SwiftUI.
I'm trying to create a "Field" component that wraps a Text and TextField. The idea is to have less code and have a control that can display the field title and its corresponding value.
I can't figure out how to assign the value of the model on my control.
This is my model:
import Foundation
class EmployeeModel {
var FullName: String = "John Doe"
var JobStartDate: String = ""
var BornDate: String = ""
var DepartmentId: Int = 0
var DepartmentName: String = ""
var isBossDepartment: Bool = false
var JobPositionId: Int = 0
var JobPositionName: String = ""
var PersonalDocNumber: String = ""
var Password: String = ""
init() {
}
}
In some part of the view...
struct EmployeeView : View {
#State private var Employee = EmployeeModel()
var body : some View {
Field("Full Name", $Employee.FullName)
}
}
This is my custom component that want to implement.
struct Field : View {
private var caption: String = ""
#State private var controlValue: String = ""
init(caption: String, value: String) {
self.caption = caption
controlValue = value
}
var body : some View {
VStack {
Text(self.caption)
TextField($controlValue)
.textFieldStyle(.roundedBorder)
}
}
}
Currently I got a message of
'Binding' is not convertible to 'String' on my Field implementation of the EmployeeView
Before going into the details of your problem, please be advised that by convention, types (classes, structs, enums, etc) should begin with a capital letter, while objects and values should start with a lower case (or underscore). By not following that convention, you make the code hard to read for others, as everyone is expecting it.
Now, on your code, there are several improvements:
controlValue must be declared as #Binding.
#Binding properties should not have an initial value, as they are supposed to be passed by the caller.
If you declare your properties non-private, you wouldn't need an initializer. Nothing wrong with that, but if you do use an initializer, there are multiple changes you need to perform (see the code below).
Your TextField is using a deprecated and discontinued initializer in beta 5.
struct EmployeeModel {
var fullName: String = "John Doe"
var jobStartDate: String = ""
var bornDate: String = ""
var departmentId: Int = 0
var departmentName: String = ""
var isBossDepartment: Bool = false
var jobPositionId: Int = 0
var jobPositionName: String = ""
var personalDocNumber: String = ""
var password: String = ""
init() {
}
}
struct EmployeeView : View {
#State private var employee = EmployeeModel()
var body : some View {
Field(caption: "Full Name", value: $employee.fullName)
}
}
struct Field : View {
private var caption: String = ""
#Binding private var controlValue: String
init(caption: String, value: Binding<String>) {
self.caption = caption
self._controlValue = value
}
var body : some View {
VStack {
Text(self.caption)
TextField("", text: $controlValue)
.textFieldStyle(.roundedBorder)
}
}
}