ContextMenuController.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import AVKit
  2. import AVFoundation
  3. import SDWebImage
  4. import DcCore
  5. protocol ContextMenuItem {
  6. var msg: DcMsg { get set }
  7. var thumbnailImage: UIImage? { get set }
  8. }
  9. // MARK: - ContextMenuController
  10. class ContextMenuController: UIViewController {
  11. let item: ContextMenuItem
  12. var msg: DcMsg {
  13. return item.msg
  14. }
  15. init(item: ContextMenuItem) {
  16. self.item = item
  17. super.init(nibName: nil, bundle: nil)
  18. }
  19. required init?(coder: NSCoder) {
  20. fatalError("init(coder:) has not been implemented")
  21. }
  22. // MARK: - lifecycle
  23. override func viewDidLoad() {
  24. super.viewDidLoad()
  25. let viewType = msg.viewtype
  26. var thumbnailView: UIView?
  27. switch viewType {
  28. case .image:
  29. thumbnailView = makeImageView(image: msg.image)
  30. case .video:
  31. thumbnailView = makeVideoView(videoUrl: msg.fileURL)
  32. case .gif:
  33. thumbnailView = makeGifView(gifImage: item.thumbnailImage)
  34. default:
  35. return
  36. }
  37. guard let contentView = thumbnailView else {
  38. return
  39. }
  40. view.addSubview(contentView)
  41. contentView.translatesAutoresizingMaskIntoConstraints = false
  42. NSLayoutConstraint.activate([
  43. contentView.leftAnchor.constraint(equalTo: view.leftAnchor),
  44. contentView.rightAnchor.constraint(equalTo: view.rightAnchor),
  45. contentView.topAnchor.constraint(equalTo: view.topAnchor),
  46. contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  47. ])
  48. }
  49. // MARK: - thumbnailView creation
  50. private func makeGifView(gifImage: UIImage?) -> UIView? {
  51. let view = SDAnimatedImageView()
  52. view.contentMode = .scaleAspectFill
  53. view.clipsToBounds = true
  54. view.backgroundColor = DcColors.defaultBackgroundColor
  55. if let image = gifImage {
  56. setPreferredContentSize(for: image)
  57. }
  58. view.image = gifImage
  59. return view
  60. }
  61. private func makeImageView(image: UIImage?) -> UIView? {
  62. guard let image = image else {
  63. safe_fatalError("unexpected nil value")
  64. return nil
  65. }
  66. let imageView = UIImageView()
  67. imageView.clipsToBounds = true
  68. imageView.contentMode = .scaleAspectFill
  69. imageView.image = image
  70. setPreferredContentSize(for: image)
  71. return imageView
  72. }
  73. private func makeVideoView(videoUrl: URL?) -> UIView? {
  74. guard let videoUrl = videoUrl, let videoSize = item.thumbnailImage?.size else { return nil }
  75. let player = AVPlayer(url: videoUrl)
  76. let playerController = AVPlayerViewController()
  77. addChild(playerController)
  78. view.addSubview(playerController.view)
  79. playerController.didMove(toParent: self)
  80. playerController.view.backgroundColor = .darkGray
  81. playerController.view.clipsToBounds = true
  82. playerController.player = player
  83. playerController.showsPlaybackControls = false
  84. player.play()
  85. // truncate edges on top/bottom or sides
  86. let resizedHeightFactor = view.frame.height / videoSize.height
  87. let resizedWidthFactor = view.frame.width / videoSize.width
  88. let effectiveResizeFactor = min(resizedWidthFactor, resizedHeightFactor)
  89. let maxHeight = videoSize.height * effectiveResizeFactor
  90. let maxWidth = videoSize.width * effectiveResizeFactor
  91. let size = CGSize(width: maxWidth, height: maxHeight)
  92. preferredContentSize = size
  93. return playerController.view
  94. }
  95. private func setPreferredContentSize(for image: UIImage) {
  96. let width = view.bounds.width
  97. let height = image.size.height * (width / image.size.width)
  98. self.preferredContentSize = CGSize(width: width, height: height)
  99. }
  100. }
  101. class ContextMenuProvider {
  102. var menu: [ContextMenuItem] = []
  103. init(menu: [ContextMenuItem] = []) {
  104. self.menu = menu
  105. }
  106. func setMenu(_ menu: [ContextMenuItem]) {
  107. self.menu = menu
  108. }
  109. // iOS 12- action menu
  110. var menuItems: [UIMenuItem] {
  111. return menu
  112. .filter({ $0.title != nil && $0.action != nil })
  113. .map({ return UIMenuItem(title: $0.title!, action: $0.action!) })
  114. }
  115. // iOS13+ action menu
  116. @available(iOS 13, *)
  117. func actionProvider(title: String = "", image: UIImage? = nil, identifier: UIMenu.Identifier? = nil, indexPath: IndexPath) -> UIMenu {
  118. var children: [UIMenuElement] = []
  119. for item in menu {
  120. //we only support 1 submenu layer for now
  121. if let subMenus = item.children {
  122. var submenuChildren: [UIMenuElement] = []
  123. for submenuItem in subMenus {
  124. submenuChildren.append(generateUIAction(item: submenuItem, indexPath: indexPath))
  125. }
  126. let submenu = UIMenu(title: "", options: .displayInline, children: submenuChildren)
  127. children.append(submenu)
  128. } else {
  129. children.append(generateUIAction(item: item, indexPath: indexPath))
  130. }
  131. }
  132. return UIMenu(
  133. title: title,
  134. image: image,
  135. identifier: identifier,
  136. children: children
  137. )
  138. }
  139. @available(iOS 13, *)
  140. private func generateUIAction(item: ContextMenuItem, indexPath: IndexPath) -> UIAction {
  141. let image = UIImage(systemName: item.imageName ?? "") ??
  142. UIImage(named: item.imageName ?? "")
  143. let action = UIAction(
  144. title: item.title ?? "",
  145. image: image,
  146. handler: { _ in item.onPerform?(indexPath) }
  147. )
  148. if item.isDestructive ?? false {
  149. action.attributes = [.destructive]
  150. }
  151. return action
  152. }
  153. func canPerformAction(action: Selector) -> Bool {
  154. return !menu.filter {
  155. $0.action == action
  156. }.isEmpty
  157. }
  158. func performAction(action: Selector, indexPath: IndexPath) {
  159. menu.filter {
  160. $0.action == action
  161. }.first?.onPerform?(indexPath)
  162. }
  163. }
  164. extension ContextMenuProvider {
  165. struct ContextMenuItem {
  166. var title: String?
  167. var imageName: String?
  168. let isDestructive: Bool?
  169. var action: Selector?
  170. var onPerform: ((IndexPath) -> Void)?
  171. var children: [ContextMenuItem]?
  172. init(title: String, imageName: String, isDestructive: Bool = false, action: Selector, onPerform: ((IndexPath) -> Void)?) {
  173. self.title = title
  174. self.imageName = imageName
  175. self.isDestructive = isDestructive
  176. self.action = action
  177. self.onPerform = onPerform
  178. }
  179. init(submenuitems: [ContextMenuItem]) {
  180. title = nil
  181. imageName = nil
  182. isDestructive = nil
  183. action = nil
  184. onPerform = nil
  185. children = submenuitems
  186. }
  187. }
  188. }