Making calculations based on state of segmented picker - swiftui

I know the below is incorrect, and I feel dumb that i can't solve this, but I'm struggling to undertand where to put the 'logic' (and if statment or other) for handeling calculations in my app, based on the entered value and the state of a segmented picker. I can't add it as part of the picker, and it throws an error if i tey and make totalTime a computed property like the below.
I created a quick example to make it simpler to undertand my question. I want to hold the value for use on another screen as well.
struct ContentView: View {
#AppStorage("totalTime") var totalTime: Double {
onChange(of: timeSelected) {
if timeSelected == "hours" {
totalTime = (Double(enteredValue) ?? 0) * multipier
} else {
totalTime = (Double(enteredValue) ?? 0)
}
}
}
#State var enteredValue = ""
#State var timeSelected = "hours"
let multipier = 2.0
let arrayLabel = ["hours", "minutes"]
var body: some View {
VStack {
Form {
Section(header: Text("Time Test")){
TextField("enter value", text: $enteredValue)
Picker("timeValue",selection: $timeSelected) {
ForEach(arrayLabel, id: \.self){
Text("\($0)")
}
}
.pickerStyle(.segmented)
}
}
}
}
}

I reworked your code so that it works like you asked, that the computation is done when the Picker is changed. However, this never triggers if the Picker isn't changed:
struct ContentView: View {
#AppStorage("totalTime") var totalTime: Double = 0.0
#State var enteredValue = ""
#State var timeSelected = "hours"
let multipier = 2.0
let arrayLabel = ["hours", "minutes"]
var body: some View {
VStack {
Form {
Section(header: Text("Time Test")){
TextField("enter value", text: $enteredValue)
Picker("timeValue",selection: $timeSelected) {
ForEach(arrayLabel, id: \.self){
Text("\($0)")
}
}
.pickerStyle(.segmented)
Text("Total time: \(totalTime, format: .number)")
}
// This can be most places within the view
.onChange(of: timeSelected) { _ in
computeTotal()
}
}
}
}
// Refactor to its own function for cleaner code.
private func computeTotal() {
if timeSelected == "hours" {
totalTime = (Double(enteredValue) ?? 0) * multipier
} else {
totalTime = (Double(enteredValue) ?? 0)
}
}
}
A better version may be:
struct TimeSelectedView: View {
#AppStorage("totalTime") var totalTime: Double = 0.0
#State var enteredValue = ""
#State var timeSelected = "hours"
// Use a computed variable that makes its computations based on enteredValue
// Every time that changes, the computed variable will update setting totalTime
var total: Double {
if timeSelected == "hours" {
totalTime = (Double(enteredValue) ?? 0) * multipier
} else {
totalTime = (Double(enteredValue) ?? 0)
}
return totalTime
}
let multipier = 2.0
let arrayLabel = ["hours", "minutes"]
var body: some View {
VStack {
Form {
Section(header: Text("Time Test")){
TextField("enter value", text: $enteredValue)
Picker("timeValue",selection: $timeSelected) {
ForEach(arrayLabel, id: \.self){
Text("\($0)")
}
}
.pickerStyle(.segmented)
Text("Total time: \(total, format: .number)")
}
}
}
}
}
This will update continuously for either a changed in enteredValue or a change in timeSelected.

Related

SwiftUI: Custom binding that get value from #StateObject property is set back to old value after StateObject property change

I'm trying to implement a passcode view in iOS. I was following the guide here.
I'm trying to improve it a bit so it allows me to create a passcode by enter same passcode twice. I added a "state" property to the #StateObject and want to clear entered passcode after user input the passcode first time.
Here is my current code:
LockScreenModel.swift
====================
import Foundation
class LockScreenModel: ObservableObject {
#Published var pin: String = ""
#Published var showPin = false
#Published var isDisabled = false
#Published var state = LockScreenState.normal
}
enum LockScreenState: String, CaseIterable {
case new
case verify
case normal
case remove
}
====================
LockScreen.swift
====================
import SwiftUI
struct LockScreen: View {
#StateObject var lockScreenModel = LockScreenModel()
let initialState: LockScreenState
var handler: (String, LockScreenState, (Bool) -> Void) -> Void
var body: some View {
VStack(spacing: 40) {
Text(NSLocalizedString("lock.label.\(lockScreenModel.state.rawValue)", comment: "")).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
}
.onAppear(perform: {lockScreenModel.state = initialState})
.onDisappear(perform: {
lockScreenModel.pin = ""
lockScreenModel.showPin = false
lockScreenModel.isDisabled = false
lockScreenModel.state = .normal
})
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<6) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 30, weight: .thin, design: .default))
Spacer()
}
}
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { lockScreenModel.pin }, set: { newValue in
if newValue.last?.isWholeNumber == true {
lockScreenModel.pin = newValue
}
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
.disabled(lockScreenModel.isDisabled)
}
private var showPinStack: some View {
HStack {
Spacer()
if !lockScreenModel.pin.isEmpty {
showPinButton
}
}
.frame(height: 20)
.padding([.trailing])
}
private var showPinButton: some View {
Button(action: {
lockScreenModel.showPin.toggle()
}, label: {
lockScreenModel.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
guard !lockScreenModel.pin.isEmpty else {
lockScreenModel.showPin = false
return
}
if lockScreenModel.pin.count == 6 {
lockScreenModel.isDisabled = true
handler(lockScreenModel.pin, lockScreenModel.state) { isSuccess in
if isSuccess && lockScreenModel.state == .new {
lockScreenModel.state = .verify
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
} else if !isSuccess {
lockScreenModel.pin = ""
lockScreenModel.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 lockScreenModel.pin.count > 6 {
lockScreenModel.pin = String(lockScreenModel.pin.prefix(6))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= lockScreenModel.pin.count {
return "circle"
}
if lockScreenModel.showPin {
return lockScreenModel.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
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
====================
The problem is the line lockScreenModel.state = .verify. If I include this line, the passcode TextField won't get cleared, but if I remove this line, the passcode TextField is cleared.
If I add a breakpoint in set method of boundPin, I can see after set pin to empty and state to verify, the set method of boundPin is called with newValue of the old pin which I have no idea why. If I only set pin to empty but don't set state to verify, that set method of boundPin won't get called which confuse me even more. I can't figure out which caused this strange behavior.

Making data persist in Swift

I'm sorry if this is a naive question, but I need help getting this form to persist in core data. The variables are declared in the data model as strings. I simply cannot get this to cooperate with me. Also, the var wisconsin: String = "" is there because I can't call this view in my NavigationView without it throwing an error.
import SwiftUI
struct WisconsinToolOld: View {
//Variable
var wisconsin: String = ""
#Environment(\.managedObjectContext) private var viewContext
#State var saveInterval: Int = 5
var rateOptions = ["<12", ">12"]
#State var rate = ""
var body: some View {
List {
Section(header: Text("Spontaneous Respirations after 10 Minutes")) {
HStack {
Text("Respiratory Rate")
Spacer()
Picker("Rate", selection: $rate, content: {
ForEach(rateOptions, id: \.self, content: { rate in
Text(rate)
})
})
.pickerStyle(.segmented)
}
Section(header: Text("Result")) {
HStack {
Text("Raw Points")
Spacer()
Text("\(WisconsinToolInterpretation())")
}
}.navigationTitle("Wisconsin Tool")
}
}
func saveTool() {
do {
let wisconsin = Wisconsin(context: viewContext)
wisconsin.rate = rate
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
func WisconsinToolInterpretation() -> Int {
var points = 0
if rate == "<12" {
points += 3
}
else {
points += 1
}
return points
}
}

Delay in SwiftUI view appearing

I have a view that displays two calculated strings. At present, I calculate the strings with .onAppear. But the view does not render until the strings are calculated, leaving the user watching the previous view for 2 to 5 seconds till the calculation is done, and the progress bar never gets shown.
The code is:
struct CalculatingProgressView: View {
var body: some View {
ProgressView {
Text("Calculating")
.font(.title)
}
}
}
struct OffspringView: View {
#State private var males: String = ""
#State private var females: String = ""
#State private var busy = true
func determineOffspring() {
let temp = theOffspring(of: sire, and: dam)
males = temp.0
females = temp.1
busy = false
}
var body: some View {
Section(header: Text("Male Offspring")) {
Text(males)
.font(.callout)
}
if busy {
CalculatingProgressView()
}
Section(header: Text("Female Offspring")) {
Text(females)
.font(.callout)
}
.onAppear { determineOffspring() }
}
}
How can I get the view to render with a progress bar so the user knows that the app is actually doing something?
your code seems to work for me. You could try this approach,
to show the CalculatingProgressView while it's calculating determineOffspring.
var body: some View {
if busy {
CalculatingProgressView()
.onAppear { determineOffspring() }
} else {
Section(header: Text("Male Offspring")) {
Text(males).font(.callout)
}
Section(header: Text("Female Offspring")) {
Text(females).font(.callout)
}
}
}
}
Note, your theOffspring(...) in determineOffspring should use a completion closure something like
the following, to "wait" until the calculations are finished:
func determineOffspring() {
theOffspring(of: sire, and: dam) { result in
males = result.0
females = result.1
busy = false
}
}

SwiftUI: Textfield shake animation when input is not valid

I want to create a shake animation when the User presses the "save"-button and the input is not valid. My first approach is this (to simplify I removed the modifiers and not for this case relevant attributes):
View:
struct CreateDeckView: View {
#StateObject var viewModel = CreateDeckViewModel()
HStack {
TextField("Enter title", text: $viewModel.title)
.offset(x: viewModel.isValid ? 0 : 10) //
.animation(Animation.default.repeatCount(5).speed(4)) // shake animation
Button(action: {
viewModel.buttonPressed = true
viewModel.saveDeck(){
self.presentationMode.wrappedValue.dismiss()
}
}, label: {
Text("Save")
})
}
}
ViewModel:
class CreateDeckViewModel: ObservableObject{
#Published var title: String = ""
#Published var buttonPressed = false
var validTitle: Bool {
buttonPressed && !(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
}
public func saveDeck(completion: #escaping () -> ()){ ... }
}
But this solution doesn't really work. For the first time when I press the button nothing happens. After that when I change the textfield it starts to shake.
using GeometryEffect,
struct ContentView: View {
#StateObject var viewModel = CreateDeckViewModel()
var body: some View {
HStack {
TextField("Enter title", text: $viewModel.title)
.modifier(ShakeEffect(shakes: viewModel.shouldShake ? 2 : 0)) //<- here
.animation(Animation.default.repeatCount(6).speed(3))
Button(action: {
viewModel.saveDeck(){
...
}
}, label: {
Text("Save")
})
}
}
}
//here
struct ShakeEffect: GeometryEffect {
func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(CGAffineTransform(translationX: -30 * sin(position * 2 * .pi), y: 0))
}
init(shakes: Int) {
position = CGFloat(shakes)
}
var position: CGFloat
var animatableData: CGFloat {
get { position }
set { position = newValue }
}
}
class CreateDeckViewModel: ObservableObject{
#Published var title: String = ""
#Published var shouldShake = false
var validTitle: Bool {
!(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
}
public func saveDeck(completion: #escaping () -> ()){
if !validTitle {
shouldShake.toggle() //<- here (you can use PassThrough subject insteadof toggling.)
}
}
}

How to edit a list of subclass objects

I have a list of items of different classes derived from the same class.
The goal: editing any object using a different view
The model:
class Paper: Hashable, Equatable {
var name: String
var length: Int
init() {
name = ""
length = 0
}
init(name: String, length: Int) {
self.name = name
self.length = length
}
static func == (lhs: Paper, rhs: Paper) -> Bool {
return lhs.length == rhs.length
}
func hash(into hasher: inout Hasher) {
hasher.combine(length)
}
}
class ScientificPaper: Paper {
var biology: Bool
override init(name: String, length: Int) {
biology = false
super.init(name: name, length: length)
}
}
class TechnicalPaper: Paper {
var electronics: Bool
override init(name: String, length: Int) {
electronics = false
super.init(name: name, length: length)
}
}
The main view containing the list.
struct TestView: View {
#Binding var papers: [Paper]
#State private var edit = false
#State private var selectedPaper = Paper()
var body: some View {
let scientificBinding = Binding<ScientificPaper>(
get: {selectedPaper as! ScientificPaper},
set: { selectedPaper = $0 }
)
VStack {
List {
ForEach(papers, id: \.self) { paper in
HStack {
Text(paper.name)
Text("\(paper.length)")
Spacer()
Button("Edit") {
selectedPaper = paper
edit = true
}
}
}
}
}
.sheet(isPresented: $edit) {
VStack {
if selectedPaper is ScientificPaper {
ScientificForm(paper: scientificBinding)
}
if selectedPaper is TechnicalPaper {
TechnicalForm(paper: technicalBinding)
}
}
}
}
}
The custom view for each class.
struct ScientificForm: View {
#Binding var paper: ScientificPaper
var body: some View {
Form {
Text("Scientific")
TextField("Name: ", text: $paper.name)
TextField("Length: ", value: $paper.length, formatter: NumberFormatter())
TextField("Biology: ", value: $paper.biology, formatter: NumberFormatter())
}
}
}
struct TechnicalForm: View {
#Binding var paper: TechnicalPaper
var body: some View {
Form {
Text("Technical")
TextField("Name: ", text: $paper.name)
TextField("Length: ", value: $paper.length, formatter: NumberFormatter())
TextField("Electronics: ", value: $paper.electronics, formatter: NumberFormatter())
}
}
}
Problem is that at run time I get the following:
Could not cast value of type 'Paper' to 'ScientificPaper'.
maybe because the selectedPaper is already initialized as Paper.
What is the right strategy to edit list items belonging to different classes?
The error is due to creating binding in body, which calculates on every refresh, so binding is invalid.
The solution is to make binding as computable property, so it is requested only after validation in correct flow.
Tested with Xcode 12.1 / iOS 14.1 (demo is for scientificBinding only for simplicity)
struct TestView: View {
#Binding var papers: [Paper]
#State private var edit = false
#State private var selectedPaper = Paper()
var scientificBinding: Binding<ScientificPaper> { // << here !!
return Binding<ScientificPaper>(
get: {selectedPaper as! ScientificPaper},
set: { selectedPaper = $0 }
)
}
var body: some View {
VStack {
List {
ForEach(papers, id: \.self) { paper in
HStack {
Text(paper.name)
Text("\(paper.length)")
Spacer()
Button("Edit") {
selectedPaper = paper
edit = true
}
}
}
}
}
.sheet(isPresented: $edit) {
VStack {
if selectedPaper is ScientificPaper {
ScientificForm(paper: scientificBinding)
}
// if selectedPaper is TechnicalPaper {
// TechnicalForm(paper: technicalBinding)
// }
}
}
}
}