Animating CALayer shadow simultaneously as UITableviewCell height animates - objective-c

I have a UITableView that I am attempting to expand and collapse using its beginUpdates and endUpdates methods and have a drop shadow displayed as that's happening. In my custom UITableViewCell, I have a layer which I create a shadow for in layoutSubviews:
self.shadowLayer.shadowColor = self.shadowColor.CGColor;
self.shadowLayer.shadowOffset = CGSizeMake(self.shadowOffsetWidth, self.shadowOffsetHeight);
self.shadowLayer.shadowOpacity = self.shadowOpacity;
self.shadowLayer.masksToBounds = NO;
self.shadowLayer.frame = self.layer.bounds;
// this is extremely important for performance when drawing shadows
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.shadowLayer.frame cornerRadius:self.cornerRadius];
self.shadowLayer.shadowPath = shadowPath.CGPath;
I add this layer to the UITableViewCell in viewDidLoad:
self.shadowLayer = [CALayer layer];
self.shadowLayer.backgroundColor = [UIColor whiteColor].CGColor;
[self.layer insertSublayer:self.shadowLayer below:self.contentView.layer];
As I understand it, when I call beginUpdates, an implicit CALayerTransaction is made for the current run loop if none exists. Additionally, layoutSubviews also gets called. The problem here is that the resulting shadow is drawn immediately based on the new size of the UITableViewCell. I really need to shadow to continue to cast in the expected way as the actual layer is animating.
Since my created layer is not a backing CALayer it animates without explicitly specifying a CATransaction, which is expected. But, as I understand it, I really need some way to grab hold of beginUpdates/endUpdates CATransaction and perform the animation in that. How do I do that, if at all?

So I guess you have something like this:
(I turned on “Debug > Slow Animations” in the simulator.) And you don't like the way the shadow jumps to its new size. You want this instead:
You can find my test project in this github repository.
See #horseshoe7's answer for a Swift translation.
It is tricky but not impossible to pick up the animation parameters and add an animation in the table view's animation block. The trickiest part is that you need to update the shadowPath in the layoutSubviews method of the shadowed view itself, or of the shadowed view's immediate superview. In my demo video, that means that the shadowPath needs to be updated by the layoutSubviews method of the green box view or the green box's immediate superview.
I chose to create a ShadowingView class whose only job is to draw and animate the shadow of one of its subviews. Here's the interface:
#interface ShadowingView : UIView
#property (nonatomic, strong) IBOutlet UIView *shadowedView;
#end
To use ShadowingView, I added it to my cell view in my storyboard. Actually it's nested inside a stack view inside the cell. Then I added the green box as a subview of the ShadowingView and connected the shadowedView outlet to the green box.
The ShadowingView implementation has three parts. One is its layoutSubviews method, which sets up the layer shadow properties on its own layer to draw a shadow around its shadowedView subview:
#implementation ShadowingView
- (void)layoutSubviews {
[super layoutSubviews];
CALayer *layer = self.layer;
layer.backgroundColor = nil;
CALayer *shadowedLayer = self.shadowedView.layer;
if (shadowedLayer == nil) {
layer.shadowColor = nil;
return;
}
NSAssert(shadowedLayer.superlayer == layer, #"shadowedView must be my direct subview");
layer.shadowColor = UIColor.blackColor.CGColor;
layer.shadowOffset = CGSizeMake(0, 1);
layer.shadowOpacity = 0.5;
layer.shadowRadius = 3;
layer.masksToBounds = NO;
CGFloat radius = shadowedLayer.cornerRadius;
layer.shadowPath = CGPathCreateWithRoundedRect(shadowedLayer.frame, radius, radius, nil);
}
When this method is run inside an animation block (as is the case when the table view animates a change in the size of a cell), and the method sets shadowPath, Core Animation looks for an “action” to run after updating shadowPath. One of the ways it looks is by sending actionForLayer:forKey: to the layer's delegate, and the delegate is the ShadowingView. So we override actionForLayer:forKey: to provide an action if possible and appropriate. If we can't, we just call super.
It is important to understand that Core Animation asks for the action from inside the shadowPath setter, before actually changing the value of shadowPath.
To provide the action, we make sure the key is #"shadowPath", that there is an existing value for shadowPath, and that there is already an animation on the layer for bounds.size. Why do we look for an existing bounds.size animation? Because that existing animation has the duration and timing function we should use to animate shadowPath. If everything is in order, we grab the existing shadowPath, make a copy of the animation, store them in an action, and return the action:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:#"shadowPath"]) { return [super actionForLayer:layer forKey:event]; }
CGPathRef priorPath = layer.shadowPath;
if (priorPath == NULL) { return [super actionForLayer:layer forKey:event]; }
CAAnimation *sizeAnimation = [layer animationForKey:#"bounds.size"];
if (![sizeAnimation isKindOfClass:[CABasicAnimation class]]) { return [super actionForLayer:layer forKey:event]; }
CABasicAnimation *animation = [sizeAnimation copy];
animation.keyPath = #"shadowPath";
ShadowingViewAction *action = [[ShadowingViewAction alloc] init];
action.priorPath = priorPath;
action.pendingAnimation = animation;
return action;
}
#end
What does the action look like? Here's the interface:
#interface ShadowingViewAction : NSObject <CAAction>
#property (nonatomic, strong) CABasicAnimation *pendingAnimation;
#property (nonatomic) CGPathRef priorPath;
#end
The implementation requires a runActionForKey:object:arguments: method. In this method, we update the animation that we created in actionForLayer:forKey: using the saved-away old shadowPath and the new shadowPath, and then we add the animation to the layer.
We also need to manage the retain count of the saved path, because ARC doesn't manage CGPath objects.
#implementation ShadowingViewAction
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict {
if (![anObject isKindOfClass:[CALayer class]] || _pendingAnimation == nil) { return; }
CALayer *layer = anObject;
_pendingAnimation.fromValue = (__bridge id)_priorPath;
_pendingAnimation.toValue = (__bridge id)layer.shadowPath;
[layer addAnimation:_pendingAnimation forKey:#"shadowPath"];
}
- (void)setPriorPath:(CGPathRef)priorPath {
CGPathRetain(priorPath);
CGPathRelease(_priorPath);
_priorPath = priorPath;
}
- (void)dealloc {
CGPathRelease(_priorPath);
}
#end

This is Rob Mayoff's answer written in Swift. Could save someone some time.
Please don't upvote this. Upvote Rob Mayoff's solution. It is awesome, and correct. (Note from mayoff: why not upvote both? 😉)
import UIKit
class AnimatingShadowView: UIView {
struct DropShadowParameters {
var shadowOpacity: Float = 0
var shadowColor: UIColor? = .black
var shadowRadius: CGFloat = 0
var shadowOffset: CGSize = .zero
static let defaultParameters = DropShadowParameters(shadowOpacity: 0.15,
shadowColor: .black,
shadowRadius: 5,
shadowOffset: CGSize(width: 0, height: 1))
}
#IBOutlet weak var contentView: UIView! // no sense in have a shadowView without content!
var shadowParameters: DropShadowParameters = DropShadowParameters.defaultParameters
private func apply(dropShadow: DropShadowParameters) {
let layer = self.layer
layer.shadowColor = dropShadow.shadowColor?.cgColor
layer.shadowOffset = dropShadow.shadowOffset
layer.shadowOpacity = dropShadow.shadowOpacity
layer.shadowRadius = dropShadow.shadowRadius
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer
layer.backgroundColor = nil
let contentLayer = self.contentView.layer
assert(contentLayer.superlayer == layer, "contentView must be a direct subview of AnimatingShadowView!")
self.apply(dropShadow: self.shadowParameters)
let radius = contentLayer.cornerRadius
layer.shadowPath = UIBezierPath(roundedRect: contentLayer.frame, cornerRadius: radius).cgPath
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "shadowPath" else {
return super.action(for: layer, forKey: event)
}
guard let priorPath = layer.shadowPath else {
return super.action(for: layer, forKey: event)
}
guard let sizeAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation else {
return super.action(for: layer, forKey: event)
}
let animation = sizeAnimation.copy() as! CABasicAnimation
animation.keyPath = "shadowPath"
let action = ShadowingViewAction()
action.priorPath = priorPath
action.pendingAnimation = animation
return action
}
}
private class ShadowingViewAction: NSObject, CAAction {
var pendingAnimation: CABasicAnimation? = nil
var priorPath: CGPath? = nil
// CAAction Protocol
func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
guard let layer = anObject as? CALayer, let animation = self.pendingAnimation else {
return
}
animation.fromValue = self.priorPath
animation.toValue = layer.shadowPath
layer.add(animation, forKey: "shadowPath")
}
}

Assuming that you're manually setting your shadowPath, here's a solution inspired by the others here that accomplishes the same thing using less code.
Note that I'm intentionally constructing my own CABasicAnimation rather than copying the bounds.size animation exactly, as in my own tests I found that toggling the copied animation while it was still in progress could cause the animation to snap to it's toValue, rather than transitioning smoothly from its current value.
class ViewWithAutosizedShadowPath: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let oldShadowPath = layer.shadowPath
let newShadowPath = CGPath(rect: bounds, transform: nil)
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let shadowPathAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowPathAnimation.duration = boundsAnimation.duration
shadowPathAnimation.timingFunction = boundsAnimation.timingFunction
shadowPathAnimation.fromValue = oldShadowPath
shadowPathAnimation.toValue = newShadowPath
layer.add(shadowPathAnimation, forKey: "shadowPath")
}
layer.shadowPath = newShadowPath
}
}

UITableView is likely not creating a CATransaction, or if it is, it's waiting until after you end the updates. My understanding is that table views just coalesce all changes between those functions and then creates the animations as necessary. You don't have a way to get a handle on the actual animation parameters it's committing, because we don't know when that actually happens. The same thing happens when you animate a content offset change in UIScrollView: the system provides no context about the animation itself, which is frustrating. There is also no way to query the system for current CATransactions.
Probably the best you can do is inspect the animation that UITableView is creating and just mimic the same timing parameters in your own animation. Swizzling add(_:forKey:) on CALayer can allow you to inspect all animations being added. You certainly don't want to actually ship with this, but I often use this technique in debugging to figure out what animations are being added and what their properties are.
I suspect that you're going to have to commit your own shadow layer animations in tableView(_:willDisplayCell:for:row:) for the appropriate cells.

Related

How to get the coordinates of Hyperlink in UIWebview

I'm loading the pdf (Having multiple Hyperlinks) document in UIWebview. I have to show UIPopover over hyperlinks dynamically.
I'm able to capture the coordinates of hyperlink using TapGesture Action method
- (void)tapAction:(UITapGestureRecognizer *)sender
{
self.point = [sender locationInView:self.myWebView];
}
And presenting the UIPopover over hyperlink by using below method
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *rqstUrl = [request URL];
if (([[rqstUrl scheme] isEqualToString: #"https"] || [[rqstUrl scheme] isEqualToString: #"http"]) && (navigationType == UIWebViewNavigationTypeLinkClicked))
{
[self.myWebView stopLoading];
CGRect rect = CGRectMake(self.point.x,self.point.y-5, 5, 5);
UIPopoverController *popController = [[UIPopoverController alloc] initWithContentViewController:contentViewController];
popController.popoverContentSize = CGSizeMake(500, 200);
self.popController = popController;
self.popController.delegate =self;
UIPopoverArrowDirection direction = UIPopoverArrowDirectionUp|UIPopoverArrowDirectionDown;
self.popController.popoverLayoutMargins = UIEdgeInsetsMake(0, rect.origin.x, 1, 1);
[self.popController presentPopoverFromRect:rect inView:webView permittedArrowDirections:direction animated:YES];
}
return YES;
}
But the problem is if I tapped in two different locations within 1 or 2 seconds like First Tap is On Hyperlink and Second Tap is on "somewhere else in UIWebview", UIPopover is presenting at second tap location only not in hyperlink location.
I have to show UIPopover based on the Hyperlink position only, not in other location.How can I resolve this issue?
Use an overlay view
Replace your method to register the tap location by an overlay with a tap through. UITapGestureRecognizer has these limitations:
When a tap occurs outside of an hyperlink, it does registers its location, thanks to the UITapGestureRecognizer.
Unfortunately, a UIWebview Hyperlink taps take precedence over the gesture recognizer, and you never get the centroid. This is the real problem, causing the popover to appear misplaced.
UIPopoverController is deprecated in iOS 9.
"UIPopoverController is deprecated. Popovers are now implemented as UIViewController presentations. Use a modal presentation style of UIModalPresentationPopover and UIPopoverPresentationController."
tapAction and shouldStartLoadWithRequest are not coupled, and can occur independently of each other. Furthermore, they are basically mutually exclusive.
Use the overlay to register location in that view, and tap-though to the views underneath. If your overlay and web view have the same frame, you can use the tap position interchangeably. The overlay will guarantee tight coupling, and the rest of your method will work as designed.
class TapOverlayView: UIView {
var centroid:CGRect = CGRect.zero
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
centroid = CGRect(origin: point, size: CGSize(width: 1, height: 1))
return nil // tap through
}
}
Delegate
extension ViewController: UIWebViewDelegate {
public func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let rqstUrl = request.url
if( rqstUrl!.scheme?.contains("http"))! && ( .linkClicked == navigationType) {
webView.stopLoading()
let contentViewController = storyboard!.instantiateViewController(withIdentifier: "popover")
contentViewController.modalPresentationStyle = .popover
contentViewController.preferredContentSize = CGSize(width: 200, height: 40)
if let popController = contentViewController.popoverPresentationController {
popController.permittedArrowDirections = .down
popController.sourceView = webView
popController.sourceRect = CGRect(origin: tap.centroid.origin, size: CGSize.zero)
present(contentViewController, animated: true, completion: nil)
}
}
return true
}
}
► Find this solution on GitHub and additional details on Swift Recipes.

ContentInset for MKMapView

UIScrollView has an excellent contentInset property which tells the view, which portion is visible on the screen. I have an MKMapView which is partially covered by a translucent view. I want the map to be visible under the view. I have to display several annotations on the map, and I want to zoom to them using -setRegion:animated:, but the map view does not respect that it is partially covered, therefore some of my annotations will be covered by the translucent view.
Is there any way to tell the map, to calculate like the scroll view does using contentInset?
UPDATE: This is what I've tried:
- (MKMapRect)mapRectForAnnotations
{
if (self.trafik) {
MKMapPoint point = MKMapPointForCoordinate(self.trafik.coordinate);
MKMapPoint deltaPoint;
if (self.map.userLocation &&
self.map.userLocation.coordinate.longitude != 0) {
MKCoordinateSpan delta = MKCoordinateSpanMake(fabsf(self.trafik.coordinate.latitude-self.map.userLocation.coordinate.latitude),
fabsf(self.trafik.coordinate.longitude-self.map.userLocation.coordinate.longitude));
deltaPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(delta.latitudeDelta, delta.longitudeDelta));
} else {
deltaPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(0.01, 0.01));
}
return MKMapRectMake(point.x, point.y, deltaPoint.x, deltaPoint.y);
} else {
return MKMapRectNull;
}
}
Use UIViews's layoutMargins.
E.g. This will force the current user's position pin to move 50pts up.
mapView.layoutMargins = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 100.0, right: 0.0)
You can do the following but it could mess with other views in your UIViewController that use bottomLayoutGuide. You'll have to test it to find out.
Override bottomLayoutGuide in the UIViewController that has your map as a subview and return a MyLayoutGuide object that looks like:
#interface MyLayoutGuide : NSObject <UILayoutSupport>
#property (nonatomic) CGFloat length;
-(id)initWithLength:(CGFloat)length;
#end
#implementation MyLayoutGuide
#synthesize length = _length;
#synthesize topAnchor = _topAnchor;
#synthesize bottomAnchor = _bottomAnchor;
#synthesize heightAnchor = _heightAnchor;
- (id)initWithLength:(CGFloat)length
{
if (self = [super init]) {
_length = length;
}
return self;
}
#end
bottomLayoutGuide that insets the MKMapView by 50 points:
- (id)bottomLayoutGuide
{
CGFloat bottomLayoutGuideLength = 50.f;
return [[MyLayoutGuide alloc] initWithLength:bottomLayoutGuideLength];
}
You can force this "inset" to be calculated again by calling setNeedsLayout on your MKMapView in the event that your time table on the bottom changes size. We've created a helper in our MKMapView subclass that can be called from the parent UIViewController:
- (void)updateBottomLayoutGuides
{
// this method ends up calling -(id)bottomLayoutGuide on its UIViewController
// and thus updating where the Legal link on the map should be.
[self.mapView setNeedsLayout];
}
Answer adapted from this answer.

Disabling NSView fade animation for NSView `setHidden:`

I am working on a project that has the concept of draggable controls, everything is working fine except that NSView seems to employ a fade in/out animation when calling setHidden:.
I have been able to work around the problem by changing the line session.animatesToStartingPositionsOnCancelOrFail = YES; to NO and implementing the image snapback myself with a custom animated NSWindow subclass. it looks great, but I know there must be an easier way.
I have tried:
using NSAnimationContext grouping with duration of 0 around the setHidden: calls
setting the view animations dictionary using various keys (alpha, hidden, isHidden) on the control and superview
overriding animationForKey: for both the control and its superview
I am not using CALayers and have even tried explicitly setting wantsLayer: to NO.
Does anybody know how to either disable this animation, or have a simpler solution then my animated NSWindow?
here is my stripped down altered code with the bare minimum to see what I'm talking about.
#implementation NSControl (DragControl)
- (NSDraggingSession*)beginDraggingSessionWithDraggingCell:(NSActionCell <NSDraggingSource> *)cell event:(NSEvent*) theEvent
{
NSImage* image = [self imageForCell:cell];
NSDraggingItem* di = [[NSDraggingItem alloc] initWithPasteboardWriter:image];
NSRect dragFrame = [self frameForCell:cell];
dragFrame.size = image.size;
[di setDraggingFrame:dragFrame contents:image];
NSArray* items = [NSArray arrayWithObject:di];
[self setHidden:YES];
return [self beginDraggingSessionWithItems:items event:theEvent source:cell];
}
- (NSRect)frameForCell:(NSCell*)cell
{
// override in multi-cell cubclasses!
return self.bounds;
}
- (NSImage*)imageForCell:(NSCell*)cell
{
return [self imageForCell:cell highlighted:[cell isHighlighted]];
}
- (NSImage*)imageForCell:(NSCell*)cell highlighted:(BOOL) highlight
{
// override in multicell cubclasses to just get an image of the dragged cell.
// for any single cell control we can just make sure that cell is the controls cell
if (cell == self.cell || cell == nil) { // nil signifies entire control
// basically a bitmap of the control
// NOTE: the cell is irrelevant when dealing with a single cell control
BOOL isHighlighted = [cell isHighlighted];
[cell setHighlighted:highlight];
NSRect cellFrame = [self frameForCell:cell];
// We COULD just draw the cell, to an NSImage, but button cells draw their content
// in a special way that would complicate that implementation (ex text alignment).
// subclasses that have multiple cells may wish to override this to only draw the cell
NSBitmapImageRep* rep = [self bitmapImageRepForCachingDisplayInRect:cellFrame];
NSImage* image = [[NSImage alloc] initWithSize:rep.size];
[self cacheDisplayInRect:cellFrame toBitmapImageRep:rep];
[image addRepresentation:rep];
// reset the original cell state
[cell setHighlighted:isHighlighted];
return image;
}
// cell doesnt belong to this control!
return nil;
}
#pragma mark NSDraggingDestination
- (void)draggingEnded:(id < NSDraggingInfo >)sender
{
[self setHidden:NO];
}
#end
#implementation NSActionCell (DragCell)
- (void)setControlView:(NSView *)view
{
// this is a bit of a hack, but the easiest way to make the control dragging work.
// force the control to accept image drags.
// the control will forward us the drag destination events via our DragControl category
[view registerForDraggedTypes:[NSImage imagePasteboardTypes]];
[super setControlView:view];
}
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
BOOL result = NO;
NSPoint currentPoint = theEvent.locationInWindow;
BOOL done = NO;
BOOL trackContinously = [self startTrackingAt:currentPoint inView:controlView];
BOOL mouseIsUp = NO;
NSEvent *event = nil;
while (!done)
{
NSPoint lastPoint = currentPoint;
event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSLeftMouseDraggedMask)
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (event)
{
currentPoint = event.locationInWindow;
// Send continueTracking.../stopTracking...
if (trackContinously)
{
if (![self continueTracking:lastPoint
at:currentPoint
inView:controlView])
{
done = YES;
[self stopTracking:lastPoint
at:currentPoint
inView:controlView
mouseIsUp:mouseIsUp];
}
if (self.isContinuous)
{
[NSApp sendAction:self.action
to:self.target
from:controlView];
}
}
mouseIsUp = (event.type == NSLeftMouseUp);
done = done || mouseIsUp;
if (untilMouseUp)
{
result = mouseIsUp;
} else {
// Check if the mouse left our cell rect
result = NSPointInRect([controlView
convertPoint:currentPoint
fromView:nil], cellFrame);
if (!result)
done = YES;
}
if (done && result && ![self isContinuous])
[NSApp sendAction:self.action
to:self.target
from:controlView];
else {
done = YES;
result = YES;
// this bit-o-magic executes on either a drag event or immidiately following timer expiration
// this initiates the control drag event using NSDragging protocols
NSControl* cv = (NSControl*)self.controlView;
NSDraggingSession* session = [cv beginDraggingSessionWithDraggingCell:self
event:theEvent];
// Note that you will get an ugly flash effect when the image returns if this is set to yes
// you can work around it by setting NO and faking the release by animating an NSWindowSubclass with the image as the content
// create the window in the drag ended method for NSDragOperationNone
// there is [probably a better and easier way around this behavior by playing with view animation properties.
session.animatesToStartingPositionsOnCancelOrFail = YES;
}
}
}
return result;
}
#pragma mark - NSDraggingSource Methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationNone;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationPrivate;
break;
}
}
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
// now tell the control view the drag ended so it can do any cleanup it needs
// this is somewhat hackish
[self.controlView draggingEnded:nil];
}
#end
There must be a layer enabled somewhere in your view hierarchy, otherwise there wouldn't be a fade animation. Here is my way of disabling such animations:
#interface NoAnimationImageView : NSImageView
#end
#implementation NoAnimationImageView
+ (id)defaultAnimationForKey: (NSString *)key
{
return nil;
}
#end
The solution you already tried by setting the view animations dictionary should work. But not for the keys you mention but for the following. Use it somewhere before the animation is triggered the first time. If you have to do it on the window or view or both, I don't know.
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderIn];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Or also just remove the keys if they are there (might not be the case as they are implicit / default):
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations removeObjectForKey:NSAnimationTriggerOrderIn];
[animations removeObjectForKey:NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Ok. I figured out that the animation I'm seeing is not the control, the superview, nor the control's window. It appears that animatesToStartingPositionsOnCancelOrFail causes NSDraggingSession to create a window (observed with QuartzDebug) and put the drag image in it and it is this window that animates back to the origin and fades out before the setHidden: call is executed (i.e. before the drag operation is concluded).
Unfortunately, the window that it creates is not an NSWindow so creating a category on NSWindow doesn't disable the fade animation.
Secondly, there is no public way that I know of to get a handle on the window, so I can't attempt directly manipulating the window instance.
It looks like maybe my workaround is the best way to do this, after all its not far from what AppKit does for you anyway.
If anybody knows how to get a handle on this window, or what class it is I would be interested to know.

Why do all backgrounds disappear on UITableViewCell select?

My current project's UITableViewCell behavior is baffling me. I have a fairly straightforward subclass of UITableViewCell. It adds a few extra elements to the base view (via [self.contentView addSubview:...] and sets background colors on the elements to have them look like black and grey rectangular boxes.
Because the background of the entire table has this concrete-like texture image, each cell's background needs to be transparent, even when selected, but in that case it should darken a bit. I've set a custom semi-transparent selected background to achieve this effect:
UIView *background = [[[UIView alloc] initWithFrame:self.bounds] autorelease];
background.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
background.opaque = NO;
[self setSelectedBackgroundView:background];
And although that yields the right look for the background, a weird side effect happens when I select the cell; all other backgrounds are somehow turnt off. Here's a screenshot. The bottom cell looks like it should and is not selected. The top cell is selected, but it should display the black and grey rectangular areas, yet they are gone!
Who knows what's going on here and even more important: how can I correct this?
What is happening is that each subview inside the TableViewCell will receive the setSelected and setHighlighted methods. The setSelected method will remove background colors but if you set it for the selected state it will be corrected.
For example if those are UILabels added as subviews in your customized cell, then you can add this to the setSelected method of your TableViewCell implementation code:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
self.textLabel.backgroundColor = [UIColor blackColor];
}
where self.textLabel would be whatever those labels are that are shown in the picture above
I'm not sure where your adding your selected view, I usually add it in the setSelected method.
Alternatively, you can subclass the UILabel and override the setHighlighted method like so:
-(void)setHighlighted:(BOOL)highlighted
{
[self setBackgroundColor:[UIColor blackColor]];
}
The cell highlighting process can seem complex and confusing if you don't know whats going on. I was thoroughly confused and did some extensive experimentation. Here's the notes on my findings that may help somebody (if anyone has anything to add to this or refute then please comment and I will endeavour to confirm and update)
In the normal “not selected” state
The contentView (whats in your XIB unless you coded it otherwise) is drawn normally
The selectedBackgroundView is HIDDEN
The backgroundView is visible (so provided your contentView is transparent you see the backgroundView or (if you have not defined a backgroundView you'll see the background colour of the UITableView itself)
A cell is selected, the following occurs immediately with-OUT any animation:
All views/subviews within the contentView have their backgroundColor cleared (or set to transparent), label etc text color's change to their selected colour
The selectedBackgroundView becomes visible (this view is always the full size of the cell (a custom frame is ignored, use a subview if you need to). Also note the backgroundColor of subViews are not displayed for some reason, perhaps they're set transparent like the contentView). If you didn't define a selectedBackgroundView then Cocoa will create/insert the blue (or gray) gradient background and display this for you)
The backgroundView is unchanged
When the cell is deselected, an animation to remove the highlighting starts:
The selectedBackgroundView alpha property is animated from 1.0 (fully opaque) to 0.0 (fully transparent).
The backgroundView is again unchanged (so the animation looks like a crossfade between selectedBackgroundView and backgroundView)
ONLY ONCE the animation has finished does the contentView get redrawn in the "not-selected" state and its subview backgroundColor's become visible again (this can cause your animation to look horrible so it is advisable that you don't use UIView.backgroundColor in your contentView)
CONCLUSIONS:
If you need a backgroundColor to persist through out the highlight animation, don't use the backgroundColor property of UIView instead you can try (probably with-in tableview:cellForRowAtIndexPath:):
A CALayer with a background color:
UIColor *bgColor = [UIColor greenColor];
CALayer* layer = [CALayer layer];
layer.frame = viewThatRequiresBGColor.bounds;
layer.backgroundColor = bgColor.CGColor;
[cell.viewThatRequiresBGColor.layer addSublayer:layer];
or a CAGradientLayer:
UIColor *startColor = [UIColor redColor];
UIColor *endColor = [UIColor purpleColor];
CAGradientLayer* gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = viewThatRequiresBGColor.bounds;
gradientLayer.colors = #[(id)startColor.CGColor, (id)endColor.CGColor];
gradientLayer.locations = #[[NSNumber numberWithFloat:0],[NSNumber numberWithFloat:1]];
[cell.viewThatRequiresBGColor.layer addSublayer:gradientLayer];
I've also used a CALayer.border technique to provide a custom UITableView seperator:
// We have to use the borderColor/Width as opposed to just setting the
// backgroundColor else the view becomes transparent and disappears during
// the cell's selected/highlighted animation
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(0, 43, 1024, 1)];
separatorView.layer.borderColor = [UIColor redColor].CGColor;
separatorView.layer.borderWidth = 1.0;
[cell.contentView addSubview:separatorView];
When you start dragging a UITableViewCell, it calls setBackgroundColor: on its subviews with a 0-alpha color. I worked around this by subclassing UIView and overriding setBackgroundColor: to ignore requests with 0-alpha colors. It feels hacky, but it's cleaner than any of the other solutions I've come across.
#implementation NonDisappearingView
-(void)setBackgroundColor:(UIColor *)backgroundColor {
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
if (alpha != 0) {
[super setBackgroundColor:backgroundColor];
}
}
#end
Then, I add a NonDisappearingView to my cell and add other subviews to it:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"cell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
UIView *background = [cell viewWithTag:backgroundTag];
if (background == nil) {
background = [[NonDisappearingView alloc] initWithFrame:backgroundFrame];
background.tag = backgroundTag;
background.backgroundColor = backgroundColor;
[cell addSubview:background];
}
// add other views as subviews of background
...
}
return cell;
}
Alternatively, you could make cell.contentView an instance of NonDisappearingView.
My solution is saving the backgroundColor and restoring it after the super call.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
UIColor *bgColor = self.textLabel.backgroundColor;
[super setSelected:selected animated:animated];
self.textLabel.backgroundColor = bgColor;
}
You also need to do the same thing with -setHighlighted:animated:.
Found a pretty elegant solution instead of messing with the tableView methods. You can create a subclass of UIView that ignores setting its background color to clear color. Code:
class NeverClearView: UIView {
override var backgroundColor: UIColor? {
didSet {
if UIColor.clearColor().isEqual(backgroundColor) {
backgroundColor = oldValue
}
}
}
}
Obj-C version would be similar, the main thing here is the idea
I created a UITableViewCell category/extension that allows you to turn on and off this transparency "feature".
You can find KeepBackgroundCell on GitHub
Install it via CocoaPods by adding the following line to your Podfile:
pod 'KeepBackgroundCell'
Usage:
Swift
let cell = <Initialize Cell>
cell.keepSubviewBackground = true // Turn transparency "feature" off
cell.keepSubviewBackground = false // Leave transparency "feature" on
Objective-C
UITableViewCell* cell = <Initialize Cell>
cell.keepSubviewBackground = YES; // Turn transparency "feature" off
cell.keepSubviewBackground = NO; // Leave transparency "feature" on
Having read through all the existing answers, came up with an elegant solution using Swift by only subclassing UITableViewCell.
extension UIView {
func iterateSubViews(block: ((view: UIView) -> Void)) {
for subview in self.subviews {
block(view: subview)
subview.iterateSubViews(block)
}
}
}
class CustomTableViewCell: UITableViewCell {
var keepSubViewsBackgroundColorOnSelection = false
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
// MARK: Overrides
override func setSelected(selected: Bool, animated: Bool) {
if self.keepSubViewsBackgroundColorOnSelection {
var bgColors = [UIView: UIColor]()
self.contentView.iterateSubViews() { (view) in
guard let bgColor = view.backgroundColor else {
return
}
bgColors[view] = bgColor
}
super.setSelected(selected, animated: animated)
for (view, backgroundColor) in bgColors {
view.backgroundColor = backgroundColor
}
} else {
super.setSelected(selected, animated: animated)
}
}
override func setHighlighted(highlighted: Bool, animated: Bool) {
if self.keepSubViewsBackgroundColorOnSelection {
var bgColors = [UIView: UIColor]()
self.contentView.iterateSubViews() { (view) in
guard let bgColor = view.backgroundColor else {
return
}
bgColors[view] = bgColor
}
super.setHighlighted(highlighted, animated: animated)
for (view, backgroundColor) in bgColors {
view.backgroundColor = backgroundColor
}
} else {
super.setHighlighted(highlighted, animated: animated)
}
}
}
All we need is to override the setSelected method and change the selectedBackgroundView for the tableViewCell in the custom tableViewCell class.
We need to add the backgroundview for the tableViewCell in cellForRowAtIndexPath method.
lCell.selectedBackgroundView = [[UIView alloc] init];
Next I have overridden the setSelected method as mentioned below.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
UIImageView *lBalloonView = [self viewWithTag:102];
[lBalloonView setBackgroundColor:[[UIColor hs_globalTint] colorWithAlphaComponent:0.2]];
UITextView *lMessageTextView = [self viewWithTag:103];
lMessageTextView.backgroundColor = [UIColor clearColor];
UILabel *lTimeLabel = [self viewWithTag:104];
lTimeLabel.backgroundColor = [UIColor clearColor];
}
Also one of the most important point to be noted is to change the tableViewCell selection style. It should not be UITableViewCellSelectionStyleNone.
lTableViewCell.selectionStyle = UITableViewCellSelectionStyleGray;

Stop drawing of CATiledLayer

Is is possible to stop CATiledLayer to draw (drawLayer:inContext)?
It draws asynchronously and when i try to release CGPDFDocumentRef, which is used by CATiledLayer, the app crashes (EXC_BAD_ACCESS).
That's my view:
#implementation TiledPDFView
- (id)initWithFrame:(CGRect)frame andScale:(CGFloat)scale{
if ((self = [super initWithFrame:frame])) {
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
tiledLayer.levelsOfDetail = 4;
tiledLayer.levelsOfDetailBias = 4;
tiledLayer.tileSize = CGSizeMake(512.0, 512.0);
myScale = scale;
}
return self;
}
// Set the layer's class to be CATiledLayer.
+ (Class)layerClass {
return [CATiledLayer class];
}
- (void)stopDrawing{
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
[tiledLayer removeFromSuperlayer];
tiledLayer.delegate = nil;
}
// Set the CGPDFPageRef for the view.
- (void)setPage:(CGPDFPageRef)newPage
{
CGPDFPageRelease(self->pdfPage);
self->pdfPage = CGPDFPageRetain(newPage);
//self->pdfPage = newPage;
}
-(void)drawRect:(CGRect)r
{
}
// Draw the CGPDFPageRef into the layer at the correct scale.
-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
// First fill the background with white.
CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
CGContextFillRect(context,self.bounds);
CGContextSaveGState(context);
// Flip the context so that the PDF page is rendered
// right side up.
CGContextTranslateCTM(context, 0.0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// Scale the context so that the PDF page is rendered
// at the correct size for the zoom level.
CGContextScaleCTM(context, myScale,myScale);
CGContextDrawPDFPage(context, pdfPage);
CGContextRestoreGState(context);
}
// Clean up.
- (void)dealloc {
CGPDFPageRelease(pdfPage);
[super dealloc];
}
And this is where i try to stop and release PDF in view controller:
v is instance of TiledPDFView
-(void) stopDwaring {
[v stopDrawing];
[v removeFromSuperview];
[v release];
[self.view removeFromSuperview];
self.view = nil;
CGPDFDocumentRelease(pdf);
}
this post helped me solving my own trouble with CATiledLayer. I used TiledPDFview.m from Apple's documentation as example.
Since I need to redraw the entire view and all tiles at some point, I use a CATiledLayer as property.
When exiting and deallocating the viewcontroller, it crashed with [CATiledLayer retain]: Message sent to deallocated instance.
Here is my dealloc method of the view controller:
- (void)dealloc {
self.tiledLayer.contents=nil;
self.tiledLayer.delegate=nil;
[self.tiledLayer removeFromSuperlayer];
// note: releasing the layer still crashes-
// I guess removeFromSuperlayer releases it already,
// but couldn't find documentation so far.
// So that's why it's commented out:
// [self.tiledLayer release], self.tiledLayer=nil;
//release the other viewcontroller stuff...
[super dealloc];
}
That works for me. Hope it helps someone.
Remove the CATiledLayer from its superlayer before releasing the CGPDFDocumentRef.
[yourTiledLayer removeFromSuperlayer];
Dont forget to set it's delegate to nil too.
yourTiledLayer.delegate = nil;
After that, you can safely release your CGPDFDocumentRef.
Edit after OP adds code:
Did you get pdfPage using CGPDFDocumentGetPage()? If so, you shouldn't release it, it is an autoreleased object.
Regarding how to add it as sublayer:
You don't actually need TiledPDFView. In your view controller, you can simply do this:
CATiledLayer *tiledLayer = [CATiledLayer layer];
tiledLayer.delegate = self; //sets where tiledLayer will look for drawLayer:inContext:
tiledLayer.tileSize = CGSizeMake(512.0f, 512.0f);
tiledLayer.levelsOfDetail = 4;
tiledLayer.levelsOfDetailBias = 4;
tiledLayer.frame = CGRectIntegral(CGRectMake(0.0f, 0.0f, 512.0f, 512.0f));
[self.view.layer addSublayer:tiledLayer];
Then move your drawLayer:inContext: implementation to your view controller.
Then in your view controller's dealloc, release it as:
[tiledLayer removeFromSuperlayer];
tiledLayer.delegate = nil;
CGPDFDocumentRelease(pdf);
Note that you can't do this on a UIView subclass, as the drawLayer:inContext: will conflict with the UIView's main layer.
object.layer.contents = Nil
This should wait for the thread to finish. It helped in my case.
TiledPDFView *pdfView;
In dealloc of pdfView's superview class, write below line of codes.
- (void)dealloc {
if (nil != self.pdfView.superview) {
self.pdfView.layer.delegate = nil;
[self.pdfView removeFromSuperview];
}
}
This works for me. Hope it will help.
I've had a similar problem.
I ended up setting a float variable "zoom" in my TiledPDFView which I set as the zoomScale of PDFScrollView in the UIScrollview Delegate method: scrollViewDidZoom
Then in my drawLayer method inside TiledPDFView I only called the contents of that method if the float variable "zoom" was above 2.
This fixes any issues of someone leaving the view without zooming. It may not be ideal for your case as this error still occurs if someone zooms above 2 then releases the viewcontroller quickly, but you might be able to find a similar technique to cover all bases.
It looks like you're doing the same thing I am, which is borrowing the ZoomingPDFView code and integrating it into your project. If your UIScrollView delegate methods in PDFScrollView are unchanged, you can solve your problem by just commenting both lines of your dealloc method (in TiledPDFView). That stuff should only be happening when you kill the parent view, anyway.