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!
1 Answer 1
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.
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
:
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
:
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.