Search code examples
iosswiftswift3ios10

outlets renaming themselves? breaking geocodeAddressString()?


Environment:

  • Xcode-8
  • iOS-10
  • Swift-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

    Summation

    I'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?


Solution

  • 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".