My Uiviews Muck-Up When I Combine Uipangesturerecognizer and Autolayout

my UIViews muck-up when I combine UIPanGestureRecognizer and autolayout

The main error is that

theta = CGFloat(atan2(Double(finger.x), Double(finger.y)))   // get angle from finger tip to centre

does not take the views (or track) center into account, and that the arguments to atan2() are the wrong way around (y comes first). It should be:

theta = atan2(finger.y - track.center.y, finger.x - track.center.x)

Another problem is that you add more and more contraints
in func constrainBall(), without removing the previous ones.
You should keep references to the constraints and modify them instead.

Finally note that the width/height constraint for the ball should be 2*ballRadius, not trackRadius.

Putting it all together (and removing some unnecessary type
conversions), it would look like this:

var ballXconstraint: NSLayoutConstraint!
var ballYconstraint: NSLayoutConstraint!

override func viewDidLoad() {
super.viewDidLoad()
createTrack()
createBall()

let touch = UIPanGestureRecognizer(target: self, action:#selector(dragBall(recognizer:)))
view.addGestureRecognizer(touch)
}

private func createTrack() {
track.translatesAutoresizingMaskIntoConstraints = false
track.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 2 * trackRadius, height: 2 * trackRadius)).cgPath
track.shapeLayer.fillColor = UIColor.clear.cgColor
track.shapeLayer.strokeColor = UIColor.red.cgColor
view.addSubview(track)

track.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
track.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
track.widthAnchor.constraint(equalToConstant: 2 * trackRadius).isActive = true
track.heightAnchor.constraint(equalToConstant: 2 * trackRadius).isActive = true
}

private func createBall() {

// Create ball:
ball.translatesAutoresizingMaskIntoConstraints = false
ball.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 2 * ballRadius, height: 2 * ballRadius)).cgPath
ball.shapeLayer.fillColor = UIColor.cyan.cgColor
ball.shapeLayer.strokeColor = UIColor.black.cgColor
view.addSubview(ball)

// Width/Height contraints:
ball.widthAnchor.constraint(equalToConstant: 2 * ballRadius).isActive = true
ball.heightAnchor.constraint(equalToConstant: 2 * ballRadius).isActive = true

// X/Y constraints:
let offset = pointOnCircumference(0.0)
ballXconstraint = ball.centerXAnchor.constraint(equalTo: track.centerXAnchor, constant: offset.x)
ballYconstraint = ball.centerYAnchor.constraint(equalTo: track.centerYAnchor, constant: offset.y)
ballXconstraint.isActive = true
ballYconstraint.isActive = true
}

func dragBall(recognizer: UIPanGestureRecognizer) {

let finger = recognizer.location(in: self.view)

// Angle from track center to touch location:
theta = atan2(finger.y - track.center.y, finger.x - track.center.x)

// Update X/Y contraints of the ball:
let offset = pointOnCircumference(theta)
ballXconstraint.constant = offset.x
ballYconstraint.constant = offset.y
}

private func pointOnCircumference(_ theta: CGFloat) -> CGPoint {
let x = cos(theta) * trackRadius
let y = sin(theta) * trackRadius
return CGPoint(x: x, y: y)
}

if we're in the real pre-commit handler we can't actually add any new fences due to CA restriction - could this mean something here?

Don't worry about the fences warning message. It seems to be harmless and not caused by anything you're doing.

There are several problems with the code you posted:

  1. You create myView and constrain its center, but you don't give it any size constraints. Furthermore, myView is a local variable and you don't add any subviews to myView. So myView is an invisible, sizeless view with no contents. Why are you creating it at all?

  2. You're drawing your “uberCircle” using a bare shape layer. By “bare”, I mean there's no view whose layer property is that layer. Bare layers don't participate in autolayout.

  3. You compute the position of each button based on the center of the top-level view's bounds. You're doing this during viewDidLoad, but viewDidLoad is called before the top-level view has been resized to fit the current device. So your wheel won't even be centered at launch on some devices.

  4. You don't set any constraints on the buttons, or set their autoresizing masks. The result is that, when the device rotates, the top-level view resizes but each button's position (relative to the top-left corner of the top-level view) stays the same.

  5. Turning on the “Upside Down” checkbox is not sufficient to allow upside-down orientation on iPhones, only on iPads.

Here are the changes you need to make:

  1. Use a view to draw the “uberCircle”. If you want to use a shape layer, make a subclass of UIView that uses a CAShapeLayer for its layer. You can copy the ShapeView class from this answer.

  2. Set constraints from the center of the uberCircle to the center of the top-level view, to keep the uberCircle centered when the top-level view changes size.

  3. For each button, set constraints from the center of the button to the center of the top-level view, to keep the button positioned properly when the top-level view changes size. These constraints need non-zero constants to offset the buttons from the center.

  4. Override supportedInterfaceOrientations to enable upside-down orientation (in addition to checking the “Upside Down” checkbox).

  5. Get rid of myView in viewDidLoad. You don't need it.

  6. Get rid of the centre property. You don't need it.

Thus:

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
createUberCircle()
createButtons()
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .all }

private let radius: CGFloat = 96
private let buttonCount = 10
private let buttonSideLength: CGFloat = 60

private func createUberCircle() {
let circle = ShapeView()
circle.translatesAutoresizingMaskIntoConstraints = false
circle.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: -radius, y: -radius, width: 2*radius, height: 2*radius)).cgPath
circle.shapeLayer.fillColor = UIColor.cyan.cgColor
view.addSubview(circle)
circle.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
circle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}

private func createButtons() {
for i in 1 ... buttonCount {
createButton(number: i)
}
}

private func createButton(number: Int) {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .white
button.layer.cornerRadius = buttonSideLength / 2
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.black.cgColor
button.clipsToBounds = true
button.titleLabel!.font = UIFont.systemFont(ofSize: buttonSideLength / 2)
button.setTitleColor(.red, for: .normal)
button.setTitle(String(number), for: .normal)
view.addSubview(button)

let radians = 2 * CGFloat.pi * CGFloat(number) / CGFloat(buttonCount) - CGFloat.pi / 2
let xOffset = radius * cos(radians)
let yOffset = radius * sin(radians)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: xOffset),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yOffset),
button.widthAnchor.constraint(equalToConstant: buttonSideLength),
button.heightAnchor.constraint(equalToConstant: buttonSideLength)
])
}

}

class ShapeView: UIView {

override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }

lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()

}

Drag ball over circle path in Swift

This line is wrong:

    let angle = tan(angleY/angleX)

Since you want to calculate the angle from the coordinates you need
the "inverse tangent of two variables"

    let angle = atan2(angleY, angleX)

Drag and drop objects and return them to their original position

You know what, I was just working on this yesterday, so here you go with a little bonus animation:

- (void)previewImageTouchUpInside:(UIButton*)aButton {
if (!previewImageDragged) {
[self selectPreviewImage:aButton];
return;
}

previewImageDragged = NO;

// If user drag photo a little and then release, we'll still treat it as a tap
// instead of drag
if ((originalPositionOfButton.x - aButton.center.x) *
(originalPositionOfButton.x - aButton.center.x) +
(originalPositionOfButton.y - aButton.center.y) *
(originalPositionOfButton.y - aButton.center.y) < 10) {
aButton.center = originalPositionOfButton;
[self selectPreviewImage:aButton];
return;
}

[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionCurveEaseOut |
UIViewAnimationOptionAllowUserInteraction animations:^{
aButton.center = originalPositionOfButton;
} completion:nil];
}

- (void)previewImageDraggedInside:(UIButton*)aButton event:(UIEvent*)event {
if (!previewImageDragged) {
originalPositionOfButton = aButton.center;
[self.view bringSubviewToFront:aButton];
[self.view bringSubviewToFront:[self.view viewWithTag:CHOOSE_PHOTO_BUTTON_TAG]];
}

UITouch* touch = [[event allTouches] anyObject];

previewImageDragged = YES;
aButton.center = CGPointMake(aButton.center.x+[touch locationInView:self.view].x-
[touch previousLocationInView:self.view].x,
aButton.center.y+[touch locationInView:self.view].y-
[touch previousLocationInView:self.view].y);
}

It still has a few magic numbers and I haven't renamed the functions and variables to suit your example, but it should be clear. I also did the indentation here since I don't manually break long lines in my code.

Why doesn't this Javascript RGB to HSL code work?

The resulting HSV array has to be interpreted as three fractions. For some programs, if you want to express HSV as integers, you multiply the "H" value by 360 and the "S" and "V" values by 100. The HSV value you quote for your green shade RGB[126, 210, 22] is HSV [87, 81, 45] in integers. You could change the function to return such integers if you want to:

function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;

if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}

return [Math.floor(h * 360), Math.floor(s * 100), Math.floor(l * 100)];
}

[edit] that said, it's still giving me something with a brightness ("L" or "V") that's considerably too dark; Gimp says that the HSV value should be [90, 80, 82], or in fractional terms [.20, .80, .82].

[another edit] well one problem could be that HSL and HSV are different schemes ... still looking around.

OK in case anybody wants RGB to HSV (like you'd see in Gimp for example) here's a version of that:

function rgbToHsv(r, g, b) {
var
min = Math.min(r, g, b),
max = Math.max(r, g, b),
delta = max - min,
h, s, v = max;

v = Math.floor(max / 255 * 100);
if ( max != 0 )
s = Math.floor(delta / max * 100);
else {
// black
return [0, 0, 0];
}

if( r == max )
h = ( g - b ) / delta; // between yellow & magenta
else if( g == max )
h = 2 + ( b - r ) / delta; // between cyan & yellow
else
h = 4 + ( r - g ) / delta; // between magenta & cyan

h = Math.floor(h * 60); // degrees
if( h < 0 ) h += 360;

return [h, s, v];
}

edit note that a couple comments suggest that Math.round() might give better answers than Math.floor(), if anybody wants to experiment.



Related Topics



Leave a reply



Submit