drawRect not responding to setNeedsDisplay (mcve) - ios

I am aware that this question has been asked many times, but I have found a partial resolution.
I have a custom class GraphView in which I have several sliders which change the graph parameters and instigate a redraw using [self setNeedsDisplay]. The only way I can get the setNeedsDisplay to work is to have the view of type GraphView just under the View Controller and the slider just under (and inside) the GraphView (in the storyboard hierarchy). This is problematic since the slider must be inside the graph.
Here is an mcve #interface:
#import <UIKit/UIKit.h>
#interface GraphView : UIView
#property float red;
#property __weak IBOutlet UITextField *redout;
#property UIBezierPath *aPath;
#property CGPoint aPoint;
- (void)drawRect:(CGRect)rect;
- (id) initWithFrame:(CGRect)frameRect;
- (IBAction)red_rabi:(id)sender;
Here is the mcve #implementation:
#import "GraphView.h"
#implementation GraphView
- (id) initWithFrame:(CGRect)frameRect
{
if ((self = [super initWithFrame:frameRect]) != nil)
{
_red=1.0;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
int i;
NSString *redstr;
float width, height;
width = rect.size.width;
height = rect.size.height;
_aPath =[UIBezierPath bezierPathWithRect:rect] ;
[_aPath setLineWidth:1.0];
_aPoint.x=0.0;
_aPoint.y=0.0;
[_aPath moveToPoint:_aPoint];
redstr = [NSString localizedStringWithFormat:#"%6.2f", _red];
for (i=1;i<400;i++)
{
_aPoint.x=i*width/400.0;
_aPoint.y=height-height*(sin(i*_red/30.)+1.0)/2.0;
[_aPath addLineToPoint:_aPoint];
}
[_aPath stroke];
}
- (IBAction)red_rabi:(id)sender
{
NSString *redstr;
UISlider *slider = (UISlider *)sender;
_red= slider.value;
redstr = [NSString localizedStringWithFormat:#"%6.2f", _red];
_redout.text = redstr;
[self setNeedsDisplay];
}
#end
If you place a generic View just underneath the View Controller (which I didn't touch), change the generic View's class to GraphView, and place a slider and TextField inside the GraphView (connecting them to the outlet and action), this app will generate a few cycles of a sine wave with the frequency controlled by the slider and its value displayed in the TextField.
If you want the slider and TextField in another view, one must use an enveloping view for all three items (GraphView, slider, text) and one cannot connect the slider and TextField to the GraphView using Ctrl_drag to the GraphView.h file. To remedy this, I placed a generic Object at the highest level and renamed it GraphView - I could then connect the slider and TextField. Although the Textfield reads correctly, the slider doesn't update the GraphView.
By the way, essentially the same code with the GraphView and slider in separate views works perfectly in OS X.
Sorry for the length of this query and thanks!

Problem solved! Due to an interesting SO post from three years ago (about connecting to subviews of UIView), I discovered that one merely drags (not Ctrl_drag!) from the action or outlet circle (in the .h file) to the control and that's it. Works perfectly even when the controls are in a different view from the subclassed UIView. Works equally well with outlets as with actions though you always drag away from the circle.
Thanks to all for their help.

Related

iOS - Resize multiple views with touch-drag separators

How can I resize views with a separator? What I'm trying to do is something like Instagram layout app. I want to be able to resize views by dragging the line that separates the views.
I already looked into this question. It is similar to what I want to accomplish and I already tried the answers but it does not work if there are more than 2 views connected to a separator (if there are 3 or more view only 2 views resize when separator moves each time). I tried to change the code but I have no idea what to do or what the code means.
In my app I will have 2-6 views. The separator should resize all the views that is next to it.
Some examples of my views:
How can I accomplish this? Where do I start?
There are lots of ways to accomplish this, but like Avinash, I'd suggest creating a "separator view" in between the various "content" UIView objects. Then you can drag that around. The trick here, though, is that you likely want the separator view to be bigger than just the narrow visible line, so that it will capture touches not only right on the separator line, but close to it, too.
Unlike that other answer you reference, nowadays I'd new recommend using autolayout so that all you need to do with the user gestures is update the location of the separator view (e.g. update the top constraint of the separator view), and then all of the other views will be automatically resized for you. I'd also suggest adding a low priority constraint on the size of the subviews, so that they're laid out nicely when you first set everything up and before you start dragging separators around, but that it will fail gracefully when the dragged separator dictates that the size of the neighboring views must change.
Finally, while we'd historically use gesture recognizers for stuff like this, with the advent of predicted touches in iOS 9, I'd suggest just implementing touchesBegan, touchesMoved, etc. Using predicted touches, you won't notice the difference on the simulator or older devices, but when you run this on a device capable of predicted touches (e.g. new devices like the iPad Pro and other new devices), you'll get a more responsive UX.
So a horizontal separator view class might look like the following.
static CGFloat const kTotalHeight = 44; // the total height of the separator (including parts that are not visible
static CGFloat const kVisibleHeight = 2; // the height of the visible portion of the separator
static CGFloat const kMargin = (kTotalHeight - kVisibleHeight) / 2.0; // the height of the non-visible portions of the separator (i.e. above and below the visible portion)
static CGFloat const kMinHeight = 10; // the minimum height allowed for views above and below the separator
/** Horizontal separator view
#note This renders a separator view, but the view is larger than the visible separator
line that you see on the device so that it can receive touches when the user starts
touching very near the visible separator. You always want to allow some margin when
trying to touch something very narrow, such as a separator line.
*/
#interface HorizontalSeparatorView : UIView
#property (nonatomic, strong) NSLayoutConstraint *topConstraint; // the constraint that dictates the vertical position of the separator
#property (nonatomic, weak) UIView *firstView; // the view above the separator
#property (nonatomic, weak) UIView *secondView; // the view below the separator
// some properties used for handling the touches
#property (nonatomic) CGFloat oldY; // the position of the separator before the gesture started
#property (nonatomic) CGPoint firstTouch; // the position where the drag gesture started
#end
#implementation HorizontalSeparatorView
#pragma mark - Configuration
/** Add a separator between views
This creates the separator view; adds it to the view hierarchy; adds the constraint for height;
adds the constraints for leading/trailing with respect to its superview; and adds the constraints
the relation to the views above and below
#param firstView The UIView above the separator
#param secondView The UIView below the separator
#returns The separator UIView
*/
+ (instancetype)addSeparatorBetweenView:(UIView *)firstView secondView:(UIView *)secondView {
HorizontalSeparatorView *separator = [[self alloc] init];
[firstView.superview addSubview:separator];
separator.firstView = firstView;
separator.secondView = secondView;
[NSLayoutConstraint activateConstraints:#[
[separator.heightAnchor constraintEqualToConstant:kTotalHeight],
[separator.superview.leadingAnchor constraintEqualToAnchor:separator.leadingAnchor],
[separator.superview.trailingAnchor constraintEqualToAnchor:separator.trailingAnchor],
[firstView.bottomAnchor constraintEqualToAnchor:separator.topAnchor constant:kMargin],
[secondView.topAnchor constraintEqualToAnchor:separator.bottomAnchor constant:-kMargin],
]];
separator.topConstraint = [separator.topAnchor constraintEqualToAnchor:separator.superview.topAnchor constant:0]; // it doesn't matter what the constant is, because it hasn't been enabled
return separator;
}
- (instancetype)init {
self = [super init];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = false;
self.userInteractionEnabled = true;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
#pragma mark - Handle Touches
// When it first receives touches, save (a) where the view currently is; and (b) where the touch started
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.oldY = self.frame.origin.y;
self.firstTouch = [[touches anyObject] locationInView:self.superview];
self.topConstraint.constant = self.oldY;
self.topConstraint.active = true;
}
// When user drags finger, figure out what the new top constraint should be
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
// for more responsive UX, use predicted touches, if possible
if ([UIEvent instancesRespondToSelector:#selector(predictedTouchesForTouch:)]) {
UITouch *predictedTouch = [[event predictedTouchesForTouch:touch] lastObject];
if (predictedTouch) {
[self updateTopConstraintOnBasisOfTouch:predictedTouch];
return;
}
}
// if no predicted touch found, just use the touch provided
[self updateTopConstraintOnBasisOfTouch:touch];
}
// When touches are done, reset constraint on the basis of the final touch,
// (backing out any adjustment previously done with predicted touches, if any).
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self updateTopConstraintOnBasisOfTouch:[touches anyObject]];
}
/** Update top constraint of the separator view on the basis of a touch.
This updates the top constraint of the horizontal separator (which moves the visible separator).
Please note that this uses properties populated in touchesBegan, notably the `oldY` (where the
separator was before the touches began) and `firstTouch` (where these touches began).
#param touch The touch that dictates to where the separator should be moved.
*/
- (void)updateTopConstraintOnBasisOfTouch:(UITouch *)touch {
// calculate where separator should be moved to
CGFloat y = self.oldY + [touch locationInView:self.superview].y - self.firstTouch.y;
// make sure the views above and below are not too small
y = MAX(y, self.firstView.frame.origin.y + kMinHeight - kMargin);
y = MIN(y, self.secondView.frame.origin.y + self.secondView.frame.size.height - (kMargin + kMinHeight));
// set constraint
self.topConstraint.constant = y;
}
#pragma mark - Drawing
- (void)drawRect:(CGRect)rect {
CGRect separatorRect = CGRectMake(0, kMargin, self.bounds.size.width, kVisibleHeight);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:separatorRect];
[[UIColor blackColor] set];
[path stroke];
[path fill];
}
#end
A vertical separator would probably look very similar, but I'll leave that exercise for you.
Anyway, you could use it like so:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *previousContentView = nil;
for (NSInteger i = 0; i < 4; i++) {
UIView *contentView = [self addRandomColoredView];
[self.view.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor].active = true;
[self.view.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor].active = true;
if (previousContentView) {
[HorizontalSeparatorView addSeparatorBetweenView:previousContentView secondView:contentView];
NSLayoutConstraint *height = [contentView.heightAnchor constraintEqualToAnchor:previousContentView.heightAnchor];
height.priority = 250;
height.active = true;
} else {
[self.view.topAnchor constraintEqualToAnchor:contentView.topAnchor].active = true;
}
previousContentView = contentView;
}
[self.view.bottomAnchor constraintEqualToAnchor:previousContentView.bottomAnchor].active = true;
}
- (UIView *)addRandomColoredView {
UIView *someView = [[UIView alloc] init];
someView.translatesAutoresizingMaskIntoConstraints = false;
someView.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0];
[self.view addSubview:someView];
return someView;
}
#end
That yields something like:
As I mentioned, a vertical separator would look very similar. If you have complicated views with both vertical and horizontal separators, you'd probably want to have invisible container views to isolate the vertical and horizontal views. For example, consider one of your examples:
That would probably consist of two views that span the entire width of the device with a single horizontal separator, and then the top view would, itself, have two subviews with one vertical separator and the bottom view would have three subviews with two vertical separators.
There's a lot here, so before you try extrapolating the above example to handle (a) vertical separators; and then (b) the views-within-views pattern, make sure you really understand how the above example works. This isn't intended as a generalized solution, but rather just to illustrate a pattern you might adopt. But hopefully this illustrates the basic idea.
I've updated #JULIIncognito's Swift class to Swift 4, added a drag indicator and fixed some typos.
SeparatorView
Just import in into your project and use it like so:
SeparatorView.addSeparatorBetweenViews(separatorType: .horizontal, primaryView: view1, secondaryView: view2, parentView: self.view)
This is how it looks like (MapView on top, TableView on bottom):
Base on Rob`s solution I created Swift class for both horizontal and vertical separator view:
https://gist.github.com/JULI-ya/1a7c293b022207bb427caa3bbb9d3ed8
There are code only for two inner views with separator, because my idea is to put each to other for creating this custom layout. It will look like a binary tree structure of views.
Use UIPanGestureRecognizers. Add a recognizer to each view. In gestureRecognizerShouldBegin: method return YES if the location of gesture is very close to the edge (use gesture's locationInView:view method). Then in gesture's action method (specified in gesture's initWithTarget: action:) you proccess your moves something like this:
-(void)viewPan:(UIPanGestureRecognizer *)sender
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
//determine the second view based on gesture's locationInView:
//for instance if close to bottom, the second view is the one under the current.
}
case UIGestureRecognizerStateChanged: {
//change the frames of the current and the second view based on sender's translationInView:
}
...
}
As per my best knowledge we can do this using UIGestureRecognizer and auto layout.
1. Use UIView as line separator.
2. Add Pan gestureRecognizer to separator line view.
3. Handle view movement in delegate protocol methods using UIView.animatewithDuration()
PanGestureRecognizer
Most important, don't forget to set/Check UserInteration Enabled for all line separator view in Attribute Inspector.

UIDynamics and Autolayout

Recently I used UIDynamics to animate an image view into place. However, because its autolayout y-pos constraint was set to off-screen, when navigating away from the screen and then returning to it, my image view was being placed off-screen again. The animation took about 3 seconds, so after three seconds I just reset the constraint. That feels a little hacky.
So my question is this: what is the proper way to handle autolayout and UIDynamics at the same time?
This is not really a dynamics problem. Autolayout is incompatible with any view animation, or any manual setting of the frame: when layout comes along, it is the constraints that will be obeyed. It is up to you, if you move a view manually in any way, to update the constraints to match its new position/size/whatever.
Having said that: with UIKit Dynamics, when the animation ends, the animator will pause, and the animator's delegate is notified:
https://developer.apple.com/library/ios/documentation/uikit/reference/UIDynamicAnimatorDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIDynamicAnimatorDelegate/dynamicAnimatorDidPause:
So that is the moment to update the constraints.
You have a nice solution provided by Geppy Parziale in this tutorial.
Basically you can create an object that conforms to UIDynamicItem:
#interface DynamicHub : NSObject <UIDynamicItem>
#property(nonatomic, readonly) CGRect bounds;
#property(nonatomic, readwrite) CGPoint center;
#property(nonatomic, readwrite) CGAffineTransform transform;
#end
That needs to init the bounds or it will crash:
- (id)init {
self = [super init];
if (self) {
_bounds = CGRectMake(0, 0, 100, 100);
}
return self;
}
And then you use UIDynamics on that object and use the intermediate values to update your constraints:
DynamicHub *dynamicHub = [[DynamicHub alloc] init];
UISnapBehavior *snapBehavior = [[UISnapBehavior alloc] initWithItem:dynamicHub
snapToPoint:CGPointMake(50.0, 150.0)];
[snapBehavior setDamping:.1];
snapBehavior.action = ^{
self.firstConstraint.constant = [dynamicHub center].y;
self.secondConstraint.constant = [dynamicHub center].x;
};
[self.animator addBehavior:snapBehavior];

Positioning a button using CALayer not working

I added an image button into the interface builder and I want to position it using CGPointMake so I added the button IBOutlet into the .h file and then added a CALayer in the .m file, here is my code...
in .h file
// This is the .h file
#interface ViewController : UIViewController
{
IBOutlet UIButton *thebtn;
}
in .m file
// This is the .m file
- (void)viewDidLoad
{
CALayer *btn = thebtn.layer;
btn.position = CGPointMake(480, 150);
btn.opacity = 0.4f;
[self.view.layer addSublayer:btn];
}
So that is great but my issue that the button position is not changing but the button opacity was changed so how can I fix that issue?
Thanks in Advance
Try to do the following:
First, move your code to viewDidAppear. Get rid of the layer code. Also, add setNeedsLayout call. Here is an example:
-(void)viewDidAppear
{
thebtn.center = CGPointMake(480, 150);
thebtn.opacity = 0.4f;
[self setNeedsLayout];
}
UPDATE
I figured out what the problem was. You have to turn off "Auto-Layout". After that your view will be repositioned to where you want.
Please see this link to see how to disable Auto Layout
Hope this helps!
I think for such purposes as changing position and opacity of a view, you'd better operate with a UIView object (UIButton in your case), instead of its CALAyer property. This should work:
- (void)viewDidLoad
{
thebtn.center = CGPointMake(x, y);
thebtn.alpha = 0.4f;
}

Vertical Parallax Scrolling Of UIView in iOS

I'm trying to add a vertical parallax scrolling effect between several views that are all the size of the screen.
This wasn't too complicated when done through buttons, however I want it to be controlled by a UIPanGesture, similar to the Year Walk Companion App for iOS. The current equation shown here doesn't work correctly.
Currently this version only moves the first two views, because once I get those working the rest should be easy. I've also not included all the checks that would stop certain views moving in certain directions because that is also not relevant to the issue.
Here is a web version of it working with buttons: http://flycarpodacus.zxq.net/iPhone%20Parallax/event.html
Hope someone can help.
ViewController.h
#interface ViewController : UIViewController {
UIPanGestureRecognizer *panGesture;
int activeView;
int nextView;
float offset;
float speed;
}
#property (weak, nonatomic) IBOutlet UIView *view1;
#property (weak, nonatomic) IBOutlet UIView *view2;
#property (weak, nonatomic) IBOutlet UIView *view3;
#property (weak, nonatomic) IBOutlet UIView *view4;
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize view1, view2, view3, view4;
- (void)viewDidLoad
{
[super viewDidLoad];
activeView = 1;
offset = 220;
speed = 1;
panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[self.view addGestureRecognizer:panGesture];
if (self.view.frame.size.height != 568) {
CGRect frame = self.view.frame;
frame.size.height = self.view.frame.size.height;
view1.frame = frame;
view2.frame = frame;
view3.frame = frame;
view4.frame = frame;
}
view4.center = CGPointMake(view4.center.x, self.view.frame.size.height/2 + offset);
view3.center = CGPointMake(view3.center.x, self.view.frame.size.height/2 + offset);
view2.center = CGPointMake(view2.center.x, self.view.frame.size.height/2 + offset);
view1.center = CGPointMake(view1.center.x, self.view.frame.size.height/2);
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint panGestureTranslation = [panGestureRecognizer translationInView:self.view];
switch (activeView) {
case 1:
view1.center = CGPointMake(view1.center.x, panGestureTranslation.y + self.view.frame.size.height/2);
view2.center = CGPointMake(view2.center.x, view2.center.y - 2.5f);
break;
default:
break;
}
}
Ok so, in my point of view you could try to do something like that :
first use only two view, in your example only two view is visible at the same time and so you could re-use the view for
the next slide you want to display.
So at the start you have 2 views (lets say they have the same size than the screen 300x500, yes I know they are not the real size,
but it will be more easy for the math :p).
In you init function place you view one in position 150x250 (the center of the screen), and the screen 2 at 150x500 (so you see only
the upper half of your view).
lets call panGestureTranslation.y : DeltaY (it will be more quick). so in the function panGestureRecognizer you want to move the two view
with the parralex effect, one simple solution in this exact case will be to move view1 of 2*DeltaY and view2 of DeltaY, like that, when the view1
will have quit the screen completly, the view two will be right in the center of it.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
you can know if is finish by test the position of view one (if the position.y is <= than 0, so it's finish.
when is finish, you have two choice, first you put the view1 in the bottom of view2 (at position 150x500 and display behind view2 ) and you feel it with your 3rd content, then switch a booleen
to do the excact opposite the next frame (you will move view2 of 2*DeltaY and view1 of DeltaY).
other solution is to move the two view at the original position (view1 at 150x250 and view two at 150x500), and feel view1 with the content of view2 (you have to do it without animation
so the user doesn't see that the view have switch), and feel view2 with you 3rd content, and so next frame, the exact same code will work again. you can have an infinity of content with this method.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
let me know if I wasn't clear on some point ^^.
I would suggest checking our article on parallax effect with detailed explanation and open source implementation => http://blog.denivip.ru/index.php/2013/08/parallax-in-ios-applications/?lang=en

UIView in UIScrollview. frame.origin not changing on scrolling, can't detect collision

I have a screen containing:
UIView A
UIScrollView, which contains UIView B
The UIScrollView overlaps with UIView A (up until about halfway through it), allowing me to "drag" UIView B and make it overlap it with UIView A. This is so that when I let go of it, UIView B bounces nicely back into its original position (I don't want to be able to drag it all around the screen, just from its starting point onto UIView A)
Now, I'm trying to detect a collision when the UIViews overlap. However, none is ever detected. I am using convertRect:toView: to get coordinates on the same system, but from tracing UIView B's coordinates in its touchesMoved I see that "dragging" (scrolling the UIScrollView, really) doesn't affect its frame's origin coordinates. They are the same in touchedBegan as they are every time touchedMoved fires.
I'm assuming this is because UIView B isn't really moving anywhere, UIScrollView is, meaning that the UIView's origin point is always the same, even if its absolute coordinates aren't. Any suggestion on how to handle this?
There will be overlap when scrollView.frame.origin.x + viewB.frame.size.width + scrollView.contentOffset.x >= viewA.frame.origin.x
After Edit:
I couldn't get the scroll view to work so I tried it another way that worked pretty well. Instead of a scroll view, I just used a rectangular view with a background color, and added the blue square to it. I gave the blue square a set width and height in IB, centered it in the long skinny view (in the y direction), and had one other constraint to the left side of that long view. The IBOutlet leftCon is connected to that constraint. I then used this code to drag it, and have it go back when I let go:
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (strong,nonatomic) UIPanGestureRecognizer *panner;
#property (strong,nonatomic) IBOutlet NSLayoutConstraint *leftCon;
#property (strong,nonatomic) CADisplayLink *displayLink;
#end
#implementation ViewController {
IBOutlet UIView *blueSquare;
}
-(void)viewDidLoad {
self.panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[blueSquare addGestureRecognizer:self.panner];
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender {
CGPoint translate = [sender translationInView:self.view];
if (self.leftCon.constant <160)
if (self.leftCon.constant > 100) NSLog(#"Bang! We have a collision");
self.leftCon.constant = translate.x;
if (sender.state == UIGestureRecognizerStateEnded){
[self startDisplayLink];
}
}
-(void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(bounceBack:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
-(void)bounceBack:(CADisplayLink *) link {
self.leftCon.constant -= 12;
if (self.leftCon.constant < 2) {
[self stopDisplayLink];
self.leftCon.constant = 2;
}
}
The numbers in the translate code (160 and 100) were determined empirically by logging. The 160 number keeps it from going off the right edge of the long view, and the 100 number is where it starts to overlap the black box.

Resources