Environment:Xcode-8iOS-10Swift-3
Overview:
I've got what, to me, is a bizarre issue with respect to Outlets, which seem to change the name of their target when being setup and, I believe, is the source of the problems I'm having with geocodeAddressString()
A Bit Of Backstory:
My view has a number of elements, but for the purposes of this posting, I'm primarily concerned about the UITextField's and how I believe they are affecting my MKMapView code (based somewhat on comments I saw here)
My UITextField's are utilizing a slightly modified form of an extension (originally by 'nhgrif') which I found here where the aim is to be able to setup a daisy-chain of textfields such that hitting the Next (or Return) button on the pop-up keyboard will automatically proceed to the desired next (or in some cases, previous) textfield.
private var kAssociationKeyNextField: UInt8 = 0
private var kAssociationKeyPreviousField: UInt8 = 1 // I added this
extension UITextField {
#IBOutlet var nextField: UITextField? {
get { return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField }
set(newField) { objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, .OBJC_ASSOCIATION_RETAIN) }
}
// I added the following
#IBOutlet var previousField: UITextField? {
get { return objc_getAssociatedObject(self, &kAssociationKeyPreviousField) as? UITextField }
set(newField) { objc_setAssociatedObject(self, &kAssociationKeyPreviousField, newField, .OBJC_ASSOCIATION_RETAIN) }
}
}
From the Xcode / Storyboard perspective, this provides the following UI's for setting the next (and/or previous) field in the daisy-chain:
Drilling down
I'm not sure how to really explain the issue I'm seeing other than with a screen-capture video, but since I cannot figure out how to post such here, a bunch of screenshots will have to do...
Start with the Name field, and set the nextField to Address:
Then select the Address field and set the previousField to Name and the nextField to City:
So far, everything seems to be working fine...
Now select the City field and set the previousField to Address and the nextField to State:
Yikes! Note that the name associated with the State field is now "Next Field"
Continue with the State field, setting the previousField to City and nextField to Zipcode:
The State field still shows up as "Next Field" - and now the Zipcode field ALSO shows up as "Next Field"
Finish with the Zipcode field, setting the previousField to State - intentionally leaving the nextField unset:
Some More Code
Here is most of the rest of this particular view class's code
class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate {
#IBOutlet weak var doGeoLocate: UISwitch!
#IBOutlet weak var name: UITextField!
#IBOutlet weak var address: UITextField!
#IBOutlet weak var city: UITextField!
#IBOutlet weak var state: UITextField!
#IBOutlet weak var zipcode: UITextField!
#IBOutlet weak var done: UIBarButtonItem!
#IBOutlet weak var map: MKMapView!
var coords: CLLocationCoordinate2D?
var locationManager: CLLocationManager = CLLocationManager()
var currentLocation: CLLocation!
override func viewDidLoad() {
super.viewDidLoad()
name.delegate = self
address.delegate = self
city.delegate = self
state.delegate = self
zipcode.delegate = self
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
currentLocation = nil
doGeoLocate.isOn = false
map.isHidden = true
done.isEnabled = false
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if doGeoLocate.isOn == true {
textField.resignFirstResponder()
}
else if textField.nextField == nil {
if (!checkFields()) {
// walk back up chain to find last non-filled text-field...
var tmpField = textField
while ((tmpField.previousField != nil) && (tmpField.previousField?.hasText)!) {
tmpField = tmpField.previousField!
}
tmpField.previousField?.becomeFirstResponder()
}
else {
textField.resignFirstResponder()
}
}
else {
textField.nextField?.becomeFirstResponder()
}
return checkFields()
}
func checkFields() -> Bool {
//... if doGeoLocate switch is on - return true
//... if ALL fields are populated, call geocodeAddress() and return true
//... otherwise return false
}
func geocodeAddress() {
print("GA") //#=#
let geoCoder = CLGeocoder()
let addr = "\(address.text) \(city.text) \(state.text) \(zipcode.text)"
print("ADDR: `\(addr)'")//#=#
geoCoder.geocodeAddressString(addr, completionHandler: {
(placemarks: [CLPlacemark]?, error: NSError?) -> Void in
print("IN geocodeAddressString")//#=#
//if error.localizedDescription.isEmpty == false {
// print("Geocode failed with error: \(error.localizedDescription)")
//}
//else if placemarks!.count > 0 {
let placemark = placemarks![0]
let location = placemark.location
self.coords = location!.coordinate
self.map.isHidden = false
//}
} as! CLGeocodeCompletionHandler) //<<<=== NOTE THIS LINE
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//...
}
#IBAction func toggleGeoLocate(_ sender: AnyObject) {
//...
}
#IBAction func useNewLocation(_ sender: AnyObject) {
//...
}
}
Upon running the app, filling in all the fields, when I click on the 'Done' button in the number-keypad associated with the Zipcode field - I get an exception. The debugging log looks like this:
TFSR: (TFSR Optional("Name") => Optional("Address"))
Returning false
TFSR: (TFSR Optional("Address") => Optional("City"))
Returning false
TFSR: (TFSR Optional("City") => Optional("State"))
Returning false
TFSR: (TFSR Optional("State") => Optional("Zipcode"))
Returning false
GA
ADDR: `Optional("2112 Murray Avenue ") Optional("Pittsburgh ") Optional("PA") Optional("15217")'
(lldb)
The exception shows up as:
func geocodeAddress() {
//...
geoCoder.geocodeAddressString(addr, completionHandler: {
(placemarks: [CLPlacemark]?, error: NSError?) -> Void in
//...
} as! CLGeocodeCompletionHandler) //<<< Thread 1: EXC_BREAKPOINT (code=1, subcode=0x10006c518)
}
And yes, I verified that I have no breakpoints set in the code
SummationI'm reasonably sure that the geocodeAddressString() code is correct (I used it in another app for Swift-2), but I'm very suspicious of the way the State and Zipcode Outlets get renamed when I attempt to chain them with the other fields.Anyone have any ideas?
I'd suggest getting rid of those various casts:
func geocodeAddress() {
//...
geoCoder.geocodeAddressString(addr) { placemarks, error in
//...
}
}
It's easiest to let it infer the correct types for you.
Regarding the naming of outlets, IB is trying to make life easier for you, naming them for you in the absence of any specified name. But with these additional outlets, its default algorithm is falling short. You can probably get around this by naming the outlets yourself in the "Document" section of the "Identity Inspector".
Related
I'm having the issue with the textfields being empty and once I hit the button it continues to the next viewcontroller. I want to disable the button until all textfields are complete.
#IBAction func buttontapped(_ sender: Any, forEvent event: UIEvent) {
let loginFunc = Login()
loginFunc.login(First_Nm: First_Nm.text!, Pw: Pw.text!, Last_Name: Last_Name.text!, Email: Email.text!) { jsonString in
let response = jsonString
print(response)
if response.range(of: "failure") == nil {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "goToHomeVC", sender: nil)
}
}
}
Add a check for the UITextFields to see if the text's isEmpty property is true before you proceed to performSegue.
#IBAction func buttontapped(_ sender: Any, forEvent event: UIEvent) {
if First_Nm.text!.isEmpty || Last_Name.text!.isEmpty || Email.text!.isEmpty {
print("Incomplete, show an alert for user's attention!")
return
}
//...
}
Add-on: Also, follow a standard naming convention for your properties. Eg: Instead of First_Nm use firstNameTextField.
If you want to achieve the disable and enable on the button based on textField input you can implement UITextFieldDelegate - textFieldDidChange
The code would look like this:
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var firstNameTextField: UITextField!
#IBOutlet weak var lastNameTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var emailTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
firstNameTextField.delegate = self
lastNameTextField.delegate = self
passwordTextField.delegate = self
emailTextField.delegate = self
}
And on the bottom of the ViewController file you can create extension for UITextFieldDelegate
extension YourViewController: UITextFieldDelegate {
func textFieldDidChangeSelection(_ textField: UITextField) {
checkButtonState()
}
func checkButtonState() {
var validFirstName = false
var validLastName = false
var validPassword = false
var validEmail = false
if firstNameTextField.text != "" {
validFirstName = true
}
if lastNameTextField.text != "" {
validLastName = true
}
if passwordTextField.text != "" {
validPassword = true
}
if emailTextField.text != "" {
validEmail = true
}
if validFirstName && validLastName && validPassword && validEmail {
loginButton.isEnabled = true
} else {
loginButton.isEnabled = false
}
}
}
Then I would also recommend another check when the button is tapped similar to above answer however I think it could be a more extensive check on email and password. (Login might not be as important but during sign up you might want to validate password and email further than not empty)
You can make up your own rules but usually its something along the lines of
Validate password:
• Required 6-8 characters
if string.count < 6 {
print("Password must be more than 6 characters")
}
• Maybe require one capital letter or one number
Validate email:
• Should contain "#"
if !string.contains("#") {
print("probably not a valid email")
}
You can check the strings in the textfield to see if they meet the criteria if not return a login or sign up error to the user. Hopefully this gets you going in the right direction
I want to call the label from ViewController and set it at different points in the code for example there are 2 different functions and both of them will set the label to lets say "hi" and 2nd function will set it to "hello"
I used Swift3 protocol way just like this: Access label.text from separate class in swift
I have another init in the class so I'm not sure why it would not set the label text to new values but is nil.
I hit the connect button first.
Here is my code:
protocol HiResponder: class {
func hi()
func hello()
}
class ViewController: UIViewController, HiResponder {
#IBOutlet weak var status: UILabel!
var test: Test!
override func viewDidLoad() {
super.viewDidLoad()
test.responder = self //gives me nil fatal error: unexpectedly found nil while unwrapping an Optional value
}
#IBAction func connectBtn(_ sender: Any) {
self.test = Test(
p1: "hey",
p2: "there"
)
self.test.connect(p1: "hey", p2: "there")
}
func hi() {
status.text = "hi"
}
hello() {
status.text = "hello"
}
}
This is the class that sets the value of these responders:
class Test {
var test:TestQQ?
var p1:String?
var p2: String?
weak var responder: HiResponder?
init(p1: String, p2:String) {
self.p1 = p1
self.p2 = p2
}
init(responder: TestResponder) {
self.responder = responder
}
// Connect
func connect(p1:String, p2:String) {
//code
}
func setHello(){
responder?.hello()
}
func setHi(){
responder?.hi()
}
}
I tried to generalize my code but thats the idea. My connect function is being called in my viewDidload of viewController.
You didn't instantiate test. Since viewdidload will run first it is of course nil. Therefore, you cannot acces anything from it. What you need to do is find a spot to instantiate it first before you use it. Maybe in connectBTN you can set the responder after you create it instead lf in viewdidload
The solution to deal with the unwrapped nil Optional value is to set test as an Optional Test object.
There is another problem though. You never actually use either of the functions from the protocol. hi() or hello()
Below I used both functions from the protocol but not from another class. In fact I didn't use Test at all. Not sure what your goal was with using a different class to set the Label text.
protocol HiResponder: class {
func hi()
func hello()
}
class ViewController: UIViewController, HiResponder {
#IBOutlet weak var status: UILabel!
var num = 1
var test: Test?
override func viewDidLoad() {
super.viewDidLoad()
test?.responder = self //gives me nil fatal error: unexpectedly found nil while unwrapping an Optional value
}
#IBAction func connectBtn(_ sender: Any) {
if num % 2 == 0 {
hi()
} else {
hello()
}
num += 1
}
func hi() {
status.text = "hi"
}
func hello() {
status.text = "hello"
}
}
class Test {
var test: Test?
var p1: String?
var p2: String?
weak var responder: HiResponder?
init(p1: String, p2:String) {
self.p1 = p1
self.p2 = p2
}
init(responder: HiResponder) {
self.responder = responder
}
// Connect
func connect(p1:String, p2:String) {
//code
}
func setHello(){
responder?.hello()
}
func setHi(){
responder?.hi()
}
}
I've a simple ViewController that displays my current location coordinates.
Everything is working properly, but when I dismiss the ViewController, the app crashes without any specific error log.
The class code goes like this:
import UIKit
class LocationViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate, UIPopoverPresentationControllerDelegate {
// General objects
#IBOutlet var closeButton: UIButton!
#IBOutlet var latitudeLabel: UILabel!
#IBOutlet var longitudeLabel: UILabel!
#IBOutlet var infoButton: UIButton!
// Global variables
var location: CLLocationManager? = CLLocationManager()
var geocoder = CLGeocoder();
var placemark = CLPlacemark();
var hasPin: Bool = false;
override func viewDidLoad() {
super.viewDidLoad()
// Ask for Authorisation from the User.
location?.requestAlwaysAuthorization();
// For use in foreground
location?.requestWhenInUseAuthorization();
getCurrentLocation();
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func closeButton(_ sender: AnyObject) {
self.dismiss(animated: true, completion: {
print("dismissing locationViewController");
self.location = nil;
});
}
#IBAction func infoButton(_ sender: AnyObject) {
// TODO
}
// MARK: - General functions
func getCurrentLocation() -> Void {
if (CLLocationManager.locationServicesEnabled()) {
location?.delegate = self;
location?.desiredAccuracy = kCLLocationAccuracyBest;
location?.startUpdatingLocation();
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("ERROR = \(error)");
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Gets the user coordinates
let locValue:CLLocationCoordinate2D = manager.location!.coordinate;
USER_LATITUDE = locValue.latitude;
USER_LONGITUDE = locValue.longitude;
longitudeLabel.text = "\(USER_LONGITUDE)";
latitudeLabel.text = "\(USER_LATITUDE)";
location?.stopUpdatingLocation()
}
Does anyone have any clue why this happens?
No error log is prompted that's what makes me even more confused.
First I thought I had to set the location variable to be optional and then set it to nil when I dismiss the VC but the crash is still happening.
Crashlytics says that the App crashes inside the LocationViewController line 0 , which is in fact weird.
I call this ViewController, from a button click inside another VC like this:
#IBAction func locationButton(_ sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil);
let viewController = storyboard.instantiateViewController(withIdentifier: "locationVC");
self.present(viewController, animated: true, completion: nil);
}
I'm using Swift3 with the latest Xcode Beta Version on iOS 10.
Thanks
Replace this:
var location: CLLocationManager? = CLLocationManager()
With this:
let location = CLLocationManager()
Change all code as necessary (this is no longer an Optional so there is nothing to unwrap) and delete the line that tries to set it to nil.
If you are worried that the location manager might be trying to get your location when you dismiss, then implement viewWillDisappear and tell it to stop updating.
You need to add the privacy entry in Info.plist and also request authorization to use location services. A good overview can be found here: http://nevan.net/2014/09/core-location-manager-changes-in-ios-8/
I try to access some IBOutlet outside of Viewdidload and IBAction, and always get nil value. In Viewdidload and IBAction, those value are ok. Did i miss a part to declare or initialize something ?
The value are modified after viewdidload() because viewdidload is called bu the IBAction.
The View is created in storyboard, coming from a UINavigation Controller.
connection table between ViewController and UIView:
The loginServer method is called by userCredential delegate, as below:
protocol userCredentialDelegate {
func didUpdateCredential (sender:String, credential: Bool?)
}
class userCredential: NSObject {
var delegate:userCredentialDelegate?
// self.delegate = ViewController() removed
func loginServer (name: String, pwd: String) -> Bool {
dispatch_sync(dispatch_get_main_queue())
{
self.delegate?.didUpdateCredential ("login", credential: credentialStatus)
}
}
Main controller:
class ViewController: UIViewController, userCredentialDelegate {
// set the shared instance
let user = userCredential.sharedInstance
#IBOutlet weak var incorrectCredentials: UITextField!
#IBOutlet weak var username: UITextField!
#IBOutlet weak var password: UITextField!
#IBOutlet weak var logButton: UIButton!
#IBAction func logButton(sender: UIButton) {
print (incorrectCredentials?.hidden)
if logButton.titleLabel!.text == "Log Out" {
user.logoutServer ()
} else {
user.loginServer(username.text!, pwd: password.text!)
}
}
func didUpdateCredential (sender: String, credential: Bool?) {
switch sender {
case "login":
if credential! {
performSegueWithIdentifier("loginSegue", sender: self)
} else {
incorrectCredentials?.hidden = false
}
default: break
}
if let credentialResponse = credential {
loginStatus = credentialResponse
}
}
var loginStatus: Bool = false {
didSet {
if loginStatus {
incorrectCredentials?.hidden = true // always nil before, now ok
} else {
incorrectCredentials?.hidden = false // always nil before, now ok
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
user.delegate = self
incorrectCredentials.hidden = true // can work here
user.getUserInfo ()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
connection table:
You need to set the delegate to your user model in your viewDidLoad function.
Before doing user.getUserInfo() make user.delegate = self
currently you create a new Instance on the user model, that has nothing todo with you real loaded ViewController.
I am trying to use Parse to edit profile and after I put the code in when I launch the app I clicked the button I made to edit profile and I get this:
fatal error: unexpectedly found nil while unwrapping an Optional value
The Segue I have leading to the edit profile controller does not open and the app crashes. When the Parse code is not implemented the segue to the view controller opens just fine.
import UIKit
import Parse
class EditProfileViewController: UIViewController {
#IBOutlet weak var profilePictureImageView: UIImageView!
#IBOutlet weak var firstNameTextField: UITextField!
#IBOutlet weak var lastNameTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var repeatPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Load user details
let userFirstName = PFUser.currentUser()?.objectForKey("first_name") as! String
let userLastName = PFUser.currentUser()?.objectForKey("last_name") as!String
firstNameTextField.text = userFirstName
lastNameTextField.text = userLastName
if(PFUser.currentUser()?.objectForKey("profile_picture") != nil)
{
let userImageFile:PFFile = PFUser.currentUser()?.objectForKey("profile_picture") as! PFFile
userImageFile.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) -> Void in
self.profilePictureImageView.image = UIImage(data: imageData!)
})
}
let image = UIImage(named: "navbar.png")
self.navigationController!.navigationBar.setBackgroundImage(image,forBarMetrics: .Default)
var nav = self.navigationController?.navigationBar
nav?.tintColor = UIColor.whiteColor()
let titleDict: NSDictionary = [NSForegroundColorAttributeName: UIColor.whiteColor()]; self.navigationController!.navigationBar.titleTextAttributes = titleDict as [NSObject : AnyObject]
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
self.view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func doneButtonTapped(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func chooseProfileButtonTapped(sender: AnyObject) {
}
#IBAction func saveButtonTapped(sender: AnyObject) {
}
}
You need to find out which line exactly throws the error. Basically, this error means that you try to access a variable with optional value, but it turns out the variable is nil!
Why don't you set some break points and see if any of your variables (esp. the ones related to Parse) return nil?
EDIT (just a shot in the dark)
From what I can see in your code, it could be that you have not correctly linked the textfields to your interface builder file. Thus, since you are not initializing them before accessing them, they will return nil and the app will crash here:
firstNameTextField.text = userFirstName
lastNameTextField.text = userLastName
Make sure the textfields are linked to your interface builder file, or, if you are unsure about how to do it, just check if this is indeed the case and insert these two lines before the above ones:
//Initialize them before accessing them
UITextField* firstNameTextField = [[UITextField alloc] init];
UITextField* lastNameTextField = [[UITextField alloc] init];
//Now you can securely access them
firstNameTextField.text = userFirstName
lastNameTextField.text = userLastName
In case the app now doesn't crash anymore, you know it's been these textfields and you need to properly link them to your xib file