- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect frame = self.navigationController.navigationBar.frame;
CGFloat size = frame.size.height - 21;
CGFloat framePercentageHidden = ((20 - frame.origin.y) / (frame.size.height - 1));
CGFloat scrollOffset = scrollView.contentOffset.y;
CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
CGFloat scrollHeight = scrollView.frame.size.height;
CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;
if (scrollOffset <= -scrollView.contentInset.top) {
frame.origin.y = 20;
} else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
frame.origin.y = -size;
} else {
frame.origin.y = MIN(20, MAX(-size, frame.origin.y - scrollDiff));
}
[self.navigationController.navigationBar setFrame:frame];
[self updateBarButtonItems:(1 - framePercentageHidden)];
self.previousScrollViewYOffset = scrollOffset;
}
ERROR:
Property 'previousScrollViewYOffset' not found on object of type 'HomeScreen'
#interface HomeScreen : UITableViewController <UIScrollViewDelegate,UIScrollViewAccessibilityDelegate>
I added ScrollViewDelegate why not see previousScrollViewYOffset any idea ?
Declaring that your class conforms to the UIScrollViewDelegate protocol does not give it access to additional properties. It means that your class will implement the methods specified in the delegate protocol, and that your class can therefore support a UIScrollView as its delegate. So, all that declaring your class is a UIScrollViewDelegate will do is enable the method you posted to get called.
Regarding the error you're getting, you should try creating the previousScrollViewYOffset property yourself and initializing it with a value such as 0.
I've never tried to create this specific effect, so I could be off, but it looks like previousScrollViewYOffset is a property on your class that you are calculating each time ScrollViewDidScroll is called. The result of that calculation is then used the next time this method is called. If that's the case, you'll need to create that property yourself, and initialize it in your class's initialization method.
Give this a shot [edited to add code] and let me know how it goes:
In your .h:
#property CGFloat thisFloat;
At the top of your .m:
#synthesize thisFloat;
In your initialization method:
thisFloat = 0.0;
Alternative to making it a property and synthesizing it - because no outside classes will access the property - you could declare it as a private variable at the top of your .m like this:
#implementation HomeScreen {
CGFloat previousScrollViewYOffset;
}
Related
I'm trying to record a UIView animation for a WatchKit app. First, I implemented the function with out the block which return zero frames being recorded. This was due to the [recorder stop] being called before the animation completed (I think). So, I added a completion block. Now, it never has self.completion is YES. I want the completion block to notify me when the animation is complete. What am I missing here?
ViewController.m
-(void)runAnimation{
ALBatteryView *batteryView = [[ALBatteryView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)];
[self.batteryIcon addSubview:batteryView];
recorder.view = _batteryIcon;
[recorder start];
[batteryView setBatteryLevelWithAnimation:YES forValue:[UIDevice currentDevice].batteryLevelInPercentage inPercent:YES];
CGFloat batteryPer = [UBattery batteryLevel];
batteryPercentage.text = [NSString stringWithFormat:#"%.0f%%", batteryPer];
battery = [NSString stringWithFormat:#"%.0f%%", batteryPer];
[batteryView batteryAnaminationWithCompletion:^(BOOL finished){
if (finished){
[recorder stop];
}
}];
}
AlBatteryView.h
#interface ALBatteryView : UIView {
UIView *batteryFill;
}
#property (nonatomic, strong) void(^completion)(BOOL finished);
- (void)setBatteryLevelWithAnimation:(BOOL)isAnimated forValue:(CGFloat)batteryLevel inPercent:(BOOL)inPercent;
- (void)reload;
- (void)batteryAnaminationWithCompletion:(void (^)(BOOL finished))completion;
#end
ALBatteryView.m
- (void)setBatteryLevelWithAnimation:(BOOL)isAnimated forValue:(CGFloat)batteryLevel inPercent:(BOOL)inPercent {
// Declare the newWidth and save the correct battery level
// based on inPercent value
CGFloat newWidth;
CGFloat newBatteryLevel = (inPercent) ? batteryLevel : batteryLevel * 100;
// Set the new width
newWidth = kOnePercent * newBatteryLevel;
// If animated proceed with the animation
// else assign the value without animates
if (isAnimated)
[UIView animateWithDuration:2.0 animations:^{
/* This direct assignment is possible
* using the UIView+ALQuickFrame category
* http://github.com/andrealufino/ALQuickFrame */
batteryFill.width = newWidth;
// Set the color based on battery level
batteryFill.backgroundColor = [self setColorBasedOnBatteryLevel:newBatteryLevel];
if (self.completion) {
self.completion(YES);
}
}];
else
batteryFill.width = newWidth;
}
-(void)batteryAnaminationWithCompletion:(void (^)(BOOL finished))completion{
self.completion = completion;
}
You need to set your block property before you call for the animation (setBatteryLevelWithAnimation). Otherwise it will be nil when you try to access it before it's set.
Also you should set your block property directly, it will be more clear, because that's what your -batteryAnaminationWithCompletion method does (btw it should be spelled "Animation")
From:
[batteryView batteryAnaminationWithCompletion:^(BOOL finished){
if (finished){
[recorder stop];
}
}];
To:
[batteryView setCompletion:^(BOOL finished){
if (finished){
[recorder stop];
}
}];
You should set your completion block before starting the animation (change code order).
Then, your block should be declared as copy.
#property (nonatomic, copy) void(^completion)(BOOL finished);
Then, delete the (wrongly named) batteryAnaminationWithCompletion method altogether, and just use the property:
batteryView.completion = ...
If I am reading it right,
When you call -(void)runAnimation
it calls the animation block right here and animation block will referencing the completion which is nil at the moment
[batteryView setBatteryLevelWithAnimation:YES forValue:[UIDevice currentDevice].batteryLevelInPercentage inPercent:YES];
If you are thinking animationWithDuration call the block 2 second later than.. it's wrong.. self.completion is nil at the moment and animation's duration is 2 second but self.completion is not animatable and it calls right up front.. you need some kind of lock if you want to wait this animation to be finished.
[UIView animateWithDuration:2.0 animations:^{
/* This direct assignment is possible
* using the UIView+ALQuickFrame category
* http://github.com/andrealufino/ALQuickFrame */
batteryFill.width = newWidth;
// Set the color based on battery level
batteryFill.backgroundColor = [self setColorBasedOnBatteryLevel:newBatteryLevel];
if (self.completion) {
self.completion(YES);
}
}];
Just print out NSLog before animateWithDuration and NSLog from inside of block to see when if (self.completion) is referenced..
The interface is unduly complex if the only objective is animation. There's no need for a block property at all. Consider the following...
// ALBatteryView.h
#interface ALBatteryView : UIView
// up to you, but I'd clean up the animating interface by making a property of this class determine
// how it handles levels in all cases as either absolute or %
#property(assign, nonatomic) BOOL inPercent;
// use naming style like the sdk...
- (void)setBatteryLevel:(CGFloat)batteryLevel animated:(BOOL)animated completion:(void (^)(BOOL))completion;
#end
In the implementation...
// ALBatteryView.m
#interface ALBatteryView ()
// hide the internal view property here. make it weak, and assign it after [self subView:...
#property(weak,nonatomic) UIView *batteryFill;
#end
In just that one animation method, using a few tricks in UIView animation, you can accomplish everything that it looks like you need...
- (void)setBatteryLevel:(CGFloat)batteryLevel animated:(BOOL)animated completion:(void (^)(BOOL))completion {
// one trick is that UIView animation with zero duration just
// changes the animatable properties and calls its completion block
NSTimeInterval duration = (animated)? 2.0 : 0.0;
CGFloat newBatteryLevel = (self.inPercent)? batteryLevel : batteryLevel * 100;
CGFloat newWidth = kOnePercent * newBatteryLevel;
// don't name that color-returning method "set" color. The "set" part happens here...
UIColor *newColor = [self colorBasedOnBatteryLevel:newBatteryLevel];
// now, the animation in just one shot, passing the block parameter
// which has the same signature as the UIView animation block param
[UIView animateWithDuration:duration animations:^{
self.batteryFill.width = newWidth;
self.batteryFill.backgroundColor = newColor;
} completion:completion];
}
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.
I'm trying to create a function to change the size of something like a UILabel or UIButton without having to type the three lines out every time. This is what I have.
-(void)setObject:(UIControl*)object SizeWidth:(NSInteger)width Height:(NSInteger)height
{
CGRect labelFrame = object.frame;
labelFrame.size = CGSizeMake(width, height);
object.frame = labelFrame;
}
However, when I give (UIControl*)object a UILabel, it says "incompatible pointer types". How can I fix this to work for anything I can put on a UIView?
UILabel is not a subclass of UIControl, it inherits from UIView.
Try changing UIControl to UIView:
-(void)setObject:(UIView*)object SizeWidth:(NSInteger)width Height:(NSInteger)height
{
CGRect labelFrame = object.frame;
labelFrame.size = CGSizeMake(width, height);
object.frame = labelFrame;
}
(UIControl inherits from UIView anyway, and frame is a UIView property)
Label is not a subclass of UIControl. You can use UIView in place of UIControl.
Here is the hierarchy for UILabel
UILabel: UIView : UIResponder : NSObject
-(void)setObject:(UIView*)object SizeWidth:(NSInteger)width Height:(NSInteger)height
{
CGRect labelFrame = object.frame;
labelFrame.size = CGSizeMake(width, height);
object.frame = labelFrame;
}
One suggestion for you is, the method name seems kind of odd to me. You can write a simple category to update the size for UIView. With the following category you can simply call
[myLabel setWidth:20 andHeight:20];
In UIView + MyCategory.h
#import <UIKit/UIKit.h>
#interface UIView (MyCategory)
- (void)setWidth:(NSInteger)aWidth andHeight:(NSInteger)aHeight;
#end
In UIView + MyCategory.m
#import "UIView + MyCategory.h"
#implementation UIView (MyCategory)
- (void)setWidth:(NSInteger)aWidth andHeight:(NSInteger)aHeight;
{
CGRect frameToUpdate = self.frame;
frameToUpdate.size = CGSizeMake(aWidth, aHeight);
self.frame = frameToUpdate;
}
#end
And to address the actual problem you're trying to solve, I highly recommend using this set of helpers https://github.com/kreeger/BDKGeometry
Following Obj-C style conventions (as selecting the right tools for the job) make it more efficient for others to read and comprehend our code. The Objective-C style here needs a bit of cleanup. See my notes after the source if you're interested. On to a more concise way to do this:
You can go the class method route (perhaps in a view manipulation class)
#implementation CCViewGeometry
+ (void)adjustView:(UIView *)view toSize:(CGSize)size
{
CGRect frame = view.frame;
frame.size = size;
view.frame = frame;
}
#end
or the UIView category route
#implementation UIView (CCGeometry)
- (void)resize:(CGSize)size
{
CGRect frame = self.frame;
frame.size = size;
self.frame = frame;
}
#end
Style notes pertinent to code found on this page:
All method params should begin with a lower-case char.
setFoo: is used in #property synthesis & by convention your method name indicates setting a property named object to the value of object. You're setting the size, not the object itself.
Be explicit. Why have a method called setObject: when you know the general type of object being passed?
Width & height in UIKit are represented (rightly) by CGFloat, not NSInteger. Why pass width + height instead of CGSize anyway?
Try using class methods when state is not required. + is your friend. Don't fire up instances for every little thing (most singleton methods I see in ObjC code should be refactored as class methods).
The programmers that don't care about the little things end up with unmaintainable code—code that will slow them and those that come after them down. Convention and style matter a lot on any decent-sized project.
I'm searching for a way to implement something like reusable cells for UI/NSTableView but for NSScrollView. Basically I want the same like the WWDC 2011 video "Session 104 - Advanced Scroll View Techniques" but for Mac.
I have several problems realizing this. The first: NSScrollView doesn't have -layoutSubviews. I tried to use -adjustScroll instead but fail in setting a different contentOffset:
- (NSRect)adjustScroll:(NSRect)proposedVisibleRect {
if (proposedVisibleRect.origin.x > 600) {
// non of them work properly
// proposedVisibleRect.origin.x = 0;
// [self setBoundsOrigin:NSZeroPoint];
// [self setFrameOrigin:NSZeroPoint];
// [[parentScrollView contentView] scrollPoint:NSZeroPoint];
// [[parentScrollView contentView] setBoundsOrigin:NSZeroPoint];
}
return proposedVisibleRect;
}
The next thing I tried was to set a really huge content view with a width of millions of pixel (which actually works in comparison to iOS!) but now the question is, how to install a reuse-pool?
Is it better to move the subviews while scrolling to a new position or to remove all subviews and insert them again? and how and where should I do that?
As best I can tell, -adjustScroll: is not where you want to tap into the scrolling events because it doesn't get called universally. I think -reflectScrolledClipView: is probably a better hookup point.
I cooked up the following example that should hit the high points of one way to do a view-reusing scroll view. For simplicity, I set the dimensions of the scrollView's documentView to "huge", as you suggest, rather than trying to "fake up" the scrolling behavior to look infinite. Obviously drawing the constituent tile views for real is up to you. (In this example I created a dummy view that just fills itself with red with a blue outline to convince myself that everything was working.) It came out like this:
// For the header file
#interface SOReuseScrollView : NSScrollView
#end
// For the implementation file
#interface SOReuseScrollView () // Private
- (void)p_updateTiles;
#property (nonatomic, readonly, retain) NSMutableArray* p_reusableViews;
#end
// Just a small diagnosting view to convince myself that this works.
#interface SODiagnosticView : NSView
#end
#implementation SOReuseScrollView
#synthesize p_reusableViews = mReusableViews;
- (void)dealloc
{
[mReusableViews release];
[super dealloc];
}
- (NSMutableArray*)p_reusableViews
{
if (nil == mReusableViews)
{
mReusableViews = [[NSMutableArray alloc] init];
}
return mReusableViews;
}
- (void)reflectScrolledClipView:(NSClipView *)cView
{
[super reflectScrolledClipView: cView];
[self p_updateTiles];
}
- (void)p_updateTiles
{
// The size of a tile...
static const NSSize gGranuleSize = {250.0, 250.0};
NSMutableArray* reusableViews = self.p_reusableViews;
NSRect documentVisibleRect = self.documentVisibleRect;
// Determine the needed tiles for coverage
const CGFloat xMin = floor(NSMinX(documentVisibleRect) / gGranuleSize.width) * gGranuleSize.width;
const CGFloat xMax = xMin + (ceil((NSMaxX(documentVisibleRect) - xMin) / gGranuleSize.width) * gGranuleSize.width);
const CGFloat yMin = floor(NSMinY(documentVisibleRect) / gGranuleSize.height) * gGranuleSize.height;
const CGFloat yMax = ceil((NSMaxY(documentVisibleRect) - yMin) / gGranuleSize.height) * gGranuleSize.height;
// Figure out the tile frames we would need to get full coverage
NSMutableSet* neededTileFrames = [NSMutableSet set];
for (CGFloat x = xMin; x < xMax; x += gGranuleSize.width)
{
for (CGFloat y = yMin; y < yMax; y += gGranuleSize.height)
{
NSRect rect = NSMakeRect(x, y, gGranuleSize.width, gGranuleSize.height);
[neededTileFrames addObject: [NSValue valueWithRect: rect]];
}
}
// See if we already have subviews that cover these needed frames.
for (NSView* subview in [[[self.documentView subviews] copy] autorelease])
{
NSValue* frameRectVal = [NSValue valueWithRect: subview.frame];
// If we don't need this one any more...
if (![neededTileFrames containsObject: frameRectVal])
{
// Then recycle it...
[reusableViews addObject: subview];
[subview removeFromSuperview];
}
else
{
// Take this frame rect off the To-do list.
[neededTileFrames removeObject: frameRectVal];
}
}
// Add needed tiles from the to-do list
for (NSValue* neededFrame in neededTileFrames)
{
NSView* view = [[[reusableViews lastObject] retain] autorelease];
[reusableViews removeLastObject];
if (nil == view)
{
// Create one if we didnt find a reusable one.
view = [[[SODiagnosticView alloc] initWithFrame: NSZeroRect] autorelease];
NSLog(#"Created a view.");
}
else
{
NSLog(#"Reused a view.");
}
// Place it and install it.
view.frame = [neededFrame rectValue];
[view setNeedsDisplay: YES];
[self.documentView addSubview: view];
}
}
#end
#implementation SODiagnosticView
- (void)drawRect:(NSRect)dirtyRect
{
// Draw a red tile with a blue border.
[[NSColor blueColor] set];
NSRectFill(self.bounds);
[[NSColor redColor] setFill];
NSRectFill(NSInsetRect(self.bounds, 2,2));
}
#end
This worked pretty well as best I could tell. Again, drawing something meaningful in the reused views is where the real work is here.
Hope that helps.
I am trying to implement a custom circular slider which uses images to draw the dial and the knob. For this purpose I have subclassed NSSliderCell and overridden the drawBarInside and drawKnob methods.
Now, if I leave the slider type as default (horizontal), my drawing functions are called and images are drawn in stead of the default bar and knob (of course it looks all wrong since images are made for a circular slider, but they are there). But as soon as I change the slider type to circular (either in IB or by setting self.sliderType = NSCircularSlider; in my slider cell) those two methods are never called and my images are not drawn (only the standard dial is created).
Am I forgetting something here, or can circular sliders not be customized at all?
Here is my code:
DialSliderCell is a subclass of NSSliderCell and is initiated and set in a class called DialSlider which is a subclass of NSSlider (this class does nothing else at the time).
#implementation DialSliderCell
- (id)init {
self = [super init];
if (self) {
dialImage = [NSImage imageNamed:#"dial"];
knobImage = [NSImage imageNamed:#"dialKnob"];
self.sliderType = NSCircularSlider;
}
NSLog(#"init");
return self;
}
- (void)drawKnob:(NSRect)knobRect {
knobRect.size = knobImage.size;
[knobImage drawInRect:knobRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSLog(#"drawKnob");
}
- (void)drawBarInside:(NSRect)aRect flipped:(BOOL)flipped {
[dialImage drawInRect:aRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSLog(#"drawBarInside");
}
#end
I have solved this problem by overriding the drawInteriorWithFrame method. The solution is based on the original method. I just removed anything not related to circular sliders, removed the original graphics and added my own. The geometry is almost entirely original.
- (void) drawInteriorWithFrame: (NSRect)cellFrame inView: (NSView*)controlView
{
cellFrame = [self drawingRectForBounds: cellFrame];
_trackRect = cellFrame;
NSPoint knobCenter;
NSPoint point;
float fraction, angle, radius;
knobCenter = NSMakePoint(NSMidX(cellFrame), NSMidY(cellFrame));
[dialImage drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
int knobDiameter = knobImage.size.width;
int knobRadius = knobDiameter / 2;
fraction = ([self floatValue] - [self minValue]) / ([self maxValue] - [self minValue]);
angle = (fraction * (2.0 * M_PI)) - (M_PI / 2.0);
radius = (cellFrame.size.height / 2) - knobDiameter;
point = NSMakePoint((radius * cos(angle)) + knobCenter.x,
(radius * sin(angle)) + knobCenter.y);
NSRect dotRect;
dotRect.origin.x = point.x - knobRadius;
dotRect.origin.y = point.y - knobRadius;
dotRect.size = knobImage.size;
[knobImage drawInRect:dotRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
The size of the cellFramerectangle is set to the size of the dialImageimage.
Notice that the method above doesn't seem to get called automatically so I also had to override the drawWithFrame method and make it call drawInteriorWithFrame:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
[self drawInteriorWithFrame:cellFrame inView:controlView];
}
All I can think of is that you are not overriding the all the designated initialisers
from the docs...
Designated Initializers. When subclassing NSCell you must implement all
of the designated initializers. Those methods are: init,
initWithCoder:, initTextCell:, and initImageCell:.
Probably one of the other 3 is called for a circular slider.