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@
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.
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
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
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/
来源:https://stackoverflow.com/questions/15608375/what-is-the-ios-or-rubymotion-idiom-for-waiting-on-a-block-that-executes-async