I want to create an app that with a textField and a label. The textField will read the input and using Alamofire to check if the input exist. (The url will return JSON Bool.) A helper function called getReturnVal is to get the return value from the url.
However, the nameExist variable is always nil and it seems like the helper function never goes into the two cases. Please advise how should I fix this problem.
Thank you in advance.
My code is as below.
import UIKit
import Alamofire
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var nameField: UITextField!
#IBOutlet weak var nameLabel: UILabel!
var nameValid: Bool = false
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == nameField {
guard let username = textField.text, !username.isEmpty else {
nameValid = false
return
}
let url = "https://bismarck.sdsu.edu/hometown/nicknameexists?name=" + username
print(url)
let nameExist = getReturnVal(url: url, type: Bool.self)
if nameExist! {
nameLabel.text = "Exist"
nameValid = false
} else {
nameLabel.text = "Valid"
nameValid = true
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func getReturnVal<T>(url: String, type: T.Type) -> T? {
var jsonObject: T?
print("enter------------")
Alamofire.request(url).validate().responseJSON { response in
switch response.result {
case .success:
if let JSON = response.result.value {
jsonObject = (JSON as! T)
print("SUCCESS--------")
print("JSON: \(String(describing: jsonObject))")
}
case .failure(let error):
print("--------------")
print(error)
jsonObject = false
}
}
return jsonObject
}
override func viewDidLoad() {
super.viewDidLoad()
nameField.delegate = self
// 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.
}
}
The Alamofire function is asynchronous, because it’s a network request. This means the function returns before the data from the network request is returned, so it will always return nil because jsonObject is returned before it’s modified. What you need to do is pass in a callback function as a parameter, so that the callback function body gets called when the Alamofire function returns.
The callback signature could be something like this:
callback: (T) -> ()
Then inside the getReturnVal function you can call callback(jsonObject) right after the variable jsonObject is set (so twice). Then when you when you call getReturnVal you need to pass in a function that takes as a parameter that jsonObject.
Related
I hope I would be able to explain my question.
I have a ViewController named TouristPictureViewController
class TouristPictureViewController: UIViewController {
#IBOutlet weak var picture: UIImageView!
var userID = Auth.auth().currentUser?.uid
var imageURL:String?
override func viewDidLoad() {
super.viewDidLoad()
getProfilePicture(SuccessCompletion: { (Img) in
self.picture.image = Img
}) { (error) in
print(error)
}
}
func getProfilePicture(SuccessCompletion: #escaping (_ image:UIImage) -> (), FailureCompletion: #escaping (_ error:Error) -> ()) {
let referecne = Storage.storage().reference(withPath: "user/\(userID!)")
referecne.getData(maxSize: 5 * 1024 * 1024) { (data, error) in
if let err = error {
print(err)
FailureCompletion(err)
}
else {
if let _data = data {
let myImage:UIImage! = UIImage(data: _data)
SuccessCompletion(myImage)
}
}
}
}
}
This VC can be called in two different conditions. First, it is being called when User want to see their profile picture and in that case, It is triggered through Storyboard.instantiateViewController and then in this VC, getProfilePicture function is called. Now, this VC is also triggered through segue, and in that case I am passing an Image URL to this VC and hence with that I want to show that particular Image in this VC. Now what condition should I put here so that the getProfile function should not be executed in viewDidLoad when I approach this VC by performing Segue?
Just check imageURL, it's nil in the instantiateViewController case
override func viewDidLoad() {
super.viewDidLoad()
if let urlString = imageURL {
// load image and assign it to self.picture.image
} else {
getProfilePicture(SuccessCompletion: { img in
self.picture.image = img
}) { error in
print(error)
}
}
}
You can declare a global boolean variable like shouldLoadProfile
in the TouristPictureViewController
var shouldLoadProfile: bool = false
And in the viewDidLoad() method, check shouldLoadProfile or not. Also, you need to set true or false into shouldLoadProfile variable before you transition to TouristPictureViewController.
override func viewDidLoad(){
super.viewDidLoad()
if shouldLoadProfile{
getProfilePicture(SuccessCompletion: { (Img) in
self.picture.image = Img
}) { (error) in
print(error)
}
}else{
//set imageURL into the UIImageView
}
}
I'm very new to ReactiveSwift and MVVM as a whole. I'm trying to validate phone numbers entered into a textfield and enable/disable a button depending on the validation result.
In the app, there is a textfield and a UIButton button called Submit. For phone number validating, I'm using an open source library called [PhoneNumberKit][1]. It also provides a UITextField subclass which formats the user input.
I mashed together a solution that looks like this.
class ViewController: UIViewController {
#IBOutlet weak var textField: PhoneNumberTextField!
#IBOutlet weak var submitButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
textField.becomeFirstResponder()
submitButton.isEnabled = false
let textValuesSignal = textField.reactive.continuousTextValues
SignalProducer(textValuesSignal).start { result in
switch result {
case .value(let value):
print(value)
self.submitButton.isEnabled = self.isPhoneNumberValid(value)
case .failed(let error):
print(error)
self.submitButton.isEnabled = false
case .interrupted:
print("inturrupted")
self.submitButton.isEnabled = false
case .completed:
print("completed")
}
}
}
func isPhoneNumberValid(_ phoneNumberString: String) -> Bool {
do {
let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
print("Phone number is valid: \(formattedPhoneNumber)")
return true
} catch let error {
print("Invalid phone number: \(error)")
return false
}
}
}
This does the job but not very elegantly. Also there is a significant lag between user input and the UI changing.
Another thing is my above solution doesn't conform to MVVM. I gave it another go.
class ViewController: UIViewController {
#IBOutlet weak var textField: PhoneNumberTextField!
#IBOutlet weak var submitButton: UIButton!
private let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
textField.becomeFirstResponder()
submitButton.reactive.isEnabled <~ viewModel.isPhoneNumberValid
viewModel.phoneNumber <~ textField.reactive.continuousTextValues
}
}
class ViewModel {
let phoneNumber = MutableProperty("")
let isPhoneNumberValid = MutableProperty(false)
init() {
isPhoneNumberValid = phoneNumber.producer.map { self.validatePhoneNumber($0) } // Cannot assign value of type 'SignalProducer<Bool, NoError>' to type 'MutableProperty<Bool>'
}
private func validatePhoneNumber(_ phoneNumberString: String) -> Bool {
do {
let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
print("Phone number is valid: \(formattedPhoneNumber)")
return true
} catch let error {
print("Invalid phone number: \(error)")
return false
}
}
}
I'm getting the below error in the initializer when I'm assigning the result from the validatePhoneNumber function's to the isPhoneNumberValid property.
Cannot assign value of type 'SignalProducer' to type 'MutableProperty'
I can't figure out how to hook up the phone number validation part with the submit button's isEnabled property and the tap action properly.
Demo project
Try setting the property in init and mapping the property itself rather than a producer:
let isPhoneNumberValid: Property<Bool>
init() {
isPhoneNumberValid = phoneNumber.map { ViewModel.validatePhoneNumber($0) }
}
You’ll have to make validatePhoneNumber a static method because self won’t be available yet.
This is a more typical reactive way of doing this because you define one property completely in terms of another one during the view model’s initialization.
I am learning RxSwift and I have tried a basic login UI using it. My implementation is as follows.
LoginViewController:
fileprivate let loginViewModel = LoginViewModel()
fileprivate var textFieldArray: [UITextField]!
override func viewDidLoad() {
super.viewDidLoad()
textFieldArray = [textFieldUserName, textFieldPassword, textFieldConfirmPassword]
textFieldUserName.delegate = self
textFieldPassword.delegate = self
textFieldConfirmPassword.delegate = self
loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
}).disposed(by: loginViewModel.bag)
}
func showValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.clear.cgColor
}
func showInValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.red.cgColor
textFieldArray[index].layer.borderWidth = 2.0
}
extension LoginViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let inputText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
switch textField {
case textFieldUserName:
loginViewModel.updateUserName(text: inputText)
case textFieldPassword:
loginViewModel.updatePassword(text: inputText)
case textFieldConfirmPassword:
loginViewModel.updateConfirmedPassword(text: inputText)
default:
return false
}
return true
}
}
LoginViewModel:
class LoginViewModel {
private var username: String!
private var password: String!
private var confirmedPassword: String!
fileprivate let combinedSubject = PublishSubject<[Bool]>()
let bag = DisposeBag()
var areValidFields: Observable<[Bool]> {
return combinedSubject.asObservable()
}
init() {
self.username = ""
self.password = ""
self.confirmedPassword = ""
}
/*deinit {
combinedSubject.onCompleted()
}*/
func updateUserName(text: String) {
username = text
if username.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([false, true, true])
}
}
func updatePassword(text: String) {
password = text
if password.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, false, true])
}
}
func updateConfirmedPassword(text: String) {
confirmedPassword = text
if confirmedPassword == password {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, true, false])
}
}
}
With this code, the disposed message gets printed when i move back the navigation stack.
However, if I move forward, the disposed message is not printed. What is the proper way to dispose the observable?
When you move forward, the view controller is not removed from the stack. It remains so that when the user taps the back button, it is ready and still in the same state as the last time the user saw it. That is why nothing is disposed.
Also, since you said you are still learning Rx, what you have is not anywhere near best practices. I would expect to see something more like this:
class LoginViewModel {
let areValidFields: Observable<[Bool]>
init(username: Observable<String>, password: Observable<String>, confirm: Observable<String>) {
let usernameValid = username.map { $0.count > 6 }
let passValid = password.map { $0.count > 6 }
let confirmValid = Observable.combineLatest(password, confirm)
.map { $0 == $1 }
areValidFields = Observable.combineLatest([usernameValid, passValid, confirmValid])
}
}
In your view model, prefer to accept inputs in the init function. If you can't do that, for e.g. if some of the inputs don't exist yet, then use a Subject property and bind to it. But in either case, your view model should basically consist only of an init function and some properties for output. The DisposeBag does not go in the view model.
Your view controller only needs to create a view model and connect to it:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext: { validArray in
for (field, valid) in zip(textFieldArray, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
})
.disposed(by: bag)
}
private let bag = DisposeBag()
}
Notice that all of the code ends up in the viewDidLoad function. That's the ideal so you don't have to deal with [weak self]. In this particular case, I would likely put the onNext closure in a curried global function, in which case it would look like this:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext:update(fields: textFieldArray))
.disposed(by: bag)
}
private let bag = DisposeBag()
}
func update(fields: [UITextField]) -> ([Bool]) -> Void {
return { validArray in
for (field, valid) in zip(fields, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
}
}
Notice here that the update(fields:) function is not in the class. That way we aren't capturing self and so don't have to worry about weak self. Also, this update function may very well be useful for other form inputs in the app.
You have added disposable in to the dispose bag of LoginViewModel object, which gets released when LoginViewController object gets released.
This means the disposable returned by LoginViewModel observable won't be disposed until LoginViewController gets released or you receive completed or error on areValidFields Observable.
This is in sync with the accepted behaviour in most of the observable cases.
But, in case if you want to dispose the observable when LoginViewController moves out of screen, you can manually dispose:
var areValidFieldsDisposbale:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
areValidFieldsDisposbale = loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
})
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
areValidFieldsDisposbale?.dispose()
}
I'm working on an app, that should request some data from my server. I'm using Alamofire to do that, and then use SWXMLHash to parse the XML data. There are two View Controllers, on the first one I can write a shipment number, then override function prepareForSegue and send that number to the next View Controller that should display data from server and updateUI on viewDidLoad, but it does not. Where is a problem?
My Class:
class Shipment {
private var _shipmentNumber: String!
private var _shipmentStatus: String!
private var _trackURL: String!
var shipmentNumber: String {
if _shipmentNumber == nil {
_shipmentNumber = ""
}
return _shipmentNumber
}
var shipmentStatus: String {
if _shipmentStatus == nil {
_shipmentStatus = ""
}
return _shipmentStatus
}
init(spNumber: String) {
self._shipmentNumber = spNumber
_trackURL = "..."
}
func requestXmlInformation(completed: DownloadComplete) {
let url = NSURL(string: _trackURL)!
Alamofire.request(.GET, url).responseData { response in
if let xmlToParse = response.data as NSData! {
let xml = SWXMLHash.parse(xmlToParse)
do {
let xmlSpWeight = try xml["fmresultset"]["resultset"]["record"]["field"].withAttr("name", "ТotalWeight")["data"].element!.text! as String
self._shipmentStatus = xmlSpStatus
print(self._shipmentStatus)
} catch let err as NSError {
print(err.debugDescription)
}
}
}
}
}
My Second View Controller
#IBOutlet weak var numberLbl: UILabel!
#IBOutlet weak var weightLbl: UILabel!
#IBOutlet weak var statusLbl: UILabel!
#IBOutlet weak var packageQtyLbl: UILabel!
var shipment: Shipment!
override func viewDidLoad() {
super.viewDidLoad()
shipment.requestXmlInformation { () -> () in
self.updateUi()
print(self.statusLbl.text)
}
}
updateUI function:
func updateUi() {
numberLbl.text = shipment.shipmentNumber
weightLbl.text = shipment.shipmentWeight
statusLbl.text = shipment.shipmentStatus
packageQtyLbl.text = shipment.shipmentPackageQty
}
It prints data in terminal but i think updateUI function does not work.
Make sure that the code in your requestXmlInformation closure is called on the main thread. You shouldn't update the UI in background threads.
shipment.requestXmlInformation { () -> () in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.updateUi()
print(self.statusLbl.text)
})
}
Also, you don't seem to call the complete closure anywhere in your requestXmlInformation method
I'm trying to send off login credentials to a GET request via dataTaskWithURL(), and then receiving a response back as a String. If the response is 401 Unauthorized, then my function attemptLogin() should return false. Otherwise, it'll return true.
At the moment this works, however only once loginButtonPressed(sender: AnyObject) is called twice does anything happen. Why could this be? I tried removing the dispatch_async() as I thought this may be slowing things down however this was not the issue. Here's my code at the moment:
//
// LoginViewController.swift
// Login App
//
// Created by James Allison on 30/11/2015.
// Copyright © 2015 James Allison. All rights reserved.
//
import UIKit
class LoginViewController: UIViewController {
#IBOutlet weak var usernameField: UITextField!
#IBOutlet weak var pinField: UITextField!
#IBAction func loginButtonPressed(sender: AnyObject) {
submitLogin()
}
var loginResponse:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
}
func submitLogin() {
// first validate the fields
if usernameField.text! == "" || Int(pinField.text!) == 0 {
// empty
print("Fill in username & password.")
}
else {
if(attemptLogin(usernameField.text!, pin: pinField.text!)) {
print("YES!")
}
else {
print("NO!")
}
}
}
func attemptLogin(username: String, pin: String) -> Bool {
// construct url
let url = NSURL(string: "http://jamesallison.co/json.php?username=" + username + "&pin=" + pin)!
let task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) in
if let urlContent = data {
// convert to string
let dataString = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
// check if 401 or not
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if dataString == "401 Unauthorized\n" {
//print("incorrect login details")
self.loginResponse = false
}
else {
//print("correct login details!")
self.loginResponse = true
}
})
}
else {
// something failed
print("Error: invalid URL, no response or something.")
}
})
task.resume()
return self.loginResponse
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "loginSuccess") {
// upcoming is set to FirstViewController (.swift)
//let upcoming: FirstViewController = segue.destinationViewController as! FirstViewController
}
}
}