Do I need capture self using thread class? - swift

I have this code:
myThreadTemp = Thread(target: self, selector: #selector(threadMain), object: nil)
#objc func threadMain(data: AnyObject) {
let runloop = RunLoop.current
runloop.add(NSMachPort(), forMode: RunLoopMode.defaultRunLoopMode)
while !Thread.current.isCancelled{
//foreground
DispatchQueue.main.async {[weak self] in
self?.somemethod()
self?.somevar = 1
print("tick")
}
if Thread.current.isCancelled {
}
Thread.sleep(forTimeInterval: 1.0)
}
runloop.run(mode: RunLoopMode.defaultRunLoopMode, before: NSDate.distantFuture)
}
or I can just do this:
DispatchQueue.main.async {
self.somemethod()
self.somevar = 1
print("tick")
}
I saw this:
Shall we always use [unowned self] inside closure in Swift
But was if #objc func is used?

The 1st example looks to spin the runloop indefinitely, waiting 1s between ticks, whereas the 2nd example will execute once, on the very next run loop iteration. There is no memory management issue in terms of capturing self in the 2nd case, indeed because it is only executed once and the block is released after it (breaking the momentary retain loop that does exist between self and the block).
Assuming you are trying to tick every 1s (as I am guessing based on your questions), there is a better way to do what you are trying to do, using a timer:
// Must be executed on main thread, or other NSRunLoop enabled thread,
// or the below code will silently do nothing.
self.timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.someMethod()
self?.someVar = 1
print("tick")
}
// Somewhere in the future, to stop the timer:
// self.timer.invalidate()
As you can see in the above example, with the timer case you might indeed want to refer to self with either an unowned or weak reference (as the timer block will otherwise make a strong reference to self, and self to the timer). The block should be released on invalidating the timer too, so even in this case the weak reference is not 100% necessary I guess.

Related

Async data loading swift

I got a function such as scrollViewDidScroll that can trigger many times. And I need to call function loadMoreDataFromRemoteServerIfNeed only single time. How could I do this more elegantly without using any "flag" variables. Maybe I should use DispathGroup|DispatchWorkItem?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
loadMoreDataFromRemoteServerIfNeed()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
DispatchQueue.global(qos: .background).async {
sleep(2)
DispatchQueue.main.async {
// <Insert New Data>
}
}
}
The thing that you are trying to describe — "Do this, but only if you are not told to do it again any time in the next 2 seconds" — has a name. It's called debouncing. This is a well-solved problem in iOS programming, so now that you know its name, you can do a search and find some of the solutions.
While I'm here telling you about this, here's a solution you might not know about. Debouncing is now built in to iOS functionality! Starting in iOS 13, it's part of the Combine framework. I'm now using Combine all over the place: instead of notifications, instead of GCD, instead of Timer objects, etc. It's great!
Here's a Combine-based solution to this type of problem. Instead of a scroll view, suppose we have a button hooked up to an action handler, and we don't want the action handler to do its task unless 2 seconds has elapsed since the last time the user tapped the button:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
#IBAction func doButton(_ sender: Any) {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.doSomething() }
}
self.pipelineStart.send()
}
func doSomething() {
print("I did it!")
}
I'm sure you can readily see how to adapt that to your own use case:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.loadMoreDataFromRemoteServerIfNeed()
}
self.pipelineStart.send()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
// <Insert New Data>
}
You can create a flag from DispatchWorkItem to observe loading state e.g.:
var item: DispatchWorkItem?
func loadMoreDataFromRemoteServerIfNeed() {
assert(Thread.isMainThread)
guard item == nil else { return }
item = DispatchWorkItem {
print("loading items")
Thread.sleep(forTimeInterval: 2)
DispatchQueue.main.async {
item = nil
print("insert items")
}
}
DispatchQueue.global().async(execute: item!)
}
NOTE: to synchronise item var you must change its value on the same thread for instance the main thread.
Yes, you could use DispatchWorkItem, keep a reference to the old one, and cancel prior one if necessary. If you were going to do that, I might consider Operation, too, as that handles cancelation even more gracefully and has other advantages.
But that having been said, given that the work that you are dispatching is immediately sleeping for two seconds, this might suggest a completely different pattern, namely a Timer. You can schedule your timer, invalidating previously scheduled timers, if any:
weak var timer: Timer?
func loadMoreDataFromRemoteServerIfNeed() {
// cancel old timer if any
timer?.invalidate()
// schedule what you want to do in 2 seconds
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
// <Insert New Data>
}
}
FWIW, if you ever find yourself sleeping, you should general consider either timers or asyncAfter. This avoids tying up the global queue’s worker thread. Sleeping is an inefficient pattern.
In this case, keeping a weak reference to the prior timer (if any) is probably the best pattern.

Dispatch Source timer schedule timout

With the following Swift code, I'm trying to create a task that runs every hour:
let queue: DispatchQueue = .main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(3600), leeway: .milliseconds(100)
timer.setEventHandler { [weak self] in
// run code
}
Now, when I have the repeating set at a lower number, say 10 or event 150 seconds, it triggers as expected both in the foreground and background (or, rather, once the foreground hits it triggers, if the timer went off while in the background).
However, when I let the app timeout to the lock screen, and wait for an hour, it doesn't display.
Is there some timeout that Apple has for DispatchSource schedules? If so, what is it? And is there any way to change or get around it?
Edit
I don't want special functionality when it backgrounds, I want the code to keep running as normal and to trigger the event handler when the timeout happens, even if it's in the background
I ended up taking matt's suggestion and saving the time every time the code gets called, as seen below. Worked well!
let timeOfLastCheck = Date()
let queue: DispatchQueue = .main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(3600), leeway: .milliseconds(100)
timer.setEventHandler { [weak self] in
timeOfLastCheck = Date()
// run code
}
And elsewhere, where the timer is actually getting created:
let notificationCenter: NotificationCenter = .default
let activeNotificationToken = notificationCenter.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: nil
) { [weak self] _ in
let now = Date()
if let `self` = self,
let timeInterval = TimeInterval(dispatchTimeInterval: self.interval), // TimeInterval is extended elsewhere to be able to take in a DispatchTimeInterval in the init
now > timeOfLastCheck.addingTimeInterval(timeInterval) {
self.timeOfLastCheck = Date()
// run code
}
}

Why does timer continue to execute after invalidation?

If you run the code below, even after I invalidate the timer, the remaining code of the timer executes without any disruption. Why?
Is it because the closure has a strong reference to itself and is retained until it completely finishes itself? Or something else?
Does this mean invalidating a timer during its moment of execution does nothing?
class ViewController: UIViewController {
var timer : Timer?
let serialQueue = DispatchQueue(label: "com.createTimer.serial")
override func viewDidLoad() {
super.viewDidLoad()
serialQueue.sync { [weak self] in
self?.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false, block: { [weak self] _ in
self?.someDummyFunc()
print("yyy")
print("\(self?.timer?.isValid)")
})
}
}
func someDummyFunc(){
print("\(timer?.isValid)")
print("xxx")
timer?.invalidate()
}
}
The prints that I get from running this code is:
Optional(true)
xxx
yyy
Optional(false) // timer.isValid is false !!!
Yet what I initially thought I would get is:
Optional(true)
xxx
The scheduledTimer(withTimeInterval:repeats:block:) method:
After interval seconds have elapsed, the timer fires, executing block.
The invalidate() method:
Stops the timer from ever firing again
You are correct in your discovery that invalidating a timer will not interrupt a currently executing block, but will only prevent future executions of that block.
Timer or not, any enqueued block will have to finish. Once it's enqueued there's no stopping it.
Suppose you had the following code inside a viewController's:
override func viewDidLoad() {
super.viewDidLoad()
let x = 10
DispatchQueue.main.async { [weak self] in
print(x)
self?.someTaskWhichTakes3seconds()
print(self?.view.backgroundColor)
print(x + 2)
}
}
and after viewDidLoad was called you immediately popped the viewController off the navigation stack (or had it deallocated somehow), in that case, still print(x+2) would happen. Why? Because the block is enqueued and it has to finish.

Scheduled Timer won't fire

I am trying out Swift as a language for CLI tool, which is supposed to serve as a simple web crawler.
In my main file I create an instance of APIFetcher class. In the initialiser of APIFetcher I instantiate an instance of Timer with the given time interval. Once I call startQuerying method, it adds Timer to the main run loop - at this point I would expect performTask method would be invoked, but it isn't. What am I doing wrong?
#available(OSX 10.12, *)
public init(with interval: TimeInterval) {
self.timer = Timer(timeInterval: interval, repeats: true) { _ in
self.performTask()
}
}
deinit {
self.timer?.invalidate()
self.timer = nil
}
public func startQuerying(_ url: URL) {
guard let unwrappedTimer = self.timer else { return }
RunLoop.main.add(unwrappedTimer, forMode: .defaultRunLoopMode)
}
func performTask() {
print("Performed scheduled task")
}
Thanks vadian you are right, I added the timer to run loop, but never actually started it. This fixes the whole issue:
RunLoop.main.add(unwrappedTimer, forMode: .defaultRunLoopMode)
RunLoop.main.run()
Also, see When Would You Use a Run Loop? documentation

How to pass a function as parameter avoiding retain cycles?

I have a view controller where I am trying to call Timer.scheduledTimer(withTimeInterval:repeats:block) by passing a function as block parameter, instead of creating a block on the fly. I have this view controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(withTimeInterval: 5,
repeats: true,
block: onTimer)
}
deinit {
print("deinit \(self)")
}
func onTimer(_ timer: Timer) {
print("Timer did fire")
}
}
The call is retaining the view controller, so the controller is never deallocated.
I know I can make it work as I want by replacing the call with:
Timer.scheduledTimer(withTimeInterval: 5,
repeats: true) { [weak self] timer in
self?.onTimer(timer)
}
But I'd like to know if there is a way to send the onTimer method directly and avoid the retain cycle.
Thanks.
You should call the invalidate() method:
This method is the only way to remove a timer from an RunLoop object.
The RunLoop object removes its strong reference to the timer, either
just before the invalidate() method returns or at some later point.
If it was configured with target and user info objects, the receiver
removes its strong references to those objects as well.
Somewhere in your code, you should implement:
timer.invalidate()
Hope this helped.