ActionSheet.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. //
  2. // ActionSheet.swift
  3. // Sheeeeeeeeet
  4. //
  5. // Created by Daniel Saidi on 2017-11-26.
  6. // Copyright © 2017 Daniel Saidi. All rights reserved.
  7. //
  8. /*
  9. This is the main class in the Sheeeeeeeeet library. You can
  10. use it to create action sheets and present them in any view
  11. controller, from any source view or bar button item.
  12. To create an action sheet, just call the initializer with a
  13. list of items and buttons and a block that should be called
  14. whenever an item is selected.
  15. ## Items
  16. You provide an action sheet with a collection of items when
  17. you create it. The sheet will automatically split the items
  18. into items and buttons. You can also create an action sheet
  19. with an empty item collection, then call `setup(items:)` at
  20. a later time. This is sometimes required if you must create
  21. the action sheet before you can create the items.
  22. ## Presentation
  23. You can inject a custom presenter if you want to change how
  24. the sheet is presented and dismissed. The default presenter
  25. for iPhone devices is `ActionSheetStandardPresenter`, while
  26. iPad devices get `ActionSheetPopoverPresenter` instead.
  27. ## Subclassing
  28. `ActionSheet` can be subclassed, which may be nice whenever
  29. you want to use your own domain model. For instance, if you
  30. want to present a list of `Food` items, you should create a
  31. `FoodActionSheet` sheet, then populate it with `Food` items.
  32. The selected value will then be of the type `Food`. You can
  33. either override the initializers or the `setup` function to
  34. change how you populate the sheet with items.
  35. ## Appearance
  36. Sheeeeeeeeet's action sheet appearance if easily customized.
  37. To change the global appearance for every sheet in your app,
  38. just modify `ActionSheetAppearance.standard`. To change the
  39. appearance of a single action sheet, modify the `appearance`
  40. property. To change the appearance of a single item, modify
  41. its `customAppearance` property.
  42. ## Handling item selections
  43. The `selectAction` is triggered when a user taps an item in
  44. the action sheet. It provides you with the action sheet and
  45. the selected item. It is very important to use `[weak self]`
  46. in this block to avoid memory leaks.
  47. ## Handling item taps
  48. Action sheets receive a call to `handleTap(on:)` every time
  49. an item is tapped. You can override it when you create your
  50. own action sheet subclasses, but you probably shouldn't.
  51. */
  52. import UIKit
  53. open class ActionSheet: UIViewController {
  54. // MARK: - Initialization
  55. public init(
  56. items: [ActionSheetItem],
  57. presenter: ActionSheetPresenter = ActionSheet.defaultPresenter,
  58. action: @escaping SelectAction) {
  59. self.presenter = presenter
  60. selectAction = action
  61. super.init(nibName: ActionSheet.className, bundle: Bundle(for: ActionSheet.self))
  62. setup(items: items)
  63. setup()
  64. }
  65. public required init?(coder aDecoder: NSCoder) {
  66. presenter = ActionSheet.defaultPresenter
  67. selectAction = { _, _ in print("itemSelectAction is not set") }
  68. super.init(coder: aDecoder)
  69. setup()
  70. }
  71. deinit { print("\(type(of: self)) deinit") }
  72. // MARK: - Setup
  73. open func setup() {}
  74. open func setup(items: [ActionSheetItem]) {
  75. self.items = items.filter { !($0 is ActionSheetButton) }
  76. buttons = items.compactMap { $0 as? ActionSheetButton }
  77. reloadData()
  78. }
  79. @available(*, deprecated, message: "setupItemsAndButtons(with:) is deprecated. Use setup(items:) instead")
  80. open func setupItemsAndButtons(with items: [ActionSheetItem]) {
  81. setup(items: items)
  82. }
  83. // MARK: - View Controller Lifecycle
  84. open override func viewDidLayoutSubviews() {
  85. super.viewDidLayoutSubviews()
  86. refresh()
  87. }
  88. // MARK: - Typealiases
  89. public typealias SelectAction = (ActionSheet, ActionSheetItem) -> ()
  90. // MARK: - Properties
  91. open var appearance = ActionSheetAppearance(copy: .standard)
  92. public let presenter: ActionSheetPresenter
  93. public var selectAction: SelectAction
  94. @available(*, deprecated, message: "itemSelectAction is deprecated. Use selectAction instead")
  95. open var itemSelectAction: SelectAction { return selectAction }
  96. // MARK: - Margin Outlets
  97. @IBOutlet weak var topMargin: NSLayoutConstraint?
  98. @IBOutlet weak var leftMargin: NSLayoutConstraint?
  99. @IBOutlet weak var rightMargin: NSLayoutConstraint?
  100. @IBOutlet weak var bottomMargin: NSLayoutConstraint?
  101. // MARK: - View Outlets
  102. @IBOutlet weak var backgroundView: UIView?
  103. @IBOutlet weak var stackView: UIStackView?
  104. // MARK: - Header Properties
  105. open var headerView: UIView? {
  106. didSet { refresh() }
  107. }
  108. @IBOutlet weak var headerViewContainer: UIView? {
  109. didSet {
  110. headerViewContainer?.backgroundColor = .clear
  111. refreshHeaderVisibility()
  112. }
  113. }
  114. @IBOutlet weak var headerViewContainerHeight: NSLayoutConstraint! {
  115. didSet { refreshHeaderVisibility() }
  116. }
  117. // MARK: - Item Properties
  118. public var items = [ActionSheetItem]()
  119. public var itemsHeight: CGFloat { return totalHeight(for: items) }
  120. public lazy var itemHandler = ActionSheetItemHandler(actionSheet: self, itemType: .items)
  121. @IBOutlet weak var itemsTableView: UITableView? {
  122. didSet { setup(itemsTableView, with: itemHandler) }
  123. }
  124. @IBOutlet weak var itemsTableViewHeight: NSLayoutConstraint?
  125. // MARK: - Button Properties
  126. public var buttons = [ActionSheetButton]()
  127. public var buttonsHeight: CGFloat { return totalHeight(for: buttons) }
  128. public lazy var buttonHandler = ActionSheetItemHandler(actionSheet: self, itemType: .buttons)
  129. @IBOutlet weak var buttonsTableView: UITableView? {
  130. didSet {
  131. setup(buttonsTableView, with: buttonHandler)
  132. refreshButtonsVisibility()
  133. }
  134. }
  135. @IBOutlet weak var buttonsTableViewHeight: NSLayoutConstraint? {
  136. didSet { refreshButtonsVisibility() }
  137. }
  138. // MARK: - Presentation Functions
  139. open func dismiss(completion: @escaping () -> () = {}) {
  140. presenter.dismiss { completion() }
  141. }
  142. open func present(in vc: UIViewController, from view: UIView?, completion: @escaping () -> () = {}) {
  143. refresh()
  144. presenter.present(sheet: self, in: vc.rootViewController, from: view, completion: completion)
  145. }
  146. open func present(in vc: UIViewController, from barButtonItem: UIBarButtonItem, completion: @escaping () -> () = {}) {
  147. refresh()
  148. presenter.present(sheet: self, in: vc.rootViewController, from: barButtonItem, completion: completion)
  149. }
  150. // MARK: - Refresh Functions
  151. open func refresh() {
  152. applyRoundCorners()
  153. refreshHeader()
  154. refreshItems()
  155. refreshButtons()
  156. stackView?.spacing = appearance.groupMargins
  157. presenter.refreshActionSheet()
  158. }
  159. open func refreshHeader() {
  160. refreshHeaderVisibility()
  161. let height = headerView?.frame.height ?? 0
  162. headerViewContainerHeight?.constant = height
  163. guard let view = headerView else { return }
  164. headerViewContainer?.addSubviewToFill(view)
  165. }
  166. open func refreshHeaderVisibility() {
  167. headerViewContainer?.isHidden = headerView == nil
  168. }
  169. open func refreshItems() {
  170. items.forEach { $0.applyAppearance(appearance) }
  171. itemsTableView?.separatorColor = appearance.itemsSeparatorColor
  172. itemsTableViewHeight?.constant = itemsHeight
  173. }
  174. open func refreshButtons() {
  175. refreshButtonsVisibility()
  176. buttons.forEach { $0.applyAppearance(appearance) }
  177. buttonsTableView?.separatorColor = appearance.buttonsSeparatorColor
  178. buttonsTableViewHeight?.constant = buttonsHeight
  179. }
  180. open func refreshButtonsVisibility() {
  181. buttonsTableView?.isHidden = buttons.count == 0
  182. }
  183. // MARK: - Protected Functions
  184. open func handleTap(on item: ActionSheetItem) {
  185. reloadData()
  186. guard item.tapBehavior == .dismiss else { return selectAction(self, item) }
  187. DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
  188. self.dismiss { self.selectAction(self, item) }
  189. }
  190. }
  191. open func margin(at margin: ActionSheetMargin) -> CGFloat {
  192. let minimum = appearance.contentInset
  193. return margin.value(in: view.superview, minimum: minimum)
  194. }
  195. open func reloadData() {
  196. itemsTableView?.reloadData()
  197. buttonsTableView?.reloadData()
  198. }
  199. }
  200. // MARK: - Private Functions
  201. private extension ActionSheet {
  202. func applyRoundCorners() {
  203. applyRoundCorners(to: headerView)
  204. applyRoundCorners(to: headerViewContainer)
  205. applyRoundCorners(to: itemsTableView)
  206. applyRoundCorners(to: buttonsTableView)
  207. }
  208. func applyRoundCorners(to view: UIView?) {
  209. view?.clipsToBounds = true
  210. view?.layer.cornerRadius = appearance.cornerRadius
  211. }
  212. func setup(_ tableView: UITableView?, with handler: ActionSheetItemHandler) {
  213. tableView?.delegate = handler
  214. tableView?.dataSource = handler
  215. tableView?.alwaysBounceVertical = false
  216. setupAppearance(for: tableView)
  217. }
  218. func setupAppearance(for tableView: UITableView?) {
  219. tableView?.estimatedRowHeight = 44
  220. tableView?.rowHeight = UITableView.automaticDimension
  221. tableView?.cellLayoutMarginsFollowReadableWidth = false
  222. }
  223. func totalHeight(for items: [ActionSheetItem]) -> CGFloat {
  224. return items.reduce(0) { $0 + $1.appearance.height }
  225. }
  226. }