Using CATiledLayer's drawLayer:inContext: - ios

I am trying to implement my own map engine by using CATiledLayer + UIScrollView.
In drawLayer:inContext: method of my implementation, if I have a certain tile image needed for the current bounding box, I immediately draw it in the context.
However, when I do not have one available in the local cache data structure, the tile image is asynchronously requested/downloaded from a tile server, and not draw anything in the context.
The problem is, when I don't draw anything in the context, that part of the view is shown as a blank tile. And the expected behavior is to show the zoom-in scaled tile view from the previous zoom level.
If you guys have faced any similar problem and found any solution for this, please let me know.

You have to setNeedsDisplayInRect: for the tile as soon as you have the data. You have to live with it being blank until the tile is available because you have no way to affect which tiles CATiledLayer is creating.

I do the same, blocking the thread until the tile has been downloaded. The performance is good, it runs smoothly. I'm using a queue to store every tile request, so I can also cancel those tile requests that are not useful anymore.
To do so, use a lock to stop the thread just after you launch your async tile request, and unlock it as soon as you have your tile cached.
Sounds that good to you? It worked for me!

Your CATiledLayer should be providing this tile from the previous zoom level as you expect. What are your levelsOfDetail and levelsOfDetailBias set to for the tiled layer?

You have to call setNeedsDisplay or setNeedsDisplayInRect: But the problem is if you call this then it will redraw every tile in the scrollView. So try using subclass of UIView instead of CATiledLayer subclass, and implement TiledView (subclass of UIView) like this,
+ (Class) layerClass {
return [CATiledLayer class];
}
-(void)drawRect:(CGRect)r {
CGRect tile = r;
int x = tile.origin.x/TILESIZE;
int y = tile.origin.y/TILESIZE;
NSString *tileName = [NSString stringWithFormat:#"Shed_1000_%i_%i", x, y];
NSString *path =
[[NSBundle mainBundle] pathForResource:tileName ofType:#"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
[image drawAtPoint:tile.origin];
// uncomment the following to see the tile boundaries
/*
UIBezierPath* bp = [UIBezierPath bezierPathWithRect: r];
[[UIColor whiteColor] setStroke];
[bp stroke];
*/
}
and
for scrollView,
UIScrollView* sv = [[UIScrollView alloc] initWithFrame:
[[UIScreen mainScreen] applicationFrame]];
sv.backgroundColor = [UIColor whiteColor];
self.view = sv;
CGRect f = CGRectMake(0,0,3*TILESIZE,3*TILESIZE);
TiledView* content = [[TiledView alloc] initWithFrame:f];
float tsz = TILESIZE * content.layer.contentsScale;
[(CATiledLayer*)content.layer setTileSize: CGSizeMake(tsz, tsz)];
[self.view addSubview:content];
[sv setContentSize: f.size];

Related

Set CALayer as SCNMaterial's diffuse contents

I've been searching all over the internet over the past couple of days to no avail. Unfortunately, the apple documentation about this specific issue is vague and no sample code is available (at least thats what I found out). What seems to be the issue you may ask...
I'm trying to set a uiview's layer as the contents of the material that is used to render an iPhone model's screen (Yep, trippy :P ). The iPhone's screen's UV mapping is set from 0 to 1 so that no issue persists in mapping the texture/layer onto the texels.
So, instead of getting this layer to appear rendered on the iPhone, same as left image, Instead, I get this rendered onto the iPhone like right image
Correct Render                                        Incorrect Render
Also note, that when I set a breakpoint and debug the actual iPhone node and view it in Xcode, a completely different render is shown and the layer gets half-fixed when I continue execution:
Now then... HOW do I fix this issue??? I've tried playing with the diffuse's contents transform matrix but nothing gets fixed. I've also tried resizing the UIView to 256x256 (since the UV seems to be 256x256 as shown in blender - the 3d modelling package), but that doesn't fix anything.
Here is the code for the layer:
UIView *screen = [[UIView alloc] initWithFrame:self.view.bounds];
screen.backgroundColor = [UIColor redColor];
UIView *temp = [[UIView alloc] initWithFrame:CGRectMake(0, 0, screen.bounds.size.width, 60)];
temp.backgroundColor = [UIColor colorWithRed:0 green:(112.f/255.f) blue:(235.f/255.f) alpha:1];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(temp.bounds, 40, 0)];
label.frame = CGRectOffset(label.frame, 40, 0);
label.textColor = [UIColor colorWithRed:0 green:(48.f/255.f) blue:(84.f/255.f) alpha:1];
label.text = #"Select Track";
label.font = [UIFont fontWithName:#"HelveticaNeue-Light" size:30];
label.minimumScaleFactor = 0.001;
label.adjustsFontSizeToFitWidth = YES;
label.lineBreakMode = NSLineBreakByClipping;
[temp addSubview:label];
UIView *separator = [[UIView alloc] initWithFrame:CGRectMake(0, temp.bounds.size.height - 2, temp.bounds.size.width, 2)];
separator.backgroundColor = [UIColor colorWithRed:0 green:(48.f/255.f) blue:(84.f/255.f) alpha:1];
[temp addSubview:separator];
[screen addSubview:temp];
screen.layer.contentsGravity = kCAGravityCenter;
Edit
What's even weirder is that if I capture a UIImage of the view using:
- (UIImage *) imageWithView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
and use that as the diffuse's content... everything works out perfectly fine?! It's really weird and frustrating since the image's size is exactly the same as the uiview's...
Edit 2
I ended up just using an image of the view as the texture, which makes things much more static than I needed. I won't set this as the answer because I'll still be waiting for a correct fix to this issue even if it in a long time. So, if you have an answer and this topic has been opened for a long time, please bump it if you can. The documentation on this section is just so poor.
New post on an old thread, but this day-in-age, it's possible to set the UIView itself as SCNMaterialProperty (diffuse) contents. Intention to support this feature is communicated directly from SceneKit engineering at Apple, though the documentation has not yet been updated to reflect it.
To tied back to the original post, do not set a UIView.layer as material property contents; instead set contents to the UIView itself.
[Update: according to Lance's comment below, support for views may be getting worse rather than getting better.]
The SceneKit docs pretty strongly suggest that, while there are cases where you can use animated CALayers as material content, that doesn't include UIView layers:
SceneKit cannot use a layer that is already being displayed elsewhere (for example, the backing layer of a UIView object).
That suggests that if you want to make animated content for your material, you're better off with either Core Animation used entirely on its own or SpriteKit.

iOS : Cropping Image from Rotated ImageView

How to crop a rectangle(red square in screen shot) of UIImage which is rotated as well as zoomed using UIScrollView.
The edges of UIImageView are hidden because of rotation(UIImageView Transformation). Please help.
Well, you can do all the complicated core graphics things or do a simple UIView screenshot. I vote for the easy solution: What you have to do is create a new view with the frame same as where that small rect lies. Then add the whole image view to that small view converting its frame so it looks the same. Then take the screenshot of the small view. After you are done simply put the image view back the way it was and remove the small view.
As this is still easier said then done here is some code to chew on (I did NOT test this so please correct the bugs if any after you succeed).
- (UIImage *)getScreenshotInRect:(CGRect)frame {
UIImageView *theImageView; //your original image view
UIView *backupSuperView = theImageView.superview; //backup original superview
CGRect backupFrame = theImageView.frame; //backup original frame
UIView *frameView = [[UIView alloc] initWithFrame:frame]; //create new view where the image should be taken at
frameView.clipsToBounds = YES; //not really necessery but can be usefull for cases like using corner radius
[self addSubview:frameView];
theImageView.frame = [theImageView.superview convertRect:theImageView.frame toView:frameView]; //set the new frame for the image view
[frameView addSubview:theImageView];
UIImage *toReturn = [self imageFromView:frameView]; //get the screenshot
theImageView.frame = backupFrame; //reset the image view frame
[backupSuperView addSubview:theImageView]; //reset the image view's superview
[frameView removeFromSuperview];
frameView = nil;
return toReturn;
}
- (UIImage *)imageFromView:(UIView *)view {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, .0f);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
I do hope this doesn't break because you have rotations. If that is the case I suggest you create another view on which the rotated image view lies and add this view to the small view.

UIVibrancyEffect On iOS 7

So I've been playing around with the iOS 8 beta and implementing the new UIEffectViews in the places that my app needed them. Now I've run into the issue that I still want to have backwards compatibility for iOS 7, but maintain the vibrancy effect because it really helps readability. I've used UIToolbars in the past for a blur effect, and they work great, but not for vibrancy. I thought I'd subclass UIView and add a toolbar subview and then do some clever rendering to sort of achieve the vibrancy effect which would look like this:
1. render the toolbar to a UIImage
2. render the vibrant content to a UIImage
3. mask the toolbar image to the vibrant content image mask
4. mess with the saturation and brightness
5. have a subview of the UIView display the final result over the toolbar
I've tried doing this in drawRect: of the UIView but it doesn't want to redraw every frame, and setting a timer really messes with animation, even though the render time isn't very high. If anyone can point me to sample code or a open source library, it would be much appreciated.
Thanks.
So I never posted an answer, but I did figure it out.
The brute force approach I tried was to use Core Image effects. I would render the superview to a UIImage, blur it, then overlay it on a toolbar with the dark style. This looked great, but even on a GPU context on my 5S, it was pretty slow, so theres no way it would work on other devices. This is the best I could get it to look, and would work great for static content, but is not practical for real-time.
I was able to achieve a real time version, but it doesn't look quite as good. Basically what I do is render all the vibrant content to a image and use it for a mask for a view. Then I make the view barely visible (like .2 alpha), and then put it over a toolbar. It doesn't look quite as vibrant as iOS8, or the original CI version, but it works great and preforms well.
Heres a bit of code you can just copy and paste if you really want:
-(instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.2];
maskingContents = [[UIView alloc] initWithFrame:self.bounds];
[self addSubview:maskingContents];
}
return self;
}
-(void)addSubview:(UIView *)view
{
if (![view isEqual:maskingContents])
{
[maskingContents setHidden:NO];
[maskingContents addSubview:view];
//now we need to mask it
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
[maskingContents.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage* mask = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//apply the mask
CALayer* maskLayer = [CALayer layer];
maskLayer.frame = self.bounds;
[maskLayer setContents:(id)mask.CGImage];
[self.layer setMask:maskLayer];
[maskingContents setHidden:YES];
} else [super addSubview:view];
}
-(void)forceVibrancyUpdate
{
[maskingContents setHidden:NO];
//now we need to mask it
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
[maskingContents.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage* mask = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//apply the mask
CALayer* maskLayer = [CALayer layer];
maskLayer.frame = self.bounds;
[maskLayer setContents:(id)mask.CGImage];
[self.layer setMask:maskLayer];
[maskingContents setHidden:YES];
}
#end
If you want to dynamically update the content inside the vibrancy view, you would call forceVibrancyUpdate, as that would re-render the mask and apply it. Hope this helped everyone.

Using GPUImage With a UIView

I'm trying to integrate GPUImage into my app. Specifically, I want to apply the Sphere Refraction filter on my main view. Thing is, GPUImage works with UIImage, not with UIView. In order to create a UIImage representation of my view hierarchy, I'm using [CALayer renderInContext], which takes a long time to complete. The net result is that my animations look clunky.
Here's the code that's called in my CADisplayLink handler:
- (void)onDisplayLink:(CADisplayLink*)theDisplayLink {
self.mainView.layer.opaque = YES;
UIGraphicsBeginImageContextWithOptions(self.sphereView.bounds.size, self.sphereView.opaque, [[UIScreen mainScreen] scale]);
[self.sphereView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage* mainViewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.sourcePicture = [[GPUImagePicture alloc] initWithImage:mainViewImage smoothlyScaleOutput:NO];
self.sphereRefractionFilter = [[GPUImageSphereRefractionFilter alloc] init];
self.sphereRefractionFilter.radius = 0.5;
self.sphereRefractionFilter.refractiveIndex = 0.25;
[self.sphereRefractionFilter setInputRotation:kGPUImageRotate180 atIndex:0];
[self.sphereRefractionFilter addTarget:self.mainView];
[self.sourcePicture addTarget:self.sphereRefractionFilter];
[self.sourcePicture processImage];
}
The view I'm trying to render using this code has a background image, and about 5-50 smaller images laid out on it, whose positions are modified in real-time. Imagine a sphere with multiple moving markers on it in various places.
Using this code, I'm able to render about 10 FPS. Question is: is there any way to do this faster?
Anyone?

Rounded Corners on UIImage

I'm trying to draw images on the iPhone using with rounded corners, a la the contact images in the Contacts app. I've got code that generally work, but it occasionally crashes inside of the UIImage drawing routines with an EXEC_BAD_ACCESS - KERN_INVALID_ADDRESS. I thought this might be related to the cropping question I asked a few weeks back, but I believe I'm setting up the clipping path correctly.
Here's the code I'm using - when it doesn't crash, the result looks fine and anybody looking to get a similar look is free to borrow the code.
- (UIImage *)borderedImageWithRect: (CGRect)dstRect radius:(CGFloat)radius {
UIImage *maskedImage = nil;
radius = MIN(radius, .5 * MIN(CGRectGetWidth(dstRect), CGRectGetHeight(dstRect)));
CGRect interiorRect = CGRectInset(dstRect, radius, radius);
UIGraphicsBeginImageContext(dstRect.size);
CGContextRef maskedContextRef = UIGraphicsGetCurrentContext();
CGContextSaveGState(maskedContextRef);
CGMutablePathRef borderPath = CGPathCreateMutable();
CGPathAddArc(borderPath, NULL, CGRectGetMinX(interiorRect), CGRectGetMinY(interiorRect), radius, PNDegreeToRadian(180), PNDegreeToRadian(270), NO);
CGPathAddArc(borderPath, NULL, CGRectGetMaxX(interiorRect), CGRectGetMinY(interiorRect), radius, PNDegreeToRadian(270.0), PNDegreeToRadian(360.0), NO);
CGPathAddArc(borderPath, NULL, CGRectGetMaxX(interiorRect), CGRectGetMaxY(interiorRect), radius, PNDegreeToRadian(0.0), PNDegreeToRadian(90.0), NO);
CGPathAddArc(borderPath, NULL, CGRectGetMinX(interiorRect), CGRectGetMaxY(interiorRect), radius, PNDegreeToRadian(90.0), PNDegreeToRadian(180.0), NO);
CGContextBeginPath(maskedContextRef);
CGContextAddPath(maskedContextRef, borderPath);
CGContextClosePath(maskedContextRef);
CGContextClip(maskedContextRef);
[self drawInRect: dstRect];
maskedImage = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(maskedContextRef);
UIGraphicsEndImageContext();
return maskedImage;
}
and here's the crash log. It looks the same whenever I get one of these crashes
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x6e2e6181
Crashed Thread: 0
Thread 0 Crashed:
0 com.apple.CoreGraphics 0x30fe56d8 CGGStateGetRenderingIntent + 4
1 libRIP.A.dylib 0x33c4a7d8 ripc_RenderImage + 104
2 libRIP.A.dylib 0x33c51868 ripc_DrawImage + 3860
3 com.apple.CoreGraphics 0x30fecad4 CGContextDelegateDrawImage + 80
4 com.apple.CoreGraphics 0x30feca40 CGContextDrawImage + 368
5 UIKit 0x30a6a708 -[UIImage drawInRect:blendMode:alpha:] + 1460
6 UIKit 0x30a66904 -[UIImage drawInRect:] + 72
7 MyApp 0x0003f8a8 -[UIImage(PNAdditions) borderedImageWithRect:radius:] (UIImage+PNAdditions.m:187)
Here is an even easier method that is available in iPhone 3.0 and up. Every View-based object has an associated layer. Each layer can have a corner radius set, this will give you just what you want:
UIImageView * roundedView = [[UIImageView alloc] initWithImage: [UIImage imageNamed:#"wood.jpg"]];
// Get the Layer of any view
CALayer * l = [roundedView layer];
[l setMasksToBounds:YES];
[l setCornerRadius:10.0];
// You can even add a border
[l setBorderWidth:4.0];
[l setBorderColor:[[UIColor blueColor] CGColor]];
I'm gonna go ahead here and actually answer the question in the title.
Try this category.
UIImage+additions.h
#import <UIKit/UIKit.h>
#interface UIImage (additions)
-(UIImage*)makeRoundCornersWithRadius:(const CGFloat)RADIUS;
#end
UIImage+additions.m
#import "UIImage+additions.h"
#implementation UIImage (additions)
-(UIImage*)makeRoundCornersWithRadius:(const CGFloat)RADIUS {
UIImage *image = self;
// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
const CGRect RECT = CGRectMake(0, 0, image.size.width, image.size.height);
// Add a clip before drawing anything, in the shape of an rounded rect
[[UIBezierPath bezierPathWithRoundedRect:RECT cornerRadius:RADIUS] addClip];
// Draw your image
[image drawInRect:RECT];
// Get the image, here setting the UIImageView image
//imageView.image
UIImage* imageNew = UIGraphicsGetImageFromCurrentImageContext();
// Lets forget about that we were drawing
UIGraphicsEndImageContext();
return imageNew;
}
#end
If appIconImage is an UIImageView, then:
appIconImage.image = [UIImage imageWithContentsOfFile:#"image.png"];
appIconImage.layer.masksToBounds = YES;
appIconImage.layer.cornerRadius = 10.0;
appIconImage.layer.borderWidth = 1.0;
appIconImage.layer.borderColor = [[UIColor grayColor] CGColor];
And also remember:
#import <QuartzCore/QuartzCore.h>
I cant offer any insight into your crash, but I thought I would offer another option for rounding the corners. I had a similar problem arise in an application i was working on. Rather than write any code I am overlaying another image which masks off the corners.
If you are calling your method (borderedImageWithRect) in a background thread, crashes might occur since UIGraphics-functions are not thread-safe. In such a case, you must create a context using CGBitmapContextCreate() - see the "Reflection" sample code from the SDK.
The easiest way is to embed a disabled[!] round-rect [not custom!] button in your view (can even do it all in the Interface Builder) and associate your image with it. The image-setting message is different for UIButton (compared to UIImageView), but the overall kludge works like a charm. Use setImage:forState: if you want a centered icon or setBackgroundImage:forState: if you want the whole image with corners cut (like Contacts). Of course if you want to display lots of these images in your drawRect this isn't the right approach, but more likely an embedded view is exactly what you needed anyway...
I would reiterate fjoachim's answer: be cautious when attempting to draw while running on a separate thread, or you may get EXC_BAD_ACCESS errors.
My workaround went something like this:
UIImage *originalImage = [UIImage imageNamed:#"OriginalImage.png"]
[self performSelectorOnMainThread:#selector(displayImageWithRoundedCorners:) withObject:originalImage waitUntilDone:YES];
(In my case I was resizing / scaling UIImages.)
I actually had a chance to talk about this with somebody from Apple at the iPhone Tech Talk in New York. When we talked about it, he was pretty sure it wasn't a threading issued. Instead, he thought that I needed to retain the graphics context that was generated when calling UIGraphicsBeginImageContext. This seems to violate the general rules dealing with retain rules and naming schemes, but this fellow was pretty sure he'd seen the issue previously.
If the memory was getting scribbled, perhaps by another thread, that would certainly explain why I was only seeing the issue occasionally.
I haven't had time to revisit the code and test out the fix, but PCheese's comment made me realize I hadn't posted the info here.
...unless I wrote that down wrong and UIGraphicsBeginImageContext should've been CGBitmapContextCreate...
If it only crashes some of the time, figure out what the crash cases have in common. Is dstRect the same every time? Are the images ever a different size?
Also, you need to CGPathRelease(borderPath), although I doubt that leak is causing your problem.
UIImage *originalImage = [UIImage imageNamed:#"OriginalImage.png"]
[self performSelectorOnMainThread:#selector(displayImageWithRoundedCorners:) withObject:originalImage waitUntilDone:YES];
In Swift 4.2 and Xcode 10.1
let imgView = UIImageView()
imgView.frame = CGRect(x: 200, y: 200, width: 200, height: 200)
imgView.image = UIImage(named: "yourimagename")
imgView.imgViewCorners()
//If you want complete round shape
//imgView.imgViewCorners(width: imgView.frame.width)//Pass ImageView width
view.addSubview(imgView)
extension UIImageView {
//If you want only round corners
func imgViewCorners() {
layer.cornerRadius = 10
layer.borderWidth = 1.0
layer.borderColor = UIColor.red.cgColor
layer.masksToBounds = true
}
//If you want complete round shape
func imgViewCorners(width:CGFloat) {
layer.cornerRadius = width/2
layer.borderWidth = 1.0
layer.borderColor = UIColor.red.cgColor
layer.masksToBounds = true
}
Set the Image in xib or storyboard (image width and height 41x41).
FirstViewController.h
#interface....
IBOutlet UIImageView *testImg;
#end
FirstViewController.m
-(void)viewDidLoad{
testImg.layer.backgroundColor=[[UIColor clearColor] CGColor];
testImg.layer.cornerRadius=20;
testImg.layer.masksToBounds = YES;
}

Resources