-
-
Save oleganza/607465 to your computer and use it in GitHub Desktop.
| /* | |
| In Gitbox (gitboxapp.com) there is a stage view on the right where you can see | |
| a list of all the changes in the working directory: untracked, modified, | |
| added, deleted, renamed files. Each change has a checkbox which you can click | |
| to stage or unstage the change ("git add", "git reset"). | |
| When the change staging finishes, we run another task to load all the changes | |
| ("git status"). | |
| When the loading task is completed we notify the UI to update the list of changes. | |
| All tasks are asynchronous. | |
| The problem: | |
| When the user quickly clicks on checkboxes we should not refresh it | |
| multiple times. Otherwise, it will flicker with the inconsistent checkbox | |
| states in between loading times. | |
| Possible (non-)solutions: | |
| 1. The synchronous execution solves the problem at the expense of | |
| slowing down the user interaction. | |
| 2. Updating UI with a delay does not solve the problem: it just makes the | |
| updates appear later and still produces flickering when the user clicks slower. | |
| So here is the real solution. | |
| Let's observe possible combinations of two pairs of tasks: staging and changes' loading | |
| when the user clicks on two checkboxes subsequently. | |
| Abbreviations: S = staging, L = loading changes, U = UI update | |
| Scenario 1: S2 starts before L1, so we should avoid running L1 at all. | |
| S1----->L1----->U1 | |
| S2----->L2----->U2 | |
| Scenario 2: S2 started after L1, so we should avoid U1. | |
| S1----->L1----->U1 | |
| S2----->L2----->U2 | |
| In both scenarios we need to know whether there are any other staging processes running or not. | |
| If there is one, we simply avoid running loading task or at least avoid updating the UI. | |
| This is solved using isStaging counter (named like a boolean because we don't care | |
| about the actual number of running tasks, we care only about the fact that they are running). | |
| isStaging is incremented before stage/unstage task begins and decremented | |
| right after it finishes. When the task is finished and the counter is not zero, we simply avoid running next tasks. | |
| However, there is another, more subtle scenario which I spotted only after some more testing: | |
| Scenario 3: L1 starts before S2, but finishes after *both* S1 and S2 have finished. | |
| S1---->L1------------>U1 | |
| S2---->L2---------->U2 | |
| In this case it is not enough to have isStaging flag. We should also ask whether there is any | |
| loading tasks still running. For that we use isLoadingChanges counter. | |
| After finding this scenario I tried to get away with just a single flag isStaging, | |
| but it turned out to be impossible: if I decrement isStaging after loading is complete, | |
| I cannot avoid starting a loading task because in scenario 1 both L1 and L2 look identical. | |
| So without an additional flag I would have to start changes loading task each time the checkbox | |
| is clicked, which drops the performance significantly. | |
| In this little snippet of code we greatly improve user experience using a lot of programming patterns: | |
| 1. Grand Central Dispatch for asynchronous operations without thread management. | |
| 2. Blocks to preserve the execution context between operations and impose a strict order of events. | |
| 3. Semaphore counters for managing the stage of operations and activity indicator (popSpinning/pushSpinning). | |
| 4. Block taking a block as an argument to wrap asynchronous operations. | |
| 5. Delegation pattern to notify the UI about new data. | |
| 6. Bindings and Key-Value-Observing for blocking a checkbox when the staging is in process (aChange.busy flag). | |
| This gives you an idea of what kind of code powers Gitbox. | |
| This code will appear in the next update. | |
| http://gitboxapp.com/ | |
| */ | |
| // NSInteger isStaging; // maintains a count of the staging tasks running | |
| // NSInteger isLoadingChanges; // maintains a count of the changes loading tasks running | |
| // This method helps to factor out common code for both staging and unstaging tasks. | |
| // Block declaration might look tricky, but it's just a convenient wrapper, nothing special. | |
| // See the stage and unstage methods below. | |
| - (void) stagingHelperForChange:(GBChange*)aChange withBlock:(void(^)(GBStage*, void(^)()))block | |
| { | |
| GBStage* stage = self.repository.stage; | |
| if (aChange.busy || !stage) return; | |
| aChange.busy = YES; | |
| [self pushSpinning]; | |
| isStaging++; | |
| block(stage, ^{ | |
| isStaging--; | |
| // Avoid loading changes if another staging is running. | |
| if (!isStaging) | |
| { | |
| [self pushSpinning]; | |
| isLoadingChanges++; | |
| [stage loadChangesWithBlock:^{ | |
| isLoadingChanges--; | |
| // Avoid publishing changes if another staging is running | |
| // or another loading task is running. | |
| if (!isStaging && !isLoadingChanges) | |
| { | |
| OAOptionalDelegateMessage(@selector(repositoryControllerDidUpdateCommitChanges:)); | |
| } | |
| [self popSpinning]; | |
| }]; | |
| } | |
| [self popSpinning]; | |
| }); | |
| } | |
| // These methods are called when the user clicks a checkbox (GBChange setStaged:) | |
| - (void) stageChange:(GBChange*)aChange | |
| { | |
| [self stagingHelperForChange:aChange withBlock:^(GBStage* stage, void(^block)()){ | |
| [stage stageChanges:[NSArray arrayWithObject:aChange] withBlock:block]; | |
| }]; | |
| } | |
| - (void) unstageChange:(GBChange*)aChange | |
| { | |
| [self stagingHelperForChange:aChange withBlock:^(GBStage* stage, void(^block)()){ | |
| [stage unstageChanges:[NSArray arrayWithObject:aChange] withBlock:block]; | |
| }]; | |
| } |
stage/unstageChanges:withBlock calls the block asynchronously when the task has completed.
stagingHelperForChange:withBlock needs to do some common stuff before task launches and after it finishes. To do something after, it needs a block. But it does not launch the task, so it passes this block into wrapping block so it will pass it to the async task. Hence the block taking a block as an argument.
Ok, no code for stage/unstageChanges:withBlock is confusing a bit.
So basically the idea is that stagingHelperForChange:withBlock does some common processing, then it calls supplied block with stage/unstage specific actions and provide common post-processing block, and then finally unstage\stageChanges:withBlock calls supplied post-processing block.
Right?
Exactly.
If the task was not synchronous there would be not point in the whole article: just stage the item on click and block the UI. This how the current version 0.9.7 behaves.
Why do you need to pass a block to another block? It looks like
stagingHelperForChange:withBlock&stage/unstageChanges:withBlockare both returns almost at once, so callingstage/unstageChanges:withBlockfrom block is redundant & it will take more time to spawn threads than to execute a block. I believe you can safely do it with single block for both.Anyway it's pretty cool :)