I'm looking for something like AndroidSlidingUpPanel for iOS. I found MBPullDownController, but it requires two ViewControllers to use, and requires a big change to the architecture of the app I'm working on to implement.
I just want something that adds a subview in an existing view controller. How would I go about doing that?
I use sliding up panels in my iOS apps quite a lot and I've found that the trick is to add a custom view to a view controller in the storyboard (or xib file) but to set its frame so that it is off the screen. You can ensure that the view stays off screen on any device using layout constraints.
Then it's just a case of animating the view on screen when appropriate. e.g.:
- (IBAction)showPanel:(id)sender
{
// panelShown is an iVar to track the panel state...
if (!panelShown) {
// myConstraint is an IBOutlet to the appropriate constraint...
// Use this method for iOS 8+ otherwise use a frame based animation...
myConstraint.constant -= customView.frame.size.height;
[UIView animateWithDuration:0.5 animations:^{
[self.view setNeedsLayout];
}];
}
else {
myConstraint.constant += customView.frame.size.height;
[UIView animateWithDuration:0.5 animations:^{
[self.view setNeedsLayout];
}];
}
}
If you want to have just a swipe up/down and that will reveal the panel you can use UISwipeGestureRecognizer like so:
- (void)viewDidLoad
{
[super viewDidLoad];
// iVar
swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(didSwipe:)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
[self.view addGestureRecognizer:swipeUp];
// Do the same again with swipeDown using UISwipeGestureRecognizerDirectionDown...
}
- (void)didSwipe:(UIGestureRecognizer *)swipe
{
if (swipe == swipeUp) {
// Show panel (see above)...
} else {
// Hide panel (see above)...
}
}
If you want the panel to track your finger like when you open the Control Center, then you can use UIPanGestureRecognizer and get the translationInView: and velocityInView: and adjust the panel accordingly. Here is a snippet of code that tracks finger movement but using the touchesBegan:withEvent: - (void)touchesMoved:withEvent: and - (void)touchesEnded:withEvent: methods in a UIViewController to give you a taste:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Don't worry too much about buttonView this is another view that I animate upwards to get out the way of the panel as it slides in from the left...
[super touchesBegan:touches withEvent:event];
CGPoint loc = [[touches anyObject] locationInView:self.view];
// Save last touch for reference...
lastTouch = loc;
// leftBeginRect is an area where the user can start to drag the panel...
// trackFinger defines whether the panel should move with the users gestures or not...
if (CGRectContainsPoint(leftBeginRect, loc) && canTrack) {
trackFinger = YES;
}
// Left view is a reference to the panel...
else if (leftView.frame.size.width >= 300) {
// This means that the panel is shown and therefore should track the user's finger back towards the edge of the screen...
CGRect frame = CGRectMake(250, 0, 100, self.view.frame.size.height);
if (CGRectContainsPoint(frame, loc) && canTrack) {
trackFinger = YES;
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
CGPoint loc = [[touches anyObject] locationInView:self.view];
// Need to work out the direction in which the user is panning...
if (lastTouch.x > loc.x) {
currentFingerDirection = RHFingerDirectionLeft;
}
else {
currentFingerDirection = RHFingerDirectionRight;
}
lastTouch = loc;
if (trackFinger) {
if (loc.x <= 300) {
// This means that the panel is somewhere between fully exposed and closed...
// This is where the frame for the left view (and the constraints) are adjusted according to the user's current finger position...
CGRect frame = leftView.frame;
frame.size.width = loc.x;
[leftView setFrame:frame];
leftViewConstraint.constant = loc.x;
if (loc.x <= 80) {
float percentage = loc.x / 80;
int amount = 100 * percentage;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = -amount;
[buttonView setFrame:otherFrame];
constraint.constant = constraintConstant + amount;
}
}
else {
CGRect frame = leftView.frame;
frame.size.width = 300;
[leftView setFrame:frame];
leftViewConstraint.constant = 300;
frame = buttonView.frame;
frame.origin.y = -100;
[buttonView setFrame:frame];
constraint.constant = constraintConstant + 100;
trackFinger = NO;
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// This method works out if the panel should pop open or spring closed when the user ends the gesture...
[super touchesEnded:touches withEvent:event];
if (trackFinger) {
CGPoint loc = [[touches anyObject] locationInView:self.view];
if (loc.x >= 50 && currentFingerDirection == RHFingerDirectionRight) {
CGRect frame = leftView.frame;
frame.size.width = 300;
leftViewConstraint.constant = 300;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = -100;
constraint.constant = constraintConstant + 100;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
else if (loc.x <= 250 && currentFingerDirection == RHFingerDirectionLeft) {
CGRect frame = leftView.frame;
frame.size.width = 0;
leftViewConstraint.constant = 0;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = 0;
constraint.constant = constraintConstant;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
else if (loc.x <= 150) {
CGRect frame = leftView.frame;
frame.size.width = 0;
leftViewConstraint.constant = 0;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = 0;
constraint.constant = constraintConstant;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
else {
CGRect frame = leftView.frame;
frame.size.width = 300;
leftViewConstraint.constant = 300;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = -100;
constraint.constant = constraintConstant + 100;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
trackFinger = NO;
}
currentFingerDirection = RHFingerDirectionNone;
}
The code is quite involved but it results in a nice panel animation that follows your finger like the Control Center.
Sorry for a late response. I created a small lib based on Rob-s answer.
https://github.com/hoomazoid/CTSlidingUpPanel
It's quite simple to use:
#IBOutlet weak var bottomView: UIView!
var bottomController:CTBottomSlideController?;
override func viewDidLoad() {
super.viewDidLoad()
//You can provide nil to tabController and navigationController
bottomController = CTBottomSlideController(parent: view, bottomView: bottomView,
tabController: self.tabBarController!,
navController: self.navigationController, visibleHeight: 64)
//0 is bottom and 1 is top. 0.5 would be center
bottomController?.setAnchorPoint(anchor: 0.7)
}
Related
I have a UITableView that is at the bottom of my main UIViewController, it currently only shows the top two rows. To show the rest I don't want it to scroll, I want the user to be able to "pull" the view up to reveal the additional 4 rows (can then swipe down on it to "push" it back into its original place), and I want the "pull" to be similar to how control center works in iOS. The spring that it has is really great.
Looks like I can add a UIPanGestureRecognizer to do the pull:
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1;
[self addGestureRecognizer:pan];
- (void)pan:(UIPanGestureRecognizer *)aPan; {
CGPoint currentPoint = [aPan locationInView:self];
[UIView animateWithDuration:0.01f
animations:^{
CGRect oldFrame = _viewToChange.frame;
_viewToChange.frame = CGRectMake(oldFrame.origin.x, currentPoint.y, oldFrame.size.width, ([UIScreen mainScreen].bounds.size.height - currentPoint.y));
}];
}
Ref this question.
Doing this does sort of work, but my UITableView flashes as you pull it up and it eventually disappears.
There is also no spring, and no way to set a "max" so that you can't pull up the view past a certain point.
Does anyone have ideas on how this can be achieved?
this is how i write the pan:. t is tableView(initially user interaction is disabled). 40 in code is for more realistic look in pan. 200 is max value i used and 60 is min value. i directly add t to tableview with height 60 and increased the height with animation.
- (void)pan:(UIPanGestureRecognizer *)aPan; {
CGPoint currentPoint = [aPan locationInView:self.view];
CGRect fr = t.frame;
if ((currentPoint.y - fr.origin.y) < 40 && (fr.size.height <= 200) )
{
float nh = (currentPoint.y >= self.view.frame.size.height - 200) ? self.view.frame.size.height-currentPoint.y : 200;
if (nh < 60) {
nh = 60;
}
[UIView animateWithDuration:0.01f animations:^{
[t setFrame:CGRectMake(0, self.view.frame.size.height-nh, t.frame.size.width, nh)];
}];
if (nh == 200) {
[t setUserInteractionEnabled:YES];
}
else
{
[t setUserInteractionEnabled:NO];
}
}
}
or without pan; with using tableview's scrollview delegate methods, when tableview begin dragging in closed state, opening the large mode and when tableview scroll to top sliding down
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(#"scroll");
if (scrollView.contentOffset.y == 0) {
[UIView animateWithDuration:0.2f animations:^{
[scrollView setFrame:CGRectMake(0, self.view.frame.size.height-200, t.frame.size.width, 200)];
}];
}
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y == 0) {
[UIView animateWithDuration:0.2f animations:^{
[scrollView setFrame:CGRectMake(0, self.view.frame.size.height-60, t.frame.size.width, 60)];
}];
}
}
i am using this code for zooming this is perfect but i want zoomout after this zoomin.
CGFloat s = 3;
CGPoint p = CGPointMake(150, 300);
CGAffineTransform tr = CGAffineTransformScale(self.view.transform, s, s);
CGFloat h = self.view.frame.size.height;
CGFloat w = self.view.frame.size.width;
[UIView animateWithDuration:2.5 delay:0 options:0 animations:^{
self.view.transform = tr;
CGFloat cx = w/2-s*(p.x-w/2);
CGFloat cy = h/2-s*(p.y-h/2);
self.view.center = CGPointMake(cx, cy); //was: (w*s/2,h-h*s/2);
} completion:^(BOOL finished) {}];
Try using hitTest and scroll view delegate method i.e, viewForZoomingInScrollView as follows.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return yourScrollView;
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return yourImageView;
}
You can set the selected image in another scrollview and handle the zoom.
- (void)setImage:(UIImage *)image {
[self.imageView setImage:image];
[self.imageView sizeToFit];
//add this image view as subview of scroll view here and set content size accordingly.
CGFloat zoomScale = 1.0;
if (self.imageView.frame.size.width < self.imageView.frame.size.height) {
zoomScale = (self.scrollView.frame.size.width / self.imageView.frame.size.width);
} else {
zoomScale = (self.scrollView.frame.size.height / self.imageView.frame.size.height);
}
[self.scrollView setContentSize:self.imageView.frame.size];
[self.scrollView setMinimumZoomScale:zoomScale];
[self.scrollView setMaximumZoomScale:1.0];
[self.scrollView setZoomScale:zoomScale];
[self.scrollView setContentOffset:CGPointMake((self.imageView.frame.size.width - self.scrollView.frame.size.width) / 2,
(self.imageView.frame.size.height - self.scrollView.frame.size.height) / 2)];
}
I have several buttons located at different sites in the view (storyboard), and I want that when I click on each of them to go to a given point,
To do this, I keep their location and initial size by:
CGPoint originalCenter1;
CGRect originalsize1;
CGPoint originalCenter2;
CGRect originalsize2;
in viewDidLoad
originalCenter1 = self.boton1.center;
originalsize1 = self.boton1.frame;
originalCenter2 = self.boton2.center;
originalsize2 = self.boton2.frame;
and the IBAction associated with each button, the animation ...
-(IBAction)move:(id)sender{
UIButton * pressedbutton = (UIButton*)sender;
[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
CGAffineTransform scale = CGAffineTransformMakeScale(3.0, 3.0);
pressedbutton.transform = scale;
switch(pressedbutton.tag)
{
case 0:
pressedbutton.center = CGPointMake(784, 340);
break;
case 1:
pressedbutton.center = CGPointMake(784, 340);
break;
When already all have moved, I have a button Refresh that puts me to the initial position.
-(IBAction)refresh:(id)sender{
self.boton1.frame = originalsize1;
self.boton1.center = originalCenter1;
self.boton1.alpha=1;
self.boton2.frame = originalsize2;
self.boton2.center = originalCenter2;
self.boton2.alpha=1;
The problem is that the next time that pulse buttons, move to the position shown in the animation but the "scale" effect, doesn't work !!
Any help ??
Thanks.
I think you should use block try this...
- (IBAction)tap:(UITapGestureRecognizer *)gesture
{
CGPoint tapLocation = [gesture locationInView:self.view1];
for (UIView *view in self.view1.subviews) {
if (CGRectContainsPoint(view.frame, tapLocation)) {
[UIView animateWithDuration:5.0 animations:^{
[self setRandomLocationForView:view];
}];
}
}
}
- (void)setRandomLocationForView:(UIView *)view
{
[view sizeToFit];
CGRect sinkBounds = CGRectInset(self.view1.bounds, view.frame.size.width/2, view.frame.size.height/2);
CGFloat x = your location.x;
CGFloat y = yout location.y;
view.center = CGPointMake(x, y);
}
I have an issue with editing table view:
I changed the minus sign location in edit mode to the right side of the cell.
The problem is that the button is sliding from the left and it makes it looks weird.
Is there a way to change this animation?
Also, there is an animation when undoing the delete option (clicking the minus sign when the red delete button is displayed), any idea why?
Here is a video showing the issue:
---edit---
This is how I changed the position:
- (void)layoutSubviews {
[super layoutSubviews];
//Indent to the left instead of right
self.contentView.frame = CGRectMake(0,
self.contentView.frame.origin.y,
self.contentView.frame.size.width,
self.contentView.frame.size.height);
if ((self.editing
&& ((state & UITableViewCellStateShowingEditControlMask)
&& !(state & UITableViewCellStateShowingDeleteConfirmationMask))) ||
((state & UITableViewCellStateShowingEditControlMask)
&& (state & UITableViewCellStateShowingDeleteConfirmationMask)))
{
float indentPoints = self.indentationLevel * self.indentationWidth;
self.contentView.frame = CGRectMake(indentPoints - 35,
self.contentView.frame.origin.y,
self.contentView.frame.size.width - indentPoints,
self.contentView.frame.size.height);
}
//Change editAccessoryView location
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.0f];
//If can't use private classes (UITableViewCellDeleteConfirmationControl..), use [self.subviews objectAtIndex:0];
for (UIView *subview in self.subviews) {
if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewCellDeleteConfirmationControl"]) {
CGRect newFrame = subview.frame;
newFrame.origin.x = 10;
subview.frame = newFrame;
}
else if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewCellEditControl"]) {
CGRect newFrame = subview.frame;
newFrame.origin.x = 280;
subview.frame = newFrame;
}
else if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewCellReorderControl"]) {
CGRect newFrame = subview.frame;
newFrame.origin.x = 200;
subview.frame = newFrame;
}
}
[UIView commitAnimations];
}
- (void)willTransitionToState:(UITableViewCellStateMask)aState {
[super willTransitionToState:aState];
self.state = aState;
}
---edit2---
I identified that the part that causes the minus sign jumping issue is the
//Change editAccessoryView location.
Without it there is no jumping but the minus button is back on the left side of the cell.
Any way around that?
Just a guess, but have you tried turning the view upside-down? You might be able to use a UIView transform.
(Code from the Internet, not sure how reliable it is)
CGAffineTransform rotateTransform;
rotateTransform = CGAffineTransformRotate(CGAffineTransformIdentity, M_PI);
myView.transform = rotateTransform;
harlanhaskins wrote:
I have a view called userSlider programmaticaly added in viewDidLoad:
self.userSlider = [[UserSliderView alloc] initWithFrame:CGRectMake(0, -120, 320, 155)];
[self.userSlider setUser:user];
[self.userSlider configureView];
[self.view addSubview:self.userSlider];
And a tableview called gameTableView.
I originally had added the userSlider into the xib as a custom view (because UserSlider has its own xib), and when I implemented these touch methods:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = (UITouch*)[touches anyObject];
start = [touch locationInView:self.view].y;
}
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(start < 0)
{
return;
}
UITouch *touch = (UITouch *)[touches anyObject];
CGFloat now = [touch locationInView:self.view].y;
CGFloat diff = now - start;
sliderDirectionIsUp = diff < 0; //sliderDirectionIsUp is a BOOL
if ((self.userSlider.frame.origin.y == 0) && ((now < 120) || (now > 155))) {
return;
}
float newCenterY = self.userSlider.center.y + diff;
if (newCenterY < roundf(self.userSlider.frame.size.height / 2)) {
self.userSlider.center = CGPointMake(self.userSlider.center.x, newCenterY);
CGRect tableViewFrame = self.gameTableView.frame;
tableViewFrame.origin.y += diff;
tableViewFrame.size.height -= diff;
[self.gameTableView setFrame:tableViewFrame];
}
start = now;
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CGRect tableViewFrame = self.gameTableView.frame;
if (sliderDirectionIsUp)
{
tableViewFrame.origin.y = 28;
tableViewFrame.size.height = (self.view.frame.size.height - tableViewFrame.origin.y);
//animate userSlider out of visible area
[UIView animateWithDuration:.3 animations:^{
self.userSlider.center = CGPointMake(self.userSlider.center.x, -roundf(self.userSlider.bounds.size.height/2.) + 35);
[self.gameTableView setFrame:tableViewFrame];
}];
}
else if(start >= 0)
{
tableViewFrame.origin.y = (self.userSlider.frame.size.height - 7);
tableViewFrame.size.height = (self.view.frame.size.height - tableViewFrame.origin.y);
//animate userSlider with top to mainviews top
[UIView animateWithDuration:.3 animations:^{
self.userSlider.center = CGPointMake(self.userSlider.center.x, roundf(self.userSlider.bounds.size.height/2.) - 0.5);
[self.gameTableView setFrame:tableViewFrame];
}];
}
}
then both views would move how I wanted them to.
But now when I add it programmatically, suddenly the tableView doesn't change unless I tap inside the area after moving it. It's really weird.
Any ideas?
If your userSlider`s frame is
CGRectMake(0, -120, 320, 155)
I doubt whether your userSlider can receive the sender event .Because if a UIControl`s frame be out of the superView , it may not receive the sender event(the outside part) . You can set the frame
CGRectMake(0, 0, 320, 155)
to have a try .