0

I have Thing and ThingManager

If no one mentions a specific Thing for a while, I want ThingManager to forget about it.

let manager = ThingManager()
let thing1 = Thing(name: "thing1")
manager.addThing(thing1)
manager.sawSomeThing(named: thing1.name)
print("Manager has this many things: ", manager.things.count)
Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false, block: { (timer) in
    // By now, the manager should have forgotten about the thing
    print("Manager has this many things: ", manager.things.count)
})

I have tried both block-based timers and RunLoop-based timers. They don't ever seem to "go off"

struct Thing {
    var name: String
}

class ThingManager {
    var things: [String: Thing] = [:]
    fileprivate var thingWatchingRunLoop = RunLoop()
    fileprivate var thingWatchingQueue = DispatchQueue.global(qos: .utility)
    fileprivate var thingWatchingTimers: [String: Timer] = [:]

    func addThing(_ thing: Thing) {
        self.things[thing.name] = thing
    }

    func sawSomeThing(named name: String) {
        self.thingWatchingQueue.async {
            // re-up the timer so we don't forget about that thing
            if let timer = self.thingWatchingTimers[name] {
                timer.invalidate()
            }
            let timer = Timer(timeInterval: 5.0, target: self, selector: #selector(self.timerWentOff(_:)), userInfo: ["name":name], repeats: false)
            self.thingWatchingRunLoop.add(timer, forMode: .commonModes)
            self.thingWatchingTimers[name] = timer
        }
    }

    @objc func timerWentOff(_ timer: Timer) {
        let info = timer.userInfo as! [String: String]
        let name = info["name"]
        self.removeThing(named: name!)
    }

    func removeThing(named name: String) {
        self.things.removeValue(forKey: name)
    }
}

Update, block-based version: https://gist.github.com/lacyrhoades/f917b971e97fdecf9607669501ef6512

snakeoil
  • 497
  • 4
  • 12

2 Answers2

1

I believe you just need to add the timer to the current runloop instead of creating a new Runloop instance.

Change:

fileprivate var thingWatchingRunLoop = RunLoop()

to:

fileprivate var thingWatchingRunLoop = RunLoop.current

and everything should be working properly!

Joel Bell
  • 2,718
  • 3
  • 26
  • 32
  • Thanks! `.current` works well in the instantiation, but then not if you ask on demand, inside the dispatch queue. `.main` seems to always work regardless of context, so I went with that. Not sure I fully grok the relationship of queues to run loops or vise versa. – snakeoil Nov 11 '16 at 14:24
  • Update: Maybe there's no need for a dispatch queue really. Was worried about things getting out of order but eh. Also, docs: "The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an NSRunLoop ..." – snakeoil Nov 11 '16 at 15:25
  • Very true I didn't notice that before. With the example you gave above order shouldn't be a problem. You're only referencing the thing by the "name" key passed through user info. The dispatch queue just seems like unnecessary complexity. – Joel Bell Nov 11 '16 at 15:49
  • Also, maybe the scope of the problem is bigger than the code posted here, but its not very clear how you would even solve an ordering problem using dispatch queues like this. – Joel Bell Nov 11 '16 at 15:55
1

Rather that one timer per Thing I would assign a staleness interval to each Thing in ThingManager and have a single timer:

struct Thing {
    var name: String
}

extension Thing: Equatable {
    static func ==(lhs: Thing, rhs: Thing) -> Bool {
        return lhs.name == rhs.name
    }
}

extension Thing: Hashable {
    var hashValue: Int {
        return name.hashValue
    }
}

class ThingManager {
    let stalenessInterval: TimeInterval = -5
    var things = [String: Thing]()
    var staleness = [Thing: Date]()
    fileprivate var pruningTimer: DispatchSourceTimer!

    init() {
        pruningTimer = DispatchSource.makeTimerSource(queue: .main)
        pruningTimer.scheduleRepeating(deadline: DispatchTime.now(), interval: DispatchTimeInterval.milliseconds(500))
        pruningTimer.setEventHandler() {
            for (name, thing) in self.things {
                if let date = self.staleness[thing], date.timeIntervalSinceNow < self.stalenessInterval {
                    self.removeThing(named: name)
                }
            }
        }
        pruningTimer.resume()
    }

    func addThing(_ thing: Thing) {
        things[thing.name] = thing
    }

    func sawSomeThing(named name: String) {
        if let thing = things[name] {
            staleness[thing] = Date()
        }
    }

    func removeThing(named name: String) {
        if let removedThing = things.removeValue(forKey: name) {
            staleness.removeValue(forKey: removedThing)
        }
    }
}
sbooth
  • 16,646
  • 2
  • 55
  • 81
  • I like the idea of one timer. I will use that. – Concerning `resume()` though, that seems to not apply to a regular old "scheduled" timer (from a class method like `Timer.scheduledTimer...`) or a timer I add manually to the main/current run loop by hand, via `runLoop.add()` What gives? – snakeoil Nov 11 '16 at 04:10
  • You're right, `resume()` is for dispatch sources. I removed that part of the answer. Adding a `Timer` to a run loop worked in my testing; have you tried the main run loop instead of a custom one? – sbooth Nov 11 '16 at 11:10
  • Very interesting! I went with using the `RunLoop.main` loop as a quick answer that works, but I want to convert to your single timer answer later today. The question was a bit long winded but I'm glad you contributed a more broad philosophical answer. – snakeoil Nov 11 '16 at 14:07