Skip to content
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

Example code for box reuse / offscreen culling with MGBoxProvider #116

Open
sobri909 opened this issue Mar 30, 2014 · 10 comments
Open

Example code for box reuse / offscreen culling with MGBoxProvider #116

sobri909 opened this issue Mar 30, 2014 · 10 comments

Comments

@sobri909
Copy link
Owner

I'm throwing some working example code in here, but will shortly also write up some more formal documentation.

- (MGScrollView *)scroller {
    if (_scroller) {
        return _scroller;
    }
    _scroller = [MGScrollView scrollerWithSize:self.view.size];
    _scroller.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
    _scroller.contentLayoutMode = MGLayoutGridStyle;
    _scroller.boxProvider = self.boxProvider;
    return _scroller;
}
- (MGBoxProvider *)boxProvider {
    if (_boxProvider) {
        return _boxProvider;
    }

    _boxProvider = MGBoxProvider.provider;

    // returns raw empty boxes 
    _boxProvider.boxMaker = ^{
        return [SGPadPerformerCell boxWithSize:(CGSize){250, 250}];
    };

    __weakSelf me = self;

    // is given a fresh box or a reused box, and customises it based on given index
    _boxProvider.boxCustomiser = ^(SGPadPerformerCell *box, NSUInteger index) {
        box.performer = me.performers[index];
        __weak SGPadPerformerCell *wBox = box;
        box.onTap = ^{
            id controller = [SGPadTeamController controllerForItem:wBox.performer];
            [me.navigationController pushViewController:controller animated:YES];
        };
    };

    _boxProvider.boxKeyMaker = ^id(NSUInteger index) {
        return [me.performers[index] key];
    };

    _boxProvider.boxSizeMaker = ^(NSUInteger index) {
        return (CGSize){250, 250};
    };

    _boxProvider.boxMarginMaker = ^(NSUInteger index) {
        return UIEdgeInsetsMake(4, 4, 0, 0);
    };

    // returns the total boxes count
    _boxProvider.counter = ^{
        return me.performers.count;
    };

    return _boxProvider;
}
@sobri909
Copy link
Owner Author

This functions the same as UITableView cell reuse, but with a less awkward API and less code requirements.

Why Block Properties Instead of Protocols

The use of block properties is optional, and you could instead subclass MGBoxProvider and overload the relevant methods to return the same results as the above example blocks. But the goal of doing it with block properties was to allow for something to be put together quickly, with less code, and using more modern patterns.

The Required Blocks

boxMaker

Should return fresh boxes without customisation. It could be as simple as return [MGBox boxWithSize:(CGSize){100, 100}];

boxCustomiser

This block is given a box, which will either be fresh or a reused box, and is expected to apply customisations to it based on the given index. This is roughly analogous to UITableViewDataSource's cellForRowAtIndexPath: except that it's not responsible for creating the boxes, only customising them. The creation of boxes is done by boxMaker and their reuse is handled internally.

boxSizer

This should return the size, including margins, of a box at a given index. If all your boxes/rows/whatevers are the same size for the entire table/grid, then the answer is easy. In my example code you can see that boxMaker is making 250x250 boxes, and boxSizer is returning 254x254, because my boxes have 4pt left and top margins.

boxCounter

Similar to UITableViewDataSource's numberOfRowsInSection: except that containers only ever have one section, so there's no fussing about with sections. If you want multiple sections in a table, just nest containers inside containers (eg MGBoxes inside MGBoxes inside MGBoxes).

@sobri909
Copy link
Owner Author

To initialise the table/grid, you should call layout. It should also be called again inside the controller's willAnimateRotationToInterfaceOrientation: to update positions during rotation. It's roughly analogous to UITableView's reloadData.

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.scroller];
    [self fetchPerformers];
}

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orient
      duration:(NSTimeInterval)duration {
    [self.scroller layout];
}

- (void)fetchPerformers {
    self.performers = [TheInternet getTheThings];
    [self.scroller layout];
}

@sobri909
Copy link
Owner Author

As of 3.2.0 boxes will now have their appeared disappeared methods fired (which by default fire their respective onAppear onDisappear block properties) when automatically added / removed by box reuse / offscreen culling (this feature needs a better name).

appeared is fired after the frame is set and after being added to the container.

@sobri909
Copy link
Owner Author

Something that's missing from the above examples is how to cope with the underlying data changing. For now the API for that is two step, when it really should be one step. I'll make a one step method for it shortly. But for now:

// get fresh data from the internets
self.performers = [TheInternet getTheThings];

// update the table after data change
[self.scroller layout];

// or alternatively, do it animated
[self.scroller layoutWithDuration:0.3 completion:nil];

Which is roughly equivalent to calling reloadData on a UITableView.

@sobri909
Copy link
Owner Author

New optional block properties on MGBoxProvider:

@property (nonatomic, copy) MGBoxAnimator appearAnimation;
@property (nonatomic, copy) MGBoxAnimator disappearAnimation;
@property (nonatomic, copy) MGBoxAnimator moveAnimation;
typedef void (^MGBoxAnimator)(id box, NSUInteger index, NSTimeInterval duration,
      CGRect fromFrame, CGRect toFrame);

They are responsible for doing their own UIView animation blocks, and responsible for setting the final frame, if appropriate. The duration value comes from the layoutWithDuration:completion: call and can be ignored at will.

For appearAnimation and disappearAnimation the fromFrame and toFrame are both the intended final frame.

Note that this hasn't been tagged with a release number or pushed upstream to CocoaPods yet, so is only available if you're pulling directly from master. It's very fresh, so I'll let it sit a day or two before pushing upstream.

@rubix28
Copy link

rubix28 commented Jul 22, 2014

I can't quite make heads or tails of how this sits together, is there any chance of updating the demo app to use proper cell reuse?

What's SGPadPerformerCell? I can't get this working with my own cells.

@sobri909
Copy link
Owner Author

There is a good chance of the demo app getting updated to show this, yes. It's high on my todo list. But I can't say when that'll happen.

SGPadPerformerCell is actually an MGBox subclass. It just happens to be called "cell" because that code used to be for a UITableView system, which we converted to using MGBoxProvider, so the class didn't get renamed.

@m4rcoperuano
Copy link

Hey sobri909. First off, great work on this! It's been a pleasure to use so far. I have the above implemented and everything is working great. The only issue that i'm having is that my MGBoxes are stuck with a fixed width and height. My boxes can be different sizes, and I can't seem to figure out how to implement it using the box provider. Please help?

Thanks!

@sobri909
Copy link
Owner Author

It's been a pleasure to use so far. I have the above implemented and everything is working great.

@m4rcoperuano Great to hear!

The only issue that i'm having is that my MGBoxes are stuck with a fixed width and height. My boxes can be different sizes, and I can't seem to figure out how to implement it using the box provider.

There's some newer bits in MGBoxProvider that allow for this. The CocoaDocs have the full details. The bits to look for are boxSizeMaker, boxMaker, and boxCustomiser. Also boxMarginMaker for if you want different margins for different boxes.

Oh, I just noticed a typo in the docs for boxMarginMaker. This line:

If no boxSizeMaker is provided, the default value of UIEdgeInsetsZero will be used.

Is actually meant to say:

If no boxMarginMaker is provided, the default value of UIEdgeInsetsZero will be used.

Anyway, there's two points at which you set box sizes. The first is in boxSizeMaker, which is the most authoritative (that is the size that will be applied to the box when it goes onscreen). The second is in boxMaker, when you create the new raw boxes.

The example code shows how to deal with differing header rows and content rows, but you can use any number of box types, and can also differ the box sizes independent of box type.

@m4rcoperuano
Copy link

Thanks! boxSizeMaker did the trick

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

3 participants