completionHandler in Swift4 return String - swift

I am trying to build a small currency converter and the problem is that my completionHandler does not work. As a result, the input Currency does not change instantly after the function was executed
I have already tried to implement a completionHandler; however, had no success yet
class CurrencyExchange: ViewController {
//Outlets
#IBOutlet weak var lblCurrency: UILabel!
#IBOutlet weak var segOutputCurrency: UISegmentedControl!
#IBOutlet weak var txtValue: UITextField!
#IBOutlet weak var segInputCurrency: UISegmentedControl!
//Variables
var inputCurrency: String!
var currencyCNY: Double!
var currencyEUR: Double!
var currencyGBP: Double!
var currencyJPY: Double!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = true
}
#IBAction func btnConvert(_ sender: Any) {
assignOutput()
if txtValue.text == "" {
self.lblCurrency.text = "Please insert value"
} else {
let inputValue = Double(txtValue.text!)!
if segOutputCurrency.selectedSegmentIndex == 0 {
let output = Double(inputValue * currencyCNY!)
self.lblCurrency.text = "\(output)¥"
} else if segOutputCurrency.selectedSegmentIndex == 1 {
let output = Double(inputValue * currencyEUR!)
self.lblCurrency.text = "\(output)€"
} else if segOutputCurrency.selectedSegmentIndex == 2 {
let output = Double(inputValue * currencyGBP!)
self.lblCurrency.text = "\(output)"
} else if segOutputCurrency.selectedSegmentIndex == 3 {
let output = Double(inputValue * currencyJPY!)
self.lblCurrency.text = "\(output)"
}
}
}
func assignOutput() {
let currencies = ["EUR", "JPY", "CNY", "USD"]
inputCurrency = currencies[segInputCurrency.selectedSegmentIndex]
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrency!)").responseJSON { (response) in
let result = response.result
let jsonCurrencies = JSON(result.value!)
let dictContent = jsonCurrencies["rates"]
self.currencyCNY = dictContent["CNY"].double
self.currencyEUR = dictContent["EUR"].double
self.currencyGBP = dictContent["GBP"].double
self.currencyJPY = dictContent["JPY"].double
}
}
}
The expected result is that everytime the btnConvert function is called the assignInput and assignOutput functions are called and the variables are set to the right values. I am a beginner so any help would be highly appreciated.

You need a completion handler in assignOutput(), I added also the minimum error handling to avoid crashes
//Variables
var inputCurrency = ""
var currencyCNY = 0.0
var currencyEUR = 0.0
var currencyGBP = 0.0
var currencyJPY = 0.0
#IBAction func btnConvert(_ sender: Any) {
assignOutput() { success in
if success {
if txtValue.text!.isEmpty {
self.lblCurrency.text = "Please insert value"
} else {
if let inputValue = Double(txtValue.text!) {
if segOutputCurrency.selectedSegmentIndex == 0 {
let output = Double(inputValue * currencyCNY)
self.lblCurrency.text = "\(output)¥"
} else if segOutputCurrency.selectedSegmentIndex == 1 {
let output = Double(inputValue * currencyEUR)
self.lblCurrency.text = "\(output)€"
} else if segOutputCurrency.selectedSegmentIndex == 2 {
let output = Double(inputValue * currencyGBP)
self.lblCurrency.text = "\(output)"
} else if segOutputCurrency.selectedSegmentIndex == 3 {
let output = Double(inputValue * currencyJPY)
self.lblCurrency.text = "\(output)"
}
} else {
self.lblCurrency.text = "Please enter a number"
}
}
} else {
self.lblCurrency.text = "Could not receive the exchange rates"
}
}
}
func assignOutput(completion: #escaping (Bool) -> Void) {
let currencies = ["EUR", "JPY", "CNY", "USD"]
inputCurrency = currencies[segInputCurrency.selectedSegmentIndex]
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrency)").responseJSON { (response) in
if let result = response.result.value {
let jsonCurrencies = JSON(result)
let dictContent = jsonCurrencies["rates"]
self.currencyCNY = dictContent["CNY"].double
self.currencyEUR = dictContent["EUR"].double
self.currencyGBP = dictContent["GBP"].double
self.currencyJPY = dictContent["JPY"].double
completion(true)
} else {
completion(false)
}
}
}

The basic idea of a completion handler is that you have some asynchronous method (i.e., a method that finishes later) and you need to give the caller the opportunity to supply what it wants the asynchronous method to do when it’s done. So, given that assignOutput is the asynchronous method, that’s the method that you would refactor with a completion handler escaping closure.
Personally, I’d configure this escaping closure to return a Result type:
For example:
func assignOutput(completion: #escaping (Result<[String: Double]>) -> Void) {
let inputCurrency = ...
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrency)").responseJSON { response in
switch response.result {
case .failure(let error):
completion(.failure(error))
case .success(let value):
let jsonCurrencies = JSON(value)
guard let dictionary = jsonCurrencies["rates"].dictionaryObject as? [String: Double] else {
completion(.failure(CurrencyExchangeError.currencyNotFound)) // this is just a custom `Error` type that I’ve defined
return
}
completion(.success(dictionary))
}
}
}
And then you could use it like so:
assignOutput { result in
switch result {
case .failure(let error):
print(error)
case .success(let dictionary):
print(dictionary)
}
}
By using Result types, you have a nice consistent pattern where you can check for .failure or .success throughout your code.
That having been said, I’d suggest a variety of other refinements:
I wouldn’t make this view controller subclass from another view controller, ViewController. It should subclass UIViewController.
(Technically you can re-subclass your own custom view controller subclasses, but it’s exceptionally uncommon. Frankly, when you’ve got so much in your view controller subclass that you need to have subclasses of subclasses, it may be code smell indicating that you’ve got too much in your view controller.)
I’d give this view controller a class name that unambiguously indicates the type of object, e.g. CurrencyExchangeViewController, not just CurrencyExchange. This habit will pay dividends in the future, when you start breaking these big view controllers into something more manageable.
You have the list of accepted currencies in four different places:
In your storyboard for segOutputCurrency
In your storyboard for segInputCurrency
In your btnConvert routine
In your assignOutput routine
This makes your code brittle, making it easy to make mistakes if you change the order of the currencies, add/remove currencies, etc. It would be better to have a list of currencies in one place, programmatically update your UISegmentedControl outlets in viewDidLoad and then have your routines all refer back to a single array of which currencies are permitted.
You should avoid using the ! forced unwrapping operator. For example, if the network request failed and you then reference result.value!, your app will crash. You want to gracefully handle errors that happen outside of your control.
If you’re going to format currencies, remember that in addition to currency symbols, you should consider that not all locales use . for decimal place (e.g. your European users may use ,). For that reason, we would generally use NumberFormatter for converting the calculated number back to a string.
Below, I’ve just used NumberFormatter for the output, but you really should use it when interpreting the user’s input too. But I will leave that to the reader.
There’s a more subtle point when dealing with currencies, above and beyond the currency symbol, namely how many decimal places the result should display. (E.g. when dealing with Japanese yen, you generally don’t have decimal places, whereas euros and US dollars and would have two decimal places.)
You can write your own conversion routine if you want, but I might associate the chosen currency codes with Locale identifiers, that way you can leverage the symbol and the number of fractional digits appropriate for each currency. And I’d format the string representations of numbers using NumberFormatters.
The convention for outlet names is usually some functional name followed by the type of control. E.g. you might have inputTextField or currencyTextField and outputLabel or convertedLabel. Likewise, I might rename the #IBAction to be didTapConvertButton(_:)
I’d personally excise the use of SwiftyJSON, which, despite the name, feels unswifty to me. I’d use JSONDecoder.
Pulling that all together, you might end up with something like:
// CurrencyViewController.swift
import UIKit
import Alamofire
// types used by this view controller
struct Currency {
let code: String // standard three character code
let localeIdentifier: String // a `Locale` identifier string used to determine how to format the results
}
enum CurrencyExchangeError: Error {
case currencyNotSupplied
case valueNotSupplied
case currencyNotFound
case webServiceError(String)
case unknownNetworkError(Data?, HTTPURLResponse?)
}
struct ExchangeRateResponse: Codable {
let error: String?
let base: String?
let rates: [String: Double]?
}
class CurrencyExchangeViewController: UIViewController {
// outlets
#IBOutlet weak var inputTextField: UITextField!
#IBOutlet weak var inputCurrencySegmentedControl: UISegmentedControl!
#IBOutlet weak var outputCurrencySegmentedControl: UISegmentedControl!
#IBOutlet weak var resultLabel: UILabel!
// private properties
private let currencies = [
Currency(code: "EUR", localeIdentifier: "fr_FR"),
Currency(code: "JPY", localeIdentifier: "jp_JP"),
Currency(code: "CNY", localeIdentifier: "ch_CH"),
Currency(code: "USD", localeIdentifier: "en_US")
]
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.isNavigationBarHidden = true
updateCurrencyControls()
}
#IBAction func didTapConvertButton(_ sender: Any) {
let inputIndex = inputCurrencySegmentedControl.selectedSegmentIndex
let outputIndex = outputCurrencySegmentedControl.selectedSegmentIndex
guard inputIndex >= 0, outputIndex >= 0 else {
resultLabel.text = errorMessage(for: CurrencyExchangeError.currencyNotSupplied)
return
}
guard let text = inputTextField.text, let value = Double(text) else {
resultLabel.text = errorMessage(for: CurrencyExchangeError.valueNotSupplied)
return
}
performConversion(from: inputIndex, to: outputIndex, of: value) { result in
switch result {
case .failure(let error):
self.resultLabel.text = self.errorMessage(for: error)
case .success(let string):
self.resultLabel.text = string
}
}
}
func updateCurrencyControls() {
outputCurrencySegmentedControl.removeAllSegments()
inputCurrencySegmentedControl.removeAllSegments()
enumerateCurrencies { index, code in
outputCurrencySegmentedControl.insertSegment(withTitle: code, at: index, animated: false)
inputCurrencySegmentedControl.insertSegment(withTitle: code, at: index, animated: false)
}
}
}
// these might better belong in a presenter or view model rather than the view controller
private extension CurrencyExchangeViewController {
func enumerateCurrencies(block: (Int, String) -> Void) {
for (index, currency) in currencies.enumerated() {
block(index, currency.code)
}
}
func errorMessage(for error: Error) -> String {
switch error {
case CurrencyExchangeError.currencyNotFound:
return NSLocalizedString("No exchange rate found for those currencies.", comment: "Error")
case CurrencyExchangeError.unknownNetworkError:
return NSLocalizedString("Unknown error occurred.", comment: "Error")
case CurrencyExchangeError.currencyNotSupplied:
return NSLocalizedString("You must indicate the desired currencies.", comment: "Error")
case CurrencyExchangeError.valueNotSupplied:
return NSLocalizedString("No value to convert has been supplied.", comment: "Error")
case CurrencyExchangeError.webServiceError(let message):
return NSLocalizedString(message, comment: "Error")
case let error as NSError where error.domain == NSURLErrorDomain:
return NSLocalizedString("There was a network error.", comment: "Error")
case is DecodingError:
return NSLocalizedString("There was a problem parsing the server response.", comment: "Error")
default:
return error.localizedDescription
}
}
func performConversion(from fromIndex: Int, to toIndex: Int, of value: Double, completion: #escaping (Result<String?>) -> Void) {
let originalCurrency = currencies[fromIndex]
let outputCurrency = currencies[toIndex]
fetchExchangeRates(for: originalCurrency.code) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let exchangeRates):
guard let exchangeRate = exchangeRates.rates?[outputCurrency.code] else {
completion(.failure(CurrencyExchangeError.currencyNotFound))
return
}
let outputValue = value * exchangeRate
let locale = Locale(identifier: outputCurrency.localeIdentifier)
let string = formatter(for: locale).string(for: outputValue)
completion(.success(string))
}
}
/// Currency formatter for specified locale.
///
/// Note, this formats number using the current locale (e.g. still uses
/// your local grouping and decimal separator), but gets the appropriate
/// properties for the target locale's currency, namely:
///
/// - the currency symbol, and
/// - the number of decimal places.
///
/// - Parameter locale: The `Locale` from which we'll use to get the currency-specific properties.
/// - Returns: A `NumberFormatter` that melds the current device's number formatting and
/// the specified locale's currency formatting.
func formatter(for locale: Locale) -> NumberFormatter {
let currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currency
currencyFormatter.locale = locale
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyFormatter.currencyCode
formatter.currencySymbol = currencyFormatter.currencySymbol
formatter.internationalCurrencySymbol = currencyFormatter.internationalCurrencySymbol
formatter.maximumFractionDigits = currencyFormatter.maximumFractionDigits
formatter.minimumFractionDigits = currencyFormatter.minimumFractionDigits
return formatter
}
}
}
// this might better belong in a network service rather than in the view controller
private extension CurrencyExchangeViewController {
func fetchExchangeRates(for inputCurrencyCode: String, completion: #escaping (Result<ExchangeRateResponse>) -> Void) {
Alamofire.request("https://api.exchangeratesapi.io/latest?base=\(inputCurrencyCode)").response { response in
guard response.error == nil, let data = response.data else {
completion(.failure(response.error ?? CurrencyExchangeError.unknownNetworkError(response.data, response.response)))
return
}
do {
let exchangeRates = try JSONDecoder().decode(ExchangeRateResponse.self, from: data)
if let error = exchangeRates.error {
completion(.failure(CurrencyExchangeError.webServiceError(error)))
} else {
completion(.success(exchangeRates))
}
} catch {
completion(.failure(error))
}
}
}
}
As indicated in the comments above, I’d probably move some of that stuff in the extensions into different objects, but I suspect even the above changes are a bit much to take in at one time, so I’ve stopped my refactoring there.

Related

Why does xcode not allow me to explicitly declare type when changing string type to integer (Swift) [duplicate]

The application basically calculates acceleration by inputting Initial and final velocity and time and then use a formula to calculate acceleration. However, since the values in the text boxes are string, I am unable to convert them to integers.
#IBOutlet var txtBox1 : UITextField
#IBOutlet var txtBox2 : UITextField
#IBOutlet var txtBox3 : UITextField
#IBOutlet var lblAnswer : UILabel
#IBAction func btn1(sender : AnyObject) {
let answer1 = "The acceleration is"
var answer2 = txtBox1
var answer3 = txtBox2
var answer4 = txtBox3
Updated answer for Swift 2.0+:
toInt() method gives an error, as it was removed from String in Swift 2.x. Instead, the Int type now has an initializer that accepts a String:
let a: Int? = Int(firstTextField.text)
let b: Int? = Int(secondTextField.text)
Basic Idea, note that this only works in Swift 1.x (check out ParaSara's answer to see how it works in Swift 2.x):
// toInt returns optional that's why we used a:Int?
let a:Int? = firstText.text.toInt() // firstText is UITextField
let b:Int? = secondText.text.toInt() // secondText is UITextField
// check a and b before unwrapping using !
if a && b {
var ans = a! + b!
answerLabel.text = "Answer is \(ans)" // answerLabel ie UILabel
} else {
answerLabel.text = "Input values are not numeric"
}
Update for Swift 4
...
let a:Int? = Int(firstText.text) // firstText is UITextField
let b:Int? = Int(secondText.text) // secondText is UITextField
...
myString.toInt() - convert the string value into int .
Swift 3.x
If you have an integer hiding inside a string, you can convertby using the integer's constructor, like this:
let myInt = Int(textField.text)
As with other data types (Float and Double) you can also convert by using NSString:
let myString = "556"
let myInt = (myString as NSString).integerValue
You can use NSNumberFormatter().numberFromString(yourNumberString). It's great because it returns an an optional that you can then test with if let to determine if the conversion was successful.
eg.
var myString = "\(10)"
if let myNumber = NSNumberFormatter().numberFromString(myString) {
var myInt = myNumber.integerValue
// do what you need to do with myInt
} else {
// what ever error code you need to write
}
Swift 5
var myString = "\(10)"
if let myNumber = NumberFormatter().number(from: myString) {
var myInt = myNumber.intValue
// do what you need to do with myInt
} else {
// what ever error code you need to write
}
edit/update: Xcode 11.4 • Swift 5.2
Please check the comments through the code
IntegerField.swift file contents:
import UIKit
class IntegerField: UITextField {
// returns the textfield contents, removes non digit characters and converts the result to an integer value
var value: Int { string.digits.integer ?? 0 }
var maxValue: Int = 999_999_999
private var lastValue: Int = 0
override func willMove(toSuperview newSuperview: UIView?) {
// adds a target to the textfield to monitor when the text changes
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
// sets the keyboard type to digits only
keyboardType = .numberPad
// set the text alignment to right
textAlignment = .right
// sends an editingChanged action to force the textfield to be updated
sendActions(for: .editingChanged)
}
// deletes the last digit of the text field
override func deleteBackward() {
// note that the field text property default value is an empty string so force unwrap its value is safe
// note also that collection remove at requires a non empty collection which is true as well in this case so no need to check if the collection is not empty.
text!.remove(at: text!.index(before: text!.endIndex))
// sends an editingChanged action to force the textfield to be updated
sendActions(for: .editingChanged)
}
#objc func editingChanged() {
guard value <= maxValue else {
text = Formatter.decimal.string(for: lastValue)
return
}
// This will format the textfield respecting the user device locale and settings
text = Formatter.decimal.string(for: value)
print("Value:", value)
lastValue = value
}
}
You would need to add those extensions to your project as well:
Extensions UITextField.swift file contents:
import UIKit
extension UITextField {
var string: String { text ?? "" }
}
Extensions Formatter.swift file contents:
import Foundation
extension Formatter {
static let decimal = NumberFormatter(numberStyle: .decimal)
}
Extensions NumberFormatter.swift file contents:
import Foundation
extension NumberFormatter {
convenience init(numberStyle: Style) {
self.init()
self.numberStyle = numberStyle
}
}
Extensions StringProtocol.swift file contents:
extension StringProtocol where Self: RangeReplaceableCollection {
var digits: Self { filter(\.isWholeNumber) }
var integer: Int? { Int(self) }
}
Sample project
swift 4.0
let stringNumber = "123"
let number = Int(stringNumber) //here number is of type "Int?"
//using Forced Unwrapping
if number != nil {
//string is converted to Int
}
you could also use Optional Binding other than forced binding.
eg:
if let number = Int(stringNumber) {
// number is of type Int
}
In Swift 4.2 and Xcode 10.1
let string = "789"
if let intValue = Int(string) {
print(intValue)
}
let integerValue = 789
let stringValue = String(integerValue)
OR
let stringValue = "\(integerValue)"
print(stringValue)
//Xcode 8.1 and swift 3.0
We can also handle it by Optional Binding, Simply
let occur = "10"
if let occ = Int(occur) {
print("By optional binding :", occ*2) // 20
}
Swift 3
The simplest and more secure way is:
#IBOutlet var textFieldA : UITextField
#IBOutlet var textFieldB : UITextField
#IBOutlet var answerLabel : UILabel
#IBAction func calculate(sender : AnyObject) {
if let intValueA = Int(textFieldA),
let intValueB = Int(textFieldB) {
let result = intValueA + intValueB
answerLabel.text = "The acceleration is \(result)"
}
else {
answerLabel.text = "The value \(intValueA) and/or \(intValueB) are not a valid integer value"
}
}
Avoid invalid values setting keyboard type to number pad:
textFieldA.keyboardType = .numberPad
textFieldB.keyboardType = .numberPad
In Swift 4:
extension String {
var numberValue:NSNumber? {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter.number(from: self)
}
}
let someFloat = "12".numberValue
Useful for String to Int and other type
extension String {
//Converts String to Int
public func toInt() -> Int? {
if let num = NumberFormatter().number(from: self) {
return num.intValue
} else {
return nil
}
}
//Converts String to Double
public func toDouble() -> Double? {
if let num = NumberFormatter().number(from: self) {
return num.doubleValue
} else {
return nil
}
}
/// EZSE: Converts String to Float
public func toFloat() -> Float? {
if let num = NumberFormatter().number(from: self) {
return num.floatValue
} else {
return nil
}
}
//Converts String to Bool
public func toBool() -> Bool? {
return (self as NSString).boolValue
}
}
Use it like :
"123".toInt() // 123
i have made a simple program, where you have 2 txt field you take input form the user and add them to make it simpler to understand please find the code below.
#IBOutlet weak var result: UILabel!
#IBOutlet weak var one: UITextField!
#IBOutlet weak var two: UITextField!
#IBAction func add(sender: AnyObject) {
let count = Int(one.text!)
let cal = Int(two.text!)
let sum = count! + cal!
result.text = "Sum is \(sum)"
}
hope this helps.
Swift 3.0
Try this, you don't need to check for any condition I have done everything just use this function. Send anything string, number, float, double ,etc,. you get a number as a value or 0 if it is unable to convert your value
Function:
func getNumber(number: Any?) -> NSNumber {
guard let statusNumber:NSNumber = number as? NSNumber else
{
guard let statString:String = number as? String else
{
return 0
}
if let myInteger = Int(statString)
{
return NSNumber(value:myInteger)
}
else{
return 0
}
}
return statusNumber
}
Usage:
Add the above function in code and to convert use
let myNumber = getNumber(number: myString)
if the myString has a number or string it returns the number else it returns 0
Example 1:
let number:String = "9834"
print("printing number \(getNumber(number: number))")
Output: printing number 9834
Example 2:
let number:Double = 9834
print("printing number \(getNumber(number: number))")
Output: printing number 9834
Example 3:
let number = 9834
print("printing number \(getNumber(number: number))")
Output: printing number 9834
About int() and Swift 2.x: if you get a nil value after conversion check if you try to convert a string with a big number (for example: 1073741824), in this case try:
let bytesInternet : Int64 = Int64(bytesInternetString)!
Latest swift3 this code is simply to convert string to int
let myString = "556"
let myInt = Int(myString)
Because a string might contain non-numerical characters you should use a guard to protect the operation. Example:
guard let labelInt:Int = Int(labelString) else {
return
}
useLabelInt()
I recently got the same issue. Below solution is work for me:
let strValue = "123"
let result = (strValue as NSString).integerValue
Swift5 float or int string to int:
extension String {
func convertStringToInt() -> Int {
return Int(Double(self) ?? 0.0)
}
}
let doubleStr = "4.2"
// print 4
print(doubleStr.convertStringToInt())
let intStr = "4"
// print 4
print(intStr.convertStringToInt())
Use this:
// get the values from text boxes
let a:Double = firstText.text.bridgeToObjectiveC().doubleValue
let b:Double = secondText.text.bridgeToObjectiveC().doubleValue
// we checking against 0.0, because above function return 0.0 if it gets failed to convert
if (a != 0.0) && (b != 0.0) {
var ans = a + b
answerLabel.text = "Answer is \(ans)"
} else {
answerLabel.text = "Input values are not numberic"
}
OR
Make your UITextField KeyboardType as DecimalTab from your XIB or storyboard, and remove any if condition for doing any calculation, ie.
var ans = a + b
answerLabel.text = "Answer is \(ans)"
Because keyboard type is DecimalPad there is no chance to enter other 0-9 or .
Hope this help !!
// To convert user input (i.e string) to int for calculation.I did this , and it works.
let num:Int? = Int(firstTextField.text!);
let sum:Int = num!-2
print(sum);
This works for me
var a:Int? = Int(userInput.text!)
for Swift3.x
extension String {
func toInt(defaultValue: Int) -> Int {
if let n = Int(self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)) {
return n
} else {
return defaultValue
}
}
}
Swift 4, Swift 5
There are different cases to convert from something to something data type, it depends the input.
If the input data type is Any, we have to use as before convert to actual data type, then convert to data type what we want. For example:
func justGetDummyString() -> Any {
return "2000"
}
let dummyString: String = (justGetDummyString() as? String) ?? "" // output = "2000"
let dummyInt: Int = Int(dummyString) ?? 0 // output = 2000
for Alternative solution. You can use extension a native type. You can test with playground.
extension String {
func add(a: Int) -> Int? {
if let b = Int(self) {
return b + a
}
else {
return nil
}
}
}
"2".add(1)
My solution is to have a general extension for string to int conversion.
extension String {
// default: it is a number suitable for your project if the string is not an integer
func toInt(default: Int) -> Int {
if let result = Int(self) {
return result
}
else {
return default
}
}
}
#IBAction func calculateAclr(_ sender: Any) {
if let addition = addition(arrayString: [txtBox1.text, txtBox2.text, txtBox3.text]) {
print("Answer = \(addition)")
lblAnswer.text = "\(addition)"
}
}
func addition(arrayString: [Any?]) -> Int? {
var answer:Int?
for arrayElement in arrayString {
if let stringValue = arrayElement, let intValue = Int(stringValue) {
answer = (answer ?? 0) + intValue
}
}
return answer
}
Question : string "4.0000" can not be convert into integer using Int("4.000")?
Answer : Int() check string is integer or not if yes then give you integer and otherwise nil. but Float or Double can convert any number string to respective Float or Double without giving nil. Example if you have "45" integer string but using Float("45") gives you 45.0 float value or using Double("4567") gives you 45.0.
Solution : NSString(string: "45.000").integerValue or Int(Float("45.000")!)! to get correct result.
An Int in Swift contains an initializer that accepts a String. It returns an optional Int? as the conversion can fail if the string contains not a number.
By using an if let statement you can validate whether the conversion succeeded.
So your code become something like this:
#IBOutlet var txtBox1 : UITextField
#IBOutlet var txtBox2 : UITextField
#IBOutlet var txtBox3 : UITextField
#IBOutlet var lblAnswer : UILabel
#IBAction func btn1(sender : AnyObject) {
let answer1 = "The acceleration is"
var answer2 = txtBox1
var answer3 = txtBox2
var answer4 = txtBox3
if let intAnswer = Int(txtBox1.text) {
// Correctly converted
}
}
Swift 5.0 and Above
Working
In case if you are splitting the String it creates two substrings and not two Strings . This below method will check for Any and convert it t0 NSNumber its easy to convert a NSNumber to Int, Float what ever data type you need.
Actual Code
//Convert Any To Number Object Removing Optional Key Word.
public func getNumber(number: Any) -> NSNumber{
guard let statusNumber:NSNumber = number as? NSNumber else {
guard let statString:String = number as? String else {
guard let statSubStr : Substring = number as? Substring else {
return 0
}
if let myInteger = Int(statSubStr) {
return NSNumber(value:myInteger)
}
else{
return 0
}
}
if let myInteger = Int(statString) {
return NSNumber(value:myInteger)
}
else if let myFloat = Float(statString) {
return NSNumber(value:myFloat)
}else {
return 0
}
}
return statusNumber }
Usage
if let hourVal = getNumber(number: hourStr) as? Int {
}
Passing String to check and convert to Double
Double(getNumber(number: dict["OUT"] ?? 0)
As of swift 3, I have to force my #%#! string & int with a "!" otherwise it just doesn't work.
For example:
let prefs = UserDefaults.standard
var counter: String!
counter = prefs.string(forKey:"counter")
print("counter: \(counter!)")
var counterInt = Int(counter!)
counterInt = counterInt! + 1
print("counterInt: \(counterInt!)")
OUTPUT:
counter: 1
counterInt: 2

UITextField check if user typed in numeric [duplicate]

The application basically calculates acceleration by inputting Initial and final velocity and time and then use a formula to calculate acceleration. However, since the values in the text boxes are string, I am unable to convert them to integers.
#IBOutlet var txtBox1 : UITextField
#IBOutlet var txtBox2 : UITextField
#IBOutlet var txtBox3 : UITextField
#IBOutlet var lblAnswer : UILabel
#IBAction func btn1(sender : AnyObject) {
let answer1 = "The acceleration is"
var answer2 = txtBox1
var answer3 = txtBox2
var answer4 = txtBox3
Updated answer for Swift 2.0+:
toInt() method gives an error, as it was removed from String in Swift 2.x. Instead, the Int type now has an initializer that accepts a String:
let a: Int? = Int(firstTextField.text)
let b: Int? = Int(secondTextField.text)
Basic Idea, note that this only works in Swift 1.x (check out ParaSara's answer to see how it works in Swift 2.x):
// toInt returns optional that's why we used a:Int?
let a:Int? = firstText.text.toInt() // firstText is UITextField
let b:Int? = secondText.text.toInt() // secondText is UITextField
// check a and b before unwrapping using !
if a && b {
var ans = a! + b!
answerLabel.text = "Answer is \(ans)" // answerLabel ie UILabel
} else {
answerLabel.text = "Input values are not numeric"
}
Update for Swift 4
...
let a:Int? = Int(firstText.text) // firstText is UITextField
let b:Int? = Int(secondText.text) // secondText is UITextField
...
myString.toInt() - convert the string value into int .
Swift 3.x
If you have an integer hiding inside a string, you can convertby using the integer's constructor, like this:
let myInt = Int(textField.text)
As with other data types (Float and Double) you can also convert by using NSString:
let myString = "556"
let myInt = (myString as NSString).integerValue
You can use NSNumberFormatter().numberFromString(yourNumberString). It's great because it returns an an optional that you can then test with if let to determine if the conversion was successful.
eg.
var myString = "\(10)"
if let myNumber = NSNumberFormatter().numberFromString(myString) {
var myInt = myNumber.integerValue
// do what you need to do with myInt
} else {
// what ever error code you need to write
}
Swift 5
var myString = "\(10)"
if let myNumber = NumberFormatter().number(from: myString) {
var myInt = myNumber.intValue
// do what you need to do with myInt
} else {
// what ever error code you need to write
}
edit/update: Xcode 11.4 • Swift 5.2
Please check the comments through the code
IntegerField.swift file contents:
import UIKit
class IntegerField: UITextField {
// returns the textfield contents, removes non digit characters and converts the result to an integer value
var value: Int { string.digits.integer ?? 0 }
var maxValue: Int = 999_999_999
private var lastValue: Int = 0
override func willMove(toSuperview newSuperview: UIView?) {
// adds a target to the textfield to monitor when the text changes
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
// sets the keyboard type to digits only
keyboardType = .numberPad
// set the text alignment to right
textAlignment = .right
// sends an editingChanged action to force the textfield to be updated
sendActions(for: .editingChanged)
}
// deletes the last digit of the text field
override func deleteBackward() {
// note that the field text property default value is an empty string so force unwrap its value is safe
// note also that collection remove at requires a non empty collection which is true as well in this case so no need to check if the collection is not empty.
text!.remove(at: text!.index(before: text!.endIndex))
// sends an editingChanged action to force the textfield to be updated
sendActions(for: .editingChanged)
}
#objc func editingChanged() {
guard value <= maxValue else {
text = Formatter.decimal.string(for: lastValue)
return
}
// This will format the textfield respecting the user device locale and settings
text = Formatter.decimal.string(for: value)
print("Value:", value)
lastValue = value
}
}
You would need to add those extensions to your project as well:
Extensions UITextField.swift file contents:
import UIKit
extension UITextField {
var string: String { text ?? "" }
}
Extensions Formatter.swift file contents:
import Foundation
extension Formatter {
static let decimal = NumberFormatter(numberStyle: .decimal)
}
Extensions NumberFormatter.swift file contents:
import Foundation
extension NumberFormatter {
convenience init(numberStyle: Style) {
self.init()
self.numberStyle = numberStyle
}
}
Extensions StringProtocol.swift file contents:
extension StringProtocol where Self: RangeReplaceableCollection {
var digits: Self { filter(\.isWholeNumber) }
var integer: Int? { Int(self) }
}
Sample project
swift 4.0
let stringNumber = "123"
let number = Int(stringNumber) //here number is of type "Int?"
//using Forced Unwrapping
if number != nil {
//string is converted to Int
}
you could also use Optional Binding other than forced binding.
eg:
if let number = Int(stringNumber) {
// number is of type Int
}
In Swift 4.2 and Xcode 10.1
let string = "789"
if let intValue = Int(string) {
print(intValue)
}
let integerValue = 789
let stringValue = String(integerValue)
OR
let stringValue = "\(integerValue)"
print(stringValue)
//Xcode 8.1 and swift 3.0
We can also handle it by Optional Binding, Simply
let occur = "10"
if let occ = Int(occur) {
print("By optional binding :", occ*2) // 20
}
Swift 3
The simplest and more secure way is:
#IBOutlet var textFieldA : UITextField
#IBOutlet var textFieldB : UITextField
#IBOutlet var answerLabel : UILabel
#IBAction func calculate(sender : AnyObject) {
if let intValueA = Int(textFieldA),
let intValueB = Int(textFieldB) {
let result = intValueA + intValueB
answerLabel.text = "The acceleration is \(result)"
}
else {
answerLabel.text = "The value \(intValueA) and/or \(intValueB) are not a valid integer value"
}
}
Avoid invalid values setting keyboard type to number pad:
textFieldA.keyboardType = .numberPad
textFieldB.keyboardType = .numberPad
In Swift 4:
extension String {
var numberValue:NSNumber? {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter.number(from: self)
}
}
let someFloat = "12".numberValue
Useful for String to Int and other type
extension String {
//Converts String to Int
public func toInt() -> Int? {
if let num = NumberFormatter().number(from: self) {
return num.intValue
} else {
return nil
}
}
//Converts String to Double
public func toDouble() -> Double? {
if let num = NumberFormatter().number(from: self) {
return num.doubleValue
} else {
return nil
}
}
/// EZSE: Converts String to Float
public func toFloat() -> Float? {
if let num = NumberFormatter().number(from: self) {
return num.floatValue
} else {
return nil
}
}
//Converts String to Bool
public func toBool() -> Bool? {
return (self as NSString).boolValue
}
}
Use it like :
"123".toInt() // 123
i have made a simple program, where you have 2 txt field you take input form the user and add them to make it simpler to understand please find the code below.
#IBOutlet weak var result: UILabel!
#IBOutlet weak var one: UITextField!
#IBOutlet weak var two: UITextField!
#IBAction func add(sender: AnyObject) {
let count = Int(one.text!)
let cal = Int(two.text!)
let sum = count! + cal!
result.text = "Sum is \(sum)"
}
hope this helps.
Swift 3.0
Try this, you don't need to check for any condition I have done everything just use this function. Send anything string, number, float, double ,etc,. you get a number as a value or 0 if it is unable to convert your value
Function:
func getNumber(number: Any?) -> NSNumber {
guard let statusNumber:NSNumber = number as? NSNumber else
{
guard let statString:String = number as? String else
{
return 0
}
if let myInteger = Int(statString)
{
return NSNumber(value:myInteger)
}
else{
return 0
}
}
return statusNumber
}
Usage:
Add the above function in code and to convert use
let myNumber = getNumber(number: myString)
if the myString has a number or string it returns the number else it returns 0
Example 1:
let number:String = "9834"
print("printing number \(getNumber(number: number))")
Output: printing number 9834
Example 2:
let number:Double = 9834
print("printing number \(getNumber(number: number))")
Output: printing number 9834
Example 3:
let number = 9834
print("printing number \(getNumber(number: number))")
Output: printing number 9834
About int() and Swift 2.x: if you get a nil value after conversion check if you try to convert a string with a big number (for example: 1073741824), in this case try:
let bytesInternet : Int64 = Int64(bytesInternetString)!
Latest swift3 this code is simply to convert string to int
let myString = "556"
let myInt = Int(myString)
Because a string might contain non-numerical characters you should use a guard to protect the operation. Example:
guard let labelInt:Int = Int(labelString) else {
return
}
useLabelInt()
I recently got the same issue. Below solution is work for me:
let strValue = "123"
let result = (strValue as NSString).integerValue
Swift5 float or int string to int:
extension String {
func convertStringToInt() -> Int {
return Int(Double(self) ?? 0.0)
}
}
let doubleStr = "4.2"
// print 4
print(doubleStr.convertStringToInt())
let intStr = "4"
// print 4
print(intStr.convertStringToInt())
Use this:
// get the values from text boxes
let a:Double = firstText.text.bridgeToObjectiveC().doubleValue
let b:Double = secondText.text.bridgeToObjectiveC().doubleValue
// we checking against 0.0, because above function return 0.0 if it gets failed to convert
if (a != 0.0) && (b != 0.0) {
var ans = a + b
answerLabel.text = "Answer is \(ans)"
} else {
answerLabel.text = "Input values are not numberic"
}
OR
Make your UITextField KeyboardType as DecimalTab from your XIB or storyboard, and remove any if condition for doing any calculation, ie.
var ans = a + b
answerLabel.text = "Answer is \(ans)"
Because keyboard type is DecimalPad there is no chance to enter other 0-9 or .
Hope this help !!
// To convert user input (i.e string) to int for calculation.I did this , and it works.
let num:Int? = Int(firstTextField.text!);
let sum:Int = num!-2
print(sum);
This works for me
var a:Int? = Int(userInput.text!)
for Swift3.x
extension String {
func toInt(defaultValue: Int) -> Int {
if let n = Int(self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)) {
return n
} else {
return defaultValue
}
}
}
Swift 4, Swift 5
There are different cases to convert from something to something data type, it depends the input.
If the input data type is Any, we have to use as before convert to actual data type, then convert to data type what we want. For example:
func justGetDummyString() -> Any {
return "2000"
}
let dummyString: String = (justGetDummyString() as? String) ?? "" // output = "2000"
let dummyInt: Int = Int(dummyString) ?? 0 // output = 2000
for Alternative solution. You can use extension a native type. You can test with playground.
extension String {
func add(a: Int) -> Int? {
if let b = Int(self) {
return b + a
}
else {
return nil
}
}
}
"2".add(1)
My solution is to have a general extension for string to int conversion.
extension String {
// default: it is a number suitable for your project if the string is not an integer
func toInt(default: Int) -> Int {
if let result = Int(self) {
return result
}
else {
return default
}
}
}
#IBAction func calculateAclr(_ sender: Any) {
if let addition = addition(arrayString: [txtBox1.text, txtBox2.text, txtBox3.text]) {
print("Answer = \(addition)")
lblAnswer.text = "\(addition)"
}
}
func addition(arrayString: [Any?]) -> Int? {
var answer:Int?
for arrayElement in arrayString {
if let stringValue = arrayElement, let intValue = Int(stringValue) {
answer = (answer ?? 0) + intValue
}
}
return answer
}
Question : string "4.0000" can not be convert into integer using Int("4.000")?
Answer : Int() check string is integer or not if yes then give you integer and otherwise nil. but Float or Double can convert any number string to respective Float or Double without giving nil. Example if you have "45" integer string but using Float("45") gives you 45.0 float value or using Double("4567") gives you 45.0.
Solution : NSString(string: "45.000").integerValue or Int(Float("45.000")!)! to get correct result.
An Int in Swift contains an initializer that accepts a String. It returns an optional Int? as the conversion can fail if the string contains not a number.
By using an if let statement you can validate whether the conversion succeeded.
So your code become something like this:
#IBOutlet var txtBox1 : UITextField
#IBOutlet var txtBox2 : UITextField
#IBOutlet var txtBox3 : UITextField
#IBOutlet var lblAnswer : UILabel
#IBAction func btn1(sender : AnyObject) {
let answer1 = "The acceleration is"
var answer2 = txtBox1
var answer3 = txtBox2
var answer4 = txtBox3
if let intAnswer = Int(txtBox1.text) {
// Correctly converted
}
}
Swift 5.0 and Above
Working
In case if you are splitting the String it creates two substrings and not two Strings . This below method will check for Any and convert it t0 NSNumber its easy to convert a NSNumber to Int, Float what ever data type you need.
Actual Code
//Convert Any To Number Object Removing Optional Key Word.
public func getNumber(number: Any) -> NSNumber{
guard let statusNumber:NSNumber = number as? NSNumber else {
guard let statString:String = number as? String else {
guard let statSubStr : Substring = number as? Substring else {
return 0
}
if let myInteger = Int(statSubStr) {
return NSNumber(value:myInteger)
}
else{
return 0
}
}
if let myInteger = Int(statString) {
return NSNumber(value:myInteger)
}
else if let myFloat = Float(statString) {
return NSNumber(value:myFloat)
}else {
return 0
}
}
return statusNumber }
Usage
if let hourVal = getNumber(number: hourStr) as? Int {
}
Passing String to check and convert to Double
Double(getNumber(number: dict["OUT"] ?? 0)
As of swift 3, I have to force my #%#! string & int with a "!" otherwise it just doesn't work.
For example:
let prefs = UserDefaults.standard
var counter: String!
counter = prefs.string(forKey:"counter")
print("counter: \(counter!)")
var counterInt = Int(counter!)
counterInt = counterInt! + 1
print("counterInt: \(counterInt!)")
OUTPUT:
counter: 1
counterInt: 2

How Can I Use Two URLs Asynchronously to Parse JSON data

So I am using a URL in the bolded text to parse JSON data retrieved remotely from that URL. My issue is that I want to parse data remotely AND asynchronously from TWO URLs not just one. The following code works great for 1 URL but I haven't the slightest idea how to do the same thing for 2. I am fairly new to Swift to any tips or pointers would be appreciated.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var customerNameLabel: UILabel!
#IBOutlet weak var cardNumberLabel: UILabel!
#IBOutlet weak var dateNTimeLabel: UILabel!
#IBOutlet weak var amountLabel: UILabel!
var customers = [Customer]()
var currentCustomerIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Retrieve JSON data from a remote server
let config = URLSessionConfiguration.default
// Create a session
let session = URLSession(configuration: config)
// Validate the URL to ensure that it is not a broken link
if let validURL = URL(string: "**THISISMYJSONURLHERE(removedforsecurity)**") {
//Create a task that will download whatever is found at validURL as a Data object
let task = session.dataTask(with: validURL, completionHandler: { (data, response, error) in
// If there is an error, we are going to bail out of this entire method (hence return)
if let error = error {
print("Data task failed with error: " + error.localizedDescription)
return
}
// If we get here that means we have received the info at the URL as a Data Object nd we can now ue it
print("Success")
//Check the response status
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let validData = data
else {print("JSON Object Creation Failed"); return}
do {
let jsonObj = try JSONSerialization.jsonObject(with: validData, options: .mutableContainers) as? [Any]
// Call our Parse method
self.ParseData(jsonObject: jsonObj)
self.displayData()
}
catch {
print(error.localizedDescription)
}
task.resume()
}
}
func ParseData(jsonObject: [Any]?) {
guard let json = jsonObject
else { print("Parse failed to unwrap the optional."); return }
for firstLevelItems in json {
guard let object = firstLevelItems as? [String: Any],
let fname = object["first_name"] as? String,
let lname = object["last_name"] as? String,
let fullName = fname + " " + lname as? String,
let customerNumber = object["customer_number"] as? Int,
let purchase = object["purchase"] as? [String: Any],
let time = purchase["time"] as? String,
let date = purchase["date"] as? String,
let amount = purchase["amount"] as? String
else { continue }
// See Note: Nested Functions
func addTransaction(_customer: Customer) {
if let cardNumber = purchase["card_number"] as? String? {
_customer.transactions.append(Transaction(firstName: fname, lastName: lname, time: time, date: date, amount: amount, cardNumber: cardNumber))
}
else {
_customer.transactions.append(Transaction(firstName: fname, lastName: lname, time: time, date: date, amount: amount))
}
}
let filteredCustomers = customers.filter({ (customer) -> Bool in
return customer.transactions[currentCustomerIndex].customerName == fullName
})
if filteredCustomers.count == 0 {
customers.append(Customer(customerNumber: customerNumber))
//Forced unwrapping here is ok because we know for a fact that customers wont be empty
addTransaction(_customer: customers.last!)
}
// If filtered array.count is 1 then that means we already have a customer object for this number
// In that case we just want to modify the existing customer object instead of creating a new one
else if filteredCustomers.count == 1 {
// filteredCustomer[0].customerNote = "This has been counted and Modified"
addTransaction(_customer: filteredCustomers[0])
}
else {
//See Note: Assertion
// Assertion Failure so that as we are building if this ever happens we know we have messed up
assertionFailure("No customers should exist twice in our customers array")
}
// print("Customer Number: \(customerNumber) has \(filteredCustomers.count) Orccurance in Customer's Array")
}
}
func displayData() {
DispatchQueue.main.async {
self.customerNameLabel.text = self.customers[self.currentCustomerIndex].customerName
self.cardNumberLabel.text = self.customers[self.currentCustomerIndex].cardNum
self.dateNTimeLabel.text = self.customers[self.currentCustomerIndex].dateNTime
self.amountLabel.text = "$" + self.customers[self.currentCustomerIndex].customerAmount.description
}
}
#IBAction func changeCustomer(_ sender: UIButton) {
currentCustomerIndex += sender.tag
if currentCustomerIndex < 0 {
currentCustomerIndex = customers.count - 1
}
else if currentCustomerIndex >= customers.count {
currentCustomerIndex = 0
}
displayData()
}
}

Swift execute functions sequentially

I have the below code which obtains the access token from a facebook login and returns it as 'accessToken'. Once I have the access token I wish to pass this to my server in a request and return the response as an array.
The issue I have is that the request to the server executes before the accessToken is obtained. I have looked into closure statements but I cannot see a way where I can order the execution of the functions without ending up nesting. I don't mind nesting in this instance but in future if I have say 5 functions this will begin to look messy.
Am I approaching this in the best way by using classes and functions? Usually when I code in swift all the code relevant to the viewController would be contained in 1 file, but as the project gets larger I am looking to implement a more OOP approach to make the project more manageable. How would I best achieve this?
import Foundation
import UIKit
class registrationPage: UIViewController {
#IBAction func facebookButton(_ sender: Any) {
// Get the access token from facebook
let accessToken = facebookLogin().login()
// Get the users currency, langage and locale.
let currency = Locale.current.currencyCode ?? "GBP"
let language = Locale.current.languageCode ?? "GB"
let region = Locale.current.regionCode ?? "GB"
let params = "accessToken=\(accessToken)&currency=\(currency)&language=\(language)&region=\(region)"
let resultArray = database().connect(endPoint: "loginfb.php?", params: "\(params)")
print(resultArray)
}
}
class facebookLogin {
var response = ""
func login(completion: (_ result: String) -> Void) {
let loginManager = LoginManager()
loginManager.logIn(readPermissions:[ReadPermission.publicProfile, ReadPermission.email], viewController: registrationPage() as UIViewController) {
loginResult in switch loginResult {
case .failed:
self.response = "ERROR"
case .cancelled:
self.response = "ERROR"
case .success:
self.response = "\(String(describing: FBSDKAccessToken.current().tokenString!))"
print(self.response)
}
}
completion(self.response)
}
}
loginManager.logIn is asynchronous, thats why it takes a closure. You can synchronize the call or as you said use nested closures where one calls the next.
To make let accessToken = facebookLogin().login() synchronous with DispatchGroup:
class facebookLogin {
func login() -> String {
let loginManager = LoginManager()
var response = ""
let group = DispatchGroup()
group.enter() // loginManager.logIn
loginManager.logIn(readPermissions:[ReadPermission.publicProfile, ReadPermission.email], viewController: registrationPage() as UIViewController) {
loginResult in switch loginResult {
case .failed:
self.response = "ERROR"
case .cancelled:
self.response = "ERROR"
case .success:
self.response = "\(String(describing: FBSDKAccessToken.current().tokenString!))"
print(self.response)
}
group.leave() // loginManager.logIn
}
group.wait()
return response
}
}
If you don't like the facebookLogin().login() { accessToken in ... } syntax, you could put the { accessToken in ... } part into its own function
func callServer(accessToken: String) {
// Get the users currency, langage and locale.
let currency = Locale.current.currencyCode ?? "GBP"
let language = Locale.current.languageCode ?? "GB"
let region = Locale.current.regionCode ?? "GB"
let params = "accessToken=\(accessToken)&currency=\(currency)&language=\(language)&region=\(region)"
let resultArray = database().connect(endPoint: "loginfb.php?", params: "\(params)")
print(resultArray)
}
and call it with
#IBAction func facebookButton(_ sender: Any) {
// Get the access token from facebook
facebookLogin().login(completion: callServer(accessToken:))
}

Cannot avoid optional() for string print statements?

I have a data struct which contains some string parameters. The struct is below:
struct pulledMessage{
var convoWithUserID: String
var convoWithUserName: String
}
I have a function which assigns a value to variables based on the values within a particular pulledMessage. For some more complicated, out-of-the-scope-of-the-question, reasons, these values come from [pulledMessage] array. The pulledMessage always changes in the actual function but for illustration purposes I will write it as a constant:
var messageArray = [pulledMessage]()
func assignValues(){
messageArray.append(pulledMessage(convoWithUserID: "abc123", convoWithUserName: "Kevin"))
let convoWithUserID = messageArray[0].convoWithUserID
let convoWithUserName = messageArray[0].convoWithUserName
print(convoWithUserID) //returns optional("abc123")
print(convoWithUserName) // returns optional("Kevin")
}
I have tried adding ! to unwrap the values in different ways:
messageArray[0]!.convoWithUserID
This tells gives me an error that I cannot unwrap a non-optional type of pulledMessage.
messageArray[0].convoWithUserID!
This gives me an error that I cannot unwrap a non-optional type of String.
This stack question suggests utilizing if let to get rid of the optional:
if let convoWithUserIDCheck = messageArray[0].convoWithUserID{
convoWithUserID = convoWithUserIDCheck
}
This gives me a warning that there is no reason to do if let with a non-optional type of string. I have no idea how to get it to stop returning the values wrapped by optional().
Update: The more complicated, complete code
The SQL Database functions:
class FMDBManager: NSObject {
static let shared: FMDBManager = FMDBManager()
let databaseFileName = "messagesBetweenUsers.sqlite"
var pathToDatabase: String!
var database: FMDatabase!
override init() {
super.init()
let documentsDirectory = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString) as String
pathToDatabase = documentsDirectory.appending("/\(databaseFileName)")
}
func loadMessageData(){//will need a struct to load the data into a struct
if openDatabase(){
let query = "select * from messages order by messageNumber asc"
do{
print(database)
let results: FMResultSet = try database.executeQuery(query, values: nil)
while results.next(){
let message = pulledMessage(convoWithUserID: String(describing: results.string(forColumn: "convoWithUserID")), convoWithUserName: String(describing: results.string(forColumn: "convoWithUserName")), messageString: String(describing: results.string(forColumn: "messageString")), senderID: String(describing: results.string(forColumn: "senderID")), timeSent: String(describing: results.string(forColumn: "timeSent")), messageNumber: Int(results.int(forColumn: "messageNumber")))
if messagesPulled == nil{
messagesPulled = [pulledMessage]()
}
messagesPulled.append(message)
print("The message that we have pulled are \(message)")
}
}
catch{
print(error.localizedDescription)
}
database.close()
}
}
}
Running the population of the data at the onset of app launch:
func applicationDidBecomeActive(_ application: UIApplication) {
// if FMDBManager.shared.createDatabase() {
// FMDBManager.shared.insertMessageData()
// }else{
// print("Not a chance, sonny")
// FMDBManager.shared.insertMessageData()
// }
FMDBManager.shared.loadMessageData()
}
Organizing the SQL data in order:
struct pulledMessage{//global struct
var convoWithUserID: String
var convoWithUserName: String
var messageString: String
var senderID: String
var timeSent: String
var messageNumber: Int
}
var messagesPulled: [pulledMessage]!
var messageConvoDictionary = [String: [pulledMessage]]()
//For the individual message convos
var fullUnorderedMessageArray = [[pulledMessage]]()
var fullOrderedMessageArray = [[pulledMessage]]()
//For the message table
var unorderedLastMessageArray = [pulledMessage]()
var orderedLastMessageArray = [pulledMessage]()
//For the table messages... FROM HERE..........................................
func organizeSQLData(messageSet: [pulledMessage]){
var i = 0
var messageUserID = String()
while i < messageSet.count{
if (messageSet[i]).convoWithUserID != messageUserID{
print("It wasn't equal")
print(messageSet[i])
messageUserID = messageSet[i].convoWithUserID
if messageConvoDictionary[messageUserID] != nil{
messageConvoDictionary[messageUserID]?.append(messageSet[i])
}else{
messageConvoDictionary[messageUserID] = []
messageConvoDictionary[messageUserID]?.append(messageSet[i])
}
i = i + 1
}else{
messageConvoDictionary[messageUserID]?.append(messageSet[i])
i = i + 1
}
}
}
func getLastMessages(messageSet: [String:[pulledMessage]]){
for (_, messages) in messageSet{
let orderedMessages = messages.sorted(by:{ $0.timeSent.compare($1.timeSent) == .orderedAscending})
let finalMessage = orderedMessages[0]
unorderedLastMessageArray.append(finalMessage)
}
print(unorderedLastMessageArray)
}
func orderLastMessage(messageSet: [pulledMessage]){
orderedLastMessageArray = messageSet.sorted(by:{ $0.timeSent.compare($1.timeSent) == .orderedDescending})
messagesListTableView.reloadData()
print("It wasn't\(orderedLastMessageArray)")
}
func getMessagesReady(){//for observer type function calls
organizeSQLData(messageSet: messagesPulled)
getLastMessages(messageSet: messageConvoDictionary)
orderLastMessage(messageSet: unorderedLastMessageArray)
//This one is for the individual full convos for if user clicks on a cell... its done last because its not required for the page to show up
orderedFullMessageConvos(messageSet: messageConvoDictionary)
let openedMessageConversation = fullOrderedMessageArray[(indexPath.row)]//not placed in its appropriate location, but it is just used to pass the correct array (actually goes in a prepareforSegue)
}
override func viewDidLoad() {
super.viewDidLoad()
getMessagesReady()
}
Then segue to the new controller (passing openedMessageConversation to messageConvo) and run this process on a button click:
let newMessage = pulledMessage(convoWithUserID: messageConvo[0].convoWithUserID, convoWithUserName: messageConvo[0].convoWithUserName, messageString: commentInputTextfield.text!, senderID: (PFUser.current()?.objectId)!, timeSent: String(describing: Date()), messageNumber: 0)
messageConvo.append(newMessage)
let newMessageSent = PFObject(className: "UserMessages")
newMessageSent["convoWithUserID"] = newMessage.convoWithUserID
newMessageSent["convoWithUserName"] = newMessage.convoWithUserName
newMessageSent["messageString"] = newMessage.messageString
newMessageSent["senderID"] = newMessage.senderID
let acl = PFACL()
acl.getPublicWriteAccess = true
acl.getPublicReadAccess = true
acl.setWriteAccess(true, for: PFUser.current()!)
acl.setReadAccess(true, for: PFUser.current()!)
newMessageSent.acl = acl
newMessageSent.saveInBackground()
It is the newMessageSent["convoWithUserID"] and newMessageSent["convoWithUserName"] that read with the optional() in the database.
So it turns out that the reason for this stems from the function run from loadMessageData. The use of String(describing: results.string(forColumn:) requires an unwrapping of results.String(forColumn:)!. This issue propagated throughout the data modification for the whole app and caused the optional() wrapping for the print statements that I was seeing.