iOS: Creating an overlay on top of an EAGLView / GLKView - ios

I'm trying to create an overlay on top of a GLKView (effectively an EAGLView). I'm aware of the performance impact, but in my situation that's not a problem, since the scene is paused in the background, it merely needs to remain visible.
I've created a custom UIView called ReaderView whose only custom code is the following:
-(CALayer*)layer {
CATextLayer *textLayer = [[CATextLayer alloc] init];
// Layer settings.
[textLayer setCornerRadius:5.0f];
// Text settings.
[textLayer setFont:CGFontCreateWithFontName((CFStringRef)READING_FONT)];
[textLayer setFontSize:READING_FONT_SIZE];
[textLayer setAlignmentMode:kCAAlignmentJustified];
[textLayer setWrapped:YES];
return textLayer;
}
I've then called the following in a GLKViewController:
-(void)onMyCustomEvent {
if (_readerView==nil) {
CGRect frame = [[self view] frame];
frame.size.width *= 0.8f;
frame.size.height *= 0.8f;
_readerView=[[ReaderView alloc] initWithFrame:frame];
[_readerView setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
}
[_readerView setText:[node content]];
[[self view] addSubview:_readerView];
}
NSLog has proven this method gets called and the reader view gets initialized. However nothing displays on top of the GLKView.
Any idea why this doesn't work?

Since you're adding it as a subview, you should use the bounds property instead of the frame property from the parent view, like this:
CGRect frame = self.view.bounds;
If that doesn't fix it, try setting the background color of the _readerView to something noticable to ensure that the problem isn't the content missing, like this:
_readerView.backgroundColor = [UIColor redColor];
Finally, if none of that solves it, then maybe directly adding subviews to an EAGL view is the problem. In that case, create a container view and add your EAGL view and your _readerView to that instead.

Related

When to layout subviews of UIViewControllers view?

In my UIViewController class, I'm creating a UIView called safeAreaView and adding it as a subview to the UIViewControllers view property. I'm making it so safeAreaView takes up the entire safe area of the UIViewControllers view property:
- (void) viewDidLoad
{
[self setToolbarWithColor: self.mainToolbarColor animated:NO];
self.tapGestureRecognizer.delegate = self;
self.view.clipsToBounds = YES;
self.view.backgroundColor = [UIColor lightGrayColor];
self.safeAreaView = [[UIView alloc] initWithFrame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
self.safeAreaView.clipsToBounds = YES;
self.safeAreaView.delegate = self;
self.safeAreaView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview: self.safeAreaView];
[self.safeAreaView.leadingAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.leadingAnchor].active = YES;
[self.safeAreaView.trailingAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.trailingAnchor].active = YES;
[self.safeAreaView.topAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.topAnchor].active = YES;
[self.safeAreaView.bottomAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.bottomAnchor].active = YES;
[self.safeAreaView loadSubviews];
}
This works fine, but my problem is, at some point after this during the UIViewControllers initialization cycle, safeAreaView updates to account for the statusbar (it's y position moves up 20 and it decreases in size by 20).
I need to layout some subviews on safeAreaView and I don't know the proper time? If I attach the subviews like above, they have the wrong height. And I can't use some auto layout features on the subviews because there are specific things that I need to do. I've also tried executing the above code in viewWillAppear with no luck.
Wondering if anyone had any suggestions?
You can override - (void)layoutSubviews on your safeAreaView class:
- (void)layoutSubviews {
[super layoutSubviews];
// Manual frame adjustment for non-autolayout participating subviews
// ...
}
Another option would be to override your safeAreaView's class frame setter, so each time the frame of your view changes, you'll get a chance to manually set any subview frames as needed.

When should I be setting borders in UIViews?

I've got a UIView that does not fill the whole screen and I would like to add a top border to that view. However, I keep getting the following:
Here is the code I am using:
CGFloat thickness = 4.0f;
CALayer *topBorder = [CALayer layer];
topBorder.frame = CGRectMake(0, 0, self.announcementCard.frame.size.width, thickness);
topBorder.backgroundColor = [UIColor blueColor].CGColor;
How I know why the border goes off the screen. This is because I put the border on the view inside the UIViews init method. When I do this the self.announcementCard.frame.size.width is 1000 and hence why the border goes off the screen. The self.announcementCard.frame.size.width has a width and height of 1000. The reason for this is because the UIView hasn't added the constraints to the UIView in its init methods.
Thus, my question is when should I be calling the code I've written above? When will self.announcementCard.frame.size.width have its constraints added to it and have its frame updated?
You should add your subviews (or sublayers) in the viewDidLoad method. However if you are using the auto-layout keep a reference of your sublayer and update it in the viewDidLayoutSubviews method:
- (void)viewDidLoad {
[super viewDidLoad];
_borderLayer = [CALayer layer];
[self.view.layer addSublayer:_borderLayer];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_borderLayer.frame = CGRectMake(0, 0, self.view.bounds.size.width, 3);
}
Otherwise you can simply clipsToBounds the view to avoid the subviews to be visible beyond the bounds.
self.view.clipsToBounds = YES;
in the init the graphics isn't made yet. You have to put all your configuration on graphics object in the viewDidLoad: or viewWillAppear: of the UIViewController.
Short Answer:
viewWillAppear
By the time viewWillAppear is called, your subviews have been laid out and the frames are valid. Doing frame-based calculations in viewDidLoad can often have issues since the frames have not been set.

Why would CALayer animation work after loading view, but not just after initialization?

I have a custom view (a small indicator derived from UIView with a rotation animation) which has basically a heart icon (UIImageView) on middle and a few balls (another UIImageView) rotating around it using layer animation. Here is my code:
-(void)performInitialization{
self.backgroundColor = [UIColor clearColor];
CGRect frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame];
[imageView setImage:[UIImage imageNamed:#"RedHeart"]];
balls = [[UIImageView alloc] initWithFrame:frame];
[balls setImage:[UIImage imageNamed:#"AngularBalls"]];
[self addSubview:imageView];
[self addSubview:balls];
[balls.layer beginRotating];
}
...where my category on CALayer has:
-(void)beginRotatingWithAngularVelocity:(float)velocity{
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.removedOnCompletion = YES;
rotationAnimation.repeatCount = 999999;
rotationAnimation.duration = velocity;
rotationAnimation.cumulative = YES;
rotationAnimation.fromValue = [NSNumber numberWithFloat:0];
rotationAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2];
[self addAnimation:rotationAnimation forKey:ROTATION_KEY];
}
-(void)beginRotating{
[self beginRotatingWithAngularVelocity:0.7];
}
performInitialization is called in the init[WithFrame|WithCoder] method of my view. I have one of these views in my storyboard's main view and the view animates perfectly. If I put several instances of my custom view into that main view, they all animate perfectly too. There is no problem if I put my view into any view on that storyboard. However, when I put that same view into a view from a nib, it won't animate. The same code, the same settings in IB (copy pasted to make sure everything is the same), but the view is still, stuck on the initial view as if there was no animation attached to the layer). Why would that happen? How can I make that animation work on nibs?
UPDATE: The problem appears to be related to having the animation on view's initializer. In some occasions, I am animating right inside initialization, but sometimes, after it is loaded (e.g. user clicked something and something is downloading). The problem appears to be consistent with the former case. My previous fallacy about being about storyboard vs. nibs apparently is just coincidence. I've updated the title accordingly.
Initialization is too early. You need to wait until the view is in your interface (signaled to the view by didMoveToWindow. Until then, the view is not part of the rendering tree (which is what does the drawing/animation of its layers).

Autolayout and shadow

I've got a problem with adding shadow to my UIView which is created in iOS 6 application with Autolayout.
Let's assume I have a method that adds a shadow on the bottom of UIView (this is actually a Category of UIView, so it's reusable):
- (void) addShadowOnBottom {
self.layer.shadowOffset = CGSizeMake(0, 2);
self.layer.shadowOpacity = 0.7;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
}
When I call this method in viewDidLoad of some UIViewController, shadow is not added, probably due to all constraints, that have to be calculated.
When I call this method in viewWillAppear the same situation.
When I call this method in viewDidAppear it works, but when new view shows up there is a short moment when there is no shadow and it appears after a while.
If I resign from setting the shadowPath and remove line self.layer.shadowPath everything works, but view transitions are not smooth.
So my question is what is the right way to add a shadow to view in iOS 6 with Autolayout turned on ?
Another thing you can add to the layer when working with AutoLayout and you need a shadow on a UIView where the frame is not yet known is this :
self.layer.rasterizationScale = [[UIScreen mainScreen] scale]; // to define retina or not
self.layer.shouldRasterize = YES;
Then remove the shadowPath property because the auto layout constraints are not yet processed, so it's irrelevant. Also at the time of execution you will not know the bounds or the frame of the view.
This improves performance a lot!
i removed self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
from your code and it is working for me in viewDidLoad, please confirm.
Increasing shdowOffset will make you see the shadow more clear.
Having the exact same issue...
Although I am unable to get the CALayer shadow on a view to animate nicely, at least the shadow does re-align properly after animation.
My solution (which works fine in my application) is the set the shadowOpacity to 0, then reset it to the desired value AFTER the animation has completed. From a user's perspective, you cannot even tell the shadow is gone because the animations are typically too fast to perceive the difference.
Here is an example of some code in my application, in which I am changing the constant value of a constraint, which is 'trailing edge to superview' NSLayoutContraint:
- (void) expandRightEdge
{
[self.mainNavRightEdge setConstant:newEdgeConstant];
[self updateCenterContainerShadow];
[UIView animateWithDuration:ANIMATION_DURATION delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"PanelLayoutChanged" object:nil];
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
nil;
}];
}
- (void) updateCenterContainerShadow
{
self.centerContainer.layer.masksToBounds = NO;
self.centerContainer.layer.shadowOpacity = 0.8f;
self.centerContainer.layer.shadowRadius = 5.0f;
self.centerContainer.layer.shadowOffset = CGSizeMake(0, 0);
self.centerContainer.layer.shadowColor = [UIColor blackColor].CGColor;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:self.centerContainer.layer.bounds].CGPath;
[self.centerContainer.layer setShadowPath:shadowPath];
// Schedule a time to fade the shadow back in until we can figure out the CALayer + Auto-Layout issue
[self performSelector:#selector(fadeInShadow) withObject:nil afterDelay:ANIMATION_DURATION+.05];
}
- (void) fadeInShadow
{
[self.centerContainer.layer setShadowOpacity:0.8f];
}
Two things:
I could have put the fadeInShadow in the completion block, but due to the way some of my other code is factored, this works better for me.
I realize I am not performing a fade in with "fadeInShadow", but given how quickly it renderes after the completion of the animation, I found it is not necessary.
Hope that helps!

Setting the accessibilityFrame of an element whose parent view will move

I have a UITableView with a custom header (i.e. I create the UIView myself). I need to tweak the accessibilityFrame of one of the subviews of the view, but I can’t figure out how to set the coordinates of the frame appropriately—they need to be relative to the window, but I’m not sure how to accomplish that.
My code looks like
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger) section
{
CGRect bounds = CGRectMake(0, 0, [tableView frame].size.width, 48);
UIView *header = [[UIView alloc] initWithFrame:bounds];
UILabel *labelOne = [[UILabel alloc] initWithFrame:
CGRectMake(0, 0, bounds.size.width - 80, 18)];
UILabel *labelTwo = [[UILabel alloc] initWithFrame:
CGRectMake(0, 20, bounds.size.width - 80, 18)];
CGRect frameOne = [labelOne frame];
CGRect frameTwo = [labelTwo frame];
[labelTwo setIsAccessibilityElement:NO];
[labelOne setAccessibilityFrame:CGRectUnion(frameOne, frameTwo)];
// ...
return header;
}
I’ve got two UILabels, which I want to combine into one for the purposes of VoiceOver. I accomplish this by ignoring the second label and extending the frame of the first label to cover the area of the second label. (The second label is immediately below the first.) The problem is getting the frames. If I use the code as shown above, the accessibility frame is the correct size, but is positioned as if the UITableView’s header were in the top left corner of the screen. I tried to modify the code to say
CGRect frameOne = [header convertRect:[labelOne frame] toView:nil];
CGRect frameTwo = [header convertRect:[labelTwo frame] toView:nil];
but the same thing happened. Shouldn’t this latter piece of code convert the UILabels’ frames into window-relative coordinates?
I thought maybe the issue is that when the UIView is created, it doesn’t know where on screen it’s going to be positioned (and as part of a UITableView it may be scrolled all over the place). Is it necessary to implement accessibilityFrame as a message which checks the UIView’s position each time it is called?
There's a helper function that will assist you with doing exactly that: UIAccessibilityConvertFrameToScreenCoordinates. This function takes a CGRect and converts it from a view's coordinate system into screen coordinates.
I don't think it's the timing of when the UIView is created, as I believe the window should be not-nil by the time tableView:viewForHeaderInSection: is called. I think the problem is the receiver of the convertRect:toView: message. Rather than passing this message to header, you should be passing it to [self view].
You're converting from the receivers coordinate system to that of another view, in this case nil or the UIWindow in your app. When header receives this message, you're converting from header's coordinate system to window's coordinate system, but header itself is a subview of [self view]. Instead, you want to ask [self view] to do the conversion, which should take into account any UINavigationBar's, etc.
CGRect frameOne = [[self view] convertRect:[labelOne frame] toView:nil];
CGRect frameTwo = [[self view] convertRect:[labelTwo frame] toView:nil];

Resources