Swift ViewController Crashes on Load - swift

I'm making a multi conversion tool in iOS to build up my portfolio. However, the distance tab will not load the view. It instantly crashes and gives me two errors.
The second one appears when I try to continue. Below are the errors and my Swift class tied to the controller as well as what the app looks like.
errors
import UIKit
class DistanceViewController: UIViewController, UITextFieldDelegate{
#IBOutlet var userDistance: UITextField!
#IBOutlet var resultLabel: UILabel!
var fromKilometerValue: Measurement<UnitLength>?{
didSet{
milesConversion()
}
}
var fromMileValue: Measurement<UnitLength>?{
didSet{
kilometerConversion()
}
}
override func viewDidLoad() {
super.viewDidLoad()
milesConversion()
kilometerConversion()
}
//Dont forget to drag a gesture recognizer
#IBAction func dismissKeyboard(_sender: UITapGestureRecognizer){
userDistance.resignFirstResponder()
}
let numberFormatter: NumberFormatter = {
let nf = NumberFormatter()
nf.numberStyle = .decimal
nf.minimumFractionDigits = 1
nf.maximumFractionDigits = 1
return nf
}()
func textField(_ userDistance: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = userDistance.text?.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
if existingTextHasDecimalSeparator != nil,
replacementTextHasDecimalSeparator != nil {
return false
} else {
return true
}
}
var toMileValue: Measurement<UnitLength>?{
if let fromKilometerValue = fromKilometerValue{
return fromKilometerValue.converted(to: .miles)
}
else{
return nil
}
}
var toKilometerValue: Measurement<UnitLength>?{
if let fromMileValue = fromMileValue{
return fromMileValue.converted(to: .kilometers)
}
else{
return nil
}
}
func milesConversion(){
if let toMileValue = toMileValue {
resultLabel.text = numberFormatter.string(from: NSNumber(value: Double(userDistance.text!)!))! + " km" + " is " + numberFormatter.string(from: NSNumber(value: toMileValue.value))! + " miles"
}
}
func kilometerConversion(){
if let toKilometerValue = toKilometerValue{
resultLabel.text = numberFormatter.string(from: NSNumber(value: Double(userDistance.text!)!))! + " miles" + " is " + numberFormatter.string(from: NSNumber(value: toKilometerValue.value))! + " km"
}
}
#IBAction func convertKilometers(_ sender: Any) {
if let input = userDistance.text, let value = Double(input) {
fromKilometerValue = Measurement(value: value, unit: .kilometers)
} else {
fromKilometerValue = nil
}
if(toMileValue == nil){
resultLabel.text = "Unable to Convert " + userDistance.text!
}
}
#IBAction func convertMiles(_ sender: Any) {
if let input = userDistance.text, let value = Double(input) {
fromMileValue = Measurement(value: value, unit: .miles)
} else {
fromMileValue = nil
}
if(toKilometerValue == nil){
resultLabel.text = "Unable to Convert " + userDistance.text!
}
}
}
The class and view are mapped properly from what I see. Anybody have any idea?
EDIT: i had old connections that didnt exist in Main.storyboard, i removed them and the view loads just fine!

Check all of your outlet connections. And read stackoverflow.com/questions/32170456/… – rmaddy

Related

Skproduct skipping product because no price was available

This is my first experience with creating purchases. The app I'm working on hasn't been released yet. I've been testing subscriptions locally using the Configuration.storekit file. Everything worked fine.
I recently encountered a problem - my subscriptions are no longer displayed in the project. I got an error like this in the terminal:
UPD:
I decided to check the application on the emulator and everything works there. As far as I remember everything broke after installing xcode 14 and updating to ios 16.
On the physical device, the problem remains.
I didn't change the code in those places. I tried to create new .storekit files, but it still doesn't work.
I tried to load the .storekit file with the synchronization. In it the price is pulled up and displayed correctly, as on the site, but in the terminal again writes the same error.
Here is the file that works with purchases:
import StoreKit
typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result<Bool, Error>
typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void
class Purchases: NSObject {
static let `default` = Purchases()
private let productIdentifiers = Set<String>(
arrayLiteral: "test.1month", "test.6month", "test.12month"
)
private var products: [String: SKProduct]?
private var productRequest: SKProductsRequest?
private var productsRequestCallbacks = [RequestProductsCompletion]()
fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?
func initialize(completion: #escaping RequestProductsCompletion) {
requestProducts(completion: completion)
}
private func requestProducts(completion: #escaping RequestProductsCompletion) {
guard productsRequestCallbacks.isEmpty else {
productsRequestCallbacks.append(completion)
return
}
productsRequestCallbacks.append(completion)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest.delegate = self
productRequest.start()
self.productRequest = productRequest
}
func purchaseProduct(productId: String, completion: #escaping (PurchaseProductResult) -> Void) {
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
guard let product = products?[productId] else {
completion(.failure(PurchasesError.productNotFound))
return
}
productPurchaseCallback = completion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func restorePurchases(completion: #escaping (PurchaseProductResult) -> Void) {
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
productPurchaseCallback = completion
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
extension Purchases: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard !response.products.isEmpty else {
print("Found 0 products")
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
return
}
var products = [String: SKProduct]()
for skProduct in response.products {
print("Found product: \(skProduct.productIdentifier)")
products[skProduct.productIdentifier] = skProduct
}
self.products = products
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load products with error:\n \(error)")
productsRequestCallbacks.forEach { $0(.failure(error)) }
productsRequestCallbacks.removeAll()
}
}
extension Purchases: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased, .restored:
if finishTransaction(transaction) {
SKPaymentQueue.default().finishTransaction(transaction)
productPurchaseCallback?(.success(true))
UserDefaults.setValue(true, forKey: "isPurchasedSubscription")
} else {
productPurchaseCallback?(.failure(PurchasesError.unknown))
}
case .failed:
productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
SKPaymentQueue.default().finishTransaction(transaction)
default:
break
}
}
productPurchaseCallback = nil
}
}
extension Purchases {
func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
let productId = transaction.payment.productIdentifier
print("Product \(productId) successfully purchased")
return true
}
}
There is also a file that is responsible for displaying available subscription options:
//
// PremiumRatesTVC.swift
// CalcYou
//
// Created by Admin on 29.08.2022.
//
import StoreKit
import UIKit
class PremiumRatesTVC: UITableViewController {
var oneMonthPrice = ""
var sixMonthPrice = ""
var twelveMonthPrice = ""
#IBOutlet weak var oneMonthPriceLabel: UILabel!
#IBOutlet weak var oneMothDailyPriceLabel: UILabel!
#IBOutlet weak var sixMonthPriceLabel: UILabel!
#IBOutlet weak var sixMonthDailyPriceLabel: UILabel!
#IBOutlet weak var twelveMonthPriceLabel: UILabel!
#IBOutlet weak var twelveMonthDailyPriceLabel: UILabel!
#IBOutlet weak var tableViewCellOneMonth: UITableViewCell!
#IBOutlet weak var tableViewCellSixMonth: UITableViewCell!
#IBOutlet weak var tableViewCellTwelveMonth: UITableViewCell!
#IBAction func cancelButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
// MARK: ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
hideSubscriptions()
navigationItem.title = "Premium PRO version"
Purchases.default.initialize { [weak self] result in
guard let self = self else { return }
switch result {
case let .success(products):
guard products.count > 0 else {
let message = "Failed to get a list of subscriptions. Please try again later."
self.showMessage("Oops", withMessage: message)
return
}
self.showSubscriptions()
DispatchQueue.main.async {
self.updateInterface(products: products)
}
default:
break
}
}
}
// MARK: Functions()
private func updateInterface(products: [SKProduct]) {
updateOneMonth(with: products[0])
updateSixMonth(with: products[1])
updateTwelveMonth(with: products[2])
}
private func hideSubscriptions() {
DispatchQueue.main.async {
self.tableViewCellOneMonth.isHidden = true
self.tableViewCellSixMonth.isHidden = true
self.tableViewCellTwelveMonth.isHidden = true
}
}
private func showSubscriptions() {
DispatchQueue.main.async {
self.tableViewCellOneMonth.isHidden = false
self.tableViewCellSixMonth.isHidden = false
self.tableViewCellTwelveMonth.isHidden = false
}
}
func showMessage(_ title: String, withMessage message: String) {
DispatchQueue.main.async {
let alert = UIAlertController(title: title,
message: message,
preferredStyle: UIAlertController.Style.alert)
let dismiss = UIAlertAction(title: "Ok",
style: UIAlertAction.Style.default,
handler: nil)
alert.addAction(dismiss)
self.present(alert, animated: true, completion: nil)
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if indexPath.section == 0 && indexPath.row == 0 {
guard let premiumBuyVC = storyboard.instantiateViewController(identifier: "PremiumBuyVC") as? PremiumBuyVC else { return }
premiumBuyVC.price = oneMonthPrice
premiumBuyVC.productId = "1month"
premiumBuyVC.period = "per month"
show(premiumBuyVC, sender: nil)
}
if indexPath.section == 1 && indexPath.row == 0 {
guard let premiumBuyVC = storyboard.instantiateViewController(identifier: "PremiumBuyVC") as? PremiumBuyVC else { return }
premiumBuyVC.price = sixMonthPrice
premiumBuyVC.productId = "6month"
premiumBuyVC.period = "per 6 month"
show(premiumBuyVC, sender: nil)
}
if indexPath.section == 2 && indexPath.row == 0 {
guard let premiumBuyVC = storyboard.instantiateViewController(identifier: "PremiumBuyVC") as? PremiumBuyVC else { return }
premiumBuyVC.price = twelveMonthPrice
premiumBuyVC.productId = "12month"
premiumBuyVC.period = "per 12 month"
show(premiumBuyVC, sender: nil)
}
}
}
extension SKProduct {
public var localizedPrice: String? {
let numberFormatter = NumberFormatter()
numberFormatter.locale = self.priceLocale
numberFormatter.numberStyle = .currency
return numberFormatter.string(from: self.price)
}
}
// MARK: Обновление информации
// в cell для 1, 6, 12 месяцев
extension PremiumRatesTVC {
func updateOneMonth(with product: SKProduct) {
let withCurrency = "\(product.priceLocale.currencyCode ?? " ")"
let daily = dailyPrice(from: Double(truncating: product.price), withMonth: 1.0)
oneMonthPriceLabel.text = "\(product.price) \(withCurrency)"
oneMothDailyPriceLabel.text = "\(daily) \(withCurrency)"
oneMonthPrice = "\(product.price) \(withCurrency)"
}
func updateSixMonth(with product: SKProduct) {
let withCurrency = "\(product.priceLocale.currencyCode ?? " ")"
let daily = dailyPrice(from: Double(truncating: product.price), withMonth: 6.0)
sixMonthPriceLabel.text = "\(product.price) \(withCurrency)"
sixMonthDailyPriceLabel.text = "\(daily) \(withCurrency)"
sixMonthPrice = "\(product.price) \(withCurrency)"
}
func updateTwelveMonth(with product: SKProduct) {
let withCurrency = "\(product.priceLocale.currencyCode ?? " ")"
let daily = dailyPrice(from: Double(truncating: product.price), withMonth: 12.0)
twelveMonthPriceLabel.text = "\(product.price) \(withCurrency)"
twelveMonthDailyPriceLabel.text = "\(daily) \(withCurrency)"
twelveMonthPrice = "\(product.price) \(withCurrency)"
}
func dailyPrice(from value: Double, withMonth: Double) -> String {
let days = withMonth * 30
let result = value / days
return String(format: "%.2f", result)
}
}
This image shows the testConfiguration.storekit file:
Also the image from the edit scheme:
also the file testConfiguration.storekit in the left menu with a question mark.
I hope I described the problem I encountered in detail and correctly. Many thanks to everyone who took the time.
I had this problem too. Try with a device on iOS 15.X.
Built with Xcode 14.0.1 iPhone 13 iOS 16.0: Skipping product because no price was available
Built with Xcode 14.0.1 iPhone 11 iOS 15.5: everything works.
I had the same problem and the same answers as #Vjardel, that this occurs on iOS 16 when started with Xcode. In my case I tested it with an iPad mini 5th generation on iOS 16 Beta 10.
Although, I discovered that this issues does not happen on the same device, if you try it with a TestFlight build. Therefore, you can test it with TestFlight, plus I assume that if the app is in the App Store the issue won't happen, as well.
My boss didn't have the Paid Apps field filled in. Be sure to look to make sure it is active.
Check this answer

How divide result from text field to label and get it on another label

I need to finish(Label) = totalAmount(TextField) / adam (Label).
Here is my code:
import UIKit
var plusAndMinus = 0
class ViewController234: UIViewController {
#IBOutlet weak var adam: UILabel!
#IBAction func plus(_ sender: Any) {
plusAndMinus += 1
adam.text = "\(plusAndMinus)"
}
#IBAction func minus(_ sender: Any) {
if(plusAndMinus != 0 ) {
plusAndMinus -= 1
}
adam.text = "\(plusAndMinus)"
}
// Write total amonunt to calculate
#IBOutlet weak var totalAmount: UITextField!
#IBAction func equal(_ sender: Any) {
var adam = 0
var totalAmount = 0
var result = 0
adam = Int(adam)
totalAmount = Int(totalAmount)
guard result != 0 else {return}
result = totalAmount / adam
finish.text = String(result)
}
//Show calculated
// finish = totalAmount / adam
#IBOutlet weak var finish: UILabel!
It looks to me like you are only accessing your local variables and not the text fields in the equal() function.
It is better to use different names for local variables and class properties to avoid confusion.
So first lets validate that the text fields contains values that can be converted to Int variables and otherwise return directly by using guard. Then it's simply a matter of dividing the values but of course we also want to avoid decision by zero so we check that to
#IBAction func equal(_ sender: Any) {
guard let text = Adam.text, let adamValue = Int(text), adamValue != 0,
let totalAmountValue = Int(totalAmount.text!) else { return }
let result = totalAmountValue / adamValue
finish.text = String(result)
}
By using integers we are doing integer division and thus loosing precision, if you want to keep the precision you can switch to Double in the code above and perhaps add some formatting of the result
#IBAction func equal(_ sender: Any) {
guard let text = Adam.text, let adamValue = Double(text), adamValue != 0,
let totalAmountValue = Double(totalAmount.text!) else { return }
let result = totalAmountValue / adamValue
finish.text = String(format: "%.2f", result)
}
I'm not sure what your finish label is because you don't have a label for it, but given that you're meaning to output your answer to it:
enum InputError: Error {
case emptyAdamLabel
case emptyTotalAmountLabel
case divideByZero
case invalidFormat
}
func calculate() throws {
if let adamText = adam.text, adamText.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
throw InputError.emptyAdamLabel
} else if let total = totalAmount.text, total.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
throw InputError.emptyTotalAmountLabel
} else if let adamText = adam.text, adamText == "0" {
throw InputError.divideByZero
} else if let adamNumber = Double(adam.text!), let totalAmountNumber = Double(totalAmount.text!) {
finish.text = String(format: "%.2f", totalAmountNumber / adamNumber)
} else {
throw InputError.invalidFormat
}
}
#IBAction func equal(_ sender: Any) {
do {
try calculate()
} catch InputError.emptyAdamLabel {
print("Adam's label cannot be empty")
} catch InputError.emptyTotalAmountLabel {
print("Total Amount's label cannot be empty")
} catch InputError.divideByZero {
print("Adam's label cannot be zero")
} catch InputError.invalidFormat {
print("Both Adam's label and Total Amount's label have to be integers")
} catch {
print("Unknown errors")
}
adam.text = "0"
totalAmount.text = "0"
}

How to check if text is underlined

I am struggling to determine if some selected text in a UITextView is underlined. I can quite easily check for bold, italics etc with the following code:
let isItalic = textView.font!.fontDescriptor.symbolicTraits.contains(.traitItalic)
However, I can't figure out how to check for underline?
I have just created a sample project and I think you could do something like the following:
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let attrText1 = NSMutableAttributedString(string: "TestTest", attributes: [.foregroundColor : UIColor.systemTeal, .underlineStyle: NSUnderlineStyle.single.rawValue])
let attrText2 = NSAttributedString(string: " - not underlined", attributes: [.foregroundColor : UIColor.red])
attrText1.append(attrText2)
textView.attributedText = attrText1
}
func isTextUnderlined(attrText: NSAttributedString?, in range: NSRange) -> Bool {
guard let attrText = attrText else { return false }
var isUnderlined = false
attrText.enumerateAttributes(in: range, options: []) { (dict, range, value) in
if dict.keys.contains(.underlineStyle) {
isUnderlined = true
}
}
return isUnderlined
}
#IBAction func checkButtonDidTap(_ sender: UIButton) {
print(isTextUnderlined(attrText: textView.attributedText, in: textView.selectedRange))
}
}
Create an extension to get the selectedRange as NSRange:
extension UITextInput {
var selectedRange: NSRange? {
guard let range = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: range.start)
let length = offset(from: range.start, to: range.end)
return NSRange(location: location, length: length)
}
}
I believe underline is not part of the font traits, it must rather be an attribute to the text. You might find the answer to this question useful. I hope it helps you! Enumerate over a Mutable Attributed String (Underline Button)
func checkForUnderline(){
let allWords = self.testView.text.split(separator: " ")
for word in allWords {
let result = self.isLabelFontUnderlined(textView: self.testView,
subString: word as NSString)
if(result == true){
print(word+" is underlined")
}else{
print(word+" is not underlined")
}
}
}
func isLabelFontUnderlined (textView: UITextView, subString:
NSString) -> Bool {
let nsRange = NSString(string: textView.text).range(of: subString as
String, options: String.CompareOptions.caseInsensitive)
if nsRange.location != NSNotFound {
return self.isLabelFontUnderlined(textView: textView,
forRange: nsRange)
}
return false
}
func isLabelFontUnderlined (textView: UITextView, forRange: NSRange) ->
Bool{
let attributedText = testView.attributedText!
var isRangeUnderline = false
attributedText.enumerateAttributes(in: forRange,
options:.longestEffectiveRangeNotRequired) { (dict, range, value) in
if dict.keys.contains(.underlineStyle) {
if (dict[.underlineStyle] as! Int == 1){
isRangeUnderline = true
} else{
isRangeUnderline = false
}
}else{
isRangeUnderline = false
}
}
return isRangeUnderline
}

How to add a grouping separator while user is typing a number into UITextField?

I am doing a currency type UITextField. The behavior I need is like this:
I managed to create the decimal part. But I have problems adding in the grouping separators for thousands. How do I group the integer part here?
Here is the code so far:
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
let DIGITS = ["0","1","2","3","4","5","6","7","8","9","0"]
let DECIMAL_SEPERATOR = ","
let THOUSAND_SEPERATOR = "."
let DECIMAL_DIGITS = 2
let DECIMAL_DIGITS_DEFAULT_STRING = "00"
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
textField.text = "0\(DECIMAL_SEPERATOR)\(DECIMAL_DIGITS_DEFAULT_STRING)"
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//Textfield
func textFieldTriggerDone(){
if !textField.text!.containsString(DECIMAL_SEPERATOR){
textField.text = textField.text! + DECIMAL_SEPERATOR + DECIMAL_DIGITS_DEFAULT_STRING
}
print("Add missing parts here")
}
func textFieldTriggerDecimalTyped(){
textField.text = textField.text! + DECIMAL_SEPERATOR
}
func textFieldShouldAddNumber(text: String, range: NSRange, replacement: String, dots: Int) -> Bool{
let nsstring = NSString(string: text)
let decimalRange = nsstring.rangeOfString(DECIMAL_SEPERATOR)
if range.location > decimalRange.location{
let parts = text.componentsSeparatedByString(DECIMAL_SEPERATOR)
if parts.count > 1{
if parts[1].characters.count > (DECIMAL_DIGITS-1){
return false
}
}
}else if range.location < decimalRange.location{
if textField.text!.characters.count == 1 && textField.text! == "0"{
textField.text = replacement
return false
}else{
let insertIndex = text.startIndex.advancedBy(range.location-dots)
var finalText = text
if replacement.characters.count > 0 {
finalText.insert(replacement.characters.first!, atIndex: insertIndex)
textField.text = finalText
let begin = textField.beginningOfDocument
let pos = textField.positionFromPosition(begin, offset: (range.location+1))
let cursorpos = textField.textRangeFromPosition(pos!, toPosition: pos!)
textField.selectedTextRange = cursorpos
//textFieldAddThousandSeperators()
return false
}
}
}
return true
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField: UITextField) {
textFieldTriggerDone()
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let dots = textField.text!.occurancesOf(THOUSAND_SEPERATOR)
if string == ""{
if textField.text!.characters.count > 1{
return true
}else{
textField.text! = "0"
return false
}
}
if range.location == 0 && string == "0"{
return false
}
if DIGITS.contains(string){
return textFieldShouldAddNumber(textField.text!, range: range, replacement: string, dots: dots)
}
if string == DECIMAL_SEPERATOR || string == THOUSAND_SEPERATOR{
if textField.text!.containsString(DECIMAL_SEPERATOR){
return false
}
textFieldTriggerDecimalTyped()
return false
}
return false
}
}
Check out this answer:
https://stackoverflow.com/a/40469426/6863743
code:
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(myTextFieldDidChange), for: .editingChanged)
}
func myTextFieldDidChange(_ textField: UITextField) {
if let amountString = textField.text?.currencyInputFormatting() {
textField.text = amountString
}
}
extension String {
// formatting text for currency textField
func currencyInputFormatting() -> String {
var number: NSNumber!
let formatter = NumberFormatter()
formatter.numberStyle = .currencyAccounting
formatter.currencySymbol = "$"
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
var amountWithPrefix = self
// remove from String: "$", ".", ","
let regex = try! NSRegularExpression(pattern: "[^0-9]", options: .caseInsensitive)
amountWithPrefix = regex.stringByReplacingMatches(in: amountWithPrefix, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.characters.count), withTemplate: "")
let double = (amountWithPrefix as NSString).doubleValue
number = NSNumber(value: (double / 100))
// if first number is 0 or all numbers were deleted
guard number != 0 as NSNumber else {
return ""
}
return formatter.string(from: number)!
}
}
To get it to convert back to something you can do calculations with, you can just write a function to take out the "," and "." and so on.
Hope this helps a bit, it's the best solution I've found for currency formatting.

How to format currency in text field [duplicate]

I have a number let’s say 0.00.
When the user taps 1. We should have 0.01
When the user taps 2. We should display 0.12
When the user taps 3. We should display 1.23
When the user taps 4. We should display 12.34
How can I do that with Swift?
For Swift 3. Input currency format on a text field (from right to left)
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(myTextFieldDidChange), for: .editingChanged)
}
#objc func myTextFieldDidChange(_ textField: UITextField) {
if let amountString = textField.text?.currencyInputFormatting() {
textField.text = amountString
}
}
extension String {
// formatting text for currency textField
func currencyInputFormatting() -> String {
var number: NSNumber!
let formatter = NumberFormatter()
formatter.numberStyle = .currencyAccounting
formatter.currencySymbol = "$"
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
var amountWithPrefix = self
// remove from String: "$", ".", ","
let regex = try! NSRegularExpression(pattern: "[^0-9]", options: .caseInsensitive)
amountWithPrefix = regex.stringByReplacingMatches(in: amountWithPrefix, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.count), withTemplate: "")
let double = (amountWithPrefix as NSString).doubleValue
number = NSNumber(value: (double / 100))
// if first number is 0 or all numbers were deleted
guard number != 0 as NSNumber else {
return ""
}
return formatter.string(from: number)!
}
}
You can create a currency text field subclassing UITextField. Add a target for UIControlEvents .editingChanged. Add a selector method to filter the digits from your textfield string. After filtering all non digits from your string you can format again your number using NumberFormatter as follow:
Xcode 11.5 • Swift 5.2 or later
import UIKit
class CurrencyField: UITextField {
var decimal: Decimal { string.decimal / pow(10, Formatter.currency.maximumFractionDigits) }
var maximum: Decimal = 999_999_999.99
private var lastValue: String?
var locale: Locale = .current {
didSet {
Formatter.currency.locale = locale
sendActions(for: .editingChanged)
}
}
override func willMove(toSuperview newSuperview: UIView?) {
// you can make it a fixed locale currency if needed
// self.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
Formatter.currency.locale = locale
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
keyboardType = .numberPad
textAlignment = .right
sendActions(for: .editingChanged)
}
override func deleteBackward() {
text = string.digits.dropLast().string
// manually send the editingChanged event
sendActions(for: .editingChanged)
}
#objc func editingChanged() {
guard decimal <= maximum else {
text = lastValue
return
}
text = decimal.currency
lastValue = text
}
}
extension CurrencyField {
var doubleValue: Double { (decimal as NSDecimalNumber).doubleValue }
}
extension UITextField {
var string: String { text ?? "" }
}
extension NumberFormatter {
convenience init(numberStyle: Style) {
self.init()
self.numberStyle = numberStyle
}
}
private extension Formatter {
static let currency: NumberFormatter = .init(numberStyle: .currency)
}
extension StringProtocol where Self: RangeReplaceableCollection {
var digits: Self { filter (\.isWholeNumber) }
}
extension String {
var decimal: Decimal { Decimal(string: digits) ?? 0 }
}
extension Decimal {
var currency: String { Formatter.currency.string(for: self) ?? "" }
}
extension LosslessStringConvertible {
var string: String { .init(self) }
}
View Controller
class ViewController: UIViewController {
#IBOutlet weak var currencyField: CurrencyField!
override func viewDidLoad() {
super.viewDidLoad()
currencyField.addTarget(self, action: #selector(currencyFieldChanged), for: .editingChanged)
currencyField.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
}
#objc func currencyFieldChanged() {
print("currencyField:",currencyField.text!)
print("decimal:", currencyField.decimal)
print("doubleValue:",(currencyField.decimal as NSDecimalNumber).doubleValue, terminator: "\n\n")
}
}
Sample project
SwiftUI version of this post here
I started with Leo Dabus' answer (which didn't work out of the box for me) and in the process of trying to simplify and make it work ended up with this, which I think is pretty lean & clean if I do say so myself 😎
class CurrencyTextField: UITextField {
/// The numbers that have been entered in the text field
private var enteredNumbers = ""
private var didBackspace = false
var locale: Locale = .current
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
}
override func deleteBackward() {
enteredNumbers = String(enteredNumbers.dropLast())
text = enteredNumbers.asCurrency(locale: locale)
// Call super so that the .editingChanged event gets fired, but we need to handle it differently, so we set the `didBackspace` flag first
didBackspace = true
super.deleteBackward()
}
#objc func editingChanged() {
defer {
didBackspace = false
text = enteredNumbers.asCurrency(locale: locale)
}
guard didBackspace == false else { return }
if let lastEnteredCharacter = text?.last, lastEnteredCharacter.isNumber {
enteredNumbers.append(lastEnteredCharacter)
}
}
}
private extension Formatter {
static let currency: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
}
private extension String {
func asCurrency(locale: Locale) -> String? {
Formatter.currency.locale = locale
if self.isEmpty {
return Formatter.currency.string(from: NSNumber(value: 0))
} else {
return Formatter.currency.string(from: NSNumber(value: (Double(self) ?? 0) / 100))
}
}
}
Try this piece of code:
struct DotNum {
private var fraction:String = ""
private var intval:String = ""
init() {}
mutating func enter(s:String) {
if count(fraction) < 2 {
fraction = s + fraction
} else {
intval = s + intval
}
}
private var sFract:String {
if count(fraction) == 0 { return "00" }
if count(fraction) == 1 { return "0\(fraction)" }
return fraction
}
var stringVal:String {
if intval == "" { return "0.\(sFract)" }
return "\(intval).\(sFract)"
}
}
var val = DotNum()
val.enter("1")
val.stringVal
val.enter("2")
val.stringVal
val.enter("3")
val.stringVal
val.enter("4")
val.stringVal
My final code thanks for your help
extension Double {
var twoDigits: Double {
let nf = NSNumberFormatter()
nf.numberStyle = NSNumberFormatterStyle.DecimalStyle
nf.minimumFractionDigits = 2
nf.maximumFractionDigits = 2
return self
}
}
var cleanText:String!
let number:String = sender.currentTitle as String!
if(amountDisplay.text != nil)
{
cleanText = String(Array(amountDisplay.text!).map{String($0)}.filter{ $0.toInt() != nil }.map{Character($0)} ) as String
cleanText = cleanText + number
}else{
cleanText = number
}
amount = (Double(cleanText.toInt()!) / 100).twoDigits
formatter.locale = NSLocale(localeIdentifier: currencies[current_currency_index])
amountDisplay.text = "\(formatter.stringFromNumber(amount!)!)"
Here is a code for swift 2
#IBOutlet weak var txtAmount: UITextField!
//MARK: - UITextField Delegate -
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
if string.characters.count == 0 {
return true
}
let userEnteredString = textField.text ?? ""
var newString = (userEnteredString as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString
newString = newString.stringByReplacingOccurrencesOfString(".", withString: "")
let centAmount : NSInteger = newString.integerValue
let amount = (Double(centAmount) / 100.0)
if newString.length < 16 {
let str = String(format: "%0.2f", arguments: [amount])
txtAmount.text = str
}
return false //return false for exact out put
}
Note : Connect delegate for textField from storyboard or programatically
Just for fun: copied Thomas's answer (full credits -and points- to him please) into a file to run as a Swift 4.1 script (with minor fixes):
dotnum.swift:
#!/usr/bin/swift
struct DotNum {
private var fraction:String = ""
private var intval:String = ""
init() {}
mutating func enter(_ s:String) {
if fraction.count < 2 {
fraction = s + fraction
} else {
intval = s + intval
}
}
private var sFract:String {
if fraction.count == 0 { return "00" }
if fraction.count == 1 { return "0\(fraction)" }
return fraction
}
var stringVal:String {
if intval == "" { return "0.\(sFract)" }
return "\(intval).\(sFract)"
}
}
var val = DotNum()
val.enter("1")
print(val.stringVal)
val.enter("2")
print(val.stringVal)
val.enter("3")
print(val.stringVal)
val.enter("4")
print(val.stringVal)
Then run it in a terminal:
$ chmod +x dotnum.swift
$ ./dotnum.swift
0.01
0.21
3.21
43.21
Thanks to everyone here. From all the answers here I managed to come out with mine.
First I set up the initial value of the textField to be:
private func commonInit() {
amountTextField.text = "0.00"
}
Then I use the UITextFieldDelegate to get the input value and the current textview.text:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//Need to check if the textfield.text can be evaluated as number or not before passing it to the function
//Get the current text value, and current user input and pass it to the
let formattedAmount = formatAmount(oldAmount: textField.text, userInput: string)
textField.text = formattedAmount
return false
}
Here go my private function to format the number to move from right to left:
private func formatAmount(currentText: String, userInput: String) -> String {
let amount = currentText.components(separatedBy: ".")
var intValue: String = amount[0]
var decimalValue: String = amount[1]
//backspace registered, need to move the number to the right
if userInput.isEmpty {
decimalValue.remove(at: decimalValue.index(before: decimalValue.endIndex))
decimalValue = intValue.last!.string + decimalValue
intValue.remove(at: intValue.index(before: intValue.endIndex))
if intValue.isEmpty {
intValue = "0"
}
} else {
//Need to consider if user paste value
if userInput.count > 2 {
decimalValue = String(userInput.suffix(2))
intValue = String(userInput.dropLast(2))
} else {
decimalValue = rmAmount[1] + userInput
//Add to int value (move to the right)
intValue = intValue + decimalValue.first!.string
if Int(intValue) == 0 {
intValue = "0" //00 -> 0
} else if intValue.first == "0" {
//remove 0 from at the first position in intValue
intValue.remove(at: intValue.startIndex) //01 -> 1
}
//Remove tenth place from decimal value since it goes to Int already
decimalValue.remove(at: decimalValue.startIndex)
}
}
return intValue + "." + decimalValue
}
This is basically it. Other extra implementations can be added by your own initiatives. Let me know if there is any problem with my implementation.
PS: This is of course only works for certain currency only, in my case, my apps is set up only for that local so thats why I use this way.
After a lot of trial and error with the suggested answers, I found a pretty straight forward solution:
The setup for the textField needs to be called in your view's setup.
In the switch statement, if the user puts in a number between 0 and 9, the number is added to the previous string value. The default case covers the backspace button and removes the last character from the string.
The locale for the numberFormatter is set to current, so it works with different currencies.
func setupTextField() {
textField.delegate = self
textField.tintColor = .clear
textField.keyboardType = .numberPad
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
setFormattedAmount(string)
return false
}
private func setFormattedAmount(_ string: String) {
switch string {
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
amountString = amountString + string
default:
if amountString.count > 0 {
amountString.removeLast()
}
}
let amount = (NSString(string: amountString).doubleValue) / 100
textField.text = formatAmount(amount)
}
private func formatAmount(_ amount: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = .current
if let amount = formatter.string(from: NSNumber(value: amount)) {
return amount
}
return ""
}