I'm trying to make sense out of an old objective-C code from my predecessor and I am quite stumped on this problem.
We have a DashboardView class, and a DashboardViewController class which updates the former. DashboardView class has UITableView *m_data_tbl that shows the data in question.
on -(void)connectionDidFinishLoading of DashboardViewController, it invokes [m_dashboard_view setNeedsLayout], where m_dashboard_view is a member variable of DashboardViewController.
setNeedsLayout of DashboardView is implemented as follows:
- (void) setNeedsLayout {
[self populateHeaderScrollView];
[self populateData];
[m_data_tbl reloadData];
}
where the first two functions populate the data and header scrolls.
The problem is that upon the first execution, only a part of the data is shown. Namely, the last two columns(the rightmost when scrolled) do not show at all. It is impossible to scroll to show the last two columns. When the user rotates from the portraits to landscape does the last two columns appear.
Needless to say, the following is the implementation of didRotateFromInterfaceOrientation of DashboardController:
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
/* animation codes are omitted */
if(UIDeviceOrientationIsLandscape) {
[m_dashboard_view adjustForType:0];
}
else {
[m_dashboard_view adjustForType:1];
}
adjustForType is implemented as:
-(void)adjustForType:(NSInteger)type
{
if(type == 0)
{
m_data_tbl.frame = CGRectMake
m_hdr_scroll.frame = CGRectMake
}
else
{
m_data_tbl.frame = CGRectMake
m_hdr_scroll.frame = CGRectMake
}
[m_data_tbl reloadData];
}
In portrait, the frame size is identical to the one is originally initialized with, and the data loaded does not get altered during the execution time; just whether the last two columns are displayed or not are different.
Not having done much iPhone development this issue is quite stumping me.
Related
I'm building an app which consists of different views which are closely related to each other. So far, I only have one UIViewController which controls these different views. View 1 and 2 share the same background, for instance, and the transition between view 1 and 2 is a custom animation.
My problem is that both view 1 and 2 have an UIScrollView. My UIViewController is their delegate and I could have the following scrollViewDidScroll to distinguish between the two scrollviews:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.tag == 1)
//handle a
else if (scrollView.tag == 2)
//handle b
else if (scrollView.tag == 3)
//handle c
}
As a lot happens with scrollView 1 and different things happen with scrollView2, the code will become really messy. Ideally, I'd like to define in a separate file what happens if scrollView1 is scrolled etc.. Yet I don't want to have another UIViewController as then transitions become more difficult. I don't have a NavBar or ToolBar, so neither UINavigationController nor UITabBarController would work very well in my case.
What should I do?
I posted a similar question here.
If you don't want two view controllers, just create a separate delegate for each scroll view. Make it an NSObject which conforms to UIScrollViewDelegate and create it at the same time as the scroll view.
Seems to combine the results you seek: one view controller, but encapsulated scroll view code.
You could have a base controller class that handles the common functionality. Each different controller can inherit from this and override with their specific functionality as required.
Aka the template pattern
Edit
To expand. You say you want only one view controller. So you should create a separate class to handle the individual functionality. The View Controller has a base class pointer which gets swapped around according to the current view.
In pseudo code :
class BaseFunctionality
-(void) handleDidScroll {}
end
class ScrollViewAFunctionality : BaseFunctionality
-(void) handleDidScroll {
// Lots of interesting technical stuff...
}
end
class ScrollViewBFunctionality : BaseFunctionality
-(void) handleDidScroll {
// Lots of interesting technical stuff...
}
end
class TheViewController : UIViewController
BaseFunctionality *functionality;
-(void) swapViews {
// Code to swap views
[this.functionality release];
if (view == A)
this.functionality = [[ScrollViewAFunctionality alloc] init]
else if ( view == B)
this.functionality = [[ScrollViewBFunctionality alloc] init]
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[this.functionality handleDidScroll];
}
end
In my MainControllerClass I have a few XIB's that I am initializing and throwing on the screen - Here is an Example
self.widgetPeopleToBuyFor = [[WidgetAddPeopleToBuyFor alloc] initWithNibName:#"WidgetAddPeopleToBuyFor" bundle:nil andUser:self.currentUser];
self.widgetPeopleToBuyFor.view.frame = CGRectMake(10,10,492,537);
self.widgetPeopleToBuyFor.delegate = self;
[self.viewMainView addSubview:self.widgetPeopleToBuyFor.view];
[self.viewMainView bringSubviewToFront:self.widgetPeopleToBuyFor.view];
if ([self.currentUser.wPeopleToBuyForRect length] > 2) {
NSLog(#"LOADING FROM DB");
CGRect rect9 = CGRectFromString(self.currentUser.wPeopleToBuyForRect);
self.widgetPeopleToBuyFor.view.frame = rect9;
}
else
{
NSLog(#"FIRST TIME");
self.widgetPeopleToBuyFor.view.frame = CGRectMake(10,10,492,537);
}
I'm building this app so the user can resize the the view to their desire, move it around etc. So when he user Exits I call this method to grab the view information.
- (NSString *) showInfoViewSize: (UIView *) view
{
return NSStringFromCGRect(view.frame);
}
So from there I save the information in the Database, it saves ok - But I just learned the size doesn't matter - I could change it to (10,10,200,200) and the view stays the same exact size on the screen.
So I know I threw a buch at you there. But in the End, the DB works fine - the IF statement gets fired off - Its just I can't resize the XIB on launch.
Should I be resizing it a different way? Should I keep track of the TRANSFORM from the pinch recoginier and just retransform it the scaleFactor at load? Seems kinda hokey to me.
Any help is appreciated - I've been beating myself up on this, and I bet its something super simple I'm missing.
view does not loads from xib until it is presented and hence at that time there will be no frame to set for view. let the view load first then set its frame afterwards.
change the view.frame in "viewWillAppear" in your controller's implementation.
Normally when something is just not working, it's because you've got a nil floating around somewhere you didn't expect. Remember, sending messages to nil is completely legal in ObjC (it just always returns nil).
So I'd step through this in the debugger and check everything to make sure it's a real instance and not nil.
But generally what you're doing (allocing a controller with a nib, setting that controller's view's frame, then adding the controller's view to a superview) is the correct way to do it.
I had a TTTableViewController used in iPad and initially I want it to be empty. When it first loads it actually calls:
- (id)initWithNavigatorURL:(NSURL*)URL query:(NSDictionary*)query {
if (self = [super init]) {
self.dataSource = nil;
}
return self;
}
However, the "loading" spinner stays in there and won't go away.
Why is this? I thought that this could happen because init wasn't called, but indeed it is.
I need some help.
When a TTTableViewController is presented on screen, it accesses it's model. If there's no model set, like in your case it creates a model in [TTModelViewController createInterstitialModel]. By default this will be a TTModel (the class not the protocol), which in turn does nothing then appearing to be loading.
In your createModel implementation you would need to create a model that does what you want and assign that to self.model.
Also note, that creating dataSources and / or model in the initializer is not optimal, consider creating your dataSources / models in createModel. They will be created only when needed (when the view appears on screen).
Do you know a simple (or not simple) way to hide a view (or anything like a view) and let the other views of the screen use the place left blank ? And make the opposite when showing back that view. Something like Android Visibility = GONE for layers.
Thank you
Many UIKit classes have a property hidden that does what you want. It is defined in UIView so you will find it in most visual elements you use.
There is no such thing as Visibility.GONE, as far as my research has shown, not even AutoLayout can help you. You have to manually replace the views affected by the optionally shown component (in my case, all the views below the optionalView on bottomView):
- (IBAction)toggleOptionalView:(id)sender {
if (!_expanded) {
self.optionalView.frame = CGRectMake(self.optionalView.frame.origin.x, self.optionalView.frame.origin.y, self.optionalView.frame.size.width, _optionalHeight);
self.bottomView.frame = CGRectMake(self.bottomView.frame.origin.x, self.bottomView.frame.origin.y+_optionalHeight, self.bottomView.frame.size.width, self.bottomView.frame.size.height);
_expanded = YES;
} else {
self.optionalView.frame = CGRectMake(self.optionalView.frame.origin.x, self.optionalView.frame.origin.y, self.optionalView.frame.size.width, 0);
self.bottomView.frame = CGRectMake(self.bottomView.frame.origin.x, self.bottomView.frame.origin.y-_optionalHeight, self.bottomView.frame.size.width, self.bottomView.frame.size.height);
_expanded = NO;
}
}
It is advisable not to hard-code the height of the optional component, otherwise your code breaks every time you edit the XIB/Storyboard. I have a field float _optionalHeight which I set in viewDidLoad, so it is always up to date.
I have a UIScrollView with 2 pages, and I can scroll horizontally between them. However, on one of my pages, I have a UIDatePicker, and the scroll view is intercepting the vertical touch events so I can no longer manipulate the date picker (except by clicking or tapping). Is there some way to tell the ScrollView to send the vertical touch events to the date picker, but send the horizontal touch events to the scroll view to switch pages?
Actually, there is a much simpler implementation than what Bob suggested. This works perfectly for me. You will need to subclass your UIScrollview if you haven't already, and include this method:-
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UIPickerView class]])
{
self.canCancelContentTouches = NO;
self.delaysContentTouches = NO;
}
else
{
self.canCancelContentTouches = YES; // (or restore bool from prev value if needed)
self.delaysContentTouches = YES; // (same as above)
}
return result;
}
The reason I use result.superview is that the view which gets the touches will actually be a UIPickerTable, which is a private API.
Cheers
I think there's two parts to this problem. The first is determining the user's intent, and the second is getting the correct control to respond to that intent.
Determining Intent
I think it's important to be clear about what the user intends. Imagine this scenario: The user starts touching the screen and moves his finger far to the left, but also up a little. The user probably intended to scroll the view, and didn't intend to change the date at all. It would be bad to both scroll the view and change the date, especially just as it moves off-screen. So to determine what the user intends I suggest the following algorithm:
When the user starts touching the screen, record the starting position. As the user's finger starts to move away from that position, the controls should not react at all. Once the touch moves past a certain threshold distance from the starting position, determine whether it moved more horizontally or vertically.
If it moved vertically, the user intends to change the date, so ignore the horizontal portion of the movement and only change the date.
If it moved more horizontally, the user intends to scroll the view, so ignore the vertical portion of the movement and only scroll the view.
Implementation
In order to implement this, you need to handle the events before the UIScrollView or date picker do. There's probably a few ways to do this, but one in particular comes to mind: Make a custom UIView called ScrollingDateMediatorView. Set the UIScrollView as a child of this view. Override the ScrollingDateMediatorView's hitTest:withEvent: and pointInside:withEvent: methods. These methods need to perform the same kind of hit testing that would normally occur, but if the result is the date picker, return self instead. This effectively hijacks any touch events that were destined for the date picker, allowing the ScrollingDateMediatorView to handle them first. Then you implement the algorithm described above in the various touches* methods. Specifically:
In the touchesBegan:withEvent method, save the starting position.
In touchesMoved:withEvent, if the user's intent isn't known yet, determine whether the touched has moved far enough away from the starting position. If it has, determine whether the user intends to scroll or change the date, and save that intent.
If the user's intent is already known and it's to change the date, send the date picker the touchedMoved:withEvent message, otherwise send the UIScrollView the touchesMoved:withEvent message.
You'll have to do some simliar work within touchesEnded:withEvent and touchesCancelled:withEvent to make sure the other views get the appropriate messages. Both of these methods should reset the saved values.
Once you have it properly propagating events, you'll probably have to try some user testing to tune the movement threshold.
Awesome help Sam! I used that to create a simple category that swizzles the method (because I was doing this in a UITableViewController and thus would have had to do some really messy stuff to subclass the scroll view).
#import <UIKit/UIKit.h>
#interface UIScrollView (withControls)
+ (void) swizzle;
#end
And the main code:
#import </usr/include/objc/objc-class.h>
#import "UIScrollView+withControls.h"
#define kUIViewBackgroundImageTag 6183746
static BOOL swizzled = NO;
#implementation UIScrollView (withControls)
+ (void)swizzleSelector:(SEL)orig ofClass:(Class)c withSelector:(SEL)new;
{
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if (class_addMethod(c, orig, method_getImplementation(newMethod),
method_getTypeEncoding(newMethod))) {
class_replaceMethod(c, new, method_getImplementation(origMethod),
method_getTypeEncoding(origMethod));
} else {
method_exchangeImplementations(origMethod, newMethod);
}
}
+ (void) swizzle {
#synchronized(self) {
if (!swizzled) {
[UIScrollView swizzleSelector:#selector(hitTest:withEvent:)
ofClass:[UIScrollView class]
withSelector:#selector(swizzledHitTest:withEvent:)];
swizzled = YES;
}
}
}
- (UIView*)swizzledHitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView* result = [self swizzledHitTest:point withEvent:event]; // actually calling the original hitTest method
if ([result.superview isKindOfClass:[UIPickerView class]]) {
self.canCancelContentTouches = NO;
self.delaysContentTouches = NO;
} else {
self.canCancelContentTouches = YES; // (or restore bool from prev value if needed)
self.delaysContentTouches = YES; // (same as above)
}
return result;
}
#end
Then, in my viewDidLoad method, I just called
[UIScrollView swizzle];