What is the iOS (or RubyMotion) idiom for waiting

2019-02-11 03:26发布

I have been pulling my hair out for weeks on this niggling problem, and I just can't find any info or tips on how or what to do, so I'm hoping someone here on the RubyMotion forums can help me out.

Apologies in advance if this is a little long, but it requires some setup to properly explain the issues. As background, I've got an app that uses a JSON/REST back-end implemented ina Rails app. This is pretty straightforward stuff. The back-end is working fine, and up to a point, so is the front end. I can make calls to populate model objects in the RubyMotion client and everything is great.

The one issue is that all of the http/json libs use async calls when processing requests. This is fine, and I understand why they are doing it, but there are a couple of situations where I need to be able to wait on a call because I need to do something with the returned results before proceeding to the next step.

Consider the example where a user wants to make a payment, and they have some saved payment information. Prior to presenting the user with a list of payment options, I want to make sure that I have the latest list at the client. So I need to make a request to a method on the user object that will grab the current list (or timeout). But I don't want to continue until I am sure that the list is either current, or the call to the back-end has failed. Basically, I want this call to block (without blocking the UI) until the results are returned.

Alternatives such as polling for changes or pushing changes from the back-end to the front are not appropriate for this scenario. I also considered simply pulling the data from the destination form (rather than pushing it into the form) but that doesn't work in this particular scenario because I want to do different things depending on whether the user has zero, one or multiple payment options saved. So I need to know in advance of pushing to the next controller, and to know in advance, I need to make a synchronous call.

My first attack was to create a shared instance (let's call it the SyncHelper) that I can use to store the returned result of the request, along with the "finish" state. It can provide a wait method that just spins using CFRunLoopRunInMode either until the request is finished, or until the request times out.

SyncHelper looks a bit like this (I've edited it to take some irrelevant stuff out):

class SyncHelper
  attr_accessor :finished, :result, :error
  def initialize()
    reset
  end
  def reset
    @finished = false
    @result   = nil
    @error    = nil
  end
  def finished?
    @finished
  end
  def finish
    @finished = true
  end
  def finish_with_result(r)
    @result   = r
    @finished = true
  end
  def error?
    !@error.nil?
  end
  def wait
    timeout = 0.0
    while !self.finished? && timeout < API_TIMEOUT
      CFRunLoopRunInMode(KCFRunLoopDefaultMode, API_TIMEOUT_TICK, false)
      timeout = timeout + API_TIMEOUT_TICK
    end
    if timeout >= API_TIMEOUT && !self.finished?
      @error = "error: timed out waiting for API: #{@error}" if !error?
    end
  end
end

Then I have a helper method like this, which would allow to me to make any call synchronous via the provision of the syncr instance to the invoked method.

def ApiHelper.make_sync(&block)
  syncr = ApiHelper::SyncHelper.new
  BubbleWrap::Reactor.schedule do
    block.call syncr
  end
  syncr.wait
  syncr.result
end

What I had hoped to do was use the async versions everywhere, but in the small number of cases where I needed to do something synchronously, I would simply wrap the call around a make_sync block like this:

# This happens async and I don't care
user.async_call(...)
result = ApiHelper.make_sync do |syncr|
  # This one is async by default, but I need to wait for completion
  user.other_async_call(...) do |result|
    syncr.finish_with_result(result)
  end
end
# Do something with result (after checking for errors, etc)
result.do_something(...)

Importantly, I want to be able to get the return value from the 'synchronised' call back into the invoking context, hence the 'result =...' bit. If I can't do that, then the whole thing isn't much use to me anyway. By passing in syncr, I can make a a call to its finish_with_result to tell anyone listening that the async task has completed, and store the result there for consumption by the invoker.

The problem with my make_sync and SyncHelper implementations as they stand (apart from the obvious fact that I'm probably doing something profoundly stupid) is that the code inside the BubbleWrap::Reactor.schedule do ... end block doesn't get called until after the call to syncr.wait has timed out (note: not finished, because the block never gets the chance to run, and hence can't store result in it). It is completely starving all other processes from access to the CPU, even tho the call to CFRunLoopRunInMode is happening inside wait. I was under the impression that CFRunLoopRunInMode in this config would spin wait, but allow other queued blocks to run, but it appears that I've got that wrong.

This strikes me as something that people would need to do from time-to-time, so I can't be the only person having trouble with this kind of problem.

Have I had too many crazy pills? Is there a standard iOS idiom for doing this that I'm just not understanding? Is there a better way to solve this kind of problem?

Any help would be much appreciated.

Thanks in advance,

M@

4条回答
SAY GOODBYE
2楼-- · 2019-02-11 03:46

You can also use synced queues.

Dispatch::Queue.new('name').sync

Dispatch::Queue.main.sync

Take a look at more examples of usage: http://blog.arkency.com/2014/08/concurrent-patterns-in-rubymotion/

查看更多
SAY GOODBYE
3楼-- · 2019-02-11 04:02

When you need to display the payment options, display a HUD, like MBProgressHUD to block the user from using the UI and then start your network call. When the network call returns, dismiss the HUD in either in your success/failure blocks or in the delegate methods and then refresh your view with the data received.

If you don't like the HUD idea you can display something appropriate in your UI, like a UILabel with "loading..." or an UIActivityIndicatorView.

If you need to get the data to display first thing, do it in viewDidAppear; if it happens on an action then move your transition to the next view (performSegueWithIdentifier or whatever) into your network success block/callback and make the network call when the action is called.

There should be examples in your networking library of how, or take a look at the usage sample code in MBProgressHUD itself https://github.com/jdg/MBProgressHUD.

查看更多
我命由我不由天
4楼-- · 2019-02-11 04:05

Here's what I do to make multi-threaded synchronized asynchronous calls.

def make_sync2(&block)
  @semaphore ||= Dispatch::Semaphore.new(0)
  @semaphore2 ||= Dispatch::Semaphore.new(1)
  BubbleWrap::Reactor.schedule do
    result = block.call("Mateus")
    @semaphore2.wait # Wait for access to @result
    @result = result
    @semaphore.signal
  end
  @semaphore.wait # Wait for async task to complete
  result = @result
  @semaphore2.signal
  result
end
查看更多
兄弟一词,经得起流年.
5楼-- · 2019-02-11 04:11

as borrrden just said, I'd use a dispatch_semaphore

 def ApiHelper.make_sync(&block)
   @semaphore = Dispatch::Semaphore.new(0)
   BubbleWrap::Reactor.schedule do
     # do your stuff
     @result = block.call()
     @semaphore.signal
   end
   @semaphore.wait
   @result
 end

this is how I'd handle it on Rubymotion

查看更多
登录 后发表回答