M13Checkbox.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. //
  2. // M13Checkbox.swift
  3. // M13Checkbox
  4. //
  5. // Created by McQuilkin, Brandon on 2/23/16.
  6. // Copyright © 2016 Brandon McQuilkin. All rights reserved.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  9. //
  10. // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  13. import UIKit
  14. /// A customizable checkbox control for iOS.
  15. @IBDesignable
  16. open class M13Checkbox: UIControl {
  17. //----------------------------
  18. // MARK: - Constants
  19. //----------------------------
  20. /**
  21. The possible states the check can be in.
  22. - Unchecked: No check is shown.
  23. - Checked: A checkmark is shown.
  24. - Mixed: A dash is shown.
  25. */
  26. public enum CheckState: String {
  27. /// No check is shown.
  28. case unchecked = "Unchecked"
  29. /// A checkmark is shown.
  30. case checked = "Checked"
  31. /// A dash is shown.
  32. case mixed = "Mixed"
  33. }
  34. /**
  35. The possible shapes of the box.
  36. - Square: The box is square with optional rounded corners.
  37. - Circle: The box is a circle.
  38. */
  39. public enum BoxType: String {
  40. /// The box is a circle.
  41. case circle = "Circle"
  42. /// The box is square with optional rounded corners.
  43. case square = "Square"
  44. }
  45. /**
  46. The possible shapes of the mark.
  47. - Checkmark: The mark is a standard checkmark.
  48. - Radio: The mark is a radio style fill.
  49. */
  50. public enum MarkType: String {
  51. /// The mark is a standard checkmark.
  52. case checkmark = "Checkmark"
  53. /// The mark is a radio style fill.
  54. case radio = "Radio"
  55. /// The mark is an add/remove icon set.
  56. case addRemove = "AddRemove"
  57. /// The mark is a disclosure indicator.
  58. case disclosure = "Disclosure"
  59. }
  60. /**
  61. The possible animations for switching to and from the unchecked state.
  62. */
  63. public enum Animation: RawRepresentable, Hashable {
  64. /// Animates the stroke of the box and the check as if they were drawn.
  65. case stroke
  66. /// Animates the checkbox with a bouncey fill effect.
  67. case fill
  68. /// Animates the check mark with a bouncy effect.
  69. case bounce(AnimationStyle)
  70. /// Animates the checkmark and fills the box with a bouncy effect.
  71. case expand(AnimationStyle)
  72. /// Morphs the checkmark from a line.
  73. case flat(AnimationStyle)
  74. /// Animates the box and check as if they were drawn in one continuous line.
  75. case spiral
  76. /// Fades checkmark in or out. (opacity).
  77. case fade(AnimationStyle)
  78. /// Start the box as a dot, and expand the box.
  79. case dot(AnimationStyle)
  80. public init?(rawValue: String) {
  81. // Map the integer values to the animation types.
  82. // This is only for interface builder support. I would like this to be removed eventually.
  83. switch rawValue {
  84. case "Stroke":
  85. self = .stroke
  86. case "Fill":
  87. self = .fill
  88. case "BounceStroke":
  89. self = .bounce(.stroke)
  90. case "BounceFill":
  91. self = .bounce(.fill)
  92. case "ExpandStroke":
  93. self = .expand(.stroke)
  94. case "ExpandFill":
  95. self = .expand(.fill)
  96. case "FlatStroke":
  97. self = .flat(.stroke)
  98. case "FlatFill":
  99. self = .flat(.fill)
  100. case "Spiral":
  101. self = .spiral
  102. case "FadeStroke":
  103. self = .fade(.stroke)
  104. case "FadeFill":
  105. self = .fade(.fill)
  106. case "DotStroke":
  107. self = .dot(.stroke)
  108. case "DotFill":
  109. self = .dot(.fill)
  110. default:
  111. return nil
  112. }
  113. }
  114. public var rawValue: String {
  115. // Map the animation types to integer values.
  116. // This is only for interface builder support. I would like this to be removed eventually.
  117. switch self {
  118. case .stroke:
  119. return "Stroke"
  120. case .fill:
  121. return "Fill"
  122. case let .bounce(style):
  123. switch style {
  124. case .stroke:
  125. return "BounceStroke"
  126. case .fill:
  127. return "BounceFill"
  128. }
  129. case let .expand(style):
  130. switch style {
  131. case .stroke:
  132. return "ExpandStroke"
  133. case .fill:
  134. return "ExpandFill"
  135. }
  136. case let .flat(style):
  137. switch style {
  138. case .stroke:
  139. return "FlatStroke"
  140. case .fill:
  141. return "FlatFill"
  142. }
  143. case .spiral:
  144. return "Spiral"
  145. case let .fade(style):
  146. switch style {
  147. case .stroke:
  148. return "FadeStroke"
  149. case .fill:
  150. return "FadeFill"
  151. }
  152. case let .dot(style):
  153. switch style {
  154. case .stroke:
  155. return "DotStroke"
  156. case .fill:
  157. return "DotFill"
  158. }
  159. }
  160. }
  161. /// The manager for the specific animation type.
  162. fileprivate var manager: M13CheckboxController {
  163. switch self {
  164. case .stroke:
  165. return M13CheckboxStrokeController()
  166. case .fill:
  167. return M13CheckboxFillController()
  168. case let .bounce(style):
  169. return M13CheckboxBounceController(style: style)
  170. case let .expand(style):
  171. return M13CheckboxExpandController(style: style)
  172. case let .flat(style):
  173. return M13CheckboxFlatController(style: style)
  174. case .spiral:
  175. return M13CheckboxSpiralController()
  176. case let .fade(style):
  177. return M13CheckboxFadeController(style: style)
  178. case let .dot(style):
  179. return M13CheckboxDotController(style: style)
  180. }
  181. }
  182. public var hashValue: Int {
  183. return self.rawValue.hashValue
  184. }
  185. }
  186. /**
  187. The possible animation styles.
  188. - Note: Not all animations support all styles.
  189. */
  190. public enum AnimationStyle: String {
  191. // The animation will focus on the stroke.
  192. case stroke = "Stroke"
  193. // The animation will focus on the fill.
  194. case fill = "Fill"
  195. }
  196. //----------------------------
  197. // MARK: - Properties
  198. //----------------------------
  199. /// The manager that manages display and animations of the checkbox.
  200. /// The default animation is a stroke.
  201. fileprivate var controller: M13CheckboxController = M13CheckboxStrokeController()
  202. //----------------------------
  203. // MARK: - Initalization
  204. //----------------------------
  205. override public init(frame: CGRect) {
  206. super.init(frame: frame)
  207. sharedSetup()
  208. }
  209. required public init?(coder aDecoder: NSCoder) {
  210. super.init(coder: aDecoder)
  211. sharedSetup()
  212. }
  213. /// The setup shared between initalizers.
  214. fileprivate func sharedSetup() {
  215. // Set up the inital state.
  216. for aLayer in controller.layersToDisplay {
  217. layer.addSublayer(aLayer)
  218. }
  219. controller.tintColor = tintColor
  220. controller.resetLayersForState(DefaultValues.checkState)
  221. let longPressGesture = M13CheckboxGestureRecognizer(target: self, action: #selector(M13Checkbox.handleLongPress(_:)))
  222. addGestureRecognizer(longPressGesture)
  223. }
  224. //----------------------------
  225. // MARK: - Values
  226. //----------------------------
  227. /// The object to return from `value` when the checkbox is checked.
  228. open var checkedValue: Any?
  229. /// The object to return from `value` when the checkbox is unchecked.
  230. open var uncheckedValue: Any?
  231. /// The object to return from `value` when the checkbox is mixed.
  232. open var mixedValue: Any?
  233. /**
  234. Returns one of the three "value" properties depending on the checkbox state.
  235. - returns: The value coresponding to the checkbox state.
  236. - note: This is a convenience method so that if one has a large group of checkboxes, it is not necessary to write: if (someCheckbox == thatCheckbox) { if (someCheckbox.checkState == ...
  237. */
  238. open var value: Any? {
  239. switch checkState {
  240. case .unchecked:
  241. return uncheckedValue
  242. case .checked:
  243. return checkedValue
  244. case .mixed:
  245. return mixedValue
  246. }
  247. }
  248. //----------------------------
  249. // MARK: - State
  250. //----------------------------
  251. /// The current state of the checkbox.
  252. open var checkState: CheckState {
  253. get {
  254. return controller.state
  255. }
  256. set {
  257. setCheckState(newValue, animated: false)
  258. }
  259. }
  260. /**
  261. Change the check state.
  262. - parameter checkState: The new state of the checkbox.
  263. - parameter animated: Whether or not to animate the change.
  264. */
  265. open func setCheckState(_ newState: CheckState, animated: Bool) {
  266. if checkState == newState {
  267. return
  268. }
  269. if animated {
  270. if enableMorphing {
  271. controller.animate(checkState, toState: newState)
  272. } else {
  273. controller.animate(checkState, toState: nil, completion: { [weak self] in
  274. self?.controller.resetLayersForState(newState)
  275. self?.controller.animate(nil, toState: newState)
  276. })
  277. }
  278. } else {
  279. controller.resetLayersForState(newState)
  280. }
  281. }
  282. /**
  283. Toggle the check state between unchecked and checked.
  284. - parameter animated: Whether or not to animate the change. Defaults to false.
  285. - note: If the checkbox is mixed, it will return to the unchecked state.
  286. */
  287. open func toggleCheckState(_ animated: Bool = false) {
  288. switch checkState {
  289. case .checked:
  290. setCheckState(.unchecked, animated: animated)
  291. break
  292. case .unchecked:
  293. setCheckState(.checked, animated: animated)
  294. break
  295. case .mixed:
  296. setCheckState(.unchecked, animated: animated)
  297. break
  298. }
  299. }
  300. //----------------------------
  301. // MARK: - Animations
  302. //----------------------------
  303. /// The duration of the animation that occurs when the checkbox switches states. The default is 0.3 seconds.
  304. @IBInspectable open var animationDuration: TimeInterval {
  305. get {
  306. return controller.animationGenerator.animationDuration
  307. }
  308. set {
  309. controller.animationGenerator.animationDuration = newValue
  310. }
  311. }
  312. /// The type of animation to preform when changing from the unchecked state to any other state.
  313. open var stateChangeAnimation: Animation = DefaultValues.animation {
  314. didSet {
  315. // Remove the sublayers
  316. if let layers = layer.sublayers {
  317. for sublayer in layers {
  318. sublayer.removeAllAnimations()
  319. sublayer.removeFromSuperlayer()
  320. }
  321. }
  322. // Set the manager
  323. let newManager = stateChangeAnimation.manager
  324. newManager.tintColor = tintColor
  325. newManager.secondaryTintColor = secondaryTintColor
  326. newManager.secondaryCheckmarkTintColor = secondaryCheckmarkTintColor
  327. newManager.hideBox = hideBox
  328. newManager.pathGenerator = controller.pathGenerator
  329. newManager.animationGenerator.animationDuration = controller.animationGenerator.animationDuration
  330. newManager.state = controller.state
  331. newManager.enableMorphing = controller.enableMorphing
  332. newManager.setMarkType(type: controller.markType, animated: false)
  333. // Set up the inital state.
  334. for aLayer in newManager.layersToDisplay {
  335. layer.addSublayer(aLayer)
  336. }
  337. // Layout and reset
  338. newManager.resetLayersForState(checkState)
  339. controller = newManager
  340. }
  341. }
  342. /// Whether or not to enable morphing between states.
  343. @IBInspectable open var enableMorphing: Bool {
  344. get {
  345. return controller.enableMorphing
  346. }
  347. set {
  348. controller.enableMorphing = newValue
  349. }
  350. }
  351. //----------------------------
  352. // MARK: - UIControl
  353. //----------------------------
  354. @objc func handleLongPress(_ sender: UILongPressGestureRecognizer) {
  355. if sender.state == .began || sender.state == .changed {
  356. isSelected = true
  357. } else {
  358. isSelected = false
  359. if sender.state == .ended {
  360. toggleCheckState(true)
  361. sendActions(for: .valueChanged)
  362. }
  363. }
  364. }
  365. //----------------------------
  366. // MARK: - Appearance
  367. //----------------------------
  368. /// The color of the checkbox's tint color when not in the unselected state. The tint color is is the main color used when not in the unselected state.
  369. @IBInspectable open var secondaryTintColor: UIColor? {
  370. get {
  371. return controller.secondaryTintColor
  372. }
  373. set {
  374. controller.secondaryTintColor = newValue
  375. }
  376. }
  377. /// The color of the checkmark when it is displayed against a filled background.
  378. @IBInspectable open var secondaryCheckmarkTintColor: UIColor? {
  379. get {
  380. return controller.secondaryCheckmarkTintColor
  381. }
  382. set {
  383. controller.secondaryCheckmarkTintColor = newValue
  384. }
  385. }
  386. /// The stroke width of the checkmark.
  387. @IBInspectable open var checkmarkLineWidth: CGFloat {
  388. get {
  389. return controller.pathGenerator.checkmarkLineWidth
  390. }
  391. set {
  392. controller.pathGenerator.checkmarkLineWidth = newValue
  393. controller.resetLayersForState(checkState)
  394. }
  395. }
  396. /// The type of mark to display.
  397. open var markType: MarkType {
  398. get {
  399. return controller.markType
  400. }
  401. set {
  402. controller.markType = newValue
  403. setNeedsLayout()
  404. }
  405. }
  406. /// Set the mark type with the option of animating the change.
  407. open func setMarkType(markType: MarkType, animated: Bool) {
  408. controller.setMarkType(type: markType, animated: animated)
  409. }
  410. /// The stroke width of the box.
  411. @IBInspectable open var boxLineWidth: CGFloat {
  412. get {
  413. return controller.pathGenerator.boxLineWidth
  414. }
  415. set {
  416. controller.pathGenerator.boxLineWidth = newValue
  417. controller.resetLayersForState(checkState)
  418. }
  419. }
  420. /// The corner radius of the box if the box type is square.
  421. @IBInspectable open var cornerRadius: CGFloat {
  422. get {
  423. return controller.pathGenerator.cornerRadius
  424. }
  425. set {
  426. controller.pathGenerator.cornerRadius = newValue
  427. setNeedsLayout()
  428. }
  429. }
  430. /// The shape of the checkbox.
  431. open var boxType: BoxType {
  432. get {
  433. return controller.pathGenerator.boxType
  434. }
  435. set {
  436. controller.pathGenerator.boxType = newValue
  437. setNeedsLayout()
  438. }
  439. }
  440. /// Wether or not to hide the checkbox.
  441. @IBInspectable open var hideBox: Bool {
  442. get {
  443. return controller.hideBox
  444. }
  445. set {
  446. controller.hideBox = newValue
  447. }
  448. }
  449. open override func tintColorDidChange() {
  450. super.tintColorDidChange()
  451. controller.tintColor = tintColor
  452. }
  453. //----------------------------
  454. // MARK: - Layout
  455. //----------------------------
  456. open override func layoutSubviews() {
  457. super.layoutSubviews()
  458. // Update size
  459. controller.pathGenerator.size = min(frame.size.width, frame.size.height)
  460. // Layout
  461. controller.layoutLayers()
  462. }
  463. }