I am trying to create a custom slider that uses a UIButton as the thumb. Since I can't seem to find a way to use a UIView as the thumb in a slider, I decided to build a custom one. However, when I run it, the thumb is sluggish to respond on the initial drag. The following is the code so far. The gif below is running slower than actual speed but illustrates the point.
import Foundation
import UIKit
class CustomSlider: UIView, UIGestureRecognizerDelegate {
let buttonView = UIButton()
var minimumPosition = CGPoint()
var maximumPosition = CGPoint()
var originalCenter = CGPoint()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
// add a pan recognizer
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(recognizer:)))
recognizer.delegate = self
addGestureRecognizer(recognizer)
backgroundColor = UIColor.green
buttonView.frame = CGRect(x: 0, y: 0, width: bounds.height, height: bounds.height)
buttonView.backgroundColor = UIColor.purple
self.addSubview(buttonView)
minimumPosition = CGPoint(x:buttonView.frame.width / 2, y: 0)
maximumPosition = CGPoint(x: bounds.width - buttonView.frame.width / 2, y: 0)
}
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
originalCenter = buttonView.center
}
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
let newX = originalCenter.x + translation.x
buttonView.center = CGPoint(x: min(maximumPosition.x, max(minimumPosition.x, newX)), y: buttonView.frame.midY)
}
}
}
To make it look smoother, you should reset the translation of the pan and just add it continuously to the center offset:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
// originalCenter = buttonView.center
}
if recognizer.state == .changed {
originalCenter = buttonView.center
let translation = recognizer.translation(in: self)
let newX = originalCenter.x + translation.x
buttonView.center = CGPoint(x: min(maximumPosition.x, max(minimumPosition.x, newX)), y: buttonView.frame.midY)
recognizer.setTranslation(.zero, in: self)
}
}
Also, for making a custom control, you can check out this tutorial:
https://www.sitepoint.com/wicked-ios-range-slider-part-one/
It might be old and in objective-c, but it gives the general idea of overriding UIControl, handling touches and creating a custom control - which you can also extend with #IBDesignable and #IBInspectable.
Hope it helps.
Finally figured it out. It only happens in the iPhone XR simulator. Tried the iPhone XS, among others, and they were smooth as glass.
Related
When using a drawing program or when using photoshop there is usually the ability to select a rectangle from the buttons panel. There you can press the button and afterwards be able to drag a rectangle on the screen depending on your chosen startpoint/endpoint.
Class
#IBDesignable class RectView: UIView {
#IBInspectable var startPoint:CGPoint = CGPoint.zero {
didSet{
self.setNeedsDisplay()
}
}
#IBInspectable var endPoint:CGPoint = CGPoint.zero {
didSet{
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
if (startPoint != nil && endPoint != nil){
let path:UIBezierPath = UIBezierPath(rect: CGRect(x: min(startPoint.x, endPoint.x),
y: min(startPoint.y, endPoint.y),
width: abs(startPoint.x - endPoint.x),
height: abs(startPoint.y - endPoint.y)))
UIColor.black.setStroke()
path.lineCapStyle = .round
path.lineWidth = 10
path.stroke()
}
}
}
Top ViewController
+Added class RectView to View(Custom Class)
class ViewController: UIViewController, UIGestureRecognizerDelegate {
let rectView = RectView()
#IBOutlet var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(panGestureMoveAround(sender:)))
tap.delegate = self
myView.addGestureRecognizer(tap)
ViewController
#objc func panGestureMoveAround(sender: UITapGestureRecognizer) {
var locationOfBeganTap: CGPoint
var locationOfEndTap: CGPoint
if sender.state == UIGestureRecognizer.State.began {
locationOfBeganTap = sender.location(in: rectView)
rectView.startPoint = locationOfBeganTap
rectView.endPoint = locationOfBeganTap
} else if sender.state == UIGestureRecognizer.State.ended {
locationOfEndTap = sender.location(in: rectView)
rectView.endPoint = sender.location(in: rectView)
} else{
rectView.endPoint = sender.location(in: rectView)
}
}
Code gives no particular errors however nothing is happening on screen. Any advice would be helpful.
You should try to focus on one issue at a time, however...
1) #IBDesignable and #IBInspectable are for use during design-time - that is, when laying out views in Storyboard / Interface Builder. That's not what you're trying to do here, so remove those.
2) CGrect() needs x, t, width and height:
let path:UIBezierPath = UIBezierPath(rect: CGRect(x: min(startPoint!.x, endPoint!.x),
y: min(startPoint!.y, endPoint!.y),
width: fabs(startPoint!.x - endPoint!.x),
height: fabs(startPoint!.y - endPoint!.y)))
3) Wrong way to instantiate your view:
// wrong
let rectView = RectView.self
// right
let rectView = RectView()
Correct those issue and see where you get. If you're still running into problems, first search for the specific issue. If you can't find the answer from searching, then come back and post a specific question.
Probably worth reviewing How to Ask
This is the code I have written so far to change the position of the view when dragged.
The view changes its center point when the UIPanGestureRecognizer is either changed or began and that happen when I let go the gesture. I do not want that. I want it to go along with my drag like what the Notification Center and Control Center does.
Thanks in advance for the help.
class ViewController: UIViewController {
let maxY = UIScreen.main.bounds.height
lazy var slideUpView: UIView = {
let view = UIView(frame: CGRect(x: 0, y: maxY - 295, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 3 ))
view.backgroundColor = .green
return view
}()
lazy var redImageView: UIView = {
let view = UIView(frame: CGRect(x: 0, y: maxY - 295, width: UIScreen.main.bounds.width, height: slideUpView.frame.height / 2 ))
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
print("MaxX: \(UIScreen.main.bounds.maxX)")
print("Max Hight: \(UIScreen.main.bounds.height)")
print("MaxY: \(UIScreen.main.bounds.maxY)")
print("Max width: \(UIScreen.main.bounds.width)")
// MARK: add the views as subview
let viewsArray = [slideUpView, redImageView]
viewsArray.forEach{view.addSubview($0)}
// Add a UIPanGestureRecognizer to it
viewsArray.forEach {createPanGestureRecognizer(targetView: $0)}
}
// The Pan Gesture
func createPanGestureRecognizer(targetView: UIView) {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(recognizer:)))
panGesture.maximumNumberOfTouches = 1
targetView.addGestureRecognizer(panGesture)
}
#objc func handlePanGesture(recognizer: UIPanGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.began || recognizer.state == UIGestureRecognizerState.changed {
let translation = recognizer.translation(in: self.view)
print(recognizer.view!.center.y)
let newPos = CGPoint(x:recognizer.view!.center.x + translation.x, y: recognizer.view!.center.y + translation.y)
if insideDraggableArea(newPos) {
guard let targetedView = recognizer.view else {
print("Error: No View to handle")
return
}
targetedView.center.y = newPos.y
recognizer.setTranslation(.zero, in: targetedView)
}
}
}
private func insideDraggableArea(_ point : CGPoint) -> Bool {
return // point.x > 50 && point.x < 200 &&
point.y > (maxY * 0.27) && point.y <= maxY
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: view)
print(position)
}
}
}
I also had this issue, at least in a playgrounds prototype...
I put all of the translation (that you have in the changed state) inside of a UIViewProperty animator that I had defined within the handlePanGesture function and then called .startAnimation() within the began/changed state.
It takes a bit of tweaking on the animation but its a lot smoother for me..
This code works fine. The issues was in the simulator I used yesterday. Everything works as I wanted it to be when tested in an iPhone.
I want to be able to drag the objects on the screen, but they wont. I tried everything but still cant.
Here are the code.
func panGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
print("Began.")
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.removeItem(forms[i])
}
}
case .changed:
let translation = gesture.translation(in: forms[1])
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: self.view)
print("\(gesture.view!.center.x)=\(gesture.view!.center.y)")
print("t;: \(translation)")
case .ended:
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.addItem(forms[i])
}
}
print("Ended.")
case .cancelled:
print("Cancelled")
default:
print("Default")
}
}
Also they have gravity. The forms are squares and circles.
Explanation:
in .began - i disable the gravity for selected form.
in .changed - i try to change the coordinates.
in .end - i enable again gravity.
ScreenShot.
Step 1 : Take one View which you want to drag in storyBoard.
#IBOutlet weak var viewDrag: UIView!
Step 2 : Add PanGesture.
var panGesture = UIPanGestureRecognizer()
Step 3 : In ViewDidLoad adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
panGesture = UIPanGestureRecognizer(target: self, action: #selector(ViewController.draggedView(_:)))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 4 : Code for draggedView.
func draggedView(_ sender:UIPanGestureRecognizer){
self.view.bringSubview(toFront: viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Step 5 : Output.
It's very easy if you subclass a view:
DraggableView...
class DraggableView: UIIView {
var fromleft: NSLayoutConstraint!
var fromtop: NSLayoutConstraint!
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
fromleft = constraint(id: "fromleft")!
fromtop = constraint(id: "fromtop")!
}
}
override func common() {
super.common()
let p = UIPanGestureRecognizer(
target: self, action: #selector(drag))
addGestureRecognizer(p)
}
#objc func drag(_ s:UIPanGestureRecognizer) {
let t = s.translation(in: self.superview)
fromleft.constant = fromleft.constant + t.x
fromtop.constant = fromtop.constant + t.y
s.setTranslation(CGPoint.zero, in: self.superview)
}
}
Drop a UIView in your scene.
As normal, add a constraint from the left (that's the x position) and add a constraint from the top (that's the y position).
In storyboard simply simply name the constraints "fromleft" and "fromtop"
You're done.
It now works perfectly - that's it.
What is that handy constraint( call ?
Notice the view simply finds its own constraints by name.
In Xcode there is STILL no way to use constraints like IBOutlets. Fortunately it is very easy to find them by "identifier". (Indeed, this is the very purpose of the .identifier feature on constraints.)
extension UIView {
func constraint(id: String) -> NSLayoutConstraint? {
let cc = self.allConstraints()
for c in cc { if c.identifier == id { return c } }
//print("someone forgot to label constraint \(id)") //heh!
return nil
}
func allConstraints() -> [NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { c in
return c.firstItem as? UIView == self ||
c.secondItem as? UIView == self
}
}
Tip...edge versus center!
Don't forget when you make the constraints on a view (as in the image above):
you can set the left one to be either:
to the left edge of the white box, or,
to the center of the white box.
Choose the correct one for your situation. It will make it much easier to do calculations, set sliders, etc.
Footnote - an "initializing" UIView, UI "I" View,
// UI "I" View ... "I" for initializing
// Simply saves you typing inits everywhere
import UIKit
class UIIView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
common()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
common()
}
func common() { }
}
Use below code for Swift 5.0
Step 1 : Take one UIView from Storyboard, drag it into your ViewController file and Create IBOutlet of UIView.
#IBOutlet weak var viewDrag: UIView!
var panGesture = UIPanGestureRecognizer()
Step 2 : In viewDidLoad() adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action:(Selector(("draggedView:"))))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 3 : Create func and add code to move the UIView as like below.
func draggedView(sender:UIPanGestureRecognizer){
self.view.bringSubviewToFront(viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Hope this will help someone.
This UIView extension makes a UIView object draggable and limits the movement to stay within the bounds of the screen.
extension UIView {
func makeDraggable() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.addGestureRecognizer(panGesture)
}
#objc func handlePan(_ gesture: UIPanGestureRecognizer) {
guard gesture.view != nil else { return }
let translation = gesture.translation(in: gesture.view?.superview)
var newX = gesture.view!.center.x + translation.x
var newY = gesture.view!.center.y + translation.y
let halfWidth = gesture.view!.bounds.width / 2.0
let halfHeight = gesture.view!.bounds.height / 2.0
// Limit the movement to stay within the bounds of the screen
newX = max(halfWidth, newX)
newX = min(UIScreen.main.bounds.width - halfWidth, newX)
newY = max(halfHeight, newY)
newY = min(UIScreen.main.bounds.height - halfHeight, newY)
gesture.view?.center = CGPoint(x: newX, y: newY)
gesture.setTranslation(CGPoint.zero, in: gesture.view?.superview)
}
}
I have a custom DraggableView that subclasses UIImageView. I take a photo with the camera, add the resulting UIImage to a DraggableView and then I can happily drag it around the screen, as intended.
Now, if the original photo was taken in landscape, I rotate it using:
if (image?.size.width > image?.size.height)
{
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
}
When I apply this transformation, the drag behaviour still works, but the directions are all wrong - if I drag left, the image moves up, not left. If I drag up, the image moves right, not up.
How do I fix this? I guess it is something to do with the UIPanGestureRecognizer being bound to the non-transformed view?
Edit: Current UIPanGestureRecognizer handler:
func onPhotoDrag(recognizer: UIPanGestureRecognizer?)
{
let translation = recognizer!.translationInView(recognizer?.view)
recognizer!.view!.center = CGPointMake(recognizer!.view!.center.x
+ translation.x, recognizer!.view!.center.y + translation.y);
recognizer?.setTranslation(CGPointZero, inView: recognizer?.view)
if (recognizer!.state == UIGestureRecognizerState.Ended)
{
let velocity = recognizer!.velocityInView(recognizer?.view)
let magnitude = sqrt((velocity.x * velocity.x)
+ (velocity.y * velocity.y))
let slideMult = magnitude / 300;
let slideFactor = 0.1 * slideMult;
let finalPoint = CGPointMake(recognizer!.view!.center.x
+ (velocity.x * slideFactor),
recognizer!.view!.center.y + (velocity.y * slideFactor));
// Animate the drag, and allow the drag delegate to do its work
DraggableView.animateWithDuration(Double(slideFactor),
delay: 0, options: UIViewAnimationOptions.CurveEaseOut,
animations: { recognizer?.view?.center = finalPoint },
completion: {(_) -> Void in self.dragDelegate?.onDragEnd(self)})
} // if: gesture ended
}
Update:
Thanks for posting your code. I pasted your code into my DraggableImageView and reproduced your problem. Although my version was handling the rotated view (without the animation), yours was going sideways.
The difference is that my code asks for the translationInView in the superview of the draggable view. You need to ask for the translationInView and velocityInView in the superview of your draggable view.
Change this line:
let translation = recognizer!.translationInView(recognizer?.view)
to:
let translation = recognizer!.translationInView(recognizer?.view?.superview)
and change this:
let velocity = recognizer!.velocityInView(recognizer?.view)
to:
let velocity = recognizer!.velocityInView(recognizer?.view?.superview)
and all will be happy.
Previous Answer:
Try this version:
class DraggableImageView: UIImageView {
override var image: UIImage? {
didSet {
if (image?.size.width > image?.size.height)
{
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
self.userInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.addTarget(self, action: #selector(draggedView(_:)))
self.addGestureRecognizer(panGestureRecognizer)
}
func moveByDeltaX(deltaX: CGFloat, deltaY: CGFloat) {
self.center.x += deltaX
self.center.y += deltaY
}
func draggedView(sender:UIPanGestureRecognizer) {
if let dragView = sender.view as? DraggableImageView, superview = dragView.superview {
superview.bringSubviewToFront(dragView)
let translation = sender.translationInView(superview)
sender.setTranslation(CGPointZero, inView: superview)
dragView.moveByDeltaX(translation.x, deltaY: translation.y)
}
}
}
Use example:
override func viewDidLoad() {
super.viewDidLoad()
let dragView = DraggableImageView(frame: CGRect(x: 50, y: 50, width: 96, height: 128))
dragView.image = UIImage(named: "landscapeImage.png")
self.view.addSubview(dragView)
}
Im trying save the position of a UIView subclass that is moved via a pan gesture recogniser and needs to stay in that position
The UIView Subclass MoveSection
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
var panGesture = UIPanGestureRecognizer(target: self, action:Selector("reposition:"))
self.addGestureRecognizer(panGesture)
let yPosition = self.frame.origin.y
}
func reposition(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self)
if let view = recognizer.view { view.center = CGPoint(x:view.center.x + translation.x, y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: self)
if recognizer.state == UIGestureRecognizerState.Ended {
//Center view
self.center.x = self.bounds.width/2
}
}
Save position in class
class ViewController
#IBOutlet var reorderSectionOne: MoveSection!
let positionDefaults = NSUserDefaults()
positionDefaults.setObject(NSStringFromCGPoint(self.reorderSectionOne.yPosition), forKey: "pointPositionSectionOne")
positionDefaults.synchronize()
Error: Does not have member named yPosition
You need to move the declaration of yPosition from inside init(coder:) to just inside your UIView subclass. Also, yPosition is a CGFloat, not a CGPoint. So I used a different instance variable, called savedCenter, to save the position as an actual CGPoint. I've refactored your code and implemented the changes below.
class MoveableView: UIView {
var savedYPosition: CGFloat!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let panRecognizer = UIPanGestureRecognizer(target: self, action:"reposition:")
self.addGestureRecognizer(panRecognizer)
self.savedYPosition = CGRectGetMinY(self.frame)
}
func reposition(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self)
if let view = recognizer.view {
view.center = CGPointMake(view.center.x + translation.x, view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: self)
if recognizer.state == .Ended {
// center view
self.center.x = CGRectGetWidth(self.bounds) / 2.0
}
}
}
And then you would put the following in your ViewController class
func savePosition() {
let defaults = NSUserDefaults()
defaults.setFloat(Float(self.reorderSectionOne.savedYPosition), forKey: "yPositionSectionOne")
defaults.synchronize()
}
Note that I assumed your UIView subclass was named MoveableView.