Subclass of NSButton: call of action method crashes - objective-c

In my application there is a subclass of NSWindowController. In - (void)awakeFromNib I create an instance of a NSView subclass to create a bottom placed button bar:
self.buttonBar = [[CNButtonBar alloc] initWithSize:NSMakeSize(tableViewRect.size.width-1, 25)];
This CNButtonBar thing has a method to add buttons to itself. I create two of it:
[buttonBar addButtonWithTitle:nil
image:[NSImage imageNamed:NSImageNameAddTemplate]
target:self
action:#selector(btnAccountAddAction:)
alignment:CNBarButtonAlignLeft];
[buttonBar addButtonWithTitle:nil
image:[NSImage imageNamed:NSImageNameRemoveTemplate]
target:self
action:#selector(btnAccountRemoveAction:)
alignment:CNBarButtonAlignLeft];
These two action methods are defined in the same instance of NSViewController where I send the two addButtonWithTitle:... messages.
The methods, used to set the action are defined as followed:
- (void)btnAccountAddAction:(id)sender
{
....
}
- (void)btnAccountRemoveAction:(id)sender
{
....
}
But, it doesn't work. Each time if I click one of these two buttons my application crashes throwing an exception like this one:
-[__NSArrayM btnAccountAddAction:]: unrecognized selector sent to instance 0x1001454e0
Examined this exception is absolutely correct. NSArray doesn't have such a method I want to call. But, from time to time the object that throws this exception is changing. Some times there is a __NSData object, some times a __NSDictionary and so on. It seems that the receiver is anything but my NSWindowController subclass. But the given instance ID 0x1001454e0 is the same my NSWindowController has.
It makes me want to tear my hair out! What's going wrong here...?
Thanks.
UPDATE
I have to say, that both, the CNButtonBar and CNButtonBarButton are completely drawn by code. The complete method of addButtonWithTitle:image:target:action:alignment: is here (remember, this happens in CNButtonBar, a subclass of NSView (should I use NSViewController instead of it?), CNButtonBarButton is a subclass of NSButton):
- (void)addButtonWithTitle:(NSString*)title image:(NSImage*)image target:(id)target action:(SEL)action alignment:(CNBarButtonAlign)align
{
if (self.buttons == nil)
self.buttons = [[NSMutableArray alloc] init];
CNButtonBarButton *button = [[CNButtonBarButton alloc] init];
[self.buttons addObject:button];
button.title = title;
button.image = image;
button.target = target;
button.action = action;
button.align = align;
NSRect buttonRect;
NSSize titleSize = NSMakeSize(0, 0);
if (button.title.length > 0) {
NSMutableParagraphStyle* textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[textStyle setAlignment: NSCenterTextAlignment];
NSColor *textColor = [[NSColor blackColor] colorWithAlphaComponent:0.9];
NSShadow* textShadow = [[NSShadow alloc] init];
[textShadow setShadowColor: [NSColor whiteColor]];
[textShadow setShadowOffset: NSMakeSize(0, -1)];
[textShadow setShadowBlurRadius: 0];
button.titleTextAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"Helvetica Neue" size:11.0], NSFontAttributeName,
textShadow, NSShadowAttributeName,
textColor, NSForegroundColorAttributeName,
textStyle, NSParagraphStyleAttributeName,
nil];
titleSize = [title sizeWithAttributes:button.titleTextAttributes];
buttonRect = NSMakeRect(0, 0, roundf(titleSize.width) + kTextInset * 2, NSHeight(self.frame) - 1);
}
if (button.image != nil) {
NSSize imageSize = [image size];
if (button.title.length == 0) {
buttonRect = NSMakeRect(0, 0, imageSize.width + kImageInset * 2, NSHeight(self.frame) - 1);
} else {
buttonRect = NSMakeRect(0, 0,
(imageSize.width + kImageInset * 2) + (roundf(titleSize.width) + kTextInset),
NSHeight(self.frame) - 1);
}
}
switch (self.align) {
case CNButtonBarAlignNormal: {
switch (button.align) {
case CNButtonBarButtonAlignLeft: {
button.frame = NSMakeRect(offsetLeft, 0, NSWidth(buttonRect), NSHeight(buttonRect));
offsetLeft += 1 * [self.buttons indexOfObject:button] + NSWidth(button.frame);
break;
}
case CNButtonBarButtonAlignRight: {
button.frame = NSMakeRect(offsetRight - NSWidth(buttonRect), 0, NSWidth(buttonRect), NSHeight(buttonRect));
offsetRight -= 1 * [self.buttons indexOfObject:button] + NSWidth(button.frame);
break;
}
}
break;
}
case CNButtonBarAlignCentered: {
break;
}
}
[self addSubview:button];
}
UPDATE 2
The problem is solved. After some debugging I found out that it must be a problem of ARCs auto retain/release stuff. And I'm right. But the problem was my NSViewController subclass. It was leaking because of making an instance as a local variable (without a strong pointer). Thanks to Sean D., that pointed me to the right way. (-;

Looks like one of two things is happening. The first possibility is that your CNButtonBar object is getting released while the buttons are still around. If the buttons are still there, they'll try to send those selectors to whatever happens to occupy the memory the CNButtonBar used to be in. I'd say make sure it isn't autoreleasing itself anywhere, and that you're not accidentally autoreleasing it in the window controller's -awakeFromNib method. (If you're anything like me, that's the most likely culprit.)
The second possibility is that your -addButtonWithTitle:image:target:action:alignment: method is setting the buttons' actions but not their targets. Make sure your implementation of that method calls -setTarget: as well as -setAction: on the button.

Related

How to check if the Button Shapes setting is enabled?

iOS 7.1 includes a new Accessibility setting calls Button Shapes that causes some button text to be automatically underlined. Is there a way to detect this mode, or customize it for individual UIButtons?
(This to allow changing button labels such as a dash or underscore so that when underlined, they don't look like an equals sign, etc.)
As of iOS 14, you can use UIAccessibility.buttonShapesEnabled or UIAccessibilityButtonShapesEnabled(), which will be true when the setting is enabled.
Old question, but hopefully this helps someone. There's still no built-in method for checking if Button Shapes is enabled on iOS, so we added this:
#pragma mark - Accessibility
/**
* There's currently no built-in way to ascertain whether button shapes is enabled.
* But we want to manually add shapes to certain buttons when it is.
*/
static BOOL _accessibilityButtonShapesEnabled = NO;
+ (BOOL)accessibilityButtonShapesEnabled {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self checkIfButtonShapesEnabled];
});
return _accessibilityButtonShapesEnabled;
}
+ (void)checkIfButtonShapesEnabled {
UIButton *testButton = [[UIButton alloc] init];
[testButton setTitle:#"Button Shapes" forState:UIControlStateNormal];
_accessibilityButtonShapesEnabled = (BOOL)[(NSDictionary *)[testButton.titleLabel.attributedText attributesAtIndex:0 effectiveRange:nil] valueForKey:NSUnderlineStyleAttributeName];
}
Because there's also no notification if Button Shapes is disabled/enabled whilst the app is running, we run checkIfButtonShapesEnabled in applicationDidBecomeActive:, and push our own notification if the value has changed. This should work in all cases, because it is not currently possible to add the Button Shapes toggle to the "Accessibility Shortcut".
It looks like you can request the button label's attributes and test to see if it contains an NSUnderlineStyleAttributeName attribute. If you remove the NSUnderlineStyleAttributeName attribute the system will put it right back so it seems the trick is to explicitly set the label's underline attribute to 0. I've added the following to my custom button:
- (void) adjustLabelProperties // override underline attribute
{
NSMutableAttributedString *attributedText = [self.titleLabel.attributedText mutableCopy];
[attributedText addAttribute: NSUnderlineStyleAttributeName value: #(0) range: NSMakeRange(0, [attributedText length])];
self.titleLabel.attributedText = attributedText;
}
I know it's an old question but this code works. Tested in iOS 9.3
NSMutableAttributedString *attrStr = [btn.titleLabel.attributedText mutableCopy];
[attrStr enumerateAttributesInRange:NSMakeRange(0, [attrStr length])
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes];
if([mutableAttributes objectForKey:NSUnderlineStyleAttributeName] != nil) {
//It's enabled for this button
}
}];
To disable button shapes for a specific button
[btn.titleLabel.attributedText addAttribute: NSUnderlineStyleAttributeName value: #(0) range: NSMakeRange(0, [attributedText length])];
I converted the code from this post to Swift (4.2):
import UIKit
public extension UIAccessibility {
public static var isButtonShapesEnabled: Bool {
let button = UIButton()
button.setTitle("Button Shapes", for: .normal)
return button.titleLabel?.attributedText?.attribute(NSAttributedString.Key.underlineStyle, at: 0, effectiveRange: nil) != nil
}
}
Usage:
if UIAccessibility.isButtonShapesEnabled {
// Apply button shapes style to custom button...
}
Tested and working in iOS 12.
Originally posted at my own question: Click
I had the same problem and I found no official solution. So the only workaround that I found until Apple releases a solution is to render a UIToolbar into an image and check if the button is underlined:
+ (BOOL)isUsesButtonShapes {
BOOL result = FALSE;
CGRect rect = CGRectMake(0, 0, 320, 44);
CGPoint point = CGPointMake(26, 33);
UIToolbar *toolbar = [[[UIToolbar alloc] initWithFrame:rect] autorelease];
toolbar.backgroundColor = [UIColor whiteColor];
toolbar.tintColor = [UIColor darkGrayColor];
toolbar.barTintColor = [UIColor whiteColor];
[toolbar setItems:#[[[[UIBarButtonItem alloc] initWithTitle:#"Test" style:UIBarButtonItemStyleBordered target:nil action:nil] autorelease]]];
toolbar.barStyle = UIBarStyleDefault;
toolbar.translucent = FALSE;
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[toolbar.layer renderInContext:context];
int bpr = CGBitmapContextGetBytesPerRow(context);
unsigned char *data = CGBitmapContextGetData(context);
if (data != NULL) {
int offset = (int) (bpr * point.y + 4 * point.x);
int blue = data[offset + 0];
result = blue < 250;
}
UIGraphicsEndImageContext();
return result;
}
It basically just renders the UIToolbar into an image:
Then it checks if there is an underline in the pixel under the "T". I know that this can easily break if Apple changes the way how the UIToolbar is rendered. But maybe this method can be improved and is at least better than nothing? Sorry, it isn't a good solution but I didn't find anything better yet.
This is only semi related, but I kind of "roll my own" for Button shapes and make the option available to the user via a settings menu.
I apologize for not being "spot on" regarding the question, but this is what I ended up with by thinking about the same question.
(The example is set up to always use a semi-circle for rounded corners regardless the size - please modify as you wish).
-(void)setBorderForButton:(UIButton*)theButton withSetting:(BOOL)theSetting{
if (theSetting == YES){
theButton.layer.cornerRadius = theButton.frame.size.height/2;
theButton.layer.borderWidth = 1;
theButton.layer.borderColor = [UIColor yourDesiredColor].CGColor;
theButton.clipsToBounds = YES;
}
}

Draw into a NSView via mouse event

i am new to programming, objective-c (and stackoverflow). I'm learning and moving forward very slowly ;) but then I ran into a problem, which google couldn't solve. I have a single window and a NSview and then added a mouse event that should draw the coordinates into my view, but it doesn't. The funny thing is: it is drawn when the mouse moves over the window buttons of my apps window...
- (void)drawRect:(NSRect)dirtyRect {
NSPoint imagePos = NSMakePoint(0, 0);
NSImage *aImage = [NSImage imageNamed:#"mw_bg01.png"];
[aImage dissolveToPoint:imagePos fraction:1.0];
}
- (void)mouseDown:(NSEvent*)theEvent;{
mouseLoc = [NSEvent mouseLocation];
mousePosX = mouseLoc.x;mousePosY = mouseLoc.y;
NSString* mouseString = [NSString stringWithFormat:#"%d", mousePosX];
NSPoint textPoint = NSMakePoint(5, 5);
NSMutableDictionary *textAttrib = [[NSMutableDictionary alloc] init];
[textAttrib setObject:[NSFont fontWithName:#"Helvetica Light" size:10]
forKey:NSFontAttributeName];
[textAttrib setObject:[NSColor grayColor] forKey:NSForegroundColorAttributeName];
[mouseString drawAtPoint:textPoint withAttributes:textAttrib];
}
I don't know how to go on, any suggestions? Thanks!
You shouldn't do drawing in the -mouseDown: method. Rather, you must do all your drawing in -drawRect: (or methods that you call from -drawRect:). Try something like this:
#interface MyView ()
#property NSPoint lastMousePoint;
#end
#implementation MyView
- (void)drawLastMousePoint
{
NSString *mouseString = NSStringFromPoint(self.lastMousePoint);
NSPoint textPoint = NSMakePoint(5, 5);
NSMutableDictionary *textAttrib = [[NSMutableDictionary alloc] init];
[textAttrib setObject:[NSFont fontWithName:#"Helvetica Light" size:10]
forKey:NSFontAttributeName];
[textAttrib setObject:[NSColor grayColor] forKey:NSForegroundColorAttributeName];
[mouseString drawAtPoint:textPoint withAttributes:textAttrib];
}
- (void)drawRect:(NSRect)dirtyRect
{
NSPoint imagePos = NSMakePoint(0, 0);
NSImage *aImage = [NSImage imageNamed:#"mw_bg01.png"];
[aImage dissolveToPoint:imagePos fraction:1.0];
[self drawLastMousePoint];
}
- (void)mouseDown:(NSEvent*)theEvent;
{
self.lastMousePoint = [theEvent locationInWindow];
[self setNeedsDisplay:YES];
}
#end
When you get a mouse down event, you simply store the location of the mouse down. The drawing is done in -drawLastMousePoint which you call in your -drawRect: method. Since you know you need to redraw anytime the mouse is clicked, you call -setNeedsDisplay: to inform the view that it needs to be redrawn. Note that the redraw doesn't happen immediately, rather, it will happen the next time through the run loop. In other words, you're saying "hey, something changed, and I need to draw my view's contents again. Please call -drawRect: again as soon as possible!"
One other note: +[NSEvent mouseLocation] is really designed for getting the current mouse location outside the event stream. Typically, in a -mouseDown: method, you call -locationInWindow on the NSEvent passed as the argument to the method. If you need to convert to local/view coordinates, you should call [self convertPoint:[theEvent locationInWindow] fromView:nil];.

Subclass of UIPageControl refresh only after uiscrollview move, not before

the problem I've met today is with my subclass of UIPageControl. When I initialize it, the frame (specifically the origin) and image of dots stays default, which is the problem, since I want it to change right after initialization. However, when I move with scrollView (as in "touch and move") after initialization, they (the dots) somehow jump to the right position with correct images.
What could be the problem?
Code:
CustomPageControl.m
- (id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
activeImage = [UIImage imageNamed:#"doton.png"];
inactiveImage = [UIImage imageNamed:#"dotoff.png"];
return self;
}
- (void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView *dot = [self.subviews objectAtIndex:i];
if (i == self.currentPage) dot.image = activeImage;
else dot.image = inactiveImage;
[dot setFrame:CGRectMake(i * 13.5, 1.5, 17, 17)];
}
}
- (void)setCurrentPage:(NSInteger)currentPage
{
[super setCurrentPage:currentPage];
[self updateDots];
}
#end
ChoosingView.m - init part
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 160, 300)];
[scrollView setBackgroundColor:[UIColor clearColor]];
[scrollView setDelaysContentTouches:NO];
[scrollView setCanCancelContentTouches:YES];
[scrollView setClipsToBounds:NO];
[scrollView setScrollEnabled:YES];
[scrollView setPagingEnabled:YES];
[scrollView setShowsHorizontalScrollIndicator:NO];
[scrollView setShowsVerticalScrollIndicator:NO];
pageControl = [[CustomPageControl alloc] initWithFrame:CGRectMake(200, 300, 80, 20)];
[pageControl setBackgroundColor:[UIColor clearColor]];
pageControl.numberOfPages = 6;
[pageControl setCurrentPage:0];
the last line is when I would expect the UIPageControl to refresh, however that does not happen.
Does this happen with the standard UIPageControl implementation?
Your problem states that your objects subViews (eg the UIImageViews) rects/size are not initialising to your desired size/position.
I implemented this code in my project with a nib rather than programmatically and I needed to call -(void)updateDots to set it as its initial condition was the standard dots..
I dont see how the UIScrollView has any bearing impact on this unless somehow its linked to your -(void)updateDots function (E.g. your setting the currentIndex of your custom page control). You state, "However, when I move with scrollView (as in "touch and move") after initialization, they (the dots) somehow jump to the right position with correct images."
Because they "jump to the right position with correct images" it means that your -(void)updateDots function must be getting called. I dont see any other explanation.
Also your iteration loop assumes that all the UIViews in your .subViews array are UIImageViews, although fairly safe to assume this, I would check to see if the UIView is a UIImageView with reflection.

Animation using array of images in sequence

I have an array of images which I want to animate by playing these images one after the other in a sequence. I want to repeat the whole loop several times. I am developing a game for iPad. Suggest to me a method to achieve this functionality in Objective-C with the Cocoa framework.
NSArray *animationArray = [NSArray arrayWithObjects:
[UIImage imageNamed:#"images.jpg"],
[UIImage imageNamed:#"images1.jpg"],
[UIImage imageNamed:#"images5.jpg"],
[UIImage imageNamed:#"index3.jpg"],
nil];
UIImageView *animationView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0,320, 460)];
animationView.backgroundColor = [UIColor purpleColor];
animationView.animationImages = animationArray;
animationView.animationDuration = 1.5;
animationView.animationRepeatCount = 0;
[animationView startAnimating];
[self.view addSubview:animationView];
[animationView release];
add your own images in the array.repeat Count 0 means infinite loop.You can give your own number also.
There are at least 3 ways to animate an array of images through a UIImageView. I'm adding 3 links to download sample code for the 3 possibilities.
The first one is the one that everyone knows. The other ones are less known.
- UIImageView.animationImages
Example Link
The problem of this one is that do not have Delegate to tell us in which moment the animation is finished. So, we can have problems if we want to display something after the animation.
In the same way, there is no possibility to kept the last image from the animation in the UIImageView automatically. If we combine both problems we can have a gap at the end of the animation if we want to kept the last frame on screen.
self.imageView.animationImages = self.imagesArray; // the array with the images
self.imageView.animationDuration = kAnimationDuration; // static const with your value
self.imageView.animationRepeatCount = 1;
[self.imageView startAnimating];
- CAKeyframeAnimation
Example Link
This way to animate works through CAAnimation. It have an easy delegate to use and we can know when the animation finish.
This is probably the best way to animate an array of images.
- (void)animateImages
{
CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
keyframeAnimation.values = self.imagesArray;
keyframeAnimation.repeatCount = 1.0f;
keyframeAnimation.duration = kAnimationDuration; // static const with your value
keyframeAnimation.delegate = self;
// keyframeAnimation.removedOnCompletion = YES;
keyframeAnimation.removedOnCompletion = NO;
keyframeAnimation.fillMode = kCAFillModeForwards;
CALayer *layer = self.animationImageView.layer;
[layer addAnimation:keyframeAnimation
forKey:#"girlAnimation"];
}
Delegate:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (flag)
{
// your code
}
}
- CADisplayLink
Example Link
A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.
This way to do it is really interesting and opens a lot of possibilities to manipulate what are we showing in screen.
DisplayLink getter:
- (CADisplayLink *)displayLink
{
if (!_displayLink)
{
_displayLink = [CADisplayLink displayLinkWithTarget:self
selector:#selector(linkProgress)];
}
return _displayLink;
}
Methods:
- (void)animateImages
{
self.displayLink.frameInterval = 5;
self.frameNumber = 0;
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes];
}
- (void)linkProgress
{
if (self.frameNumber > 16)
{
[self.displayLink invalidate];
self.displayLink = nil;
self.animationImageView.image = [UIImage imageNamed:#"lastImageName"];
self.imagesArray = nil;
return;
}
self.animationImageView.image = self.imagesArray[self.frameNumber++];
self.frameNumber++;
}
GENERAL PROBLEM:
Even though we have this 3 possibilities, if your animation is with a lot of big images, consider using a video instead. The usage of memory will decrease a lot.
A General problem you will face doing this is in the moment of the allocation of the images.
If you use [UIImage imageNamed:#"imageName"] you will have cahe problems.
From Apple:
This method looks in the system caches for an image object with the specified name and returns that object if it exists. If a matching image object is not already in the cache, this method locates and loads the image data from disk or asset catelog, and then returns the resulting object. You can not assume that this method is thread safe.
So, imageNamed: stores the image in a private Cache.
- The first problem is that you can not take control of the cache size.
- The second problem is that the cache did not get cleaned in time and if you are allocating a lot of images with imageNamed:, your app, probably, will crash.
SOLUTION:
Allocate images directly from Bundle:
NSString *imageName = [NSString stringWithFormat:#"imageName.png"];
NSString *path = [[NSBundle mainBundle] pathForResource:imageName
// Allocating images with imageWithContentsOfFile makes images to do not cache.
UIImage *image = [UIImage imageWithContentsOfFile:path];
Small problem:
Images in Images.xcassets get never allocated. So, move your images outside Images.xcassets to allocate directly from Bundle.
See the animationImages property of UIImageView. It’s hard to say if it fits your needs as you don’t give us details, but it’s a good start.
I have added a swift 3.0 extension for this
extension UIImageView {
func animate(images: [UIImage], index: Int = 0, completionHandler: (() -> Void)?) {
UIView.transition(with: self, duration: 0.5, options: .transitionCrossDissolve, animations: {
self.image = images[index]
}, completion: { value in
let idx = index == images.count-1 ? 0 : index+1
if idx == 0 {
completionHandler!()
} else {
self.animate(images: images, index: idx, completionHandler: completionHandler)
}
})
}
}
Best solution for me use CADisplayLink. UIImageView doesn't have completion block and you can't catch steps of animation. In my task i must changing background of view with image sequencer step by step. So CADisplayLink allows you handling steps and finishing animation. If we talk about usage of memory, i think best solution load images from bundle and delete array after finishing
ImageSequencer.h
typedef void (^Block)(void);
#protocol ImageSequencerDelegate;
#interface QSImageSequencer : UIImageView
#property (nonatomic, weak) id <ImageSequencerDelegate> delegate;
- (void)startAnimatingWithCompletionBlock:(Block)block;
#end
#protocol ImageSequencerDelegate <NSObject>
#optional
- (void)animationDidStart;
- (void)animationDidStop;
- (void)didChangeImage:(UIImage *)image;
#end
ImageSequencer.m
- (instancetype)init {
if (self = [super init]) {
_imagesArray = [NSMutableArray array];
self.image = [self.imagesArray firstObject];
}
return self;
}
#pragma mark - Animation
- (void)startAnimating {
[self startAnimatingWithCompletionBlock:nil];
}
- (void)startAnimatingWithCompletionBlock:(Block)block {
self.frameNumber = 0;
[self setSuccessBlock:block];
self.displayLink.frameInterval = 5;
if (self.delegate && [self.delegate respondsToSelector:#selector(animationDidStart)]) {
[self.delegate animationDidStart];
}
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes];
}
-(void)stopAnimating {
self.image = [self.imagesArray lastObject];
[self.displayLink invalidate];
[self setDisplayLink:nil];
Block block_ = [self successBlock];
if (block_) {
block_();
}
if (self.delegate && [self.delegate respondsToSelector:#selector(animationDidStop)]) {
[self.delegate animationDidStop];
}
[self.imagesArray removeAllObjects];
}
- (void)animationProgress {
if (self.frameNumber >= self.imagesArray.count) {
[self stopAnimating];
return;
}
if (self.delegate && [self.delegate respondsToSelector:#selector(didChangeImage:)]) {
[self.delegate didChangeImage:self.imagesArray[self.frameNumber]];
}
self.image = self.imagesArray[self.frameNumber];
self.frameNumber++;
}
#pragma mark - Getters / Setters
- (CADisplayLink *)displayLink {
if (!_displayLink){
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(animationProgress)];
}
return _displayLink;
}
- (NSMutableArray<UIImage *> *)imagesArray {
if (_imagesArray.count == 0) {
// get images from bundle and set to array
}
return _imagesArray;
}
#end
This is a simple and working code for animation./
-(void)move
{
[UIView animateWithDuration:5
delay:0.0
options: UIViewAnimationCurveEaseOut
animations:^{
[_imgbox setFrame:CGRectMake(100, 100, _imgbox.frame.size.width, _imgbox.frame.size.height)];
}
completion:^(BOOL finished){
NSLog(#"Done!");
}];
}

UISlider with ProgressView combined

Is there an apple-house-made way to get a UISlider with a ProgressView. This is used by many streaming applications e.g. native quicktimeplayer or youtube.
(Just to be sure: i'm only in the visualization interested)
cheers Simon
Here's a simple version of what you're describing.
It is "simple" in the sense that I didn't bother trying to add the shading and other subtleties. But it's easy to construct and you can tweak it to draw in a more subtle way if you like. For example, you could make your own image and use it as the slider's thumb.
This is actually a UISlider subclass lying on top of a UIView subclass (MyTherm) that draws the thermometer, plus two UILabels that draw the numbers.
The UISlider subclass eliminates the built-in track, so that the thermometer behind it shows through. But the UISlider's thumb (knob) is still draggable in the normal way, and you can set it to a custom image, get the Value Changed event when the user drags it, and so on. Here is the code for the UISlider subclass that eliminates its own track:
- (CGRect)trackRectForBounds:(CGRect)bounds {
CGRect result = [super trackRectForBounds:bounds];
result.size.height = 0;
return result;
}
The thermometer is an instance of a custom UIView subclass, MyTherm. I instantiated it in the nib and unchecked its Opaque and gave it a background color of Clear Color. It has a value property so it knows how much to fill the thermometer. Here's its drawRect: code:
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
[[UIColor whiteColor] set];
CGFloat ins = 2.0;
CGRect r = CGRectInset(self.bounds, ins, ins);
CGFloat radius = r.size.height / 2.0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, CGRectGetMaxX(r) - radius, ins);
CGPathAddArc(path, NULL, radius+ins, radius+ins, radius, -M_PI/2.0, M_PI/2.0, true);
CGPathAddArc(path, NULL, CGRectGetMaxX(r) - radius, radius+ins, radius, M_PI/2.0, -M_PI/2.0, true);
CGPathCloseSubpath(path);
CGContextAddPath(c, path);
CGContextSetLineWidth(c, 2);
CGContextStrokePath(c);
CGContextAddPath(c, path);
CGContextClip(c);
CGContextFillRect(c, CGRectMake(r.origin.x, r.origin.y, r.size.width * self.value, r.size.height));
}
To change the thermometer value, change the MyTherm instance's value to a number between 0 and 1, and tell it to redraw itself with setNeedsDisplay.
This is doable using the standard controls.
In Interface Builder place your UISlider immediately on top of your UIProgressView and make them the same size.
On a UISlider the background horizontal line is called the track, the trick is to make it invisible. We do this with a transparent PNG and the UISlider methods setMinimumTrackImage:forState: and setMaximumTrackImage:forState:.
In the viewDidLoad method of your view controller add:
[self.slider setMinimumTrackImage:[UIImage imageNamed:#"transparent.png"] forState:UIControlStateNormal];
[self.slider setMaximumTrackImage:[UIImage imageNamed:#"transparent.png"] forState:UIControlStateNormal];
where self.slider refers to your UISlider.
I've tested the code in Xcode, and this will give you a slider with an independent progress bar.
Solution that suits my design:
class SliderBuffering:UISlider {
let bufferProgress = UIProgressView(progressViewStyle: .Default)
override init (frame : CGRect) {
super.init(frame : frame)
}
convenience init () {
self.init(frame:CGRect.zero)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.minimumTrackTintColor = UIColor.clearColor()
self.maximumTrackTintColor = UIColor.clearColor()
bufferProgress.backgroundColor = UIColor.clearColor()
bufferProgress.userInteractionEnabled = false
bufferProgress.progress = 0.0
bufferProgress.progressTintColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.5)
bufferProgress.trackTintColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
self.addSubview(bufferProgress)
}
}
Create a UISlider:
// 1
// Make the slider as a public propriety so you can access it
playerSlider = [[UISlider alloc] init];
[playerSlider setContinuous:YES];
[playerSlider setHighlighted:YES];
// remove the slider filling default blue color
[playerSlider setMaximumTrackTintColor:[UIColor clearColor]];
[playerSlider setMinimumTrackTintColor:[UIColor clearColor]];
// Chose your frame
playerSlider.frame = CGRectMake(--- , -- , yourSliderWith , ----);
// 2
// create a UIView that u can access and make it the shadow of your slider
shadowSlider = [[UIView alloc] init];
shadowSlider.backgroundColor = [UIColor lightTextColor];
shadowSlider.frame = CGRectMake(playerSlider.frame.origin.x , playerSlider.frame.origin.y , playerSlider.frame.size.width , playerSlider.frame.origin.size.height);
shadowSlider.layer.cornerRadius = 4;
shadowSlider.layer.masksToBounds = YES;
[playerSlider addSubview:shadowSlider];
[playerSlider sendSubviewToBack:shadowSlider];
// 3
// Add a timer Update your slider and shadow slider programatically
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(updateSlider) userInfo:nil repeats:YES];
-(void)updateSlider {
// Update the slider about the music time
playerSlider.value = audioPlayer.currentTime; // based on ur case
playerSlider.maximumValue = audioPlayer.duration;
float smartWidth = 0.0;
smartWidth = (yourSliderFullWidth * audioPlayer.duration ) / 100;
shadowSlider.frame = CGRectMake( shadowSlider.frame.origin.x , shadowSlider.frame.origin.y , smartWidth , shadowSlider.frame.size.height);
}
Enjoy! P.S. I might have some typos.
Idea 1:
You could easily use the UISlider as a progress view by subclassing it. It responds to methods such as 'setValue:animated:' with which you can set the value (i.e: progress) of the view.
Your only 'restriction' creating what you see in your example is the buffer bar, which you could create by 'creatively' skinning the UISlider (because you can add custom skins to it), and perhaps set that skin programmatically.
Idea 2:
Another (easier) option is to subclass UIProgressView, and create a UISlider inside that subclass. You can skin the UISlider to have a see-through skin (no bar, just the knob visible) and lay it over the UIProgressView.
You can use the UIProgressView for the pre-loading (buffering) and the UISlider for movie control / progress indication.
Seems fairly easy :-)
Edit: to actually answer your question, there is no in-house way, but it would be easy to accomplish with the tools given.
You can do some trick like this, it's more easy and understanding. Just insert the code bellow in your UISlider subclass.
- (void)layoutSubviews
{
[super layoutSubviews];
if (_availableDurationImageView == nil) {
// step 1
// get max length that our "availableDurationImageView" will show
UIView *maxTrackView = [self.subviews objectAtIndex:self.subviews.count - 3];
UIImageView *maxTrackImageView = [maxTrackView.subviews objectAtIndex:0];
_maxLength = maxTrackImageView.width;
// step 2
// get the right frame where our "availableDurationImageView" will place in superView
UIView *minTrackView = [self.subviews objectAtIndex:self.subviews.count - 2];
_availableDurationImageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:#"MediaSlider.bundle/4_jindu_huancun.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 2, 0, 2)]];
_availableDurationImageView.opaque = NO;
_availableDurationImageView.frame = minTrackView.frame;
[self insertSubview:_availableDurationImageView belowSubview:minTrackView];
}
}
- (void)setAvailableValue:(NSTimeInterval)availableValue
{
if (availableValue >=0 && availableValue <= 1) {
// use "maxLength" and percentage to set our "availableDurationImageView" 's length
_availableDurationImageView.width = _maxLength * availableValue;
}
}
Adding on matt's solution, note that as of iOS 7.0, implementing trackRectForBounds: is rendered impossible. Here is my solution to this problem :
In your UISlider subclass, do this :
-(void)awakeFromNib
{
[super awakeFromNib];
UIImage* clearColorImage = [UIImage imageWithColor:[UIColor clearColor]];
[self setMinimumTrackImage:clearColorImage forState:UIControlStateNormal];
[self setMaximumTrackImage:clearColorImage forState:UIControlStateNormal];
}
with imageWithColor as this function :
+ (UIImage*) imageWithColor:(UIColor*)color
{
return [UIImage imageWithColor:color andSize:CGSizeMake(1.0f, 1.0f)];
}
That will properly take care of this annoying trackRectangle.
I spent too much time looking for a solution to this problem, here's hoping that'll save some time to another poor soul ;).
Here is a solution in Objective C. https://github.com/abhimuralidharan/BufferSlider
The idea is to create a UIProgressview as a property in the UISlider subclass and add the required constraints programatically.
#import <UIKit/UIKit.h> //.h file
#interface BufferSlider : UISlider
#property(strong,nonatomic) UIProgressView *bufferProgress;
#end
#import "BufferSlider.h" //.m file
#implementation BufferSlider
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
-(void)setup {
self.bufferProgress = [[UIProgressView alloc] initWithFrame:self.bounds];
self.minimumTrackTintColor = [UIColor redColor];
self.maximumTrackTintColor = [UIColor clearColor];
self.value = 0.2;
self.bufferProgress.backgroundColor = [UIColor clearColor];
self.bufferProgress.userInteractionEnabled = NO;
self.bufferProgress.progress = 0.7;
self.bufferProgress.progressTintColor = [[UIColor blueColor] colorWithAlphaComponent:0.5];
self.bufferProgress.trackTintColor = [[UIColor lightGrayColor] colorWithAlphaComponent:2];
[self addSubview:self.bufferProgress];
[self setThumbImage:[UIImage imageNamed:#"redThumb"] forState:UIControlStateNormal];
self.bufferProgress.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0.75]; // edit the constant value based on the thumb image
NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:self.bufferProgress attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
[self addConstraints:#[left,right,centerY]];
[self sendSubviewToBack:self.bufferProgress];
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
#end
for Swift5
First, add tap gesture to slider:
let tap_gesture = UITapGestureRecognizer(target: self, action: #selector(self.sliderTapped(gestureRecognizer:)))
self.slider.addGestureRecognizer(tap_gesture)
Then, implement this function:
#objc func sliderTapped(gestureRecognizer: UIGestureRecognizer) {
let locationOnSlider = gestureRecognizer.location(in: self.slider)
let maxWidth = self.slider.frame.size.width
let touchLocationRatio = locationOnSlider.x * CGFloat(self.slider.maximumValue) / CGFloat(maxWidth)
self.slider.value = Float(touchLocationRatio)
print("New value: ", round(self.slider.value))
}