UIView animations - When does it animate certain properties like scale and position? - objective-c

I'm currently working with a UITableViewController which contains some UITableViewCells subclasses.
When layoutSubviews is called on these UITableViewCells, I change some of the cells' subviews' scales and positions depending on the width and height of the contentView. (The UITableViewCells have some subviews, and I change their scales and positions)
A good example is toggling edit mode, since it shortens the contentView by a bit.
When I do this, the scales and positions of my UITableViewCell's subview do animate
func toggleEdit() {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(0.35)
self.tableView.beginUpdates()
self.tableView.editing = !self.tableView.editing
self.tableView.endUpdates()
UIView.commitAnimations()
}
When I do this, ONLY the positions animate, the scales change immediately which looks really ugly:
func toggleEdit() {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(0.35)
self.tableView.editing = !self.tableView.editing
UIView.commitAnimations()
}
I kind of found this out by accident, so I'm wondering now what kind of magic begin/end tableViewUpdate does, and how I can control myself in any scenario which properties should animate and which shouldn't.

The docs state
When you call endUpdates, UITableView animates the operations simultaneously
You could try changing things that you don't want animated in a performWithoutAnimation(_:) block
UIView.performWithoutAnimation {
// things you don't want to animate
}

Related

Starting Position of NSSplitView divider

How can I set the start position of a NSSplitView?
The closest thing I've found that looks like it would work is setPosition
//Set splitView position.
[splitView setPosition:330 ofDividerAtIndex:0];
This doesn't seem to do anything though, my splitview still starts with the divider in the center.
ANy ideas?
You don't set the position of the divider, you set the sizes of your NSSplitView's subviews. The divider is then repositioned automatically.
This is how I positioned my divider and subview size (in swift):
let subview: NSView = mySplitView.subviews[1] as NSView
subview.setFrameSize(NSMakeSize(subview.frame.size.width, 100))
In the view's class housing the split view
override func viewWillAppear() {
self.mySplitView.setPosition(120, ofDividerAtIndex: 0)
}
or wherever you want it to start.
NSSplitView needs initial non-sized bounds to make them layout correctly.
If your view has zero-size, then it will not show expected layout.
The best way is providing non-zero layout (this is what IB does), but sometimes this is impossible.
If you cannot provide non-zero size, then I think you have to provide proper - (void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize delegate method implementation to layout everything manually yourself. (this is my current best practice)
Maybe that is the center? If splitView is correctly hooked up to your split view, that code should work. You should probably log [splitView minPossiblePositionOfDividerAtIndex:0] and [splitView maxPossiblePositionOfDividerAtIndex:0] before trying to set the position of the divider so you know the possible values.

UIScrollView - Custom Map - Prevent marker subview on map from scaling with map

I have a custom map of a limited area, and have it set up to correctly show the users' location. The map is a 1600px square image within a UIScrollView.
I have a crosshair image to show the current location of the user, which at zoomScale 1.0 is the desired size. When I pinch and zoom the scrollView, the crosshair scales with it. I would like to have the subview remain the same size on screen.
I haven't been able to find any information on this, what would be the best way to go about this?
If there is anything I can provide you with to help the answer, please let me know.
Many thanks!
EDIT -
Having looked in to this further, there is a UIScrollViewDelegate method - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale which I tried using to take the marker's current center and size, then adjust, but this only scales at the end of the zoom. I would prefer to have the marker remain the same size while the user is zooming.
EDIT 2-
Cake has provided a great answer below, but I haven't been able to implement this in the way I imagined it would be.
I have the UIImageView as a placeholder, with alpha set to 0. This placeholder moves around relative to the map to show the user location. This operates as I expect it to. Unfortunately, this resizes with the map, as it is a subview of the map (so it stays in place).
Taking Cake's below answer, I have created the non-scaling crosshair image, and added it as a sibling subview to the scrollview. The maths, once Cake had pointed them out, were quite simple to get the new frame for the crosshair:
CGPoint ULPC = userLocationPlaceholder.center;
float zs = scrollView.zoomScale;
CGRect newFrame = CGRectMake(((ULPC.x * zs) - scrollView.contentOffset.x) - 20, ((ULPC.y * zs) - scrollView.contentOffset.y) - 20, 40, 40);
Where the image is 40points wide. This matches the centers perfectly.
The problem I now have is that I cannot get the crosshair image to stay locked to the placeholder.
I have tried using a self calling animation as such:
-(void)animeUserLocationAttachment
{
[UIView animateWithDuration:0.05
delay:0
options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear )
animations:^{
userLocationDotContainer.frame = newFrame;
} completion:^(BOOL finished){
// Call self
[self animateUserLocationAttachment];
}];
}
As soon as I start scrolling/zooming, this locks the animation so that the crosshair just sits in place until I release the scrolling/zooming, then it correctly updates it's location.
Is there any way I can get around this, or an alternative method I can apply?
Many thanks
EDIT 3 -
I've re-accepted Cake's answer as it covers 90% of the issue. Further to his answer I have implemented the ScrollViewDelegate methods scrollViewWillBeginDragging: andscrollViewWillBeginDecelerating: to scale the placeholder to match the current size of the crosshair relative to the map, show the placeholder (that is a subview of the map image) and hide the crosshair image. The delegate method scrollviewWillBeginZooming:withView: does not show the placeholder because it scales with the map. As Cake recommends, I'll make a new question for this issue.
The counterpart methods (scrollViewDidEndZooming:withView:atScale:, scrollViewDidEndDragging:willDecelerate: and -scrollViewDidEndDecelerating:`) all hide the placeholder, and re-show the crosshair.
The question is old but for the future similar questions I've recently resolved a similar problem applying the hint of Andrew Madsen of another post.
I'had a UIScrollView, with an UIImageView in it. Attached to the UIImageView I had many MKAnnotationView (those are my subviews that I didn't want scaling with the superview).
I did subclass UIImageView and implement setTransform: method like here:
#import "SLImageView.h"
#implementation SLImageView
- (void)setTransform:(CGAffineTransform)transform
{
[super setTransform:transform];
CGAffineTransform invertedTransform = CGAffineTransformInvert(transform);
for (id obj in self.subviews)
{
if ([obj isKindOfClass:[MKAnnotationView class]])
{
[((UIView *)obj) setTransform:invertedTransform];
}
}
}
#end
This works perfectly!
Mick.
Create another crosshair image that's associated with the view or view controller that contains the scrollview. Then have this one always snap to the center of the crosshair image you already have. Then, hide your original crosshair image. Then you can avoid having the scrollview scale the disassociated crosshair, and it should stay the same size.
Relative coordinate systems
Each view in cocoa touch has a frame property that has an origin. In order to position an object owned by one view properly relative to another view, all you have to do is figure out the differences in their origins. If one view is a subview of another, then this isn't too difficult.
Get the origin of the container view
Get the location of the subview inside of the container view
Get the origin of the subview
Calculate the difference in the positions of the origins
Get the location of the object you want to overlap (relative to the subview)
Calculate the location of the object you want to overlap relative to the container view
Move your crosshair to this position
Swift equivalent for Mick's answer:
class MapContainerView:UIView {
#IBOutlet var nonScalingViews: [UIView]!
override var transform: CGAffineTransform {
didSet {
guard let nonScalingViews = nonScalingViews else {
return
}
let invertedTransform = CGAffineTransformInvert(transform)
for view in nonScalingViews {
view.transform = invertedTransform
}
}
}
}

Possible to manually rotate UIPopoverController's view?

The view system in my app is highly customized and uses a number of views that are manually rotated from portrait to landscape based on user interactions (the rotation is done by applying an affine transform to the view/layer).
I want to present a popover inside one of these rotated views, but the orientation of the popover always appears relative to the orientation of the device (i.e., not relative to the view). I'm guessing the answer is no, but just in case someone has a clever idea: is there any way to manually rotate the view that is presented by UIPopoverController?
Sean, I just tested it for kicks, yes it works.
It has to be done (in my case at least) in viewDidAppear (if done in viewWillAppear, it gets knocked back to the original setting.)
This worked just fine (just tested now) to have a popover at a 90 degree angle. i.e in my case my main view is in portrait mode and the popover is turned 90 deg.
self.navigationController.view.superview.superview.transform = CGAffineTransformMakeRotation (M_PI/2.0);
Are you trying to rotate the popover or just the content shown in the popover? You can control some of the former by setting which arrow orientations are possible. I'm interested in the latter, and it seems to work just by grabbing the content view controller. E.g.:
aPopoverController.contentViewController.view.transform = CGAffineTransformMakeRotation(M_PI);
DISCLAIMER: If you're at all interested in trying to get your app into the store, this code is almost certainly grounds for rejection. It dives into UIKit's private API's which is a big no-no as far as apple is concerned.
#RunningPink had the right idea. Depending on how the view hierarchy is set up, the popover may be back up farther than two superviews. The popover itself it an instance of the (private) class _UIPopover (at least in iOS 5). You can find this view by doing:
UIView *possiblePopover = popoverController.contentViewController.view;
while (possiblePopover != nil) {
// Climb up the view hierarchy
possiblePopover = possiblePopover.superview;
if ( [NSStringFromClass([possiblePopover class]) isEqualToString:#"_UIPopoverView"] ) {
// We found the popover, break out of the loop
break;
}
}
if (nil != possiblePopover) {
// Do whatever you want with the popover
}
In doing this, I found that transforming the view often ended up making the popover look blurry. I found the reason was that the popover's superview was an instance of another private class called UIDimmingView which is responsible for accepting touches outside of the popover and causing the popover to dismiss. Performing the rotation on the dimming view removed the blurriness I was seeing in the popover.
However, transforming the dimming can result in weirdness where certain parts of the window are not "covered" by the dimming view so the popover will not dismiss if these parts of the window are tapped. To get around this, I applied the rotation to the dimming view, reset the dimming view's frame to cover the screen, and then translated the popover view into place.
if (nil != possiblePopover) {
// Found the popover view
CGAffineTransform rotation = CGAffineTransformMakeRotation(-M_PI_2);
CGAffineTransform translation = // Whatever translation in necessary here
// Rotate the UIDimming View and reset its frame
[possiblePopover.superview setTransform:rotation];
[possiblePopover.superview setFrame:CGRectMake(0, 0, possiblePopover.superview.frame.size.height, possiblePopover.superview.frame.size.width)];
// Translate the popover view
[possiblePopover setTransform:translation];
}

Why aren't my views being positioned correctly on interfaceOrientation changes?

I have a UIViewController one UIWebView in it. I'd like the UIWebView to be positioned in the centre of the iPad screen in landscape and portrait modes. So, I've implemented it like this
// UIViewController
// InfoGraphicView is the UIWebView
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
return YES;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration {
if (toInterfaceOrientation == UIInterfaceOrientationPortrait ||
toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
[self layoutPortrait];
} else {
[self layoutLandscape];
}
}
- (void)layoutLandscape {
NSLog(#"Layout Landscape");
infoGraphicView.frame = CGRectMake(100, 100, 936, 700);
}
- (void)layoutPortrait {
NSLog(#"Layout Portrait");
infoGraphicView.frame = CGRectMake(100, 100, 700, 936);
}
However, it's not behaving as I expected. In the above code, I would expectt he UIWebView to be 100px (or points or whatever the unit is) away from the top and the left. But it's not. In Portrait mode it appears flush with the top left of the screen, and in Landscape mode it seems to be partially offscreen in the top left.
If I set the frame as CGRectMake(-100, 100, 700, 936) then I get it positioned in the center of the screen as I'd like it to be, but I've no idea why.
As usual, there's most likely something simple I'm overlooking but I can't figure it out. Any help greatly appreciated as always.
The coordinates you set on infoGraphicView are relative to its superview, not to the screen generally. And views don't necessarily clip their subviews. Furthermore, the shape set automatically to self.view will depend on the scaling flags set in Interface Builder. However, I think that by default it is set to fill the whole screen.
That said, I think the mistake is in your use of willRotateToInterfaceOrientation:duration:. That is called before the rotation begins, so self.view has the old size (ie, it'll still be portrait sized if rotating from portrait to landscape and vice versa). Probably better to hook willAnimateRotationToInterfaceOrientation:duration: — then the correct size has been set and you'll be within the CoreAnimation block so your view will grow/shrink as part of the rotation animation.
It's also worth checking which resizing flags you have set on infoGraphicView. They'll take effect automatically, in addition to any changes you make. So you probably want to disable them all.
This probably is an issue with the view that the web view is in. The coordinate system used is that of the view’s superview. If that view isn’t being resized on rotation, then you’ll see unexpected layout like this. You can access the superview of a view through the superview property; one way to see its frame would be to use its description. Put this line in one of your layout methods:
NSLog(#"Superview: %#", [infoGraphicView superview]);
That should print out a description of the view.
Once you get that figured out, if you want the web view to have the same layout, you can use its autoresizingMask property. If you set it like this:
infoGraphicView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Then the view will automatically change its width and height to keep the top, left, right, and bottom margins the same.

UIView subclass draws background despite completely empty drawRect: - why?

So, I have a custom UIView subclass which enables drawing of rounded edges. The thing draws perfectly, however the background always fills the whole bounds, despite clipping to a path first. The border also draws above the rectangular background, despite the fact that I draw the border in drawRect: before the background. So I removed the whole content of drawRect:, which is now virtually empty - nevertheless the background gets drawn!
Anybody an explanation for this? I set the backgroundColor in Interface Builder. Thanks!
Sorry for this monologue. :)
The thing is that the UIView's layer apparently draws the background, which is independent from drawRect:. This is why you can't get rid of the background by overriding drawRect:.
You can either override - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx and make the layer draw whatever you want, or you override - (void) setBackgroundColor:(UIColor *)newColor and don't assign newColor to backgroundColor, but to your own ivar, like myBackgroundColor. You can then use myBackgroundColor in drawRect; to draw the background however you like.
Overriding setBackgroundColor:
Define an instance variable to hold your background color, e.g. myBackgroundColor. In your init methods, set the real background color to be the clearColor:
- (id) initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super init...])) {
[super setBackgroundColor:[UIColor clearColor]];
}
return self;
}
Override:
- (void) setBackgroundColor:(UIColor *)newColor
{
if (newColor != myBackgroundColor) {
[myBackgroundColor release];
myBackgroundColor = [newColor retain];
}
}
Then use myBackgroundColor in your drawRect: method. This way you can use the color assigned from Interface Builder (or Xcode4) in your code.
Here's an easier fix:
self.backgroundColor = [UIColor clearColor];
I hesitate to suggest this because I don't want to offend you, but have you set opaque to NO?
This is easily reproducible:
Create a new View-Based iPhone Application. Create an UIView subclass and leave the drawRect: method completely empty. In Interface Builder, drag a UIView into the main view, assign it a background color and set the class of the view to your subclass. Save, build and run, and see, the view shows the background color.
I have circumvented this behaviour by overriding setBackgroundColor: and assigning the color to my own ivar, always leaving the backgroundColor property nil. This works, however I'm still wondering why this works this way.
You should set
clearsContextBeforeDrawing = NO
It's that simple.
From the UIView Reference Docs
Oops. I misunderstood the question. The OP (original poster) wants their custom control to support the standard "backgroundColor" property (eg, set by the gui designer), but does NOT want the system to paint that color. The OP wants to paint the bg himself so that he can round the corners.
In that case, the post about overriding the layer level drawing is correct.
The solution I posted will prevent the UI system from clearing your buffer before drawing. When you have no bg defined, if clearsContextBeforeDrawing is set, the iOS will clear your view's buffer, setting all pixels to transparent black. Doing this for a full-screen view takes about 5ms in an iPad3, so it's not free (pushing that 2048x1536 pixels never is). For comparison drawing a full-screen bitmap (using kCGBlendModeCopy to force blitting) takes ~25ms (using quartz, not GPU).
While it is possible to override the code that copies the background colour into the view's layer, I'm not a fan of this approach. I believe it can cause issues with views intended to be opaque. My suggestion is to create a custom colour variable like this:
#IBInspectable var foregroundColor: UIColor = .black {
didSet { setNeedsDisplay() }
}
Now, set your view's backgroundColor to clear and use foregroundColor in your drawing code in its place. The code above takes care of updating it when the value changes, and also exposes it to Interface Builder if required.
Another alternative is to use your view's tintColor instead, and to ensure that it updates correctly when tint colour changes. You may be reserving tint colour for other uses though, so this is not necessarily ideal. If you use tintColor, don't forget to include the following:
override func tintColorDidChange() {
setNeedsDisplay()
}