Adam Stoller Adam Stoller - 2 months ago 20
iOS Question

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?

Rob Rob
Answer

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.