I have been trying for a few hours. I am adding a subclass of UIView to the view of my UITableViewController. I am using the code at the bottom of the this post.
I get a crash that says:
2014-06-15 12:19:58.724 Block Party[4712:60b] *** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit/UIKit-2935.137/UIView.m:8794
2014-06-15 12:19:58.726 Block Party[4712:60b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'
*** First throw call stack:
(0x2e940f0b 0x390d7ce7 0x2e940ddd 0x2f2ede2f 0x311703c5 0x30dec31b 0x30de7b3f 0x311854a1 0x3e387 0x311a5a3b 0x311bb8a5 0x3e221 0x3d267 0x31174a53 0x31174811 0x31300c13 0x3121e48f 0x3121e299 0x3121e231 0x31170305 0x30dec31b 0x30de7b3f 0x30de79d1 0x30de73e5 0x30de71f7 0x30de0f1d 0x2e90c039 0x2e9099c7 0x2e909d13 0x2e874769 0x2e87454b 0x337e16d3 0x311d3891 0x3acb5 0x395d5ab7)
libc++abi.dylib: terminating with uncaught exception of type NSException
I attempted to resolve this by adding the layoutSublayerofLayer method but it didn't change anything. Also, if I comment out [self.locationNeededView setTranslatesAutoresizingMaskIntoConstraints:NO]; then it doesn't crash but my constraints are ignored because the autoresizingmaskconstraints take priority.
I'm totally out of ideas for how to solve this. Basically I want this view to appear on top of my table, centered horizontally and vertically.
Thanks in advance for any help you can offer.
-Jeff
- (void) layoutSublayersOfLayer:(CALayer *)layer {
[super layoutSublayersOfLayer:layer];
}
- (void)loadLocationNeededView {
UINib *nib = [UINib nibWithNibName:#"LocationNeededUIView" bundle:nil];
self.locationNeededView = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
[self.locationNeededView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem: self.view
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.locationNeededView
attribute:NSLayoutAttributeCenterX
multiplier:0
constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem: self.view
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.locationNeededView
attribute:NSLayoutAttributeCenterY
multiplier:0
constant:0]];
[self.view addSubview:self.locationNeededView];
[self.view layoutSubviews];
}
Edit - Update
I gave up on using constraints and I'm just using the frame. I got it to work pretty well with the following code. I also found if I add it to the view of the navigation controller, then it won't scroll with the tableview. Which is exactly what I wanted.
- (void)loadLocationNeededView {
//allocate subview
UINib *nib = [UINib nibWithNibName:#"LocationNeededUIView" bundle:nil];
self.locationNeededView = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
//set frame for it offscreen so it can be animated in
self.locationNeededView.frame = CGRectMake((self.view.frame.size.width / 2) - (self.locationNeededView.frame.size.width / 2), [[UIScreen mainScreen] bounds].size.height, self.locationNeededView.frame.size.width, self.locationNeededView.frame.size.height);
//addsubview to navigation controller so that it does not scroll with the tableview
[self.navigationController.view addSubview:self.locationNeededView];
[self animateLocationNeededViewIn];
}
- (void) animateLocationNeededViewIn {
[UIView animateWithDuration:0.35
delay:1
usingSpringWithDamping:0.7
initialSpringVelocity:1.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^ {
self.locationNeededView.frame = CGRectMake((self.view.frame.size.width / 2) - (self.locationNeededView.frame.size.width / 2), (self.view.frame.size.height / 2) - (self.locationNeededView.frame.size.height / 2), self.locationNeededView.frame.size.width, self.locationNeededView.frame.size.height);
[self.view layoutIfNeeded];} completion:^ (BOOL fin) {
if (fin) {
NSLog(#"After Animating View Frame: Origin X = %f. Origin Y = %f. Width = %f. Height = %f", self.locationNeededView.frame.origin.x, self.locationNeededView.frame.origin.y, self.locationNeededView.frame.size.width, self.locationNeededView.frame.size.height);
}
}];
}
Issues: Constraint Multipliers should be 1, not 0. also, you shouldn't need to call layoutSubviews, just call layoutIfNeeded. Also your view needs width and height constraints unless you have defined intrinsiccontentSize in your view.
I have a subclass of NSScrollView. Within that scrollView's documentView I have a couple of NSViews. Finally, I add an NSView called aView to the documentView.
I want aView to be at the bottom of the documentView as long as there's no scrolling needed. Scrolling is only possible along the y-axis.
If the documentView is too high for the contentView - so that scrolling is needed - I want aView to be displayed at the bottom of the contentView.
This works fine with the code that you see below.
Here's my Problem:
The moment I start to scroll, I want aView to stay at the bottom of the contentView but aView just scrolls with all the other views within documentView.
In other words: I want aView's position to be fixed at the bottom of the visible rect of my scrollView if scrolling is needed and to stick to the bottom of the documentView if the contentView is high enough to show the whole documentView.
Is there a way to do that? Where do I go wrong?
Here's the code in my subclassed NSScrollView:
[documentView addSubview:aView];
aView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *documentAViewBottom = [NSLayoutConstraint constraintWithItem:documentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:aView attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
documentAViewBottom.priority = NSLayoutPriorityDefaultHigh;
[documentView addConstraint:documentAViewBottom];
NSLayoutConstraint *aViewMaxYEdge = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:aView attribute:NSLayoutAttributeBottom multiplier:1 constant:5];
[self addConstraint:aViewMaxYEdge];
[documentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[aView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(aView)]];
I am kinda oldschool and NSLayoutConstraints don't appeal to me, so here is a alternate proposed manual solution
from your subclass of NSScrollView when you set up the document view,
[self.contentView setPostsBoundsChangedNotification:YES];
then subscribe to the bounds changed
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contentViewDidScroll:)
name:NSViewBoundsDidChangeNotification object:self.contentView];
then in
-(void)contentViewDidScroll
{
double widthOfAView = aView.frame.size.width;
double heightOfAView = aView.frame.size.height;
[aView setFrameOrigin:NSMakePoint(NSMinX(self.contentView.bounds) - widthOfAView, NSMaxY(self.contentView.bounds) - heightOfAView)];
}
All of this assuming your isFlipped is overridden to YES, of course.
When a popover is displaying how can I make its size variable depending on condition check? Is this functionality supported or not when working with a popover?
Here is my sample for changing frame size:
CGFloat newHeight = 200;
NSRect newFrame = [parentView frame];
newFrame.size.height = newHeight;
NSView* childView = someView;
[parentView setFrame:newFrame];
[self.view addSubview:childView];
Don't know if anything was changed but in 10.9 all you have to do is setContentSize on the popover, e.g.:
[popoverView setFrame:NSMakeRect(0, 0, 581, 581)];
[thePopover setContentSize:popoverView.frame.size];
You need to change the popupwindow.contentViewController property of the NSPopover to a new NSViewController with a different-sized view.
That is, resizing the view's frame will only result in weird drawing problems. To get the popover window to change size, the contentViewController must be changed.
Ideally, you would set up a new view controller which is essentially a copy of your existing NSViewController, but containing a view that is taller/shorter. But in the worst-case scenario, you can do something like this:
gPopoverWindow.contentViewController = [[[NSViewController alloc] initWithNibName: #"tempNibName" bundle: nil] autorelease];
NSView *v = self.view;
NSRect b = [v frame];
b.size.height += 25;
[v setFrame: b];
gPopoverWindow.contentViewController = self;
In my testing, this resulted in the popover beginning a shrink animation (because of being set to the temp view controller), then growing to its new size.
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))
}
I've created an NSScrollView which itself contains a NSClippedView as content view (this is all default, created by IB). Inside the contents view there is the (default) document view.
This NSScrollView has horizontal scroller disabled and vertical enabled and, most importantly auto hide scrollers enabled.
When I add new views (via code, runtime) to the document view the scroller does not unhide automatically, until the moment I vertically resize the window (and which in turn resizes the scrollview as well). 1px is enough. Just the new painting of the window seems enough.
What I am looking for is triggering this by code: so when I add views to the scrollviews' content view I would like the scrollbar to appear.
int numberOfChildViews = 10; //hard coded for example here
int childViewHeight = 80; //same as above
NSRect rect = NSMakeRect(0, 0, [[self.scrollView contentView] bounds].size.width, [numberOfChildViews*childViewHeight);
[[self.scrollView documentView] setFrame:rect];
[[self.scrollView documentView] setBounds:rect]; //just added to make sure
Then I added the custom views to the document view, like:
for (int i=0; i<numberOfChildViews; i++) {
NZBProgressViewController *item = [nzbProgressArray objectAtIndex:i];
int y=i*[[item view] bounds].size.height;
rect= NSMakeRect(0, y, [[scrollView contentView] frame].size.width, [[item view] bounds].size.height);
[[item view] setFrame:rect];
currentPosition++;
}
I am using a FlippedView so the origin will be displayed in left-top, like so:
#interface NSFlippedClipView : NSClipView {
}
#implementation NSFlippedClipView
- (BOOL)isFlipped {
return YES;
}
- (BOOL)isOpaque {
return YES;
}
#end
And added the following code to the awakeFromNib
NSFlippedClipView *documentView = [[NSFlippedClipView alloc] init];
[documentView setAutoresizingMask:NSViewWidthSizable];
[documentView setBackgroundColor:[self.scrollView backgroundColor]];
[self.scrollView setDocumentView:documentView];
[documentView release];
The scrollbars should become visible as soon as the document view is resized to be larger than the current viewport of the scroll view. Are you resizing the document view when you add your subviews to it?
Ahh, it's my own bad. For future reference: if you want to move the origin (0,0) to left-top use a NSView instead of NSClippedView extended class with IsFlipped method overriden to YES.
Thanks irsk for answering.