4
\$\begingroup\$

I want to know what do you think about the structure of this code. If you think that there is some structural improvement to be made, tell me!

Here is a github link: https://github.com/LucianoPolit/LPDropdownMenu

Here is the code:

//
//  LPDropdownMenu.swift
//  Created by Luciano Polit on 1/8/16.
//  Copyright (c) 2016 Luciano Polit. All rights reserved.
//



import UIKit



@objc protocol LPDropdownMenuViewDelegate {

    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, shouldSwipeToPosition position: CGFloat) -> Bool
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, shouldTouchToPosition position: CGFloat) -> Bool
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, shouldDoubleTouchToPosition position: CGFloat) -> Bool
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, shouldAproximateToPosition position: CGFloat) -> Bool
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, shouldFinishSlidingAtPosition position: CGFloat) -> Bool

    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, willSwipeToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, willTouchToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, willDoubleTouchToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, willAproximateToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, willFinishSlidingAtPosition position: CGFloat)

    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didSwipeToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didTouchToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didDoubleTouchToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didAproximateToPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didFinishSlidingAtPosition position: CGFloat)

    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didStartMovingAtPosition position: CGFloat)
    optional func dropdownMenuView(dropdownMenuView: LPDropdownMenuView, didFinishMovingAtPosition position: CGFloat)

}



class LPDropdownMenuView: UIView {

    private var panGestureRecognizer: UIPanGestureRecognizer!
    private var tapGestureRecognizer: UITapGestureRecognizer!
    private var doubleTapGestureRecognizer: UITapGestureRecognizer!
    private var movingCondition: Bool = false

    var delegate: LPDropdownMenuViewDelegate?

    var freeSlideable: Bool = true
    var swipeable: Bool = true
    var touchable: Bool = true
    var doubleTouchable: Bool = true
    var autoSetProperties: Bool = true

    var marginProximity: CGFloat = 50
    var swipeSensibility: CGFloat = 600
    var minPosition: CGFloat!
    var maxPosition: CGFloat!

    var contentFrame: CGRect! {
        didSet {
            setProperties()
        }
    }

    var barView: UIView! {
        willSet {
            if (self.barView != nil) {
                self.barView.removeFromSuperview()
            }
        }
        didSet {
            let height = barView.frame.size.height + (contentFrame.maxY - barView.frame.maxY)
            frame = CGRectMake(barView.frame.origin.x, barView.frame.origin.y, barView.frame.size.width, height)
            barView.frame.origin.y = 0
            addSubview(barView)
            setPosition(frame.origin.y)
            setProperties()
            setGestures()
        }
    }

    var menuView: UIView! {
        willSet {
            if (self.menuView != nil) {
                self.menuView.removeFromSuperview()
            }
        }
        didSet {
            if (!(menuView is UIScrollView)) {
                let newValue = menuView
                newValue.frame.origin.y = 0
                let height = frame.size.height - barView.frame.size.height
                menuView = UIScrollView(frame: CGRectMake(0, barView.frame.size.height, newValue.frame.size.width, height))
                (menuView as! UIScrollView).contentSize = CGSize(width: newValue.frame.size.width, height: newValue.frame.size.height)
                (menuView as! UIScrollView).bounces = false
                menuView.addSubview(newValue)
            } else {
                let height = frame.size.height - barView.frame.size.height
                menuView.frame = CGRectMake(0, barView.frame.size.height, menuView.frame.size.width, height)
            }
            addSubview(menuView)
        }
    }

    init (frame: CGRect, barView: UIView, menuView: UIView) {
        super.init(frame: frame)
        self.contentFrame = frame
        initView(barView, menuView: menuView)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.contentFrame = frame
        let barView = DefaultBarView(frame: CGRectMake(0, frame.size.height - 60, frame.size.width, 60))
        let menuView = DefaultMenuView(frame: CGRectMake(0, barView.frame.size.height, frame.size.width, 600))
        initView(barView, menuView: menuView)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func initView(barView: UIView, menuView: UIView) {
        self.initGestures()
        self.barView = barView
        self.menuView = menuView
    }

}



extension LPDropdownMenuView {

    func getPosition() -> CGFloat {
        return frame.origin.y
    }

    func setProperties() {
        if (contentFrame != nil && autoSetProperties) {
            minPosition = contentFrame.maxY / 3
            maxPosition = contentFrame.maxY - barView.frame.size.height
        }
    }

    func setPosition(position: CGFloat) {
        self.frame.origin.y = position
        self.frame.size.height = self.contentFrame.size.height - position
        if (menuView != nil) {
            self.menuView.frame.size.height = self.contentFrame.size.height - self.barView.frame.size.height - position
        }
    }

    func setPositionWithAnimation(position: CGFloat, time: NSTimeInterval, completion: ((Void) -> Void)?) {
        UIView.animateWithDuration(time, delay: 0, options: .CurveEaseOut, animations: {
            self.frame.origin.y = position
            self.frame.size.height = self.contentFrame.size.height - position
            self.menuView.frame.size.height = self.contentFrame.size.height - position - self.barView.frame.size.height
            }, completion: { _ in
                if (completion != nil) {
                    completion!()
                }
        })
    }

}



extension LPDropdownMenuView {

    private func initGestures() {
        doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: "handleDoubleTapGestures:")
        doubleTapGestureRecognizer.numberOfTapsRequired = 2

        tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "handleTapGestures:")
        tapGestureRecognizer.numberOfTapsRequired = 1
        tapGestureRecognizer.requireGestureRecognizerToFail(doubleTapGestureRecognizer)

        panGestureRecognizer = UIPanGestureRecognizer(target: self, action: "handlePanGestures:")
        panGestureRecognizer.minimumNumberOfTouches = 1
        panGestureRecognizer.maximumNumberOfTouches = 1
    }

    private func setGestures() {
        barView.addGestureRecognizer(doubleTapGestureRecognizer)
        barView.addGestureRecognizer(tapGestureRecognizer)
        barView.addGestureRecognizer(panGestureRecognizer)
    }

    internal func handleDoubleTapGestures(sender: UITapGestureRecognizer) {
        if (doubleTouchable) {
            let minPosition = contentFrame.origin.y
            let maxPosition = contentFrame.maxY - barView.frame.size.height
            let fartherPosition = getFartherPosition(minPosition, maxPosition: maxPosition)
            if (delegate?.dropdownMenuView?(self, shouldDoubleTouchToPosition: fartherPosition) != false) {
                delegate?.dropdownMenuView?(self, willDoubleTouchToPosition: fartherPosition)
                setPositionWithAnimation(fartherPosition, time: 0.3, completion: {
                    delegate?.dropdownMenuView?(self, didDoubleTouchToPosition: fartherPosition)
                })
            }
        }
    }

    internal func handleTapGestures(sender: UITapGestureRecognizer) {
        if (touchable) {
            let fartherPosition = getFartherPosition(minPosition, maxPosition: maxPosition)
            if (delegate?.dropdownMenuView?(self, shouldTouchToPosition: fartherPosition) != false) {
                delegate?.dropdownMenuView?(self, willTouchToPosition: fartherPosition)
                setPositionWithAnimation(fartherPosition, time: 0.3, completion: {
                    delegate?.dropdownMenuView?(self, didTouchToPosition: fartherPosition)
                })
            }
        }
    }

    internal func handlePanGestures(sender: UIPanGestureRecognizer) {
        let location = sender.locationInView(sender.view!.superview!)

        if (sender.state != .Ended && sender.state != .Failed) {
            if (!movingCondition) {
                movingCondition = true
                delegate?.dropdownMenuView?(self, didStartMovingAtPosition: frame.origin.y)
            }
            let minPosition = contentFrame.origin.y
            let maxPosition = contentFrame.maxY - barView.frame.size.height
            let newPosition = frame.origin.y + location.y - (sender.view!.frame.size.height / 2)
            if (newPosition > minPosition && newPosition < maxPosition) {
                setPosition(newPosition)
            } else {
                if (newPosition < minPosition) {
                    setPosition(minPosition)
                } else {
                    setPosition(maxPosition)
                }
            }
        }

        if (sender.state == .Ended) {

            movingCondition = false
            delegate?.dropdownMenuView?(self, didFinishMovingAtPosition: frame.origin.y)

            if (checkMarginProximity(contentFrame.origin.y)) {
                if (delegate?.dropdownMenuView?(self, shouldAproximateToPosition: contentFrame.origin.y) != false) {
                    delegate?.dropdownMenuView?(self, willAproximateToPosition: contentFrame.origin.y)
                    setPositionWithAnimation(contentFrame.origin.y, time: 0.3, completion: {
                        delegate?.dropdownMenuView?(self, didAproximateToPosition: contentFrame.origin.y)
                    })
                    return
                }
            }

            if (checkMarginProximity(contentFrame.maxY - barView.frame.size.height)) {
                let proximatePosition = contentFrame.maxY - barView.frame.size.height
                if (delegate?.dropdownMenuView?(self, shouldAproximateToPosition: proximatePosition) != false) {
                    delegate?.dropdownMenuView?(self, willAproximateToPosition: proximatePosition)
                    setPositionWithAnimation(contentFrame.maxY - barView.frame.size.height, time: 0.3, completion: {
                        delegate?.dropdownMenuView?(self, didAproximateToPosition: proximatePosition)
                    })
                    return
                }
            }

            if (swipeable) {
                let swipePosition = getSwipePosition(sender.velocityInView(sender.view))
                if (swipePosition != nil) {
                    let swipeCondition = delegate?.dropdownMenuView?(self, shouldSwipeToPosition: swipePosition!) != false
                    if (swipeCondition) {
                        delegate?.dropdownMenuView?(self, willSwipeToPosition: swipePosition!)
                        setPositionWithAnimation(swipePosition!, time: 0.3, completion: {
                            delegate?.dropdownMenuView?(self, didSwipeToPosition: swipePosition!)
                        })
                        return
                    }
                }
            }

            if (freeSlideable && delegate?.dropdownMenuView?(self, shouldFinishSlidingAtPosition: frame.origin.y) == false) {
                let nearestPosition = getNearestPosition()
                delegate?.dropdownMenuView?(self, willAproximateToPosition: nearestPosition)
                setPositionWithAnimation(nearestPosition, time: 0.3, completion: {
                    delegate?.dropdownMenuView?(self, didAproximateToPosition: nearestPosition)
                })
                return
            }

            if (!freeSlideable) {
                let nearestPosition = getNearestPosition()
                delegate?.dropdownMenuView?(self, willAproximateToPosition: nearestPosition)
                setPositionWithAnimation(nearestPosition, time: 0.3, completion: {
                    delegate?.dropdownMenuView?(self, didAproximateToPosition: nearestPosition)
                })
                return
            }

            delegate?.dropdownMenuView?(self, willFinishSlidingAtPosition: frame.origin.y)
            delegate?.dropdownMenuView?(self, didFinishSlidingAtPosition: frame.origin.y)

        }
    }

    private func checkMarginProximity(position: CGFloat) -> Bool {
        if (frame.origin.y < position + marginProximity && frame.origin.y > position - marginProximity) {
            return true
        }
        return false
    }

    private func getNearestPosition() -> CGFloat {
        let currentPosition = frame.origin.y + (barView.frame.size.height / 2)
        let positions = [contentFrame.origin.y, minPosition, maxPosition, contentFrame.maxY]
        var nearestPosition: CGFloat!
        var distance: CGFloat!
        for position in positions {
            if (distance == nil || distance > abs(currentPosition - position)) {
                distance = abs(currentPosition - position)
                nearestPosition = position
            }
        }
        return nearestPosition
    }

    private func getSwipePosition(velocity: CGPoint) -> CGFloat? {
        if (velocity.y > swipeSensibility) {
            return maxPosition
        }
        if (velocity.y < -swipeSensibility) {
            if (frame.origin.y < minPosition) {
                return contentFrame.origin.y
            } else {
                return minPosition
            }
        }
        return nil
    }

    private func getFartherPosition(minPosition: CGFloat, maxPosition: CGFloat) -> CGFloat {
        let rangeMinPosition: CGFloat = 1.5
        let rangeMaxPosition: CGFloat = 0.5
        let distanceFromMin = (frame.origin.y - minPosition) * rangeMinPosition
        let distanceFromMax = (maxPosition - frame.origin.y) * rangeMaxPosition

        if (distanceFromMin > distanceFromMax) {
            return minPosition
        } else {
            return maxPosition
        }
    }

}



class DefaultBarView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.blueColor()
        initLabel()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func initLabel() {
        let label = UILabel(frame: CGRectMake(0, 0, frame.size.width, frame.size.height))
        label.textAlignment = .Center
        label.textColor = UIColor.whiteColor()
        label.font = UIFont.systemFontOfSize(22)
        label.text = "Something cool here!"
        addSubview(label)
    }

}
class DefaultMenuView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.grayColor()
        initLabel()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func initLabel() {
        let label = UILabel(frame: CGRectMake(0, 0, frame.size.width, frame.size.height))
        label.textAlignment = .Center
        label.textColor = UIColor.whiteColor()
        label.font = UIFont.systemFontOfSize(22)
        label.text = "An amazing view here!"
        addSubview(label)
    }

}

The code works perfect, but I want your opinion!

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

There is a whole lot of code here, but I actually want to focus strictly on the protocol that we've created for delegates of this menu.

There are a few things that immediately jump out to me, but the first thing, I'll illustrate with a screenshot.

enter image description here

The protocol has a total of 17 methods. All of which start with the same 69 characters. Now, that alone is not that problematic. If we take a look at other protocols (even, some of Apple's, like table views and collection views), we'll see the same thing. But here, the problem is, we have provided no comments and the organization of the methods leaves a lot to be desired.

Let's compare, for example, the documentation for UITableViewDelegate:

enter image description here

Notice the organization into more useful categories. Your organization is this:

  • should happen
  • will happen
  • did happen

And then a final "did happen" section that is somewhat inexplicably separated from the other "did happen" methods... but actually makes some as its own section (if we take a look at the Apple organization).

I'd rather see your protocol organized into six section of roughly 3 methods each. The natural organization looks more like this...

  • swiping...
    • should swipe
    • will swipe
    • did swipe
  • touching...
    • should touch
    • will touch
    • did touch
  • etc...

Moreover, I would expect every single on of these method declarations to have appropriate Apple docs with them.

This, for example, is what the shouldSwipeToPosition method might have: enter image description here

Your methods are well named, but I'm certainly going to expect documentation like this for a delegate protocol. It's the biggest clue I have as to exactly how and when this method works. Your description should even be more detailed than what I've provided.

Consider, for example, the documentation that Apple provides for a similar protocol method found in UIKit:

enter image description here

This documentation is on their website, but it's also available via Xcode in the same manner as I showed in the screenshot demonstrating an example of the documentation you should be providing.


You've dumped everything into a single file. For what you've provided, I'd expect six different files. This means reconsidering your access modifiers.

The thing is, you've designed this with the idea in mind that the user will copy & paste this file into their project and just go... the thing is, that's a terrible way to bring 3rd party code into my project.

When I bring code into my projects, I expect to bring it in via Cocoapods. As a rule (with the rare exception), I completely pass over libraries that aren't Cocoapod compatible.

It's important to note that if our code IS being used via a Cocoapod, then only the things marked as public are seen by the outside world. This is nice. It allows us to access code across our entire library, allowing us to use multiple files, and still preventing the outside world from calling these directly.

It is something you should consider seriously.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.