Luke
Luke

Reputation: 125

Converting Units and Formatting using the MeasurementFormatter SwiftUI

Hello I’m really new to SwiftUI and especially converting uints. I’m trying to convert inches to feet and then use the MeasurementFormatter() to display the value as feet rather than a decimal. For some reason, I keep getting an error in my code when trying to assign my output value the string from the formatter. Would love any suggestions anyone has.

Func Code:

func convertToFeet() {
        let formatter = MeasurementFormatter()
        var distanceInFeet = Measurement(value: Double(inputValue) ?? 0, unit: UnitLength.inches)
        distanceInFeet.convert(to: UnitLength.feet)
        //formatter.unitStyle = MeasurementFormatter.UnitStyle.long
        formatter.string(from: distanceInFeet) // 3,626.81 miles
        
        outputValue = formatter.description
    }

this is my function

All Code:

//
//  ContentView.swift
//  AC Converstion
//
//  Created by Luke Jamison on 11/7/21.
//

import SwiftUI
import Foundation


struct ContentView: View {
    
    @State private var inputValue = ""
    @State private var outputValue = ""
    @State var value: Double = 0
    @State var length: Measurement = .init(value: 1, unit: UnitLength.inches)
    private var massFormatter = MeasurementFormatter()
    
    var body: some View {
        NavigationView {
            VStack {
                Spacer()
                Text("\(outputValue)").font(.title2)
                Form {
                    Section(header: Text("Inches to Feet")) {
                        TextField("Enter Inches", text: $inputValue).keyboardType(.decimalPad)
                        Button(action: {
                            self.convertToFeet()
                        }, label: {
                            Label("Convert", systemImage: "car")
                        })
                    }
                }.navigationTitle("Convert")
            }
            
            
        }
        
    }
    func convertToFeet() {
        let formatter = MeasurementFormatter()
        var distanceInFeet = Measurement(value: Double(inputValue) ?? 0, unit: UnitLength.inches)
        distanceInFeet.convert(to: UnitLength.feet)
        //formatter.unitStyle = MeasurementFormatter.UnitStyle.long
        formatter.string(from: distanceInFeet) // 3,626.81 miles
        
        outputValue = formatter.description
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 1

Views: 2200

Answers (2)

lorem ipsum
lorem ipsum

Reputation: 29383

The great thing about Measurement is that both the value and the unit are always together and you don't have to disconnect them.

You can eliminate the multiple sources of truth by maximizing generics and SwiftUI's features.

import SwiftUI

struct MeasurementEditorParentView: View {
    @State private var measurement: Measurement = .init(value: 200, unit: UnitLength.inches)
    var body: some View {
        VStack {
            //View non-localized Measurement
            Text(measurement
                .formatted(.measurement(
                    width: .abbreviated,
                    usage: .asProvided, //Show the non-localized measurement
                    numberFormatStyle: .number
                        .precision(.fractionLength(0...2))
                ))
            )
            //Measurement Editor
            HStack {
                MeasurementEditorView(measurement: $measurement, options: [.feet, .inches])
            }.fixedSize()
        }
    }
}

If you pick "general" instead of asProvided it will show units that "seem" appropriate for the locale. Such as inches for something small, feet for medium or miles for something long.

        Text(measurement
            .formatted(.measurement(
                width: .abbreviated,
                usage: .general, 
                numberFormatStyle: .number
                    .precision(.fractionLength(0...2))
            ))
        )

You can also pick usages like person or personHeight to show appropriate units with context.

The MeasurementEditorView is as follow, you can just add this code to a swift file and use it as above.

struct MeasurementEditorView<U: Dimension>: View {
    @Binding var measurement: Measurement<U>
    ///Options for the Picker
    let options: [U]
    /// two-way connection for the measurement's value
    var value: Binding<Double> {
        .init {
            measurement.value
        } set: { newValue in
            measurement = .init(value: newValue, unit: measurement.unit)
        }
    }
    /// two-way connection for the measurement's unit
    var unit: Binding<U> {
        .init {
            measurement.unit
        } set: { newValue in
            measurement = Measurement(value: measurement.value, unit: newValue)
        }
    }
    /// options + current selection if not included
    var adjOptions: [U] {
        if options.contains(measurement.unit) {
            return options
        } else {
            return options + [measurement.unit]
        }
    }
    
    var body: some View {
        TextField("Enter value", value: value, format: .number).textFieldStyle(.roundedBorder)
        Picker("Select unit", selection: unit) {
            ForEach(adjOptions, id:\.symbol) { u in
                Text(u.symbol).tag(u as U)
            }
        }
    }
}

Upvotes: 0

Paulw11
Paulw11

Reputation: 114920

In general the description property in Swift should only be used for debug purposes. Its value isn't guaranteed to be consistent over different versions of a particular class.

The correct way to get a string value from a measurement formatter is to call the string(from:) function as you are doing. This function returns a string. You aren't doing anything with the string value that is returned which is what the compiler is warning you about.

Rather than relying on a side-effects, I would change your function to accept an input parameter and return a value.

You will also need to set the formatter's unitOptions property to .providedUnit to ensure you get output in feet; If you don't then you will get a locale-specific output (ie kilometres in metric locales)

struct ContentView: View {
    
    @State private var inputValue = ""
    @State private var outputValue = ""
    @State var value: Double = 0
    @State var length: Measurement = .init(value: 1, unit: UnitLength.inches)
    private var massFormatter = MeasurementFormatter()
    
    var body: some View {
        NavigationView {
            VStack {
                Spacer()
                Text("\(outputValue)").font(.title2)
                Form {
                    Section(header: Text("Inches to Feet")) {
                        TextField("Enter Inches", text: $inputValue).keyboardType(.decimalPad)
                        Button(action: {
                            self.outputValue = self.convertToFeet(inches: self.inputValue)
                        }, label: {
                            Label("Convert", systemImage: "car")
                        })
                    }
                }.navigationTitle("Convert")
            }
        }
    }
    
    func convertToFeet(inches: String)-> String {
        
        let formatter = MeasurementFormatter()
        
        var distanceInFeet = Measurement(value: Double(inches) ?? 0, unit: UnitLength.inches)
        
        distanceInFeet.convert(to: UnitLength.feet)
        formatter.unitOptions = .providedUnit

        return formatter.string(from: distanceInFeet)
    }
    
}

Upvotes: 2

Related Questions