0
votes

I'm trying to create a subclass of UIView in order to let expand the view with a pan over it. It should work this way: if the user makes a pan toward the top the view's height decrease, instead if the pan is toward the bottom it should increase. In order to achieve that functionality, I'm trying to add a UIPanGestureRecognizer to the view but it doesn't seem to work. I've done it this way:

The first snippet is the uiView subclass declaration

class ExpandibleView: UIView {
    //Here I create the reference to the recognizer
    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))

    //And here I set its minimumNumberOfTouches and maximumNumberOfTouches properties and add it to the view
    func initialize() {
        panGestureRecognizer.minimumNumberOfTouches = 1
        panGestureRecognizer.maximumNumberOfTouches = 1
        self.addGestureRecognizer(panGestureRecognizer)
    }

    //here's the function that should handle the pan but who instead doesn't seem to been called at all
    @objc func handlePan(_ sender:UIPanGestureRecognizer) {
        //Here's I handle the Pan
    }
}

The second one instead is the implementation inside the View Controller.

class MapViewController: UIViewController {
    @IBOutlet weak var profileView: ExpandibleView!

    //MARK: - ViewController Delegate Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        //Here I set the View
        profileView.isUserInteractionEnabled = true
        profileView.initialize()
        profileView.minHeight = 100
        profileView.maxHeight = 190
    }

}

I set inside the storyboard the view's class as the subclass I created but the recognizer doesn't trigger at all the handler.

2

2 Answers

2
votes

The problem you're experiencing is due to your definition of the panGestureRecognizer variable in the class definition here:

//Here I create the reference to the recognizer
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))

You can initialize it this way, but it seems like self is not setup when this variable is created. So your action is never registered.

There are a couple ways you can fix this using your code, you can continue to initialize it as an instance variable, but you'll need to setup your target/action in your initialize() function

let panGestureRecognizer = UIPanGestureRecognizer()

func initialize() {
    // add your target/action here
    panGestureRecognizer.addTarget(self, action: #selector(handlePan(_:)))
    panGestureRecognizer.minimumNumberOfTouches = 1
    panGestureRecognizer.maximumNumberOfTouches = 1
    addGestureRecognizer(panGestureRecognizer)
}

Or you can simply initialize your gesture recognizer in your initialize function and not use an instance variable

func initialize() {
    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
    panGestureRecognizer.minimumNumberOfTouches = 1
    panGestureRecognizer.maximumNumberOfTouches = 1
    addGestureRecognizer(panGestureRecognizer)
}

My original answer

Here's a solution that works using constraints with a view defined in the storyboard or by manipulating the frame directly.

Example with Constraints

import UIKit

// Delegate protocol for managing constraint updates if needed
protocol MorphDelegate: class {

    // tells the delegate to change its size constraints
    func morph(x: CGFloat, y: CGFloat)
}

class ExpandableView: UIView {

    var delegate: MorphDelegate?

    init() {
        // frame is set later if needed by creator when using this init method
        super.init(frame: CGRect.zero)
        configureGestureRecognizers()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configureGestureRecognizers()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        configureGestureRecognizers()
    }

    // setup UIPanGestureRecognizer
    internal func configureGestureRecognizers() {
        let panGR = UIPanGestureRecognizer.init(target: self, action: #selector(didPan(_:)))
        addGestureRecognizer(panGR)
    }

    @objc func didPan(_ panGR: UIPanGestureRecognizer) {

        // get the translation
        let translation = panGR.translation(in: self).applying(transform)

        if let delegate = delegate {

            // tell delegate to change the constraints using this translation
            delegate.morph(x: translation.x, y: translation.y)

        } else {

            // If you want the view to expand/contract in opposite direction
            // of drag then swap the + and - in the 2 lines below
            let newOriginY = frame.origin.y + translation.y
            let newHeight = frame.size.height - translation.y

            // expand self via frame manipulation
            let newFrame = CGRect(x: frame.origin.x, y: newOriginY, width: frame.size.width, height: newHeight)
            frame = newFrame
        }

        // reset translation
        panGR.setTranslation(CGPoint.zero, in: self)
    }

}

If you want to use this class by defining it in the storyboard and manipulating it's constraints you'd go about it like this.

First define your view in the storyboard and constrain it's width and height. For the demo I constrained it's x position to the view center and it's bottom to the SafeArea.bottom

Create an IBOutlet for the view, as well as it's height constraint to your ViewController file.

I set the background color to blue for this example.

storyboard setup

In the view controller I defined a function to setup the view (currently only sets the delegate for constraint manipulation callbacks) and then defined an extension to handle delegate calls for updating constraints.

class ViewController: UIViewController {

    @IBOutlet weak var expandingView: ExpandableView!
    @IBOutlet weak var constraint_expViewHeight: NSLayoutConstraint!


    override func viewDidLoad() {
        super.viewDidLoad()

        // call to configure our expanding view
        configureExpandingView()
    }

    // this sets the delegate for an expanding view defined in the storyboard
    func configureExpandingView() {
        expandingView.delegate = self
    }
}

// setup the delegate callback to handle constraint manipulation
extension ViewController: MorphDelegate {

    func morph(x: CGFloat, y: CGFloat) {

        // this will update the view's height based on the amount
        // you drag your finger in the view. You can '+=' below if
        // you want to reverse the expanding behavior based on pan
        // movements.
        constraint_expViewHeight.constant -= y
    }
}

Doing it this way via constraints gives me this when I run the project:

animation with constraints

Example with frame manipulation

To use this and manipulate the height using just the frame property the view controller might implement the view creation something like this:

override func viewDidLoad() {
    super.viewDidLoad()

    setupExpandingView()
}

func setupExpandingView() {

    let newView = ExpandableView()
    newView.backgroundColor = .red
    newView.frame = CGRect(x: 20, y: 200, width: 100, height: 100)
    view.addSubview(newView)
}

Using just frame manipulation I get this:

Demo Gif

0
votes

Try change this

class ExpandibleView: UIView {

    func initialize() {


    }

    //here's the function that should handle the pan but who instead doesn't seem to been called at all
     func handlePan() {
        //your function
    }
}

class MapViewController: UIViewController {
    @IBOutlet weak var profileView: ExpandibleView!

    //MARK: - ViewController Delegate Methods

    override func viewDidLoad() {
        super.viewDidLoad()
     let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
     panGestureRecognizer.minimumNumberOfTouches = 1
        panGestureRecognizer.maximumNumberOfTouches = 1
     profileView.addGestureRecognizer(panGestureRecognizer)
        //Here I set the View
        profileView.isUserInteractionEnabled = true
        profileView.initialize()
        profileView.minHeight = 100
        profileView.maxHeight = 190
    }

   @objc func handlePan(_ sender:UIPanGestureRecognizer) {
       profileView.handlePan()
    }
}

Or you can create a delegate for call in other view.

good luck!