How to make table scrolling work: UIControl within UITableView within UIControl - ios

Here's the setup: I have UIControls in table cells that allow sliding from right to left for a delete function (like Clear)
The table cells live inside a UITableView.
The TableView lives inside another UIControl that allows swiping from left to right or right to left in order to change days. When this happens a new TableView gets created to the right or left of the main one, and the new one is pulled in from left or right until a threshold is met and an animation takes over and then replaces the old with the new.
In some conditions all of these interactions actually work correctly. The issue is that after a couple of interactions (table slides seem to be problematic) it becomes difficult / impossible to scroll the table.
Here is the code for the TableViewSlider (the top level UIControl that contains the TableViews). Any ideas would be greatly appreciated!
#implementation OSETableViewSlider
- (void) initialize {
self.backgroundColor = [UIColor backgroundFlatColor];
self.mainTableView = [self createUITableView];
self.mainTableView.frame = CGRectMake(0,0, self.frame.size.width, self.frame.size.height);
self.mainTableView.hidden = NO;
[self addSubview:self.mainTableView];
self.transitionInProgress = NO;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initialize];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if(self) {
[self initialize];
}
return self;
}
- (UITableView *)createUITableView {
UITableView *newTable = [[UITableView alloc] init];
newTable.hidden = YES;
newTable.separatorStyle = UITableViewCellSeparatorStyleNone;
newTable.scrollEnabled = YES;
newTable.bounces = YES;
newTable.dataSource = self;
newTable.delegate = self;
newTable.backgroundColor = [UIColor backgroundFlatColor];
return newTable;
}
- (void)setFrame:(CGRect)frame {
super.frame = frame;
self.mainTableView.frame = CGRectMake(0,0, frame.size.width, frame.size.height);
}
- (void)transitionToDate:(NSDate *)date fromRight:(BOOL)rightToLeft {
[self.viewController beginTransitionToDate:date];
self.transitionTableView = [self createUITableView];
self.transitionTableView.frame = CGRectMake( rightToLeft ? self.frame.size.width : (-1 * self.frame.size.width), 0,
self.frame.size.width, self.frame.size.height );
self.transitionTableView.hidden = NO;
[self addSubview:self.transitionTableView];
self.transitionInProgress = YES;
[UIView animateWithDuration:0.2f animations:^{
self.transitionTableView.frame = CGRectMake(0,0, self.frame.size.width, self.frame.size.height);
self.mainTableView.frame = CGRectMake( rightToLeft ? (-1 * self.frame.size.width) : self.frame.size.width, 0,
self.frame.size.width, self.frame.size.height);
} completion:^(BOOL completed){
[self.viewController endTransitionDidChange:YES];
[self.mainTableView removeFromSuperview];
self.mainTableView = self.transitionTableView;
self.transitionInProgress = NO;
}];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.viewController activityCountForTransition:(tableView!=self.mainTableView)];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.viewController cellNumber:indexPath.item forTransition:(tableView!=self.mainTableView)];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.viewController cellNumber:indexPath.item forTransition:(tableView!=self.mainTableView)].frame.size.height;
}
- (void)layoutSubviews {
if(!self.transitionInProgress) {
[self.mainTableView setFrame:CGRectMake(0,0, self.frame.size.width, self.frame.size.height)];
}
}
- (BOOL) beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(#"begin track");
self.locationInView = [touch locationInView:self.superview];
self.fingerTracking = YES;
self.fingerMoved = NO;
self.transitionTableView = nil;
return YES;
}
- (CGFloat) calcOffsetForTouch:(UITouch *)touch {
CGFloat fingerOffset = [touch locationInView:self.superview].x - self.locationInView.x;
return fingerOffset + self.startingOffset;
}
- (BOOL) continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
self.fingerMoved = YES;
CGFloat offset = [self calcOffsetForTouch:touch];
//NSLog(#"offset is: %f flarb: %f", offset, fabs(offset));
if(offset < 0 && (!self.transitioningLeft || [OSEUtils isNull:self.transitionTableView])) {
[self.viewController beginTransitionToDate:[OSEUtils daysOffset:1 fromDate:self.viewController.selectedDate withTimeZone:[NSTimeZone timeZoneWithName:#"UTC"]]];
[self.transitionTableView removeFromSuperview];
self.transitionTableView = [self createUITableView];
self.transitioningLeft = YES;
NSLog(#"going left");
[self addSubview:self.transitionTableView];
[self.transitionTableView reloadData];
}
if(offset > 0 && (self.transitioningLeft || [OSEUtils isNull:self.transitionTableView])) {
[self.viewController beginTransitionToDate:[OSEUtils daysOffset:-1 fromDate:self.viewController.selectedDate withTimeZone:[NSTimeZone timeZoneWithName:#"UTC"]]];
[self.transitionTableView removeFromSuperview];
self.transitionTableView = [self createUITableView];
self.transitioningLeft = NO;
NSLog(#"going right");
[self addSubview:self.transitionTableView];
[self.transitionTableView reloadData];
}
CGFloat mult = self.transitioningLeft ? 1 : -1;
if(fabs(offset) > (self.frame.size.width / 3.0f)) {
if(self.transitioningLeft) {
offset = -1 * self.frame.size.width;
} else {
offset = self.frame.size.width;
}
[UIView animateWithDuration:0.2f animations:^{
self.mainTableView.frame = CGRectMake(offset, 0, self.frame.size.width, self.frame.size.height);
self.transitionTableView.frame = CGRectMake(offset + (mult * self.frame.size.width), 0,
self.frame.size.width, self.frame.size.height);
} completion:^(BOOL finished) {
[self.mainTableView removeFromSuperview];
if(self.transitioningLeft) {
[self.viewController.daySelector jumpToNext];
} else {
[self.viewController.daySelector jumpToPrevious];
}
self.mainTableView = self.transitionTableView;
self.transitionTableView = nil;
[self.viewController endTransitionDidChange:YES];
}];
return NO;
}
self.mainTableView.frame = CGRectMake(offset, 0, self.frame.size.width, self.frame.size.height);
self.transitionTableView.frame = CGRectMake(offset + (mult * self.frame.size.width), 0, self.frame.size.width, self.frame.size.height);
self.transitionTableView.hidden = NO;
return YES;
}
- (void) endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(#"end tableslide track");
[UIView animateWithDuration:0.2f animations:^{
self.mainTableView.frame = CGRectMake(0,0,self.frame.size.width, self.frame.size.height);
self.transitionTableView.frame = CGRectMake(((self.transitioningLeft ? 1 : -1 ) * self.frame.size.width), 0, self.frame.size.width, self.frame.size.height);
} completion:^(BOOL completion){
[self.transitionTableView removeFromSuperview];
self.transitionTableView = nil;
[self.viewController endTransitionDidChange:NO];
}];
self.fingerTracking = NO;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *tableHitTest = [self.mainTableView hitTest:point withEvent:event];
if([tableHitTest isKindOfClass:[OSECellPanelControl class]]) {
return tableHitTest;
} else {
return self;
}
}
#end

Why are you creating a new UITableView when the user swipes left or right? It would be much more efficient to use the built-in deleteRowsAtIndexPath: and insertRowsAtIndexPath: methods to animate the rows sliding in and out without having to slide in and out entire UITableView's.
You can take a look at this for a reference on animating the tableview content changes as you swipe across dates.
I don't know about your current set up, but when I tried this exact same thing with swipeable UITableViewCell's and it's superview being swipe able, I had issues with getting the interaction that I wanted. If this is contributing to your problem, you can look at detecting where the user swipes. If the user swipes within a specific offset from the left and right edge of the UITableViewCell, then it swipes the cell, anything outside of that offset (center of the cell) would have the UITableView itself swiped. You would then animate out the deleting and insertion of the new rows.
Also, by re-instancing a new UITableView each time, you are forcing your UITableViewCell's to be re-instanced each time as well. By using the delete and insert methods, you can continue to use your existing cells via the dequeueReusableCellWithIdentifier method. Each time you re-instance the UITableView, your cells are set to nil and discarded requiring the new tableview to create complete new instances of your cells. This is extremely inefficient.

You're getting in deep with touch tracking and hit detection. Use a higher level API instead - gesture recognisers. With gestures you can set precedence (this one has to fail before this one can begin).
So, you want a couple of custom gestures on the container view (which no longer needs to be a control). These fail if the initial touch point isn't at the edge of the view (within some bound). These trigger the overall table view change.
And the table cells have swipe gestures that each require the custom ones to fail. These trigger your cell deletion logic.

Your idea share a few similarities with how the iOS7 unlock screen and calendar is built. In the unlock screen, you can swipe on notifications, move the notification list up and down, and swipe to unlock. In the calendar app, you can swipe to change the current day, or swipe the header to change the week. Thus I recommend watching the WWDC 2013 Session video #217 "Exploring Scroll Views on iOS 7" that explain and demonstrate how they have done it, and how to replicate the functionality.
Basically, what you could do (and what they did for the unlock screen and calendar screen) is: Either (A) nest UIScrollViews in a UITableView OR (B) use a swipe/pan gesture delegate on every cells, and (C) nest that UITableView along with every other UITableViews representing days in a UIScrollView with paging control enabled.
(A) will allow you to expose a delete button in cells. You could either put the delete button under the UIScrollView that sits in the cell in a way that sliding the content uncover it.
(B) will allow you to track a gesture in cells, cancel it if required, and play with the cell's contentView to display a delete button.
(C) will allow you to swipe from one UITableView to another quite easily, and with paging enable you will get a free "snap into place or bounce back" animation PLUS you can easily reuse two UITableViews instead of allocating one per day.
Personally, I would go with (B) and (C).

Turns out there was a problem with my hitTest method. Replacing [self.mainTableView hitTest...] with [super hitTest...] solved my problem. I think I was messing up some coordinates that was causing this method to return nil when it should have returned a view. I'm not 100% sure exactly what is wrong with it, but it does work better now.

Related

Animating UIImageView flip from middle

I'm trying for a while to build an ebook, flipping page (from left to right) and zooming.
So far I've tested a few projects but none of them was good enough to do both.
I've decided to build one of my own, so I started with basic flip animation like this:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.scrollView.delegate = self;
self.scrollView.maximumZoomScale = 5.0f;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
tapGesture.delegate = self;
tapGesture.numberOfTapsRequired = 1;
tapGesture.numberOfTouchesRequired = 1;
[self.scrollView addGestureRecognizer:tapGesture];
}
#pragma mark - UIScrollView Delegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
#pragma mark - UIGestureRecognizer Delegate
- (void)handleTap:(UITapGestureRecognizer *)tapGestur
{
NSLog(#"%s", __PRETTY_FUNCTION__);
if (tapGestur.state == UIGestureRecognizerStateEnded) {
[UIView transitionWithView:self.imageView duration:1 options:UIViewAnimationOptionTransitionFlipFromRight|UIViewAnimationOptionCurveEaseInOut animations:^{
if (!flipped) {
self.imageView.image = [UIImage imageNamed:#"2-IPAD-P.jpg"];
flipped = YES;
}
else {
self.imageView.image = [UIImage imageNamed:#"1-IPAD-P.jpg"];
}
} completion:^(BOOL finished) {
//
}];
}
}
What I'm trying to do now is transition flipping page (left to right) from the middle of the image.
Any idea how can I do this using UIVIew? Or maybe better options?
You should also give a look to CATansition. Have a look at this example.
Also, have you looked at UIViewAnimationOptions's UIViewAnimationOptionTransitionCurlUp and UIViewAnimationOptionTransitionCurlDown for page animations ?

animating UITableViewHeader to grow/shrink

Im very new to iOS and am trying to figure out how I can animate the tableView header to appear like its sliding down from the top of the view, stay there for 3 seconds, then slide back up and disappear.
I havent done any animation of any kind before so I could really use some help. I dont know where to start. Ive looked at other answers on here but cant seem to figure out how to do it.
ive got my table view appearing just fine with this code in my viewDidLoad of my UITableViewController class
self.headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320,15)];
self.headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 15)];
self.headerLabel.textAlignment = NSTextAlignmentCenter;
self.headerLabel.text = #"some text";
[self.headerView addSubview:self.headerLabel];
self.tableView.tableHeaderView = self.headerView;
self.tableView.tableHeaderView.frame = CGRectMake(0, 0, 320, 15);
I assume I need to animate the height of my frame.
edit: I found this code which seems like it should work, however it crashes my app when the animation runs
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[UIView animateWithDuration:3.0f
delay:0.0f
options: UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.headerView.frame = CGRectMake(0.0f, 0.0f, 320.f, 0.0f);
}
completion:^(BOOL finished) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
}];
}
I don't know if you can animate a table view's header view like you want to. With a complex UI object like a Table view Apple tends to treat the views that make up the object as private, and bad things tend to happen when you try to manipulate the view hierarch.
If you're not using auto-layout and you want to animate another view that is part of YOUR view hierarchy then your code might be as simple as something like this:
CGPoint center = myView.center;
CGFloat myViewBottom = myView.frame.origin.y + myView.frame.origin.size.height;
//Move the view above the top of it's superview before starting the animation.
center.y -= myViewBottom;
//Animate the view down into position
[UIView animateWithDuration: .2
animations:
^{
myView.center += myViewBottom;
};
completion:
^{
//As the completion block for the first animation, trigger another
//animation to animate the view away again. This time delay
//for 3 second before the 2nd animation.
[UIView animateWithDuration: .2
delay: 3.0
options: 0
animations:
^{
myView.center -= myViewBottom;
};
completion: nil
}
];
Try to use beginUpdates and endUpdates, here is an example:
in your header
#property NSInteger newOffset;
implementation:
- (void) colapseHeader
{
_newOffset = 50;
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 50 + _newOffset;
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
_newOffset = 100;
[self.tableView beginUpdates];
[self.tableView endUpdates];
[self performSelector:#selector(colapseHeader) withObject:nil afterDelay:3];
}

How hide cell wile reordering TableVirew

Aim is to hide cell which is reordering when reordering animation starts and show cell when reordering animation stops.
PS
When you moving cell, it's alpha is changed, i think there is a way to set alpha to 0, but how? It is necessary for me to hide else shadow of moving cell, because i try show diferent picture instead moving cell.
[yourtableviewcell setHidden:YES];
If you want to fade it away, animate the alpha to 0 first.
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
yourtableviewcell.alpha = 0;
[UIView commitAnimations];
There answer to shadow question
I used next code in my CustomCell class and result is in the picture. Cell is hidden and it's good.
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
NSLog(#"Drugging"); // Dragging started
self.hidden=YES;
} else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
NSLog(#"Drugging End"); // Dragging ended
self.hidden=NO;
}
}
- (void)layoutSubviews {
[super layoutSubviews];
for (UIView *view in self.subviews) {
if ([NSStringFromClass ([view class]) rangeOfString:#"ReorderControl"].location != NSNotFound) { // UITableViewCellReorderControl
if (view.gestureRecognizers.count == 0) {
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
gesture.cancelsTouchesInView = NO;
gesture.minimumPressDuration = 0.150;
[view addGestureRecognizer:gesture];
}
}
}
}

iOS Switching Views Function

I have an application which has two views on one view controller. One view is above the other. When someone swipes or presses a button, the view on top moves to the side to display the bottom view. When someone opens a new view and wants to go back to the view controller with the two views, I want to have the view on top to reveal the bottom view automatically.
This is my code:
#interface ViewController ()
#end
#implementation ViewController
#synthesize topLayer = _topLayer;
#synthesize layerPosition = _layerPosition;
- (void)viewDidLoad
{
[super viewDidLoad]
self.topLayer.layer.shadowOffset = CGSizeMake(-1,0);
self.topLayer.layer.shadowOpacity = .9;
self.layerPosition = self.topLayer.frame.origin.x;
}
#define VIEW_HIDDEN 264
-(void) animateLayerToPoint:(CGFloat)x
{
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationCurveEaseOut
animations:^{
CGRect frame = self.topLayer.frame;
frame.origin.x = x;
self.topLayer.frame = frame;
}
completion:^(BOOL finished){
self.layerPosition =self.topLayer.frame.origin.x;
}];
}
- (IBAction)toggleLayer:(id)sender {
if (self.layerPosition == VIEW_HIDDEN) {
[self animateLayerToPoint:0];
} else {
[self animateLayerToPoint:VIEW_HIDDEN];
}
}
- (IBAction)panLayer:(UIPanGestureRecognizer*)pan {
if (pan.state == UIGestureRecognizerStateChanged) {
CGPoint point = [pan translationInView:self.topLayer];
CGRect frame = self.topLayer.frame;
frame.origin.x = self.layerPosition + point.x;
if (frame.origin.x < 0 ) frame.origin.x = 0;
self.topLayer.frame = frame;
}
if (pan.state == UIGestureRecognizerStateEnded) {
if (self.topLayer.frame.origin.x <= 160) {
[self animateLayerToPoint:0];
} else {
[self animateLayerToPoint: VIEW_HIDDEN];
}
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Override -viewWillAppear to arrange the views however you want them based on the state of the app. If the user hasn't swiped or whatever, put the first view on top. If they have swiped or whatever, move the view to the side.
-viewDidLoad is the right place to perform any setup that needs to happen when the view is first loaded. -viewWillAppear is the place to do things that should happen just before the view controller's view is displayed, and it's called every time that happens.

UIScrollView costum pagination size

first take a look on this picture from localScope app :
i have 2 (simple?) questions :
how can i paginate my icons like this?
how can i detect witch icon is " selected "
thank you.
Answer to the first question: You have to make your scroll view as big as the page size, enable its pagingEnabled property, then somehow make it to display elements and respond to touches outside of its bounds. See this code and these links:
#interface SmallPagedScrollView: UIScrollView <UIScrollViewDelegate> {
UIEdgeInsets responseInsets;
NSMutableArray *items;
}
#implementation SmallPagedScrollView
#synthesize responseInsets;
- (id)init
{
if ((self = [super initWithFrame:CGRectMake(x, y, w, h)]))
{
self.backgroundColor = [UIColor clearColor];
self.pagingEnabled = YES;
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
self.clipsToBounds = NO;
CGFloat hInset = 3 * self.width / 2;
self.responseInsets = UIEdgeInsetsMake(0.0f, hInset, 0.0f, hInset);
self.delegate = self;
items = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc
{
[items release];
[super dealloc];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint parentLocation = [self convertPoint:point toView:self.superview];
CGRect responseRect = self.frame;
responseRect.origin.x -= self.responseInsets.left;
responseRect.origin.y -= self.responseInsets.top;
responseRect.size.width += self.responseInsets.left + self.responseInsets.right;
responseRect.size.height += self.responseInsets.top + self.responseInsets.bottom;
return CGRectContainsPoint(responseRect, parentLocation);
}
See also Paging UIScrollView in increments smaller than frame size (Split's answer)
Answer to the second question: you can calculate the selected page using this formula:
int selectedIndex = (scrollView.contentOffset + scrollView.size.width / 2) / scrollView.size.width;
Well one clean & memory efficient approach is to have a UINavigationController & UIToolBar like so -
When the user taps on any button in the UIToolBar invoke that particular viewController by popping and pushing them.
I hope its clear that the look and feel can be achieved close to what you are showing in the image, I am talking about the functionality.

Resources