ActionSheetCell.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /*****************************************************************************
  2. * ActionSheetCell.swift
  3. *
  4. * Copyright © 2018 VLC authors and VideoLAN
  5. * Copyright © 2018 Videolabs
  6. *
  7. * Authors: Soomin Lee <bubu@mikan.io>
  8. *
  9. * Refer to the COPYING file of the official project for license.
  10. *****************************************************************************/
  11. enum ActionSheetCellAccessoryType {
  12. case toggleSwitch
  13. case checkmark
  14. case disclosureChevron
  15. }
  16. class ActionSheetCellImageView: UIImageView {
  17. override var image: UIImage? {
  18. didSet {
  19. super.image = image
  20. isHidden = false
  21. }
  22. }
  23. override init(image: UIImage? = nil) {
  24. super.init(image: image)
  25. isHidden = true
  26. }
  27. required init?(coder aDecoder: NSCoder) {
  28. fatalError("init(coder:) has not been implemented")
  29. }
  30. }
  31. /// Model that determines the layout presentation of the ActionSheetCell.
  32. @objc (VLCActionSheetCellModel)
  33. @objcMembers class ActionSheetCellModel: NSObject {
  34. var title: String
  35. var iconImage: UIImage?
  36. var viewToPresent: UIView?
  37. var accessoryType: ActionSheetCellAccessoryType
  38. var cellIdentifier: MediaPlayerActionSheetCellIdentifier?
  39. init(
  40. title: String,
  41. imageIdentifier: String,
  42. accessoryType: ActionSheetCellAccessoryType = .checkmark,
  43. viewToPresent: UIView? = nil,
  44. cellIdentifier: MediaPlayerActionSheetCellIdentifier? = nil) {
  45. self.title = title
  46. iconImage = UIImage(named: imageIdentifier)?.withRenderingMode(.alwaysTemplate)
  47. self.accessoryType = accessoryType
  48. self.viewToPresent = viewToPresent
  49. self.cellIdentifier = cellIdentifier
  50. }
  51. }
  52. @objc (VLCActionSheetCellDelegate)
  53. protocol ActionSheetCellDelegate {
  54. func actionSheetCellShouldUpdateColors() -> Bool
  55. func actionSheetCellDidToggleSwitch(for cell: ActionSheetCell, state: Bool)
  56. }
  57. @objc(VLCActionSheetCell)
  58. class ActionSheetCell: UICollectionViewCell {
  59. /// UIViewController to present on cell selection
  60. weak var viewToPresent: UIView?
  61. /// Rightmost accessory view that the cell should use. Default `checkmark`.
  62. /// If `viewControllerToPresent` is set, defaults to `disclosureChevron`, otherwise `checkmark` is main default.
  63. private(set) var accessoryView = UIView ()
  64. weak var delegate: ActionSheetCellDelegate?
  65. var identifier: MediaPlayerActionSheetCellIdentifier?
  66. @objc static var identifier: String {
  67. return String(describing: self)
  68. }
  69. override var isSelected: Bool {
  70. didSet {
  71. updateColors()
  72. // only checkmarks should be hidden if they arent selected
  73. if accessoryType == .checkmark {
  74. accessoryView.isHidden = !isSelected
  75. }
  76. }
  77. }
  78. let icon: ActionSheetCellImageView = {
  79. let icon = ActionSheetCellImageView()
  80. icon.translatesAutoresizingMaskIntoConstraints = false
  81. icon.setContentHuggingPriority(.required, for: .horizontal)
  82. icon.contentMode = .scaleAspectFit
  83. return icon
  84. }()
  85. let name: UILabel = {
  86. let name = UILabel()
  87. name.textColor = PresentationTheme.current.colors.cellTextColor
  88. name.font = UIFont.systemFont(ofSize: 15)
  89. name.translatesAutoresizingMaskIntoConstraints = false
  90. return name
  91. }()
  92. lazy private var accessoryTypeImageView: UIImageView = {
  93. let imageView = UIImageView()
  94. imageView.contentMode = .scaleAspectFit
  95. imageView.backgroundColor = .none
  96. imageView.setContentHuggingPriority(.required, for: .horizontal)
  97. imageView.translatesAutoresizingMaskIntoConstraints = false
  98. return imageView
  99. }()
  100. lazy private var toggleSwitch: UISwitch = {
  101. let toggleSwitch = UISwitch()
  102. toggleSwitch.onTintColor = .orange
  103. toggleSwitch.translatesAutoresizingMaskIntoConstraints = false
  104. toggleSwitch.addTarget(self, action: #selector(switchToggled(_:)), for: .valueChanged)
  105. return toggleSwitch
  106. }()
  107. private(set) var accessoryType: ActionSheetCellAccessoryType = .checkmark {
  108. didSet {
  109. switch accessoryType {
  110. case .checkmark:
  111. accessoryTypeImageView.image = UIImage(named: "checkmark")?.withRenderingMode(.alwaysTemplate)
  112. add(view: accessoryTypeImageView, to: accessoryView)
  113. case .disclosureChevron:
  114. accessoryTypeImageView.image = UIImage(named: "disclosureChevron")?.withRenderingMode(.alwaysTemplate)
  115. add(view: accessoryTypeImageView, to: accessoryView)
  116. case .toggleSwitch:
  117. add(view: toggleSwitch, to: accessoryView)
  118. }
  119. if accessoryType == .checkmark {
  120. accessoryView.isHidden = !isSelected
  121. } else {
  122. accessoryView.isHidden = false
  123. }
  124. }
  125. }
  126. let stackView: UIStackView = {
  127. let stackView = UIStackView()
  128. stackView.spacing = 15.0
  129. stackView.axis = .horizontal
  130. stackView.alignment = .center
  131. stackView.translatesAutoresizingMaskIntoConstraints = false
  132. return stackView
  133. }()
  134. override init(frame: CGRect) {
  135. super.init(frame: frame)
  136. setupViews()
  137. }
  138. required init?(coder aDecoder: NSCoder) {
  139. super.init(coder: aDecoder)
  140. setupViews()
  141. }
  142. convenience init(withCellModel model: ActionSheetCellModel) {
  143. self.init()
  144. configure(withModel: model)
  145. setupViews()
  146. }
  147. private func updateColors() {
  148. let shouldUpdateColors = delegate?.actionSheetCellShouldUpdateColors() ?? true
  149. let colors = PresentationTheme.current.colors
  150. if shouldUpdateColors {
  151. name.textColor = isSelected ? colors.orangeUI : colors.cellTextColor
  152. tintColor = isSelected ? colors.orangeUI : colors.cellDetailTextColor
  153. }
  154. if accessoryType != .toggleSwitch {
  155. accessoryView.tintColor = isSelected && accessoryType == .checkmark ? colors.orangeUI : colors.cellDetailTextColor
  156. }
  157. }
  158. @objc private func switchToggled(_ sender: UISwitch) {
  159. delegate?.actionSheetCellDidToggleSwitch(for: self, state: sender.isOn)
  160. }
  161. override func prepareForReuse() {
  162. super.prepareForReuse()
  163. toggleSwitch.removeFromSuperview()
  164. accessoryType = .checkmark
  165. updateColors()
  166. }
  167. private func add(view: UIView, to parentView: UIView) {
  168. view.translatesAutoresizingMaskIntoConstraints = false
  169. parentView.subviews.forEach { $0.removeFromSuperview() }
  170. parentView.addSubview(view)
  171. NSLayoutConstraint.activate([
  172. view.topAnchor.constraint(equalTo: parentView.topAnchor),
  173. view.bottomAnchor.constraint(equalTo: parentView.bottomAnchor),
  174. view.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
  175. view.trailingAnchor.constraint(equalTo: parentView.trailingAnchor)
  176. ])
  177. }
  178. func configure(withModel model: ActionSheetCellModel) {
  179. if model.accessoryType == .disclosureChevron {
  180. assert(model.viewToPresent != nil, "ActionSheetCell: Cell with disclosure chevron must have accompanying presentable UIView")
  181. }
  182. name.text = model.title
  183. icon.image = model.iconImage
  184. viewToPresent = model.viewToPresent
  185. identifier = model.cellIdentifier
  186. // disclosure chevron is set as the default accessoryView if a viewController is present
  187. accessoryType = model.viewToPresent != nil ? .disclosureChevron : model.accessoryType
  188. }
  189. func setToggleSwitch(state: Bool) {
  190. if accessoryType == .toggleSwitch {
  191. toggleSwitch.isOn = state
  192. }
  193. }
  194. private func setupViews() {
  195. backgroundColor = PresentationTheme.current.colors.background
  196. stackView.addArrangedSubview(icon)
  197. stackView.addArrangedSubview(name)
  198. stackView.addArrangedSubview(accessoryView)
  199. addSubview(stackView)
  200. // property observers only trigger after the first time the values are set.
  201. // allow the didSet to set the checkmark image
  202. accessoryType = .checkmark
  203. var guide: LayoutAnchorContainer = self
  204. if #available(iOS 11.0, *) {
  205. guide = safeAreaLayoutGuide
  206. }
  207. NSLayoutConstraint.activate([
  208. stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 20),
  209. stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -20),
  210. stackView.heightAnchor.constraint(equalTo: heightAnchor),
  211. stackView.topAnchor.constraint(equalTo: topAnchor)
  212. ])
  213. }
  214. }