Search code examples
swiftrecursionclosuresstack-overflow

Swift recursive closure stack overflow bug


Update: It should be noted that the question below is of academic nature and the use of core location, or the polling of location data is not relevant to the question - the proper way to do this is always through the core location delegate method. My original question eventually boiled down to: "Is infinite recursion ever possible in swift? (or tail recursion)". The answer to this is no. This is what caused my error due to stack space exhaustion.

Original question: I'm having an issue with a recursive function that passes values through a closure. I'm a longtime Objective-C developer but have not been programming in Swift long, so maybe I'm missing something obvious. Here is the function and how I'm calling it:

public func getLocation(completion: @escaping (CLLocationCoordinate2D?) -> ())
{
    completion(self.currentLocation)
    weak var weakSelf = self
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        weakSelf?.getLocation(completion: {location in
            completion(weakSelf?.currentLocation)
        })
    }
}

LocationManager.shared.getLocation(completion: {location in
        if(location != nil)
        {
            weakSelf?.lonLabel.text = "Lat: " + location!.latitude.description
            weakSelf?.latLabel.text = "Lon: " + location!.longitude.description
        }
    })

The bug (description Thread 1: EXC_BAD_ACCESS (code=2, address=0x16f1b7fd0)) I'm getting after running for a period of time is this:

enter image description here

What I'm trying to accomplish is pass an auto-updating location value to a location manager object. I'm thinking another way to accomplish is with performSelector withObject afterDelay but at this point, I'm just wondering why this is crashing?


Solution

  • You're causing a stack overflow, by having getLocation(completion:) call getLocation(completion:), which calls getLocation(completion:), ... until you run out of stack space.

    You could use a DispatchSourceTimer instead:

    import Dispatch
    
    let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
    timer.schedule(deadline: .now(), repeating: .seconds(5), leeway: .milliseconds(100))
    
    timer.setEventHandler { [weak self] in
        guard let strongSelf = self,
              let location = LocationManager.shared.currentLocation else { return }
        strongSelf.lonLabel.text = "Lat: \(location.latitude)"
        strongSelf.latLabel.text = "Lon: \(location.longitude)"
    }
    
    timer.resume()
    

    But this whole polling approach just doesn't make any sense. Your location delegate is already being informed of location changes. You can just change your labels then.