-
-
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:withBlock
are both returns almost at once, so callingstage/unstageChanges:withBlock
from 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 :)