I am fairly new to programming in general, and have been following the CS193p videos on iTunesU. I am currently doing assignment 3, and am having trouble getting a bit of information from the View sent to the View Controller.
I believe I have set up the whole delegation thing correctly, so the question really is to how to get my View Controller to see a bit of information (such as self.bounds.size.width), which is a property that only the View has. Would this involve using self.dataSource? And if so, through what means could I pass this bit of information? My end goal is to have the View Controller perform some transformation to the View's properties, and send it back to the View's drawRect so that it could be drawn.
Thanks!!
** Edit, as requested, I have posted parts of my drawRect code below
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat scale = 32;
CGPoint midPoint;
midPoint.x = self.bounds.origin.x + self.bounds.size.width/2;
midPoint.y = self.bounds.origin.y + self.bounds.size.height/2;
// ---- finding the y starting point
float totalXsInWidth;
totalXsInWidth = self.bounds.size.width / scale;
float leftMostX = totalXsInWidth / -2;
float graphResultY = sin(leftMostX); // ** in theory, I want "leftMostX" to be modifed by the equation entered (in the CONTROLLER)
NSLog(#"The leftMostX is %f", leftMostX);
[self.dataSource passingVariable:leftMostX]; //** Here I pass the variable from drawRect to get modifyed in the CONTROLLER
float graphResultY1;
graphResultY1 = 5; //this is a test, I want to see if the controller actually effect a change
graphResultY1 = [self.dataSource calcResult:self]; //this should now be a different number than 5 or 0 (the init value)
NSLog(#"From graphingview, the result is %f", graphResultY1); //** unfortunately = 0... :(
Having had a read of CS193p Assignment 3 (for anyone interested, it is available here in PDF format), it looks like you are being asked to create a protocol for your UIView subclass, and have the delegate of that protocol (the view's managing view controller) provide the data used for drawing.
If you have set up your protocol correctly, the view's drawRect method should be asking the view controller for data through the protocol's method, something like:
DataObject *data = [self.delegate getData];
// It might be [self.dataSource getData]; in your case
That should call the getData delegate method that you should have written into the view controller (I have made up a method signature for this, adapt to the one used by your protocol. Also, this code doesn't consider relevant memory management, if required):
- (DataObject *)getData
{
// Get data from the model, and return
DataObject *dataObjectToReturn = [Model getRelevantData];
return dataObjectToReturn;
}
The view's drawRect should now have the relevant instance of DataObject and can go about using that data to draw what it needs to draw.
Below is my original answer, which is not relevant to the specific problem above, but does show another method of view/view controller interaction. (This method isn't applicable to the above problem because the data needed for drawing shouldn't be owned by the view itself.)
To access the view's properties from that view's controller you would use self.view. Thus, to get the view's width, like in your example, you would use:
CGFloat viewWidth = self.view.bounds.size.width;
You can also set the controller's view, though note that for your example you have to supply an entire CGRect for the frame, as manipulating the width directly is not allowed:
// This gets a reference to the view's frame, then uses values from it to create a new
// CGRect that is 10.0 points wider, and sets the frame of the view to the new frame
CGRect viewFrame = self.view.frame;
self.view.frame = CGRectMake(viewFrame.origin.x, viewFrame.origin.y, viewFrame.size.width + 10.0f, viewFrame.size.height);
Im not famaliar with the assignment but if you are using a view within a view controller you can access that views property like so:
SomeView *someView = [SomeView new];
CGFloat width = someView.bounds.size.width;
Related
I am working on multiple terminal screen app, for that I have a custom UIView subclass for the terminal views. Every time I need a new terminal screen, I prepare a new view.
This view class draws the text using a CGContextRef. The problem I am facing is that the context only draws the text of the last view that was created, e.g. if I have 3 terminals and drawing on first/second, it still draws on the third one.
My code so far:
-(void)drawRect:(CGRect)rect{
contxt = UIGraphicsGetCurrentContext();
}
-(void)setNeedsDisplayInRect:(CGRect)rect{
UIGraphicsPushContext(contxt);
//CGContextSaveGState(contxt);
CGContextSetTextMatrix(contxt,CGAffineTransformIdentity);
if (translated) {
CGContextScaleCTM(contxt, 1, -1);
translated = NO;
}
CGRect rectConvert = CGRectMake(rect.origin.x, rect.origin.y-screenWindowHeight, rect.size.width, rect.size.height);
CGContextSetFillColorWithColor(contxt, bgColor.CGColor);
CGContextFillRect(contxt, rectConvert);
if (!isDeleteChar) {
CGContextSetFillColorWithColor(contxt, fgColor.CGColor);
[displayString drawInRect:rectConvert withFont:font lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentLeft];
}
if (ul == EXTENDED_5250_UNDERLINE) {
CGContextSetFillColorWithColor(contxt, fgColor.CGColor);
[#"_" drawInRect:rectConvert withFont:font lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentLeft];
}
//CGContextRestoreGState(contxt);
UIGraphicsPopContext();
}
Finally I solved it by own using
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]]; just after setNeedsDisplay.
First and foremost, you should not be doing drawing in the -setNeedsDisplayRect: method, all of your drawing code should be in drawRect: instead. This way, the main runloop can better organize redrawing of the views.
Second, I suspect the variables that you are using for your CGRect conversions are faulty and are drawing outside of the view bounds. You can test this premise by clipping the view's drawing (set layer.masksToBounds to YES for the views)
If this is the case, you can adjust them to be relative to the view (all drawing within the view should be relative to its bounds, not its frame). When drawing the view, assume a canvas that stretches the bounds property of the view, i.e origin at (0,0) and size of (width,height).
Now, it is worth also discussing what the rect property passed to drawRect: really is, it is not guaranteed to be the entire bounds of the view, so you should not assume that. It is the portion of the view that needs to be redrawn, however, common practice (for simpler views) is to ignore that parameter and just redraw the entire view. Once this becomes too expensive (or you need the drawing to be more optimal), you can look into doing partial redraws of your view.
All in all, it is difficult to diagnose the full problem without seeing the entire UIView subclass code.
In the documentation of layoutSubviews, Apple says:
You should not call this method directly.
I'm trying to implement sizeToFit. I want it to put a tight bounding box on all of the subviews. I have to layout the subviews before determining such a bounding box. That means I must call layoutSubviews, which Apple frowns upon. How would I solve this dilemma without violating Apple's rules?
- (void)layoutSubviews
{
[super layoutSubviews];
self.view0.frame = something;
self.view1.frame = somethingElse;
}
- (void)sizeToFit
{
[self layoutSubviews];
self.frame = CGRectMake(
self.frame.origin.x,
self.frame.origin.y,
MAX(
self.view0.frame.origin.x + self.view0.frame.size.width,
self.view1.frame.origin.x + self.view1.frame.size.width
),
MAX(
self.view0.frame.origin.y + self.view0.frame.size.height,
self.view1.frame.origin.y + self.view1.frame.size.height
)
);
}
One should not override -sizeToFit. Instead override -sizeThatFits: which is internally called by -sizeToFit with the view's current bounds size.
You should not override this method. If you want to change the default sizing information for your view, override the sizeThatFits: instead. That method performs any needed calculations and returns them to this method, which then makes the change. – UIView Class Reference
Also not that even if you would override -sizeToFit, there is most likely no reason to perform layout immediately. You only size the view, i.e. set its bounds size. This triggers a call to -setNeedsLayout, marking the view as needing layout. But unless you want to animate the view, the new layout does not have to be applied right away.
The point of this delayed update pattern is that it saves a lot of time if you perform multiple consecutive updates, since the actual update is only performed once.
I typically do this. It works like a charm.
#pragma mark - Layout & Sizing
- (void)layoutSubviews
{
[self calculateHeightForWidth:self.bounds.size.width applyLayout:YES];
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat const width = size.width;
CGFloat const height = [self calculateHeightForWidth:width applyLayout:NO];
return CGSizeMake(width, height);
}
- (CGFloat)calculateHeightForWidth:(CGFloat)width applyLayout:(BOOL)apply
{
CGRect const topViewFrame = ({
CGRect frame = CGRectZero;
...
frame;
});
CGRect const bottomViewFrame = ({
CGRect frame = CGRectZero;
...
frame;
});
if (apply) {
self.topView.frame = topViewFrame;
self.bottomView.frame = bottomViewFrame;
}
return CGRectGetMaxY(bottomViewFrame);
}
Note that the sample code is for a view that can be displayed at any width and the container would ask for the preferred height for a certain width.
One can easily adjust the code for other layout styles though.
I think it's very rare that you will need to use frame when using Auto Layout. Considering you want to solve the problem without breaking Apple rules, I would suggest using the Auto Layout way:
Setup constraints to determine the size of the container view.
Basically the constraints are setup in a way so that the container's width and height can be determined by the auto layout system.
For example, You can setup the constraints like:
Note that view0 and view1 must set both width and height constraints.
When you instantiate the view somewhere in your project, you don't need to setup the width and height constraint anymore. Auto layout will guess its size (called intrinsicContentSize) by the constraints you previously setup.
If you want to force layoutSubviews to happen, but don't want to call it directly, set the needsLayout flag, and then ask it to layout.
[self setNeedsLayout];
[self layoutIfNeeded];
I have an UIScrollView with UIViews being added by lazy-loading algorithm. The UIViews contain previews of previous and next image, so at some point the actual views have to be used more than once (e.g. preview of the 2nd picture is NEXT now, but after 2 page scrolls it is PREVIOUS).
All the UIViews are stored in NSMutableArray and are added dynamically. I have the code, which checks, whether particular UIView is already added to the UIScrollView. If not, the code checks, is there needed UIView in NSMutableArray and if not, creates a new one...
The problem occurs when I try to get an UIView from NSMutableArray, change it's position on the screen and add to UIScrollView again. It just changes it's position and I see white squares where some UIViews have to be.
Is it possible to have 2 UIViews with pointer referred to one memory location or UIImage but with only one different parameter (position)? Is it good idea to copy already created UIView to NSMutable array twice to use with different position? (Win: creation process takes place only once, loss: amount of memory required is doubled).
From my point of view, I havent fount the good solution, but this worked for me - I just implemented copy method in UIView subclass, something like this:
- (ExhibitPreview *)copy {
ExhibitPreview *exhibitPreview =[[ExhibitPreview alloc] initWithImage:self.image];
exhibitPreview.number.text = self.number.text;
exhibitPreview.author.text = self.author.text;
exhibitPreview.title.text = self.title.text;
exhibitPreview.contentMode = UIViewContentModeScaleAspectFill;
exhibitPreview.clipsToBounds = YES;
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
CGRect frame = CGRectMake(0.0f, 0.0f, screenWidth/2, PREVIEW_HEIGHT);
exhibitPreview.frame = frame;
return exhibitPreview;
}
The first view is view itself and the second I create is a copy.
I've written a custom layout for a collection view. Part of the collection view involves inserting new items at the top with an animation beginning from offscreen. In order to do this I'm using the usual initialLayouts method. I'm able to animate the object in correctly but there seems to be a strange problem that I can't work out.
The cell that I'm animating in appears immediately in it's final position and then a second cell animates into place from the expected offscreen position, disappearing once the animation is complete. All of my other animations for deletion, bounds change etc are fine, so I don't believe my layout caches are wrong and normal layout is perfect. Potentially this is a cell reuse issue?
I've created a quick video demoing the problem (inserts start from 7 seconds) http://cl.ly/WMei
Has anyone seen this behaviour before and could point me in the right direction?
Unfortunately I can't share the whole layout class however here are my layoutAttributes methods and the initial insert attributes, I can try and give more info where asked for. I appreciate this is a hard one to debug so thanks a lot for taking the time to check it out :).
Here are the attributes I apply for top inserts:
MCLTXGridLayoutAttributes *att = [(MCLTXGridLayoutAttributes*)[self.dataStructure itemAtIndexPath:itemIndexPath] copy];
att.alpha = 1.0;
CGRect newFrame = att.frame;
newFrame.origin.y = self.collectionView.bounds.origin.y - att.totalHeight;
att.frame = newFrame;
return att;
and the layout attributes method:
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
MCLTXGridLayoutAttributes *att = [MCLTXGridLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGFloat randomTopPadding = (arc4random() % kMaxVerticalSpacing);
randomTopPadding += kMinVerticalSpacing;
NSInteger randomExtraPadding = (NSInteger)arc4random() % kOneInXChanceOfRecievingExtraPadding;
att.topPadding = randomTopPadding + (randomExtraPadding == 1 ? kItemRandomExtraPadding : 0);
NSUInteger column = [self.dataStructure indexForShortestColumn];
att.columnIndex = column;
CGFloat y = MAX(self.tmpCachedHeight, [self.dataStructure heightForColumnWithIndex:column] + kMinItemSpacing) + att.topPadding;
CGFloat height = [self.datasource collectionView:self.collectionView heightForItemAtIndex:indexPath];
CGRect frame = CGRectMake([self xOriginForColumn:column], y, self.cachedColumnWidth, height);
att.frame = frame;
att.layerPriority = [self.datasource collectionView:self.collectionView layerPriorityForItemAtIndexPath:indexPath];
att.zIndex = att.layerPriority * -10;
att.transform3D = CATransform3DIdentity;
att.transform = CGAffineTransformIdentity;
return att;
}
I did eventually figure this out. We were using Magical Record and the problem came with the saving.
With my testing I was creating fake objects and saving using a background thread as you should do with MR. The problem was in our networking code that completely by accident was calling a write + save on the main thread context. This was a very small write but it was enough to upset the rendering and cause the visual ghosting effect.
If you're seeing this issue and you're using something like Magical Record or multithreaded core data along with fetched results controllers, then double check your save calls to make sure you aren't accidentally hitting the main thread.
I'm trying to get the offset of my viewControllers' views relative to the top of the screen.
I thought I could convert the origin of the view to the window's coordinates so I tried something like this in my viewControllers:
CGPoint basePoint = [self.view convertPoint:self.view.frame.origin toView:nil];
CGFloat offset = basePoint.y;
It works as expected in some cases, however in other cases it returns different values even when the parameters are the same.
Any ideas on what's going on behind the scenes of this convertPoint:toView: that might be resulting in different return values?
Or if you have any other suggestions to get the offset of my viewControllers' views relative to the top of the screen it would be very much appreciated!
Thanks
You are converting your point to a point in a nil view.
The initial window, has a rootViewController which has a view and so you can access to that view:
UIView *firstView = [[[(YourClassAppDelegate *)[UIApplication sharedApplication] window] rootViewController] view];
CGPoint basePoint = [self.view convertPoint:self.view.frame.origin toView:firstView];
CGFloat offset = basePoint.y;
remember to import in the implementation, your class of the app delegate.
I end up crawling up the chain of superviews and adding up all the offsets.y from each view. Not sure if it's the best approach, especially performance wise, but for now it works.
UIView *view = self.view;
CGFloat offset = view.frame.origin.y;
while (view.superview != nil) {
offset += view.superview.frame.origin.y;
view = view.superview;
}
try change this
CGPoint basePoint = [self.view convertPoint:self.view.frame.origin toView:nil];
to
CGPoint basePoint = [self.view.superview convertPoint:self.view.frame.origin toView:nil];
Your two views must have common superview (for example UIWindow), otherwise you'll get weird results.
This might happen when you try to use UIViewController's view in -viewDidLoad with convertPoint, because view is not yet added to the view hierarchy.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%#", self.view.superview); // nil, not added to view hierachy
// If you try to convert point using self.view (or any self.view subview),
// and any other view, which is not self.view subview (for example, UIWindow),
// it will fail, because self.view is not yet added to the view hierachy.
}