I created a simple window with the purpose of being something like a "Wizard" (I know Apple guidelines basically forbid you to, I tried to convince the customer, but whatever.
It is just a simple view with two Custom Views inside, one in the bottom part which contains a "previous" and "next" button, and a bigger view at the top which takes most of the space.
I called the bottom view "NavigationView" and the top one "ContainerView".
I created an array to hold a series of views the user is supposed to navigate through with the "next" and "previous" buttons.
So, here's my code.
- (IBAction) next:(id)sender{
currentViewIndex++;
[self animatePushView:YES];
}
- (IBAction)previous:(id)sender{
currentViewIndex--;
[self animatePushView:NO];
}
- (void) animatePushView:(BOOL)forward{
NSView *nextView = [viewCollection objectAtIndex:currentViewIndex];
for (NSView *subView in [containerView subviews]) {
[subView removeFromSuperview];
}
[containerView addSubview:nextView];
[nextView setFrame:containerView.bounds];
[containerView setNeedsDisplay:YES];
}
It's pretty straightforward I think. I have an array which contains the next view to be displayed.
What happens is that I find the next view centered in the lower left part of the ContainerView. Why does this happen?
Also, as you may have guessed, I'm a newbie at managing views, even though I've been working on objective-c for quite some time, so if there's some best practice I'm missing I'm open to suggestions.
Thanks!
EDIT:
I forgot to add:
Some of these views have different sizes, and I would like to be able to change the window size according to the view size.
[nextView setFrame:containerView.bounds];
You are assigning container view bounds to the next view frame (doc).
What you probably want is assigning the current view frame to the next view frame, and possibly adjust width and height.
Keep a reference to the current displayed view, something like this (_currentView is an ivar of type NSView *) :
- (IBAction) next:(id)sender{
currentViewIndex++;
[self animatePushView:YES];
}
- (IBAction)previous:(id)sender{
currentViewIndex--;
[self animatePushView:NO];
}
- (void) animatePushView:(BOOL)forward{
NSView *nextView = [viewCollection objectAtIndex:currentViewIndex];
[nextView setFrame:_currentView.frame];
[_currentView removeFromSuperview]; // _currentView is retained in the collection
[containerView addSubview:nextView];
_currentView = nextView;
[containerView setNeedsDisplay:YES];
}
Ok, I figured it out.. Finally..
The problem was that the first view I had to show was already contained in the container view in the .xib file.
I don't really know why, but it probably caused some problem with the retain count of the container view, because it was released on the first click.
Releasing the container view would reposition the view on (0,0) probably because its frame was null, and the view would flash because it wasn't retained correctly.
Removing the view from the .xib file and adding it via code works properly anyway.
Related
Here's how the scroll views work: One scroll view is paging enabled in the horizontal direction. Each 'page' of this scroll view contains a vertically scrolling UITableView. Without modification, this works OK, but not perfectly.
The behaviour that's not right: When the user scrolls up and down on the table view, but then wants to flick over to the next page quickly, the horizontal flick/swipe will not work initially - it will not work until the table view is stationary (even if the swipe is very clearly horizontal).
How it should work: If the swipe is clearly horizontal, I'd like the page to change even if the table view is still scrolling/bouncing, as this is what the user will expect too.
How can I change this behaviour - what's the easiest or best way?
NOTE For various reasons, a UIPageViewController as stated in some answers will not work. How can I do this with cross directional UIScrollViews (/one is a table view, but you get the idea)? I've been banging my head against a wall for hours - if you think you can do this then I'll more than happily award a bounty.
According to my understanding of the question, it is only while the tableView is scrolling we want to change the default behaviour. All the other behaviour will be the same.
SubClass UITableView. UITableViews are subClass of UIScrollViews. On the UITableView subClass implement one UIScrollView's UIGestureRecognizer's delegate method
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer
{
//Edit 1
//return self.isDecelerating;
//return self.isDecelerating | self.bounces; //If we want to simultaneous gesture on bounce and scrolling
//Edit 2
return self.isDecelerating || self.contentOffset.y < 0 || self.contentOffset.y > MAX(0, self.contentSize.height - self.bounds.size.height); // #Jordan edited - we don't need to always enable simultaneous gesture for bounce enabled tableViews
}
As we only want to change the default gesture behaviour while the tableView is decelerating.
Now change all 'UITableView's class to your newly created tableViewSubClass and run the project, swipe should work while tableView is scrolling. :]
But the swipe looks a little too sensitive while tableView is scrolling. Let's make the swipe a little restrictive.
SubClass UIScrollView. On the UIScrollView subclass implement another UIGestureRecognizer's delegate method gestureRecognizerShouldBegin:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self];
if (abs(velocity.y) * 2 < abs(velocity.x)) {
return YES;
}
}
return NO;
}
We want to make the "swipe is clearly horizontal". Above code only permits gesture begin if the gesture velocity on x axis is double than on y axis. [Feel free to increase the hard coded value "2" if your like. The higher the value the swipe needs to be more horizontal.]
Now change the `UiScrollView' class (which has multiple TableViews) to your ScrollViewSubClass. Run the project. :]
I've made a project on gitHub https://github.com/rishi420/SwipeWhileScroll
Although apple doesn't like this method too much:
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
I've found a great way to accomplish this.
This is a complete solution for the problem. In order to scroll the UIScrollView while your UITableView is scrolling you'll need to disable the interaction you have it.
- (void)viewDidLoad
{
[super viewDidLoad];
_myScrollView.contentSize = CGSizeMake(2000, 0);
data = [[NSMutableArray alloc]init];
for(int i=0;i<30;i++)
{
[data addObject:[NSString stringWithFormat:#"%d",i]];
}
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer
{
[_myTableView setContentOffset:_myTableView.contentOffset animated:NO];
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = YES;
}
To sum up the code above, if the UITableView is scrolling, set userInteractionEnabled to NO so the UIScrollView will detect the swipe. If the UITableView is scrolling and the user taps on the screen, userInteractionEnabled will be set to YES.
Instead of using UIScrollView as a container for these multiple table views, try using a UIPageViewController.
You can even integrate this into your existing view controller setup as a child view controller (directly replacing the UIScrollView).
In addition, you'll likely want to implement the required methods from UIPageViewControllerDataSource and possibly one or more of the methods from UIPageViewControllerDelegate.
Did you try the methods : directionalLockEnabled of both your table and scroll and set them up to horizontal for one and vertical for the other ?
Edit :
1)
What you want to do is very complicate since the touch wait some time (like 0.1s) to know what your movement will be. And if your table is moving, it will take your touch immediately whatever it is (because it's suppose to be reactive movement on it).
I don't see any other solution for you but to override touch movement from scratch to detect immediately the kind of mouvement you want (like if the movement will be horizontal) but it will be more than hard to do it good.
2)
Another solution I can advise you is to make your table have left and right margin, where you can touch the parent scroll (pages thing so) and then even if your table is scrolling, if you touch here, only your paging scroll will be touched. It's simpler, but could not fit with your design maybe...
Use UIPageViewController and in the -viewDidLoad method (or any other method what best suits your needs or design) get UIPageViewController's UIScrollView subview and assign a delegate to it. Keep in mind that, its delegate property won't be nil. So optionally, you can assign it to another reference, and then assign your object, which conforms to UIScrollViewDelegate, to it. For example:
id<UIScrollViewDelegate> originalPageScrollViewDelegate = ((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]).delegate;
[((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]) setDelegate:self];
So that you can implement UIScrollViewDelegate methods with ease. And your UIPageViewController will call your delegate's -scrollViewDidScroll: method.
By the way, you may be obliged to keep original delegate, and respond to delegate methods with that object. You can see an example implementation in ViewPagerController class on my UI control project here
I faced the same thing recently. My UIScrollview was on paging mode and every page contained a UITableView and like you described it worked but not as you'd expected it to work. This is how solved it.
First I disabled the scrolling of the UIScrollview
Then I added a UISwipeGestureRecognizer to the actual UITableView for left and right swipes.
The action for those swipes were:
[scroll setContentOffset:CGPointMake(currentPointX + 320, PointY) animated:YES];
//Or
[scroll setContentOffset:CGPointMake(currentPointX - 320 , PointY) animated:YES];
This works flawlessly, the only down side is that if the user drags his finger on the UITableVIew that will be considered as a swipe. He won't be able to see half of screen A and half of screen B on the same screen.
You could subclass your scroll view and your table views, and add this gesture recognizer delegate method to each of them...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I can't be sure this is exactly what you are after, but it may come close.
I have a app out for testing right now that's almost completely done - just a few bug fixes left. Unfortunately, the customer decided that they'd like the entire main page of the app to be inside of a scroll view (so that there's more room for the table at the bottom). I already have everything set up and I don't really want to move everything and change references. Is there an easy way to change the class of the main view to a scroll view? I've already tried changing the class in IB and setting the class type in the init method. If there isn't I'll probably just throw the top section of the view into a nib file and load it as a custom cell.
-EDIT- I ended up changing the class type in IB and then doing
[(UIScrollView *) self.view setScrollEnabled:YES];
[(UIScrollView *) self.view setContentSize:CGSizeMake(0,2000)];
in viewDidLoad. Thanks for the help, wish I could accept all your answers.
When you are referring to [self view], I am going to assume you mean in a view controller. The view of a view controller can be any view that derives from UIView. Thus a scrollview is completely acceptable.
I don't really want to move everything and change references.
what would you have to move? why would you have to change references? Only thing you should need to do is add a scroll view to your view controller, set the view controllers view to it, and add the current view as a subview to the new scroll view. No references need to be changed, nothing has to be moved.
Refer to loadView method in documentation of view controller.
Here is a simple (untested!) example
- (void)loadView {
UIScrollView *scrollView = [[UIScrollView alloc] init] autorelease];
//Set the properties of scrollview appropriately....
self.view = scrollView;
}
Now the root view of your view controller will be a scroll view.
Note
- As the documentation states, do not do this if you are using interface builder to initialize your views/view controller. I could not tell from your description if this was the case or not. If it is, you should be able to change the view type in interface builder.
You need to set the contentSize property of your scrollview.
Since you are using IB, the easiest way to do this is to put all your UI elements into a view and add this single view to your scroll view. In the viewDidLoad method, set the content size of the scrollview to be the same size as the view that contains all your UI.
As an aside, there are much easier ways to reference views than walking down the view hierarchy, as you seem to be doing. viewcontroller.view.something.tableview. Add a connection to the tableview from your view controller in IB and it doesn't matter where that tableview is in the view hierarchy. You'll always be able to reach it from viewcontroller.tableview, no matter how you rearrange your nibs.
I think you have to use a pointer with proper type. Example for Google Maps: let's say you changed you base view's class to GMSMapView.
MapViewController.h
#property GMSMapView *mapView;
MapViewController.m
-(void)awakeFromNib{
[super awakeFromNib];
self.mapView = (GMSMapView*)self.view;
// ... etc.
}
In my application window I have two NSViews. On the left the NSView ("Menu") contains a few buttons. When one of the buttons is clicked it should change the contents of the right NSView ("Content").
For each of the views on the right I have a separate NSViewControllers that get loaded and their views gets added as a subview. When a further button gets pressed on the left the added subviews on the right should be removed and the new view should be loaded as a subview.
To accomplish this I load my Menu in AppDelegate with the following:
MenuVC *menuSubView = [[MenuVC alloc] initWithNibName:#"MenuVC" bundle: nil];
menuSubView.contentView = (NSView*)[self contentView];
[[self menuView] addSubview:[menuSubView view]];
This works fine. As you can see I have a NSView pointer in the Menu VC which points to the contentView so that I can populate it with the subviews.
Now as a method for one of the button presses I do the following:
SomeContentVC *subView = [[SomeContentVC alloc] initWithNibName:#"SomeContentVC" bundle:nil];
[self.contentView addSubview:[subView view]];
This does not work.
If I however add a subview from the awakeFromNib method of the MenuViewController implementation (in the case of default content when the app opens) it works. However when I try to remove that subview using
[[self.contentView setSubviews:[NSArray array]];
I can't. Interesting is also that if I try to count the number of subviews (even after having added one in the awakeFromNib method) it returns 0 subviews for self.contentView. Why? How can I get it to work properly?
Thanks
The fact that messaging self.contentView achieves nothing except, for some things, returning 0 probably means that self.contentView is nil.
Do you perhaps have two instances of MenuVC by accident? Perhaps one instantiated in a NIB and one instantiated in code?
When in doubt, log everything. Log self in various methods. Log menuSubView just after you create it. Log menuSubView.contentView just after you assign it. Etc. Eventually, you'll probably see that you're interacting with different objects than you thought you were.
Is there a proper way to determine if a NSView is actually drawn in the current view hierarchy or not, considering cases like:
The view is completely offscreen (not mandatory)
The view is not on top of the view hierarchy
The -isHidden and -isHiddenOrHasHiddenAncestor are unfortunately not set when e.g. a view disappears because a tab view switches to another tab.
The reason for this is that I have an attached child window and I would like to be able to hide it as well when the view that it is attached to is not drawn.
I have found a trick to tell if it is visible, but it requires subclassing. It works by toggling an ivar on 2 events.
- (void)discardCursorRects {
isDrawn_ = NO;
[super discardCursorRects];
}
- (void)resetCursorRects {
isDrawn_ = YES;
[super resetCursorRects];
}
Whether (or when) it's drawn is supposed to be "none of your business" and really have nothing to do with whether or not it's on-screen. Use NSView's -viewDidMoveToSuperview or -viewDidMoveToWindow to manage this.
I'm trying to add a split view inside of a tab bar, and since the split view isn't the root, it doesn't properly get the rotation notifications, so the delegate's methods are never called to add the button to the toolbar in the detail view.
I've rigged it up so I can generate the popover when rotated, but when this method is called, the view dissappears from the landscape mode, and if you activate it and then rotate back into landscape, it's a black empty box where the master view used to be. How do I get rid of this occuring?
-(void) displayPopover:(id)sender
{
//Toggle the popover: if it's showing, hide it
if (popoverController != nil && [popoverController isPopoverVisible])
{
[popoverController dismissPopoverAnimated:NO];
}
else
{
//Create a Popover displaying the master view
if (popoverController == nil)
{
popoverController=[[UIPopoverController alloc] initWithContentViewController:self->rootController];
popoverController.popoverContentSize=CGSizeMake(300, 500);
}
[popoverController presentPopoverFromBarButtonItem:[detailController.toolbar.items objectAtIndex:0] permittedArrowDirections:UIPopoverArrowDirectionAny animated:NO];
}
You will need to remove all the objects from window using:
[appdelegate window ] subviews] objectAtIndex:0] removeFromSuperview];
Then add your splitview to the window, you can get the view callbacks.
I would recommend either finding a way to get your SplitViewController to be root, or creating a custom subclass of the UISplitViewController that allows for non-root placement.
I really like what Matt Gemmell did here: http://mattgemmell.com/2010/07/31/mgsplitviewcontroller-for-ipad
Using a custom subclass like Matt's will allow you to benefit from all the same delegate methods that a SplitView as root would allow. I used it in a project where I wanted my SplitView to appear as a modal - almost impossible with a traditional UISplitViewController.
so your split view has rotation enabled (shouldAutorotateToInterfaceOrientation:) now you have to make sure that the tab controller has also rotation enabled (should be the appDelegate, am I right?) AND you have to make sure that every other view that is in your TabBar has also rotation enabled!
so if your TabBar contains 2 tabs you have to set the rotation in 3 classes.