Skip to content

Download Progress #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alexvbush opened this issue Jul 18, 2016 · 9 comments
Closed

Download Progress #38

alexvbush opened this issue Jul 18, 2016 · 9 comments

Comments

@alexvbush
Copy link
Member

Hi @ashfurrow, thanks for doing all this great work! Trying to use Action in my current project and have gotten into a bit of a jam - I have a download button that will pull several files from our backend and I am creating a CocoaAction for it. But I don't know how I can pull progress from the underlying Observable that actually does the work (I have a Observable<Int> that emits progress events, each event is one percent of overall progress upto 100% when it sends onComplete). Seems like CocoaAction's signature is Action<Void, Void> which means it takes no argument (makes sense because a button calls it) and it emits no values in its elements Observable (I've used your trick from here to map to Void).

I'm sure I'm missing something trivial but how can I observe my download progress and get a notification when the whole process/action is done (onComplete)?

Thanks in advance!

@ashfurrow
Copy link
Member

Good question! This is super-tricky, we're trying to clean it up in #17 . It's kind of messy, but you can get around this by emitting the progress things from within in Action's block before it is mapped to void. Pseudocode:

let action = Action { [weak self] _ in
  return download.onNext { progress in
    self?.progressSignal.update(progress)
  }.map(void)
}

Like I said, it's not totally clean but it should work. Let me know 👍

@alexvbush
Copy link
Member Author

thanks @ashfurrow, that's a solution I considered but I don't feel comfortable doing sideeffects this way. Makes that Action a bit "impure" in a sense that if someone else's trying to use it doesn't know that it updates UI on progress changes they might get in trouble.

What I ended up doing is a workaround where I use Action instead of CocoaAction and duplicate the behavior of rx_action = setter in my code. Turns out it's not that complicated and is clear by just skimming through UIButton extension source in UIButton+Rx.swift file.
In public var rx_action: CocoaAction? setter method we mainly do 3 things:

  1. nullify dispose bag to get rid of previous subscriptions if any with self.resetActionDisposeBag()
  2. bind enabled state of the Action to rx_enabled of the button so that it can be clicked only when appropriate action.enabled.bindTo(self.rx_enabled).addDisposableTo(self.actionDisposeBag)
  3. add rx_tap observer to call execute() on the button when it's tapped.
controlEvent.subscribeNext { _ -> Void in
      action.execute()
}.addDisposableTo(self.actionDisposeBag)

So in my view(table cell) where I subscribe to signals from my download action I do the following:

// somewhere else in the cell file
//  @IBOutlet weak var downloadButton: UIButton!
//  @IBOutlet weak var downloadProgressView: UIView!
//  @IBOutlet weak var downloadProgressViewWidth: NSLayoutConstraint!
//  var viewModel: JRItineraryViewModel!
//  var disposeBag = DisposeBag()

let downloadOfflineItineraryAction = viewModel.downloadOfflineItineraryAction

// step 1. bind enabled state
downloadOfflineItineraryAction.enabled.bindTo(downloadButton.rx_enabled).addDisposableTo(disposeBag)


// step 2. listen for progress events from the action
downloadOfflineItineraryAction.elements.observeOn(MainScheduler.instance).subscribeNext({ [weak self] (progress) in

                if let strongSelf = self {
                    strongSelf.downloadProgressView.hidden = false
                    strongSelf.downloadButton.setTitle("Saving \(progress)%", forState: UIControlState.Normal)
                    strongSelf.downloadProgressViewWidth.constant = CGFloat(progress) / 100 * strongSelf.downloadButton.frame.size.width
                    strongSelf.setNeedsLayout()
                }

            }).addDisposableTo(disposeBag)


// step 3. listen for error events from the action

downloadOfflineItineraryAction.errors.subscribeNext({ [weak self] (error) in
                self?.downloadProgressView.hidden = true
                self?.downloadProgressViewWidth.constant = 0
                self?.downloadButton.setTitle("There was an error", forState: UIControlState.Normal)
                self?.setNeedsLayout()
            }).addDisposableTo(disposeBag)


// step 4. bind tap event to execute the action

downloadButton.rx_tap.subscribeNext({ [weak self] (_) in

                if let strongSelf = self {

                    // omitted analytics code here.

                    downloadOfflineItineraryAction.execute().subscribe(onCompleted: { [weak self] in
                        self?.downloadProgressView.hidden = true
                        self?.downloadProgressViewWidth.constant = 0
                        self?.setNeedsLayout()
                    }, onDisposed: { [weak self] in
                        self?.downloadProgressView.hidden = true
                        self?.downloadProgressViewWidth.constant = 0
                        self?.setNeedsLayout()
                    }).addDisposableTo(strongSelf.disposeBag)
                }

            }).addDisposableTo(disposeBag)

And my action that's created in viewmodel looks like this:

func downloadOfflineItineraryAction(itinerary: JRItinerary) -> Action<Void, Int> {

return Action(workFactory: { [unowned self] (input) -> Observable<Int> in
            return Observable.combineLatest(self.HTMLFilesDownloadSignal(itinerary).startWith(0), self.mapDownloadSignal(itinerary).startWith(0)) { (progress1, progress2) -> Int in

                return (progress1 + progress2) / 2

                }.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
        })

}

Notice that downloadOfflineItineraryAction returns Action<Void, Int>instead ofCocoaAction. HTMLFilesDownloadSignalandmapDownloadSignaldo the actual work downloading files and emitInts for their respective progress. That's why I have tocombineLatest` from them to calculate total progress.

@ashfurrow let me know what you think about this solution and I hope it will be helpful for others who want to show download progress in their apps too

@ashfurrow
Copy link
Member

Yeah, it's pretty much a choice of where the side effects make sense to put – that's all FRP really is, isolating side effects. Subscribing to the controlEvent makes sense, too!

Where do you think makes sense to document this?

@alexvbush
Copy link
Member Author

I'm working on a blog post to describe it in details. I think I can send a
PR to add it to readme after I'm done if that's ok
On Tue, Jul 26, 2016 at 7:01 AM Ash Furrow [email protected] wrote:

Yeah, it's pretty much a choice of where the side effects make sense to
put – that's all FRP really is, isolating side effects. Subscribing to the
controlEvent makes sense, too!

Where do you think makes sense to document this?


You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
#38 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAS_JaDDZqWmWDvG1GF97_9Rqm0mRi79ks5qZhM_gaJpZM4JOW-f
.

@ashfurrow
Copy link
Member

ashfurrow commented Jul 27, 2016

I think a blog post is the perfect spot, adding a link to the readme is a great idea, I'd happily accept that pull request.

@alexvbush
Copy link
Member Author

@ashfurrow awesome. was working on it today. unfortunately can't finish tonight. my work in progress is here https://www.softcover.io/read/41bc8669/rx_action

I'll post here and create a PR for README when I'm done.

Thx.

@ashfurrow
Copy link
Member

Cool, looking forward to the PR! No rush though – don't stress about it!

@alexvbush
Copy link
Member Author

@ashfurrow just published it http://www.sm-cloud.com/rxswift-action/

a PR is coming in soon

@alexvbush
Copy link
Member Author

closed with this PR #39

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants