SwiftUI TextField Number Input - swift

I'm trying to have the user input two numbers and then have those numbers be displayed and also added together. At the moment in order for the state variable to be updated you have the press return. Is there a way to have the state update like it does with text? I also have had the code inputed as a string but haven't been able to convert that to int so the numbers can be added together correctly. If anyone knows how to have it convert properly I would appreciate all the help I can get.
struct ContentView: View {
#State private var numOne: Int = 0
#State private var numTwo: Int = 0
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Number One", value: $numOne, formatter: NumberFormatter())
.keyboardType(.numberPad)
TextField("Number Two", value: $numTwo, formatter: NumberFormatter())
.keyboardType(.numberPad)
}
NavigationLink(
destination: addedView(numOne: $numOne, numTwo: $numTwo),
label: {
Text("Navigate")
}
)
}
}
}
}
}
struct addedView: View {
#Binding var numOne: Int
#Binding var numTwo: Int
#State private var added: Int = 0
var body: some View {
VStack {
Text("\(numOne)")
Text("\(numTwo)")
}
}
}

This shows how to enforce the TextFields be integers, and how to add them together for a result.
The main difference is that the formatter's numberStyle is .decimal. This means you will only get whole numbers / integers.
The result is added while they are Ints, so the numbers are added. If you add when they are both Strings, they will concatenate together. E.g. you want 5 + 10 to be 15, not 510.
You can then pass the result to a child view or NavigationLink if you wish.
struct ContentView: View {
#State private var numOne: Int = 0
#State private var numTwo: Int = 0
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
private var result: Int {
numOne + numTwo
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Number One", value: $numOne, formatter: Self.formatter)
.keyboardType(.numberPad)
TextField("Number Two", value: $numTwo, formatter: Self.formatter)
.keyboardType(.numberPad)
}
Section {
Text("Result: \(result)")
}
}
}
}
}
}
Note that the NumberFormatter is created statically, and only once. This is because it is quite expensive to create them, especially twice every render.

This uses Combine to make sure that the text entered is a digit, and then changes it to an Int to use in computations. The return value is a closure that you can use to
struct EditableInt : View {
var intString: String = ""
var onChanged: (Int) -> Void
init(_ int: Int?, onChanged: #escaping (Int) -> Void) {
if let int = int,
let formattedInt = Formatter.numberFormatter.string(from: int as NSNumber){
self.intString = formattedInt
} else {
intString = ""
}
self.onChanged = onChanged
}
#State private var editableInt: String = ""
var body: some View {
TextField(" " + intString + " ", text: $editableInt)
.onReceive(Just(editableInt)) { newValue in
let editableInt = newValue.filter { "0123456789".contains($0) }
if editableInt != intString {
if let returnInt = Int(editableInt) {
onChanged(returnInt)
}
}
}
.onAppear { self.editableInt = self.intString }
}
}
The above is reusable, and is called from another view like this:
struct OtherView: View {
#State var otherViewInt = 0
var body: some View {
EditableInt(otherViewInt) { newInt in
otherViewInt = newInt
}
}
}

Related

Sheet inside ForEach leads to compiler being unable to type-check this expression Error

I am trying to open a sheet when tapping on an item. I followed this questions Sheet inside ForEach doesn't loop over items SwiftUI answer. I get this Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions, I don't understand what is causing it. I tried multiple solutions and they all lead to the same Error.
#State var selectedSong: Int? = nil
#State var songList: [AlbumSong] = []
VStack {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = index
}
}
}
}
.sheet(item: self.$selectedSong) { selectedMovie in
SongPickerEdit(songList: $songList, songIndex: selectedMovie)
I also tried setting songIndex to being an AlbumSong and then implemented this sheet:
.sheet(item: self.$selectedSong) {
SongPickerEdit(songList: $songList, songIndex: self.songList[$0])
}
struct SongPickerEdit: View {
#Binding var songList: [AlbumSong]
#State var songIndex: Int?
var body: some View {
}
}
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
How about making selectedSong an AlbumSong?? The item: parameter needs to be an Identifiable binding, but Int is not Identifiable.
#State var selectedSong: AlbumSong? = nil
#State var songList: [AlbumSong] = []
var body: some View {
List {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = song
}
}
}.sheet(item: $selectedSong) { song in
SongPickerEdit(songList: $songList, song: song)
}
}
Note that SongPickerEdit would look like this:
struct SongPickerEdit: View {
#State var song: AlbumSong
var body: some View {
Text("\(song.title), \(song.artist)")
}
}
If you really need the index for some reason, you can add the song list binding back in and use songList.index { $0.id == song.id } to find the index if the list is not too long.
Otherwise, you can make your own Identifiable type SongAndIndex that uses the same id as AlbumSong, but with an extra index property, and use that as the type of selectedSong.
A third way would be to use the sheet(isPresented:) overload, but this way you end up with 2 sources of truth:
#State var selectedSongIndex: Int? = nil {
didSet {
if selectedSongIndex != nil {
isSheetPresented = true
}
}
}
#State var isSheetPresented: Bool = false {
didSet {
if !isSheetPresented {
selectedSongIndex = nil
}
}
}
...
}.sheet(isPresented: $isSheetPresented) {
SongPickerEdit(songList: $songList, songIndex: selectedSongIndex)
}
selectedSongIndex also won't be set to nil when the user dismisses the sheet.

#propertywrapper in SwiftUI

I created a #propertyWrapper to limit the number a variable can reach. I tried it in a SwiftUI view with a button that increases the value of the variable and it works, the variable stops at the maximum number set in the initializer. However if I try it with a Textflied it doesn't work, if I insert a higher number than the one set nothing happens, it makes me do it. How can I solve this problem, I know the problem has to do with Binding but I don't know exactly what it is, here is the code:
import SwiftUI
struct ContentView: View {
#Maximum(maximum: 12) var quantity: Int
var body: some View {
NavigationView{
Form{
TextField("", value: $quantity, format: .number, prompt: Text("Pizza").foregroundColor(.red))
Button {
quantity += 1
} label: {
Text("\(quantity)")
}
}
}
}
}
#propertyWrapper
struct Maximum<T: Comparable> where T: Numeric {
#State private var number: T = 0
var max: T
var wrappedValue: T {
get { number }
nonmutating set { number = min(newValue, max) }
}
var projectedValue: Binding<T> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
init(maximum: T){
max = maximum
}
}
extension Maximum: DynamicProperty {
}
#Asperi is exactly right as to how to create the property wrapper. However, that does not solve the problem of the TextField(). The issue seems to be that You are not actually using $quantity on your TextField, but rather are using a string from the formatter that is derived from $quantity. That just doesn't seem to allow the update mechanism to work properly.
However, you can fix this simply by feeding a #State string into the TextField, and then updating everything in an .onChange(of:). This allows you to set quantity to the Int value of the TextField, and the maximum prevents the quantity from going too high. You then turn around and set your string to the quantity.description to keep everything in sync.
One last thing, I changed the keyboardType to .decimalPad to make inputting easier.
struct ContentView: View {
#Maximum(maximum: 12) var quantity: Int
#State private var qtyString = "0"
var body: some View {
NavigationView{
Form{
TextField("", text: $qtyString, prompt: Text("Pizza").foregroundColor(.red))
.onChange(of: qtyString) { newValue in
if let newInt = Int(newValue) {
quantity = newInt
qtyString = quantity.description
}
}
.keyboardType(.decimalPad)
Button {
quantity += 1
} label: {
Text("\(quantity)")
}
}
}
}
}
#propertyWrapper
struct Maximum<T: Comparable>: DynamicProperty where T: Numeric {
let number: State<T> = State(initialValue: 0)
var max: T
var wrappedValue: T {
get { number.wrappedValue }
nonmutating set { number.wrappedValue = min(newValue, max) }
}
var projectedValue: Binding<T> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
init(maximum: T){
max = maximum
}
}

Unable to store selected Picker value in SwiftUI

I have a simple SwiftUI view with a Picker containing a list of objects from a data array. The Picker lists the objects just fine, but the selected value is not being saved to the binding variable $selectedCar. It returns empty string. This is the view in question:
struct GarageSpace: View {
var currentUserID: String
#Environment(\.presentationMode) var presentationMode
#Binding var selectedPlaceID: String
#Binding var selectedPlaceName: String
#Binding var selectedPlaceDate: Date
#Binding var selectedSpaceID: String
#State var selectedCar: String
#Binding var cars: CarArrayObject
var body: some View {
VStack{
Group{
Picker("Car", selection: $selectedCar) {
if let cars = cars{
ForEach(cars.dataArray, id: \.self) {car in
let year = car.year! as String
let make = car.make as String
let model = car.model! as String
let string = year + " " + make + " " + model
Text(string) //displays correctly in Picker
}
}
}
Spacer()
if let cars = cars {
Button {
print("yes")
print(selectedCar) //returns empty string
} label: {
Text("Confirm")
}
}
}
}
}
}
The above view is displayed via a NavigationLink on the previous screen:
NavigationLink(destination: GarageSpace(currentUserID: currentUserID, selectedPlaceID: $selectedPlaceID, selectedPlaceName: $selectedPlaceName, selectedPlaceDate: $selectedPlaceDate, selectedSpaceID: $selectedSpaceID, selectedCar: "", cars: $cars)) {
}
This NavigationLink might be the culprit because I'm sending an empty string for selectedCar. However, it forces me to initialize a value with the NavigationLink.
Any ideas? Thanks!
EDIT:
Added a tag of type String, still same outcome:
Text(string).tag(car.carID)
EDIT: FOUND THE ISSUE! However, I'm still stumped. The selection variable is empty because I wasn't pressing on the Picker since I only had one item in the array. How can I get the Picker to "select" an item if it's the only one in the array by default?
With tag, all works well in my simple tests. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
GarageSpace()
}
}
struct GarageSpace: View {
#State var selectedCar: String = ""
#State var cars: CarArrayObject? = CarArrayObject(car: CarModel(make: "Ford"))
var body: some View {
VStack {
Group {
Picker("Car", selection: $selectedCar) {
if let cars = cars {
ForEach(cars.dataArray, id: \.self) { car in
Text(car.make).tag(car.carID)
}
}
}
Spacer()
if let cars = cars {
Button {
print("----> selectedCar carID: \(selectedCar)")
} label: {
Text("Show selected carID")
}
}
}
}
// optional, to select the first car
.onAppear {
if let cars = cars {
selectedCar = (cars.dataArray.first != nil) ? cars.dataArray.first!.carID : ""
}
}
}
}
struct CarModel: Hashable {
var make = ""
var carID = UUID().uuidString
}
class CarArrayObject: ObservableObject{
// for testing
#Published var dataArray = [CarModel(make: "Toyota"), CarModel(make: "Suzuki"), CarModel(make: "VW")]
/// USED FOR SINGLE CAR SELECTION
init(car: CarModel) {
self.dataArray.append(car)
}
/// USED FOR GETTING CARS FOR USER PROFILE
init(userID: String) {
// print("GET CARS FOR USER ID \(userID)")
// DataService.instance.downloadCarForUser(userID: userID) { (returnedCars) in
//
// let sortedCars = returnedCars.sorted { (car1, car2) -> Bool in
// return car1.dateCreated > car2.dateCreated
// }
// self.dataArray.append(contentsOf: sortedCars)
// }
}
}

SwiftUI #AppStorage doesn't refresh in a function

I am trying to reload the data every .onAppear, but if I change the #AppStorage nearMeter's value in the SettingsView, it isn't updating the value in the reloadNearStops func and using the previous #AppStorage value.
struct SettingsView: View {
#AppStorage(“nearMeter”) var nearMeter: Int = 1
#State var meters = ["100 m","200 m","400 m","500 m","750 m","1 km"]
var body: some View {
………
Picker(selection: $nearMeter, label: HStack {
Text(NSLocalizedString(“near_stops_distance”, comment: ""))
}) {
ForEach(0 ..< meters.count, id: \.self) {
Text(meters[$0])
}
}}}
struct FavouritesView: View {
#AppStorage(“nearMeter”) var nearMeter: Int = 1
func reloadNearStops(nearMeter: Int) {
print(nearMeter)
readNearStopsTimeTable.fetchTimeTable(nearMeter: getLonLatSpan(nearMeter: nearMeter), lat: (locationManager.lastLocation?.coordinate.latitude)!, lon: (locationManager.lastLocation?.coordinate.longitude)!)
}
func getLonLatSpan(nearMeter: Int) -> Double {
let meters = [100,200,400,500,750,1000]
if nearMeter < meters.count {
return Double(meters[nearMeter]) * 0.00001
}
else {
return 0.001
}
}
var body: some View {
.....
……….
.onAppear() {
if locationManager.lastLocation?.coordinate.longitude != nil {
if hasInternetConnection {
reloadNearStops(nearMeter: nearMeter)
}
}
}}
AppStorage won't call a function but onChange can call a function when AppStorage has changed.
struct StorageFunctionView: View {
#AppStorage("nearMeter") var nearMeter: Int = 1
#State var text: String = ""
var body: some View {
VStack{
Text(text)
Button("change-storage", action: {
nearMeter = Int.random(in: 0...100)
})
}
//This will listed for changes in AppStorage
.onChange(of: nearMeter, perform: { newNearMeter in
//Then call the function and if you need to pass the new value do it like this
fetchSomething(value: newNearMeter)
})
}
func fetchSomething(value: Int) {
text = "I'm fetching \(value)"
}
}

#State variables seem to be getting mistakenly triggered in SwiftUI

I have the following code for a simple SwiftUI project:
import SwiftUI
enum Unit: String, CaseIterable {
case m = "Meters"
case km = "Kilometers"
}
struct ContentView: View {
// Note that this always has to be an int index to the array used in the picker
#State private var inputUnit = 0
#State private var outputUnit = 1
#State private var inputAmount = ""
let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.alwaysShowsDecimalSeparator = false
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
return formatter
}()
var inputUnitText: String {
Unit.allCases[inputUnit].rawValue
}
var outputUnitText: String {
Unit.allCases[outputUnit].rawValue
}
var outputAmount: Double {
let input: Unit = Unit.allCases[inputUnit]
let output: Unit = Unit.allCases[outputUnit]
switch input {
case .m:
switch output {
case .m: return Double(inputAmount) ?? 0
case .km: return (Double(inputAmount) ?? 0) / 1000
}
case .km:
switch output {
case .km: return Double(inputAmount) ?? 0
case .m: return (Double(inputAmount) ?? 0) * 1000
}
}
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Input Unit")) {
Picker("", selection: $inputUnit) {
ForEach(0..<Unit.allCases.count) {
Text(Unit.allCases[$0].rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Input amount")) {
TextField(inputUnitText, text: $inputAmount)
.keyboardType(.numberPad)
}
Section(header: Text("Output Unit")) {
Picker("", selection: $outputUnit) {
ForEach(0..<Unit.allCases.count) {
Text(Unit.allCases[$0].rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Output amount")) {
Text("\(numberFormatter.string(from: outputAmount as NSNumber)!) \(outputUnitText)")
}
}
.navigationBarTitle("WeConvert")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If I choose a segment on one of the segment controls or update the number the other segment controls seems to be moving. I assume their state is updating. For example:
If I choose an option in the second picker the first picker will also wobble like something has updated. I don't understand this because the states for both pickers are independent. Any ideas what is going on here?