SegmentedControl.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. //
  2. // SegmentedControl.swift
  3. // SegmentedControl
  4. //
  5. // Created by Xin Hong on 15/12/29.
  6. // Copyright © 2015年 Teambition. All rights reserved.
  7. //
  8. import UIKit
  9. public protocol SegmentedControlDelegate: class {
  10. func segmentedControl(_ segmentedControl: SegmentedControl, didSelectIndex selectedIndex: Int)
  11. func segmentedControl(_ segmentedControl: SegmentedControl, didLongPressIndex longPressIndex: Int)
  12. }
  13. public extension SegmentedControlDelegate {
  14. func segmentedControl(_ segmentedControl: SegmentedControl, didSelectIndex selectedIndex: Int) {
  15. }
  16. func segmentedControl(_ segmentedControl: SegmentedControl, didLongPressIndex longPressIndex: Int) {
  17. }
  18. }
  19. open class SegmentedControl: UIControl {
  20. open weak var delegate: SegmentedControlDelegate?
  21. open fileprivate(set) var selectedIndex = 0 {
  22. didSet {
  23. setNeedsDisplay()
  24. }
  25. }
  26. open var segmentWidth: CGFloat?
  27. open var minimumSegmentWidth: CGFloat?
  28. open var maximumSegmentWidth: CGFloat?
  29. open var isAnimationEnabled = true
  30. open var isUserDragEnabled = true
  31. open fileprivate(set) var style: SegmentedControlStyle = .text
  32. open var selectionBoxStyle: SegmentedControlSelectionBoxStyle = .none
  33. open var selectionBoxColor = UIColor.blue
  34. open var selectionBoxCornerRadius: CGFloat = 0
  35. open var selectionBoxEdgeInsets = UIEdgeInsets.zero
  36. open var selectionIndicatorStyle: SegmentedControlSelectionIndicatorStyle = .none
  37. open var selectionIndicatorColor = UIColor.black
  38. open var selectionIndicatorHeight = SelectionIndicator.defaultHeight
  39. open var selectionIndicatorEdgeInsets = UIEdgeInsets.zero
  40. open var titleAttachedIconPositionOffset: (x: CGFloat, y: CGFloat ) = (0, 0)
  41. open fileprivate(set) var titles = [NSAttributedString]()
  42. open fileprivate(set) var selectedTitles: [NSAttributedString]?
  43. open fileprivate(set) var images = [UIImage]()
  44. open fileprivate(set) var selectedImages: [UIImage]?
  45. open fileprivate(set) var titleAttachedIcons: [UIImage]?
  46. open fileprivate(set) var selectedTitleAttachedIcons: [UIImage]?
  47. open var isLongPressEnabled = false {
  48. didSet {
  49. if isLongPressEnabled {
  50. longPressGesture = UILongPressGestureRecognizer()
  51. longPressGesture!.addTarget(self, action: #selector(segmentedControlLongPressed(_:)))
  52. longPressGesture!.minimumPressDuration = longPressMinimumPressDuration
  53. scrollView.addGestureRecognizer(longPressGesture!)
  54. longPressGesture!.delegate = self
  55. } else if let _ = longPressGesture {
  56. scrollView.removeGestureRecognizer(longPressGesture!)
  57. longPressGesture!.delegate = nil
  58. longPressGesture = nil
  59. }
  60. }
  61. }
  62. open var isUnselectedSegmentsLongPressEnabled = false
  63. open var longPressMinimumPressDuration: CFTimeInterval = 0.5 {
  64. didSet {
  65. assert(longPressMinimumPressDuration >= 0.5, "MinimumPressDuration of LongPressGestureRecognizer must be no less than 0.5")
  66. if let longPressGesture = longPressGesture {
  67. longPressGesture.minimumPressDuration = longPressMinimumPressDuration
  68. }
  69. }
  70. }
  71. open fileprivate(set) var isLongPressActivated = false
  72. fileprivate lazy var scrollView: SCScrollView = {
  73. let scrollView = SCScrollView()
  74. scrollView.scrollsToTop = false
  75. scrollView.isScrollEnabled = true
  76. scrollView.showsHorizontalScrollIndicator = false
  77. scrollView.showsVerticalScrollIndicator = false
  78. return scrollView
  79. }()
  80. fileprivate lazy var selectionBoxLayer = CALayer()
  81. fileprivate lazy var selectionIndicatorLayer = CALayer()
  82. fileprivate var longPressGesture: UILongPressGestureRecognizer?
  83. // MARK: - Public functions
  84. open class func initWithTitles(_ titles: [NSAttributedString], selectedTitles: [NSAttributedString]?) -> SegmentedControl {
  85. let segmentedControl = SegmentedControl(frame: CGRect.zero)
  86. segmentedControl.style = .text
  87. segmentedControl.titles = titles
  88. segmentedControl.selectedTitles = selectedTitles
  89. return segmentedControl
  90. }
  91. open class func initWithImages(_ images: [UIImage], selectedImages: [UIImage]?) -> SegmentedControl {
  92. let segmentedControl = SegmentedControl(frame: CGRect.zero)
  93. segmentedControl.style = .image
  94. segmentedControl.images = images
  95. segmentedControl.selectedImages = selectedImages
  96. return segmentedControl
  97. }
  98. open func setTitles(_ titles: [NSAttributedString], selectedTitles: [NSAttributedString]?) {
  99. style = .text
  100. self.titles = titles
  101. self.selectedTitles = selectedTitles
  102. }
  103. open func setImages(_ images: [UIImage], selectedImages: [UIImage]?) {
  104. style = .image
  105. self.images = images
  106. self.selectedImages = selectedImages
  107. }
  108. open func setTitleAttachedIcons(_ titleAttachedIcons: [UIImage]?, selectedTitleAttachedIcons: [UIImage]?) {
  109. self.titleAttachedIcons = titleAttachedIcons
  110. self.selectedTitleAttachedIcons = selectedTitleAttachedIcons
  111. }
  112. open func setSelected(at index: Int, animated: Bool) {
  113. if !(0..<segmentsCount() ~= selectedIndex) {
  114. return
  115. }
  116. selectedIndex = index
  117. scrollToSelectedIndex(animated: animated)
  118. if !animated {
  119. selectionBoxLayer.actions = ["position": NSNull(), "bounds": NSNull()]
  120. selectionIndicatorLayer.actions = ["position": NSNull(), "bounds": NSNull()]
  121. selectionBoxLayer.frame = frameForSelectionBox()
  122. selectionIndicatorLayer.frame = frameForSelectionIndicator()
  123. } else {
  124. selectionBoxLayer.actions = nil
  125. selectionIndicatorLayer.actions = nil
  126. }
  127. }
  128. // MARK: - Initialization
  129. public override init(frame: CGRect) {
  130. super.init(frame: frame)
  131. commonInit()
  132. }
  133. public required init?(coder aDecoder: NSCoder) {
  134. super.init(coder: aDecoder)
  135. commonInit()
  136. }
  137. open override func awakeFromNib() {
  138. super.awakeFromNib()
  139. commonInit()
  140. }
  141. fileprivate func commonInit() {
  142. addSubview(scrollView)
  143. contentMode = .redraw
  144. if let parentViewController = scrollView.parentViewController {
  145. parentViewController.automaticallyAdjustsScrollViewInsets = false
  146. }
  147. }
  148. // MARK: - Overriding
  149. open override func layoutSubviews() {
  150. super.layoutSubviews()
  151. update()
  152. }
  153. open override var frame: CGRect {
  154. didSet {
  155. update()
  156. }
  157. }
  158. open override func willMove(toSuperview newSuperview: UIView?) {
  159. super.willMove(toSuperview: newSuperview)
  160. if newSuperview == nil {
  161. return
  162. }
  163. update()
  164. }
  165. open override func draw(_ rect: CGRect) {
  166. backgroundColor?.setFill()
  167. UIRectFill(bounds)
  168. scrollView.layer.sublayers?.removeAll(keepingCapacity: true)
  169. selectionBoxLayer.backgroundColor = selectionBoxColor.cgColor
  170. selectionIndicatorLayer.backgroundColor = selectionIndicatorColor.cgColor
  171. switch style {
  172. case .text:
  173. drawTitles()
  174. case .image:
  175. drawImages()
  176. }
  177. if selectionIndicatorStyle != .none {
  178. drawSelectionIndicator()
  179. }
  180. if selectionBoxStyle != .none {
  181. drawSelectionBox()
  182. }
  183. }
  184. open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  185. if isLongPressActivated {
  186. return
  187. }
  188. if let touch = touches.first {
  189. let touchLocation = touch.location(in: self)
  190. if !bounds.contains(touchLocation) {
  191. return
  192. }
  193. if singleSegmentWidth() == 0 {
  194. return
  195. }
  196. let touchIndex = Int((touchLocation.x + scrollView.contentOffset.x) / singleSegmentWidth())
  197. if 0..<segmentsCount() ~= touchIndex {
  198. if let delegate = delegate {
  199. delegate.segmentedControl(self, didSelectIndex: touchIndex)
  200. }
  201. if touchIndex != selectedIndex {
  202. setSelected(at: touchIndex, animated: isAnimationEnabled)
  203. }
  204. }
  205. }
  206. }
  207. }
  208. public extension SegmentedControl {
  209. // MARK: - Events
  210. fileprivate func update() {
  211. scrollView.contentInset = UIEdgeInsets.zero
  212. scrollView.frame = CGRect(origin: CGPoint.zero, size: frame.size)
  213. scrollView.isScrollEnabled = isUserDragEnabled
  214. scrollView.contentSize = CGSize(width: totalSegmentsWidth(), height: frame.height)
  215. scrollToSelectedIndex(animated: false)
  216. }
  217. fileprivate func scrollToSelectedIndex(animated: Bool) {
  218. let rectToScroll: CGRect = {
  219. var rectToScroll = self.rectForSelectedIndex()
  220. let scrollOffset = self.frame.width / 2 - self.singleSegmentWidth() / 2
  221. rectToScroll.origin.x -= scrollOffset
  222. rectToScroll.size.width += scrollOffset * 2
  223. return rectToScroll
  224. }()
  225. scrollView.scrollRectToVisible(rectToScroll, animated: animated)
  226. }
  227. }
  228. extension SegmentedControl: UIGestureRecognizerDelegate {
  229. // MARK: - UIGestureRecognizerDelegate
  230. open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  231. if gestureRecognizer == longPressGesture {
  232. if let longPressIndex = locationIndex(for: gestureRecognizer) {
  233. return isUnselectedSegmentsLongPressEnabled ? true : longPressIndex == selectedIndex
  234. }
  235. }
  236. return false
  237. }
  238. @objc func segmentedControlLongPressed(_ gesture: UIGestureRecognizer) {
  239. switch gesture.state {
  240. case .possible:
  241. print("LongPressGesture Possible!")
  242. break
  243. case .began:
  244. print("LongPressGesture Began!")
  245. isLongPressActivated = true
  246. longPressDidBegin(gesture)
  247. break
  248. case .changed:
  249. print("LongPressGesture Changed!")
  250. break
  251. case .ended:
  252. print("LongPressGesture Ended!")
  253. isLongPressActivated = false
  254. break
  255. case .cancelled:
  256. print("LongPressGesture Cancelled!")
  257. isLongPressActivated = false
  258. break
  259. case .failed:
  260. print("LongPressGesture Failed!")
  261. isLongPressActivated = false
  262. break
  263. }
  264. }
  265. fileprivate func locationIndex(for gesture: UIGestureRecognizer) -> Int? {
  266. let longPressLocation = gesture.location(in: self)
  267. if !bounds.contains(longPressLocation) {
  268. return nil
  269. }
  270. if singleSegmentWidth() == 0 {
  271. return nil
  272. }
  273. let longPressIndex = Int((longPressLocation.x + scrollView.contentOffset.x) / singleSegmentWidth())
  274. return longPressIndex
  275. }
  276. fileprivate func longPressDidBegin(_ gesture: UIGestureRecognizer) {
  277. if let longPressIndex = locationIndex(for: gesture) {
  278. if longPressIndex != selectedIndex && !isUnselectedSegmentsLongPressEnabled {
  279. return
  280. }
  281. if 0..<segmentsCount() ~= longPressIndex {
  282. if let delegate = delegate {
  283. delegate.segmentedControl(self, didLongPressIndex: longPressIndex)
  284. }
  285. }
  286. }
  287. }
  288. }
  289. public extension SegmentedControl {
  290. // MARK: - Drawing
  291. fileprivate func drawTitles() {
  292. for (index, title) in titles.enumerated() {
  293. let titleSize = sizeForAttributedString(title)
  294. let xPosition: CGFloat = {
  295. return singleSegmentWidth() * CGFloat(index) + (singleSegmentWidth() - titleSize.width) / 2
  296. }()
  297. let yPosition: CGFloat = {
  298. let yPosition = (frame.height - titleSize.height) / 2
  299. var yPositionOffset: CGFloat = 0
  300. switch selectionIndicatorStyle {
  301. case .top:
  302. yPositionOffset = selectionIndicatorHeight / 2
  303. case .bottom:
  304. yPositionOffset = -selectionIndicatorHeight / 2
  305. default:
  306. break
  307. }
  308. return round(yPosition + yPositionOffset)
  309. }()
  310. let attachedIcon = index == selectedIndex ? selectedTitleAttachedIcon(at: index) : titleAttachedIcon(at: index)
  311. var attachedIconRect = CGRect.zero
  312. let titleRect: CGRect = {
  313. var titleRect = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: titleSize)
  314. if let attachedIcon = attachedIcon {
  315. let addedWidth = attachedIcon.size.width + titleAttachedIconPositionOffset.x
  316. titleRect.origin.x -= addedWidth / 2
  317. let xPositionOfAttachedIcon = titleRect.origin.x + titleRect.width + titleAttachedIconPositionOffset.x
  318. let yPositionOfAttachedIcon: CGFloat = {
  319. let yPositionOfAttachedIcon = (frame.height - attachedIcon.size.height) / 2
  320. var yPositionOffset = titleAttachedIconPositionOffset.y
  321. switch selectionIndicatorStyle {
  322. case .top:
  323. yPositionOffset += selectionIndicatorHeight / 2
  324. case .bottom:
  325. yPositionOffset += -selectionIndicatorHeight / 2
  326. default:
  327. break
  328. }
  329. return round(yPositionOfAttachedIcon + yPositionOffset)
  330. }()
  331. attachedIconRect = CGRect(x: round(xPositionOfAttachedIcon), y: round(yPositionOfAttachedIcon), width: round(attachedIcon.size.width), height: round(attachedIcon.size.height))
  332. }
  333. return CGRect(x: round(titleRect.origin.x), y: round(titleRect.origin.y), width: round(titleRect.width), height: round(titleRect.height))
  334. }()
  335. let titleString: NSAttributedString = {
  336. if index == selectedIndex {
  337. if let selectedTitle = selectedTitle(at: index) {
  338. return selectedTitle
  339. }
  340. }
  341. return title
  342. }()
  343. let titleLayer: CATextLayer = {
  344. let titleLayer = CATextLayer()
  345. titleLayer.frame = titleRect
  346. titleLayer.alignmentMode = CATextLayerAlignmentMode.center
  347. if #available(iOS 10.0, *) {
  348. titleLayer.truncationMode = CATextLayerTruncationMode.none
  349. } else {
  350. titleLayer.truncationMode = CATextLayerTruncationMode.end
  351. }
  352. titleLayer.string = titleString
  353. titleLayer.contentsScale = UIScreen.main.scale
  354. return titleLayer
  355. }()
  356. if let attachedIcon = attachedIcon {
  357. let attachedIconLayer = CALayer()
  358. attachedIconLayer.frame = attachedIconRect
  359. attachedIconLayer.contents = attachedIcon.cgImage
  360. scrollView.layer.addSublayer(attachedIconLayer)
  361. }
  362. scrollView.layer.addSublayer(titleLayer)
  363. }
  364. }
  365. fileprivate func drawImages() {
  366. for (index, image) in images.enumerated() {
  367. let xPosition: CGFloat = {
  368. return singleSegmentWidth() * CGFloat(index) + (singleSegmentWidth() - image.size.width) / 2
  369. }()
  370. let yPosition: CGFloat = {
  371. let yPosition = (frame.height - image.size.height) / 2
  372. var yPositionOffset: CGFloat = 0
  373. switch selectionIndicatorStyle {
  374. case .top:
  375. yPositionOffset = selectionIndicatorHeight / 2
  376. case .bottom:
  377. yPositionOffset = -selectionIndicatorHeight / 2
  378. default:
  379. break
  380. }
  381. return round(yPosition + yPositionOffset)
  382. }()
  383. let imageRect: CGRect = {
  384. let imageRect = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: image.size)
  385. return CGRect(x: round(imageRect.origin.x), y: round(imageRect.origin.y), width: round(imageRect.width), height: round(imageRect.height))
  386. }()
  387. let contents: CGImage? = {
  388. if index == selectedIndex {
  389. if let selectedImage = selectedImage(at: index) {
  390. return selectedImage.cgImage
  391. }
  392. }
  393. return image.cgImage
  394. }()
  395. let imageLayer: CALayer = {
  396. let imageLayer = CALayer()
  397. imageLayer.frame = imageRect
  398. imageLayer.contents = contents
  399. return imageLayer
  400. }()
  401. scrollView.layer.addSublayer(imageLayer)
  402. }
  403. }
  404. fileprivate func drawSelectionBox() {
  405. selectionBoxLayer.frame = frameForSelectionBox()
  406. selectionBoxLayer.cornerRadius = selectionBoxCornerRadius
  407. if selectionBoxLayer.superlayer == nil {
  408. scrollView.layer.insertSublayer(selectionBoxLayer, at: 0)
  409. }
  410. }
  411. fileprivate func drawSelectionIndicator() {
  412. selectionIndicatorLayer.frame = frameForSelectionIndicator()
  413. if selectionBoxLayer.superlayer == nil {
  414. if let _ = selectionIndicatorLayer.superlayer {
  415. scrollView.layer.insertSublayer(selectionIndicatorLayer, above: selectionBoxLayer)
  416. } else {
  417. scrollView.layer.insertSublayer(selectionIndicatorLayer, at: 0)
  418. }
  419. }
  420. }
  421. }
  422. public extension SegmentedControl {
  423. // MARK: - Helper
  424. fileprivate func sizeForAttributedString(_ attributedString: NSAttributedString) -> CGSize {
  425. let size = attributedString.size()
  426. return CGRect(origin: CGPoint.zero, size: size).integral.size
  427. }
  428. fileprivate func selectedImage(at index: Int) -> UIImage? {
  429. if let selectedImages = selectedImages {
  430. if 0..<selectedImages.count ~= index {
  431. return selectedImages[index]
  432. }
  433. }
  434. return nil
  435. }
  436. fileprivate func selectedTitle(at index: Int) -> NSAttributedString? {
  437. if let selectedTitles = selectedTitles {
  438. if 0..<selectedTitles.count ~= index {
  439. return selectedTitles[index]
  440. }
  441. }
  442. return nil
  443. }
  444. fileprivate func titleAttachedIcon(at index: Int) -> UIImage? {
  445. if let titleAttachedIcons = titleAttachedIcons {
  446. if 0..<titleAttachedIcons.count ~= index {
  447. return titleAttachedIcons[index]
  448. }
  449. }
  450. return nil
  451. }
  452. fileprivate func selectedTitleAttachedIcon(at index: Int) -> UIImage? {
  453. if let selectedTitleAttachedIcons = selectedTitleAttachedIcons {
  454. if 0..<selectedTitleAttachedIcons.count ~= index {
  455. return selectedTitleAttachedIcons[index]
  456. }
  457. }
  458. return nil
  459. }
  460. fileprivate func segmentsCount() -> Int {
  461. switch style {
  462. case .text:
  463. return titles.count
  464. case .image:
  465. return images.count
  466. }
  467. }
  468. fileprivate func frameForSelectionBox() -> CGRect {
  469. if selectionBoxStyle == .none {
  470. return CGRect.zero
  471. }
  472. let xPosition: CGFloat = {
  473. return singleSegmentWidth() * CGFloat(selectedIndex)
  474. }()
  475. let fullRect = CGRect(x: xPosition, y: 0, width: singleSegmentWidth(), height: frame.height)
  476. let boxRect = CGRect(x: fullRect.origin.x + selectionBoxEdgeInsets.left,
  477. y: fullRect.origin.y + selectionBoxEdgeInsets.top,
  478. width: fullRect.width - (selectionBoxEdgeInsets.left + selectionBoxEdgeInsets.right),
  479. height: fullRect.height - (selectionBoxEdgeInsets.top + selectionBoxEdgeInsets.bottom))
  480. return boxRect
  481. }
  482. fileprivate func frameForSelectionIndicator() -> CGRect {
  483. if selectionIndicatorStyle == .none {
  484. return CGRect.zero
  485. }
  486. let xPosition: CGFloat = {
  487. return singleSegmentWidth() * CGFloat(selectedIndex)
  488. }()
  489. let yPosition: CGFloat = {
  490. switch selectionIndicatorStyle {
  491. case .bottom:
  492. return frame.height - selectionIndicatorHeight
  493. case .top:
  494. return 0
  495. default:
  496. return 0
  497. }
  498. }()
  499. let fullRect = CGRect(x: xPosition, y: yPosition, width: singleSegmentWidth(), height: selectionIndicatorHeight)
  500. let indicatorRect = CGRect(x: fullRect.origin.x + selectionIndicatorEdgeInsets.left,
  501. y: fullRect.origin.y + selectionIndicatorEdgeInsets.top,
  502. width: fullRect.width - (selectionIndicatorEdgeInsets.left + selectionIndicatorEdgeInsets.right),
  503. height: fullRect.height - (selectionIndicatorEdgeInsets.top + selectionIndicatorEdgeInsets.bottom))
  504. return indicatorRect
  505. }
  506. fileprivate func rectForSelectedIndex() -> CGRect {
  507. return CGRect(x: singleSegmentWidth() * CGFloat(selectedIndex), y: 0, width: singleSegmentWidth(), height: frame.height)
  508. }
  509. fileprivate func singleSegmentWidth() -> CGFloat {
  510. func defaultSegmentWidth() -> CGFloat {
  511. if segmentsCount() == 0 {
  512. return 0
  513. }
  514. var segmentWidth = frame.width / CGFloat(segmentsCount())
  515. if let minimumSegmentWidth = minimumSegmentWidth {
  516. if segmentWidth < minimumSegmentWidth {
  517. segmentWidth = minimumSegmentWidth
  518. }
  519. }
  520. if let maximumSegmentWidth = maximumSegmentWidth {
  521. if segmentWidth > maximumSegmentWidth {
  522. segmentWidth = maximumSegmentWidth
  523. }
  524. }
  525. return segmentWidth
  526. }
  527. if let segmentWidth = segmentWidth {
  528. return segmentWidth
  529. }
  530. return defaultSegmentWidth()
  531. }
  532. fileprivate func totalSegmentsWidth() -> CGFloat {
  533. return CGFloat(segmentsCount()) * singleSegmentWidth()
  534. }
  535. }