0

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>
      }
    }
  }

3 Answers 3

1

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>
}
Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.