I have been playing with MeasurementFormatter to try and display imperial lengths as 10'6" or 10 ft 6 in unsuccessfully. LengthFormatter does this correctly when isForPersonHeightUse is set to true, but it does not adapt to the user's locale well (i.e. countries where length is measured in metric, except for when referring to height). Is there any way to force this behaviour in a formatter?
EDIT
This question is to determine how to choose the units for a measurement. I am able to choose feet or inches, but want to display fractional feet as inches as in: 6'3" instead of 6.25 ft.
public struct LengthFormatters {
public static let imperialLengthFormatter: LengthFormatter = {
let formatter = LengthFormatter()
formatter.isForPersonHeightUse = true
return formatter
}()
}
extension Measurement where UnitType : UnitLength {
var heightOnFeetsAndInches: String? {
guard let measurement = self as? Measurement<UnitLength> else {
return nil
}
let meters = measurement.converted(to: .meters).value
LengthFormatters.imperialLengthFormatter.string(fromMeters: meters)
}
}
Example of using:
let a = Measurement(value: 6.34, unit: UnitLength.feet)
print(a.heightOnFeetsAndInches ?? "")
let b = Measurement(value: 1.5, unit: UnitLength.feet)
print(b.heightOnFeetsAndInches ?? "")
Will print:
6 ft, 4.08 in
1 ft, 6 in
I modified (simplified) #maslovsa's answer to meet my needs. I have a Core Data object called "Patient". It has a height parameter in inches that is an Int64. I want a string that I display to the user, so here's my property on my patient object for doing so:
var heightInFeetString : String {
let measurement = Measurement(value: Double(self.height) / 12.0, unit: UnitLength.feet)
let meters = measurement.converted(to: .meters).value
return LengthFormatter.imperialLengthFormatter.string(fromMeters: meters)
}
Of course, I had to implement the imperialLengthFormatter as well, but I did it as an extension to LengthFormatter itself, like this:
extension LengthFormatter {
public static let imperialLengthFormatter: LengthFormatter = {
let formatter = LengthFormatter()
formatter.isForPersonHeightUse = true
return formatter
}()
}
This actually doesn't kill performance as suggested in the comments for #maslova's answer. Due to the property being static, it only gets initialized once.
// When creating the Patient object
let patient = Patient(...) // Create in maanged object context
patient.height = 71
// Later displays in a collection view cell in a view controller
cell.heightLabel.Text = patient.heightInFeetString
Displays this in my table cell:
5 ft, 11 in
How to display Feet and Inches in SwiftUI
In case anyone arrives here looking for a SwiftUI answer.
struct MeasurementTestView: View {
#State private var height = Measurement(value: 68, unit: UnitLength.inches)
var body: some View {
VStack {
Text(height, format: .measurement(width: .narrow, usage: .personHeight))
Text(height, format: .measurement(width: .abbreviated, usage: .personHeight))
Text(height, format: .measurement(width: .wide, usage: .personHeight))
}
.font(.title)
}
}
Result
Related
So i need to make the decimal places smaller than the actual number i will show you what i mean below.
this is the code i have:
import SwiftUI
struct BalanceDetailsView: View {
//MARK: - PROPERTIES
var balance: Float
var balanceString: String {
return balance.formattedWithSeparator
}
//MARK: - BODY
var body: some View {
VStack{
HStack{
Text(balanceString)
.font(.custom(K.fonts.gilroyBold, size: 24))
.multilineTextAlignment(.center)
.padding(.top)
}//:VSTACK
}//:HSTACK
}
}
struct BalanceDetailsView_Previews: PreviewProvider {
static var previews: some View {
BalanceDetailsView(balance: 43678)
.previewLayout(.sizeThatFits)
}
}
//Formatter extension i used to get this code
extension Formatter {
static let withSeparator: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
// minimum decimal digit, eg: to display 2 as 2.00
formatter.minimumFractionDigits = 2
// maximum decimal digit, eg: to display 2.5021 as 2.50
formatter.maximumFractionDigits = 2
return formatter
}()
}
extension Numeric {
var formattedWithSeparator: String { Formatter.withSeparator.string(for: self) ?? "" }
}
Result I get
Result I need
When you know exact format of your string, like in this case minimum string length will be 4("0.00") , you can safely use dropLast and dropFirst.
I suggest moving 2 to priceFractionDigits constant to reduce constants usage in your code.
Then you can use string concatenation, it'll align Text by baseline.
struct BalanceText: View {
var balance: Float
var balanceString: String {
return balance.formattedWithSeparator
}
var body: some View {
Text(balanceString.dropLast(priceFractionDigits))
.font(.system(size: 24))
+
Text(balanceString.dropFirst(balanceString.count - priceFractionDigits))
.font(.system(size: 18))
}
}
private let priceFractionDigits = 2
extension Formatter {
static let withSeparator: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
// minimum decimal digit, eg: to display 2 as 2.00
formatter.minimumFractionDigits = priceFractionDigits
// maximum decimal digit, eg: to display 2.5021 as 2.50
formatter.maximumFractionDigits = priceFractionDigits
return formatter
}()
}
Usage
BalanceText(balance: balance)
Here is an example using the new AttributedString in SwiftUI 3 (iOS 15, macOS 12 etc)
var balanceString: AttributedString {
var attributedString = AttributedString(balance.formattedWithSeparator)
guard let separator = Formatter.withSeparator.decimalSeparator else { return attributedString }
if let range = attributedString.range(of: separator) {
attributedString[attributedString.startIndex...attributedString.index(beforeCharacter: range.lowerBound)]
.font = Font.largeTitle
attributedString[attributedString.index(afterCharacter: range.lowerBound)..<attributedString.endIndex]
.font = Font.caption
}
return attributedString
}
I used some built in font styles here but that should be easy to replace. Also note that since we set the .font attribute here it should be removed from Text
You can concatenate Text together with the + operator.
I slightly change the way you use NumberFormatter, so it's split by the correct decimal character for the locale.
struct BalanceDetailsView: View {
private let wholeNumberPart: String
private let decimalNumberPart: String
init(balance: Double) {
(wholeNumberPart, decimalNumberPart) = balance.formattedSplittingBySeparator
}
var body: some View {
Text(wholeNumberPart).font(.system(size: 24)) +
Text(decimalNumberPart).font(.system(size: 16))
}
}
extension Numeric {
var formattedSplittingBySeparator: (whole: String, decimal: String) {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
let str = formatter.string(for: self) ?? "0\(formatter.decimalSeparator!)00"
let split = str.components(separatedBy: formatter.decimalSeparator)
let whole = (split.first ?? "0") + formatter.decimalSeparator
let decimal = split.count == 2 ? split[1] : "00"
return (whole: whole, decimal: decimal)
}
}
Usage:
BalanceDetailsView(balance: 43678)
Result:
You could split the string by the decimal separator and display the parts in a zero-spacing HStack.
struct BalanceView : View {
let balance : Float
var body: some View {
let components = balance
.formattedWithSeparator
.components(separatedBy: Formatter.withSeparator.decimalSeparator)
HStack(alignment: .firstTextBaseline, spacing: 0) {
Text(components[0])
if components.count > 1 {
Text(".")
Text(components[1])
.font(.title)
}
}
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding(.top)
}
}
Replace the fonts with your custom fonts
In your environment you could use it
struct BalanceDetailsView: View {
//MARK: - PROPERTIES
var balance: Float
//MARK: - BODY
var body: some View {
VStack{
BalanceView(balance: balance)
}//:VSTACK
}
}
Currently I'm using default iOS speech to text conversion without adding any code for it. When the user says 'five', it is displayed as 'five' or '5'. But, I need it to be converted as '5' always. Is there anything I can do with SFSpeechRecognizer or any other way to achieve this?
This can get you started, but it is not able to handle mixed strings that contain a number AND a non-number. Ideally, you would need to process each word as it comes through, but then that has potential effects for combined numbers (thirty four) for example.
let fiveString = "five"
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .spellOut
print(numberFormatter.number(from: fiveString)?.stringValue) // 5
let combinedString = "five dogs"
print(numberFormatter.number(from: combinedString)?.stringValue) // nil
let cString = "five hundred"
print(numberFormatter.number(from: cString)?.stringValue) // 500
let dString = "five hundred and thirty-seven"
print(numberFormatter.number(from: dString)?.stringValue) // 537
You could try to build a simple string extention like so:
extension String {
var byWords: [String] {
var byWords:[String] = []
enumerateSubstrings(in: startIndex..<endIndex, options: .byWords) {
guard let word = $0 else { return }
byWords.append(word)
}
return byWords
}
func wordsToNumbers() -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .spellOut
let formattedString = self.byWords.map {
return numberFormatter.number(from: $0)?.stringValue ?? $0
}
return formattedString.joined(separator: " ")
}
}
This is a untested (not run / performance not checked) example
basically I want to remove the decimal places for my step calories burnt calculation. I know I need to add something to self.total2.text = ("\(String(bmi)) kcal") but just need a quick bit of advice on achieving this.
#IBAction func calc2(_ sender: Any) {
if self.value111.text! != "" && self.value222.text! != "" {
let textfieldInt = Float(value111.text!)
let textfield2Int = Float(value222.text!)
var bmi:Float = (((textfieldInt! * 2.204623) * 0.5) / 1500) * textfield2Int!
self.total2.text = ("\(String(bmi)) kcal")
let banner = StatusBarNotificationBanner(title: "Calculation Done! Submit to save. ✅", style: .warning)
banner.show(queuePosition: .front)
}
}
#IBAction func submit2(_ sender: Any) {
let dict = (["kcal": self.total.text!, "date": self.getDate()])
ref?.child("user").child(Auth.auth().currentUser!.uid).child("measurements").child("calculations").child("kcal").child("\(getDate())").setValue(dict)
total.text = ""
let banner = NotificationBanner(title: "Success, Step Calories Burnt has been saved today ⚖️", style: .success)
banner.show(queuePosition: .front)
}
Use a NumberFormatter (have a look here)
let bmi:Float = (((textfieldInt! * 2.204623) * 0.5) / 1500) * textfield2Int!
let formatter = NumberFormatter()
//Set the min and max number of digits to your liking, make them equal if you want an exact number of digits
formatter.minimumFractionDigits = 1
formatter.maximumFractionDigits = 3
self.total2.text = formatter.string(from: NSNumber(value: bmi)) + " kcal"
Have a look at the NumberFormatter class, which lets you fine-tune the string representation of a numeric value, whether integer or floating-point. Set up the NumberFormatter to produce the output in the format you want, then call string(from:) to create a string from your number.
This is similar to a question I asked yesterday but the answer I got doesn't seem to work in this case.
I'm getting altitude values in meters from Core Location. I want to display these in a localized form. As an example, the altitude where I am right now is 1839m above sea level. This should be displayed as 6033 feet. The best I can do with MeasurementFormatter is "1.143 mi".
let meters : Double = 1839
let metersMeasurement = Measurement(value: meters, unit: UnitLength.meters)
let measurementFormatter = MeasurementFormatter()
measurementFormatter.locale = Locale(identifier: "en_US")
let localizedString = measurementFormatter.string(from: metersMeasurement)
The .naturalScale option that answered my previous question doesn't help here. I think this is a limitation of the framework, but I wonder if anyone has a workaround for now.
You just need to convert your UnitLength from meters to feet. You can also create a custom US measurement formatter to display it as needed:
extension Measurement where UnitType == UnitLength {
private static let usFormatted: MeasurementFormatter = {
let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.unitOptions = .providedUnit
formatter.numberFormatter.maximumFractionDigits = 0
formatter.unitStyle = .long
return formatter
}()
var usFormatted: String { Measurement.usFormatted.string(from: self) }
}
Playground
let value: Double = 1839
let meters: Measurement<UnitLength> = .init(value: value, unit: .meters)
let feet = meters.converted(to: .feet)
let formatted = feet.usFormatted
print(formatted) // "6,033 feet"\n
I think you are correct there's no way to specify this kind of context. You could do something like:
extension MeasurementFormatter
{
func altitudeString(from measurement: Measurement<UnitLength>) -> String
{
var measurement = measurement
let unitOptions = self.unitOptions
let unitStyle = self.unitStyle
self.unitOptions = .naturalScale
self.unitStyle = .long
var string = self.string(from: measurement)
if string.contains(self.string(from: UnitLength.miles))
{
self.unitStyle = unitStyle
measurement.convert(to: UnitLength.feet)
self.unitOptions = .providedUnit
string = self.string(from: measurement)
}
else if string.contains(self.string(from: UnitLength.kilometers))
{
self.unitStyle = unitStyle
measurement.convert(to: UnitLength.meters)
self.unitOptions = .providedUnit
string = self.string(from: measurement)
}
else
{
self.unitStyle = unitStyle
string = self.string(from: measurement)
}
self.unitOptions = unitOptions
return string
}
}
Maybe there are other culturally specific ways of measuring elevation, but this would seem better than miles and kilometers.
How do I format the data that is shown in the linechart of Charts? By default the data is shown as double, but I want it to be displayed as int.
let pre = [20.0, 4.0, 6.0, 3.0, 12.0, 16.0]
for i in 0..<pre.count {
let preDataEntry = ChartDataEntry(value: pre[i], xIndex: i)
preEntries.append(preDataEntry)
}
let preChartDataSet = LineChartDataSet(yVals: preEntries, label: "Pre")
lineChartView.data = lineChartData
FIXED FOR SWIFT 3
YAxisValueFormatter.swift
import Foundation
import Charts
class YAxisValueFormatter: NSObject, IAxisValueFormatter {
let numFormatter: NumberFormatter
override init() {
numFormatter = NumberFormatter()
numFormatter.minimumFractionDigits = 1
numFormatter.maximumFractionDigits = 1
// if number is less than 1 add 0 before decimal
numFormatter.minimumIntegerDigits = 1 // how many digits do want before decimal
numFormatter.paddingPosition = .beforePrefix
numFormatter.paddingCharacter = "0"
}
/// Called when a value from an axis is formatted before being drawn.
///
/// For performance reasons, avoid excessive calculations and memory allocations inside this method.
///
/// - returns: The customized label that is drawn on the axis.
/// - parameter value: the value that is currently being drawn
/// - parameter axis: the axis that the value belongs to
///
public func stringForValue(_ value: Double, axis: AxisBase?) -> String {
return numFormatter.string(from: NSNumber(floatLiteral: value))!
}
}
create YAxisValueFormatterclass, and assign object to graph leftAxis
// Number formatting of YAxis
chartView.leftAxis.valueFormatter = YAxisValueFormatter()
There are some formatter properties you could use:
valueFormatter in ChartDataSet
formatters in ChartYAxis:
/// the formatter used to customly format the y-labels
public var valueFormatter: NSNumberFormatter?
/// the formatter used to customly format the y-labels
internal var _defaultValueFormatter = NSNumberFormatter()
should be enough