How do I lock my background image in place when rotating device? - objective-c

I'd like to "lock" my background image (a UIImageView) such that it doesn't look like it's getting cropped when I rotate my device. It looks correct in landscape view, but doesn't follow along with the rotation.
I've tried stretching it using setAutoresizingMask, but the results look horrible. I've fiddled around with the transform property of the UIImageView, but fail to make it look correct.
Any pointers would be appreciated. I'm not even sure if I'm on the right track.

You can have different images for portrait/landscape orientations and switch between the two with UIView animations on willRotate callbacks. I tried it and the result is quite smooth.

OK, I managed to make it work using the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
...
UIColor *color = [[UIColor alloc] initWithPatternImage:[UIImage imageNamed:#"background.png"]];
self.background.backgroundColor = color;
[color release];
...
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration {
float angle = (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) ? M_PI_2 : 0.0;
self.background.transform = CGAffineTransformMakeRotation(angle);
}
Bear in mind that background~ipad.png is 1024x768, that is landscape as default. If your image is portrait you need to check for landscape instead of portrait in the conditional cases.

Related

How to improve magnification and rotation gesture recogniser methods in Objective-C?

I'm playing with NSGestureRecognizer(s) in Objective-C.
I have a simple Custom View in the XIB canvas to which I applied the press, pan, magnify, and rotate gesture recognisers. From each one I have created an action in AppDelegate.m and then added code to it. They all work to some extent but the way magnify and rotate behave does not satisfy me.
Here is the code for both of them:
- (IBAction)magnifyView:(NSMagnificationGestureRecognizer *)sender {
CGFloat magnification = sender.magnification + 1.0;
NSView *view = sender.view;
CGAffineTransform transform = CGAffineTransformMakeScale(magnification, magnification);
[[view layer] setAffineTransform:transform];
sender.magnification = 0;
}
and ...
- (IBAction)rotateView:(NSRotationGestureRecognizer *)sender {
CGFloat rotation = sender.rotation;
NSView *view = sender.view;
CGAffineTransform transform = CGAffineTransformMakeRotation(rotation);
[[view layer] setAffineTransform:transform];
sender.rotation = 0;
}
I'm using a 2016 MBP with macOS 12.4 and Xcode 13.4.1 to realise this and, using the trackpad for the gestures, I see that magnifyView seems to stutter unless I strongly open my fingers in an "unpinch" gesture—which risks triggering the macOS show desktop, while rotateView only rotates of a few degrees before coming back.
The whole project can be found here, if you feel like giving it a look. It doesn't contain much else anyway, it's an experiment.
Could you please give it a look (or even just at the methods above) and tell me what I could do to improve it?
Thank you!

NSScrollView: fade in a top-border like Messages.app

What I Want to Do:
In Messages.app on OS 10.10, when you scroll the left-most pane (the list of conversations) upwards, a nice horizontal line fades in over about 0.5 seconds. When you scroll back down, the line fades back out.
What I Have:
I am trying to achieve this effect in my own app and I've gotten very close. I subclassed NSScrollView and have done the following:
- (void) awakeFromNib
{
_topBorderLayer = [[CALayer alloc] init];
CGColorRef bgColor = CGColorCreateGenericGray(0.8, 1.0f);
_topBorderLayer.backgroundColor = bgColor;
CGColorRelease(bgColor);
_topBorderLayer.frame = CGRectMake(0.0f, 0.0f, self.bounds.size.width, 1.0f);
_topBorderLayer.autoresizingMask = kCALayerWidthSizable;
_topBorderLayer.zPosition = 1000000000;
_fadeInAnimation = [[CABasicAnimation animationWithKeyPath:#"opacity"] retain];
_fadeInAnimation.duration = 0.6f;
_fadeInAnimation.fromValue = #0;
_fadeInAnimation.toValue = #1;
_fadeInAnimation.removedOnCompletion = YES;
_fadeInAnimation.fillMode = kCAFillModeBoth;
[self.layer insertSublayer:_topBorderLayer atIndex:0];
}
- (void) layoutSublayersOfLayer:(CALayer *)layer
{
NSPoint origin = [self.contentView documentVisibleRect].origin;
// 10 is a fudge factor for blank space above first row's actual content
if (origin.y > 10)
{
if (!_topBorderIsShowing)
{
_topBorderIsShowing = YES;
[_topBorderLayer addAnimation:_fadeInAnimation forKey:nil];
_topBorderLayer.opacity = 1.0f;
}
}
else
{
if (!_topBorderIsShowing)
{
_topBorderIsShowing = NO;
// Fade out animation here; omitted for brevity
}
}
}
The Problem
The "border" sublayer that I add is not drawing over top of all other content in the ScrollView, so that we end up with this:
The frames around the image, textfield and checkbox in this row of my outlineView are "overdrawing" my border layer.
What Causes This
I THINK this is because the scrollView is contained inside an NSVisualEffectView that has Vibrancy enabled. The reason I think this is that if I change the color of my "border" sublayer to 100% black, this issue disappears. Likewise, if I turn on "Reduce Transparency" in OS X's System Preferences > Accessibility, the issue disappears.
I think the Vibrancy compositing is taking my grey border sublayer and the layers that represent each of those components in the outlineView row and mucking up the colors.
So... how do I stop that for a single layer? I've tried all sorts of things to overcome this. I feel like I'm 99% of the way to a solid implementation, but can't fix this last issue. Can anyone help?
NB:
I am aware that it's dangerous to muck directly with layers in a layer-backed environment. Apple's docs make it clear that we can't change certain properties of a view's layer if we're using layer-backing. However: adding and removing sublayers (as I am) is not a prohibited action.
Update:
This answer, while it works, causes problems if you're using AutoLayout. You'll start to get warnings that the scrollView still needs update after calling Layout because something dirtied the layout in the middle of updating. I have not been able to find a workaround for that, yet.
Original solution:
Easiest way to fix the problem is just to inset the contentView by the height of the border sublayer with this:
- (void) tile
{
id contentView = [self contentView];
[super tile];
[contentView setFrame:NSInsetRect([contentView frame], 0.0, 1.0)];
}
Should have thought of it hours ago. Works great. I'll leave the question for anyone who might be looking to implement these nice fading-borders.

CALayer cornerRadius has no effect on 4" device (simulator)

I work on dynamically, programmatically layout a view for 3.5" devices as well as for 4" devices.
As such that works fine.
But I want rounded corners so that my images appear like playing cards.
And I get rounded corners nicely displayed in 3,5 inch devices on the simulator for simulated iOS 6.1 and 7 alike.
But when I choose iPhone retina 4 inch on 6.1 or 7, then the UIImage in the UIImageView is fully displayed.
It works nicely on simulated iPad devices (in iPhone simulation mode - it is an iPhone only app).
As for today, I do not have any 4" device with me to test it. I can test on a device during the upcoming week.
Hiere is the relevant code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.imageV.image = self.image; // The image property was set by the caller.
// Layout imageV within self.view with a margin of MARGIN
self.imageV.frame = CGRectMake(self.view.frame.origin.x + MARGIN, self.view.frame.origin.y + MARGIN, self.view.frame.size.width - 2 * MARGIN, self.view.frame.size.height - 2 * MARGIN);
// set the raidus and the mask to follow the rounded corners.
self.imageV.layer.cornerRadius = CORNER_RADIUS;
self.imageV.layer.masksToBounds = YES;
}
BTW: CORNER_RADIUS is 18 and MARGIN is 15. Changing these values has no effect on the issue.
UPDATE: Thanks to Matt I figured out that the problem disappears when I create the UIImageView programmatically. That is some really nice workaround plus it points into the right diretion, I guess, but it is not a solution. Any ideas what setting in the storyboard editor might have caused the problem?
As far as I can see, auto layout is disabled for all view controllers in this storyboard.
The answer is simple. The code did work. It did add round corners to the UIImageView object and the maskToBounds worked well.
But the actual image displayed is smaller. I used AspectFit as mode to ensure that the actual image is not squeesed but displayed in its original aspect ration. Because of the longer layout of the iPhone5 dimensions the image only filled a part of its owning UIImageView. I changed the background color to gray for the screenshot and now it gets clear.
So the solution will be that I'll have to calculate the proper size of the image view so that it matches exactly the size of the scaled image. Then it should work.
(I'll update this answer when it is done).
Update: this is what I finally did: I removed the UIImageView from the Storyboard and deal with it programmatically.
Don't get confused by the complexity. I added another view just to throw a shadow, although this is not related to the original question. The shadow I wanted to add anyway. And it turned out that CALayer's shadow and masksToBounds=YES don't really agree on. That is why I added a regular UIView which lies in between the card view and the background view.
Finally this is so much of a hassle for displaying a simple rectangle image, that I think, just subclassing UIView and drawing everything with openGL or so directly into the CALayer would be probably much easier. :-)
Anyway, this is my code:
- (void)viewDidLoad {
[super viewDidLoad];
self.state = #0;
// Create an image view to carry the image with round rects
// and create a regular view to create the shadow.
// Add the shadow view first so that it appears behind
// the actual image view.
// Explanation: We need a separate view for the shadow with the same
// dimenstions as the imageView. This is because the imageView's image
// is rectangular and will only be clipped to round rects when the
// property masksToBounds is set to YES. But this setting will also
// clip away any shadow that the imageView's layer may have.
// Therfore we add a separate mainly empty UIView just behind the
// UIImageview to throw the shadow.
self.shadowV = [[UIView alloc] init];
self.imageV = [[UIImageView alloc] initWithImage:self.image];
[self.view addSubview:self.shadowV];
[self.shadowV addSubview:self.imageV];
// set the raidus and the mask to follow the rounded corners.
[self.imageV.layer setCornerRadius:CORNER_RADIUS];
[self.imageV.layer setMasksToBounds:YES];
[self.imageV setContentMode:UIViewContentModeScaleAspectFit];
// set the shadows properties
[self.shadowV.layer setShadowColor:[UIColor blackColor].CGColor];
[self.shadowV.layer setShadowOpacity:0.4];
[self.shadowV.layer setShadowRadius:3.0];
[self.shadowV.layer setShadowOffset:CGSizeMake(SHADOW_OFFSET, SHADOW_OFFSET)];
[self.shadowV.layer setCornerRadius:CORNER_RADIUS];
[self.shadowV setBackgroundColor:[UIColor whiteColor]]; // The view needs to have some content. Otherwise it is not displayed at all, not even its shadow.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Just to be save
if (!self.image) {
return;
}
self.imageV.image = self.image; // The image property was set by the caller.
// Layout imageV within self.view with a margin of MARGIN
self.imageV.frame = CGRectMake(MARGIN, MARGIN, self.view.bounds.size.width - 2 * MARGIN, self.view.bounds.size.height - 2 * MARGIN);
// Calculate the size and position of the image and set the image view to
// the same dimensions
// This works under the assumption, that the image content mode is aspectFit.
// Well, as we are doing so much of the layout manually, it would work with a number of content modes. :-)
float imageWidth, imageHeight;
float heightWidthRatioImageView = self.view.frame.size.height / self.view.frame.size.width;
float heightWidthRatioImage = self.image.size.height / self.image.size.width;
if (heightWidthRatioImageView > heightWidthRatioImage) {
// The ImageView is "higher" than the image itself.
// --> The image width is set to the imageView width and its height is scaled accordingly.
imageWidth = self.imageV.frame.size.width;
imageHeight = imageWidth * heightWidthRatioImage;
} else {
// The ImageView is "wider" than the image itself.
// --> The image height is set to the imageView height and its width is scaled accordingly.
imageHeight = self.imageV.frame.size.height;
imageWidth = imageHeight / heightWidthRatioImage;
}
// Layout imageView and ShadowView accordingly.
CGRect imageRect =CGRectMake((self.view.bounds.size.width - imageWidth) / 2,
(self.view.bounds.size.height - imageHeight) / 2,
imageWidth, imageHeight);
[self.shadowV setFrame:imageRect];
[self.imageV setFrame:CGRectMake(0.0f, 0.0f, imageWidth, imageHeight)]; // Origin is (0,0) because it overlaps its superview which just throws the shadow.
}
And this is how it finally looks like:
The problem is due to some issue with code or configuration you have not told us about. Proof: I ran the following and it works fine. Note that I create the image view in code (to avoid the auto layout problem) and fixed your frame/bounds confusion, and that I've skipped your self.image, but none of that is really relevant to the issue you are seeing:
#define CORNER_RADIUS 18
#define MARGIN 15
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.imageV = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"im"]];
[self.view addSubview:self.imageV];
// Layout imageV within self.view with a margin of MARGIN
self.imageV.frame = CGRectMake(self.view.bounds.origin.x + MARGIN, self.view.bounds.origin.y + MARGIN, self.view.bounds.size.width - 2 * MARGIN, self.view.bounds.size.height - 2 * MARGIN);
// set the raidus and the mask to follow the rounded corners.
self.imageV.layer.cornerRadius = CORNER_RADIUS;
self.imageV.layer.masksToBounds = YES;
}
It works fine (and you can prove that to yourself). Here is a screen shot of the 4-inch simulator:
Therefore the problem is outside the code that you quote in your question, and cannot be analyzed without further information.

Resize MKMapView

Looks like simple task. But when I try to resize using setFrame method I got glitches. There are some other UIViews resized using setFrame method and it works perfectly. I made custom application with slider bar and map view. SlideBar changes X position for MKMapView, but keeps width equals to screen width. This approach works fine for all Views. But MKMapView resizes with smooth troubles. Can anyone please give a clue why it's happening and how to solve it?
I had the same problem, which seems to occur only on iOS 6 (Apple Maps) and not on iOS 5 (Google Maps).
My solution was to take a "screenshot" of the map when the user start dragging the divider handle, replace the map with this screenshot during the drag, and put the map back when the finger is released.
I used the code from How to capture UIView to UIImage without loss of quality on retina display for UIView screenshot and Nikolai Ruhe's answer at How do I release a CGImageRef in iOS for a nice background color.
My UIPanGestureRecognizer action is something like this (the (MKMapView)self.map is a subview of (UIView)self.mapContainer with autoresizing set on Interface Builder):
- (IBAction)handleMapPullup:(UIPanGestureRecognizer *)sender
{
CGPoint translation = [sender translationInView:self.mapContainer];
// save current map center
static CLLocationCoordinate2D centerCoordinate;
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
// Save map center coordinate
centerCoordinate = self.map.centerCoordinate;
// Take a "screenshot" of the map and set the size adjustments
UIImage *mapScreenshot = [UIImage imageWithView:self.map];
self.mapImage = [[UIImageView alloc] initWithImage:mapScreenshot];
self.mapImage.autoresizingMask = self.map.autoresizingMask;
self.mapImage.contentMode = UIViewContentModeCenter;
self.mapImage.clipsToBounds = YES;
self.mapImage.backgroundColor = [mapScreenshot mergedColor];
// Replace the map with a screenshot
[self.map removeFromSuperview];
[self.mapContainer insertSubview:self.mapImage atIndex:0];
} break;
case UIGestureRecognizerStateChanged:
break;
default:
// Resize the map to the new dimension
self.map.frame = self.mapImage.frame;
// Replace the screenshot with the resized map
[self.mapImage removeFromSuperview];
[self.mapContainer insertSubview:self.map atIndex:0];
// Empty screenshot memory
self.mapImage = nil;
break;
}
// resize map container according do the translation value
CGRect mapFrame = self.mapContainer.frame;
mapFrame.size.height += translation.y;
// reset translation to make a relative read on next event
[sender setTranslation:CGPointZero inView:self.mapContainer];
self.mapContainer.frame = mapFrame;
self.map.centerCoordinate = centerCoordinate; // keep center
}

How to ignore transformations when drawing an image with drawAsPatternInRect:

I draw a background image in a view with the following code:
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
UIImage *image = [UIImage imageNamed:#"background.jpg"];
[image drawAsPatternInRect:rect];
CGContextRestoreGState(currentContext);
This works well. But when I change the size of that view animated the drawn image is scaled until it get's redraw. How can I ignore this transformation during the animation?
At the moment I don't think that's possible. Since the iPhone doesn't have a very fast processor Apple choose to disable the LiveRedraw-feature (that actually is available on Mac).
Try setting the view's contentMode to UIViewContentModeRedraw.
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
// Get the view to redraw when it's frame is changed
self.contentMode = UIViewContentModeRedraw;
}
return self;
}
I finally found a solution. I use contentMode UIViewContentModeTopLeft so it doesn't get scaled. And before the resize animation I only redraw if the new size is greater then the old one. And after the animation I always redraw.