ShareViewController.swift 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. //
  2. // ShareViewController.swift
  3. // AppBuilderShare
  4. //
  5. // Created by Qindi on 11/02/25.
  6. //
  7. import UIKit
  8. import Social
  9. import UniformTypeIdentifiers
  10. import AVFoundation
  11. import QuickLook
  12. class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UITextViewDelegate, QLPreviewControllerDataSource {
  13. let tableView = UITableView()
  14. let searchBar = UISearchBar()
  15. var contacts: [Contact] = []
  16. var filteredContacts: [Contact] = []
  17. var selectedContact: Contact!
  18. private var textViewBottomConstraint: NSLayoutConstraint?
  19. private var containerBottomConstraint: NSLayoutConstraint?
  20. private var heightTextView: NSLayoutConstraint?
  21. let vcHandleText = UIViewController()
  22. let vcHandleImage = UIViewController()
  23. let vcHandleVideo = UIViewController()
  24. let vcHandleFile = UIViewController()
  25. var textView = UITextView()
  26. var typeShareNum = 0
  27. var selectedImage: URL!
  28. var selectedVideo: URL!
  29. var selectedFile: URL!
  30. private var previewView: VideoPreviewView?
  31. let previewController = QLPreviewController()
  32. override func viewDidLoad() {
  33. super.viewDidLoad()
  34. loadCustomContacts()
  35. registerKeyboardNotifications()
  36. }
  37. deinit {
  38. NotificationCenter.default.removeObserver(self) // Remove observers when view controller deallocates
  39. }
  40. override func viewDidAppear(_ animated: Bool) {
  41. setupUI()
  42. }
  43. func setupUI() {
  44. let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelAction))
  45. cancelButton.tintColor = .label
  46. self.navigationItem.leftBarButtonItem = cancelButton
  47. // Search Bar (Right)
  48. searchBar.placeholder = "Search"
  49. searchBar.delegate = self
  50. searchBar.sizeToFit()
  51. self.navigationItem.titleView = searchBar
  52. self.navigationController?.navigationBar.backgroundColor = .systemBackground
  53. self.navigationController?.navigationBar.tintColor = .label
  54. // TableView Setup
  55. tableView.translatesAutoresizingMaskIntoConstraints = false
  56. tableView.delegate = self
  57. tableView.dataSource = self
  58. tableView.register(ContactCell.self, forCellReuseIdentifier: "ContactCell")
  59. tableView.separatorStyle = .singleLine
  60. view.addSubview(tableView)
  61. // Auto Layout Constraints
  62. NSLayoutConstraint.activate([
  63. tableView.topAnchor.constraint(equalTo: view.topAnchor),
  64. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  65. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  66. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  67. ])
  68. }
  69. func loadCustomContacts() {
  70. if let userDefaults = UserDefaults(suiteName: "group.nexilis.share"),
  71. let value = userDefaults.string(forKey: "shareContacts") {
  72. if let jsonData = value.data(using: .utf8) {
  73. do {
  74. if let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] {
  75. for json in jsonArray {
  76. let id = json["id"] as? String ?? ""
  77. let name = json["name"] as? String ?? ""
  78. let imageId = json["image"] as? String ?? ""
  79. let type = json["type"] as? Int ?? 0
  80. var profileImage = type == 0 ? UIImage(systemName: "person.fill") : UIImage(systemName: "bubble.right.fill")
  81. if !imageId.isEmpty {
  82. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
  83. let sharedFileURL = appGroupURL.appendingPathComponent(imageId)
  84. if FileManager.default.fileExists(atPath: sharedFileURL.path) {
  85. profileImage = UIImage(contentsOfFile: sharedFileURL.path)
  86. }
  87. }
  88. }
  89. contacts.append(Contact(id: id, name: name, profileImage: profileImage, imageId: imageId, typeContact: "\(type)"))
  90. }
  91. filteredContacts = contacts
  92. tableView.reloadData()
  93. }
  94. } catch {
  95. print("Error parsing JSON: \(error)")
  96. }
  97. }
  98. }
  99. }
  100. // TableView DataSource Methods
  101. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  102. return filteredContacts.count
  103. }
  104. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  105. return 60
  106. }
  107. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  108. let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
  109. cell.separatorInset = UIEdgeInsets(top: 0, left: 65, bottom: 0, right: 25)
  110. let contact = filteredContacts[indexPath.row]
  111. cell.configure(with: contact)
  112. return cell
  113. }
  114. // Handle Contact Selection
  115. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  116. tableView.deselectRow(at: indexPath, animated: true)
  117. selectedContact = filteredContacts[indexPath.row]
  118. handleSharedContent(selectedContact)
  119. }
  120. // Cancel Button Action
  121. @objc func cancelAction() {
  122. self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
  123. }
  124. @objc func sendAction() {
  125. if let userDefaults = UserDefaults(suiteName: "group.nexilis.share") {
  126. do {
  127. var dataShared: [String: Any] = [:]
  128. dataShared["typeShare"] = typeShareNum
  129. dataShared["typeContact"] = selectedContact.typeContact
  130. dataShared["idContact"] = selectedContact.id
  131. dataShared["data"] = textView.text
  132. if typeShareNum == TypeShare.image {
  133. let compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)"
  134. let thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)"
  135. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
  136. let sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
  137. let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
  138. try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
  139. try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.5)?.write(to: sharedImageURL)
  140. }
  141. dataShared["thumb"] = thumbName
  142. dataShared["image"] = compressedImageName
  143. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  144. if let jsonString = String(data: jsonData, encoding: .utf8) {
  145. userDefaults.set(jsonString, forKey: "sharedItem")
  146. userDefaults.synchronize()
  147. }
  148. } else if typeShareNum == TypeShare.video {
  149. let dataVideo = try? Data(contentsOf: selectedVideo)
  150. if let dataVideotoCompress = dataVideo {
  151. let sizeInKB = Double(dataVideotoCompress.count) / 1024.0
  152. let sizeOfVideo = sizeInKB / 1024.0
  153. if (sizeOfVideo > 10.0) {
  154. let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
  155. compressVideo(inputURL: selectedVideo,
  156. outputURL: compressedURL) { exportSession in
  157. guard let session = exportSession else {
  158. return
  159. }
  160. switch session.status {
  161. case .unknown:
  162. break
  163. case .waiting:
  164. break
  165. case .exporting:
  166. break
  167. case .completed:
  168. guard let compressedData = try? Data(contentsOf: compressedURL) else {
  169. return
  170. }
  171. self.sendVideoToMainApp(compressedData, dataShared)
  172. case .failed:
  173. break
  174. case .cancelled:
  175. break
  176. @unknown default:
  177. break
  178. }
  179. }
  180. return
  181. } else {
  182. self.sendVideoToMainApp(dataVideotoCompress, dataShared)
  183. }
  184. }
  185. } else if typeShareNum == TypeShare.file {
  186. let fileName = selectedFile.lastPathComponent
  187. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
  188. let sharedFileURL = appGroupURL.appendingPathComponent(fileName)
  189. try? Data(contentsOf: selectedFile).write(to: sharedFileURL)
  190. }
  191. dataShared["file"] = fileName
  192. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  193. if let jsonString = String(data: jsonData, encoding: .utf8) {
  194. userDefaults.set(jsonString, forKey: "sharedItem")
  195. userDefaults.synchronize()
  196. }
  197. } else {
  198. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  199. if let jsonString = String(data: jsonData, encoding: .utf8) {
  200. userDefaults.set(jsonString, forKey: "sharedItem")
  201. userDefaults.synchronize()
  202. }
  203. }
  204. } catch {
  205. }
  206. }
  207. self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
  208. }
  209. private func sendVideoToMainApp(_ data: Data, _ dataShared: [String: Any]) {
  210. do {
  211. var dataShared = dataShared
  212. let originalVideoName = self.selectedVideo.lastPathComponent
  213. let renamedVideoName = "Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName)"
  214. let thumbName = "THUMB_Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName)"
  215. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
  216. let sharedVideoURL = appGroupURL.appendingPathComponent(renamedVideoName)
  217. let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
  218. try? data.write(to: sharedVideoURL)
  219. let asset = AVURLAsset(url: self.selectedVideo, options: nil)
  220. let imgGenerator = AVAssetImageGenerator(asset: asset)
  221. imgGenerator.appliesPreferredTrackTransform = true
  222. let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
  223. let thumbnail = UIImage(cgImage: cgImage)
  224. try? thumbnail.jpegData(compressionQuality: 1.0)?.write(to: sharedThumbURL)
  225. dataShared["thumb"] = thumbName
  226. dataShared["video"] = renamedVideoName
  227. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  228. if let jsonString = String(data: jsonData, encoding: .utf8) {
  229. let userDefaults = UserDefaults(suiteName: "group.nexilis.share")
  230. userDefaults!.set(jsonString, forKey: "sharedItem")
  231. userDefaults!.synchronize()
  232. }
  233. }
  234. } catch {
  235. }
  236. }
  237. func compressVideo(inputURL: URL,
  238. outputURL: URL,
  239. handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
  240. let urlAsset = AVURLAsset(url: inputURL, options: nil)
  241. guard let exportSession = AVAssetExportSession(asset: urlAsset,
  242. presetName: AVAssetExportPresetMediumQuality) else {
  243. handler(nil)
  244. return
  245. }
  246. exportSession.outputURL = outputURL
  247. exportSession.outputFileType = .mp4
  248. exportSession.shouldOptimizeForNetworkUse = true
  249. exportSession.exportAsynchronously {
  250. handler(exportSession)
  251. }
  252. }
  253. @objc func backAction() {
  254. if let previewView = previewView {
  255. previewView.stopVideo()
  256. }
  257. self.dismiss(animated: false, completion: nil)
  258. }
  259. // SearchBar Delegate Methods
  260. func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  261. if searchText.isEmpty {
  262. filteredContacts = contacts
  263. } else {
  264. filteredContacts = contacts.filter { $0.name.lowercased().contains(searchText.lowercased()) }
  265. }
  266. tableView.reloadData()
  267. }
  268. private func registerKeyboardNotifications() {
  269. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  270. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
  271. }
  272. @objc private func keyboardWillShow(_ notification: Notification) {
  273. if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
  274. let keyboardHeight = keyboardFrame.height
  275. let info:NSDictionary = notification.userInfo! as NSDictionary
  276. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  277. updateTextViewBottomConstraint(-keyboardHeight - 20, duration)
  278. }
  279. }
  280. @objc private func keyboardWillHide(_ notification: Notification) {
  281. let info:NSDictionary = notification.userInfo! as NSDictionary
  282. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  283. updateTextViewBottomConstraint(-20, duration) // Reset bottom constraint
  284. }
  285. private func updateTextViewBottomConstraint(_ constant: CGFloat, _ duration: CGFloat) {
  286. if typeShareNum == TypeShare.text {
  287. textViewBottomConstraint?.constant = constant
  288. } else {
  289. containerBottomConstraint?.constant = constant + 20
  290. }
  291. UIView.animate(withDuration: TimeInterval(duration)) { // Smooth animation
  292. self.view.layoutIfNeeded()
  293. }
  294. }
  295. func textViewDidChange(_ textView: UITextView) {
  296. vcHandleText.navigationItem.rightBarButtonItem?.isEnabled = !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
  297. }
  298. func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  299. if text == "\n" {
  300. textView.resignFirstResponder()
  301. return false
  302. }
  303. return true
  304. }
  305. func textViewDidChangeSelection(_ textView: UITextView) {
  306. if typeShareNum == TypeShare.text {
  307. return
  308. }
  309. let cursorPosition = textView.caretRect(for: textView.selectedTextRange!.start).origin
  310. let doubleCurrentLine = cursorPosition.y / textView.font!.lineHeight
  311. if doubleCurrentLine.isFinite {
  312. let currentLine = Int(doubleCurrentLine)
  313. UIView.animate(withDuration: 0.3) {
  314. let numberOfLines = textView.textContainer.lineBreakMode == .byWordWrapping ? Int(textView.contentSize.height / textView.font!.lineHeight) - 1 : 1
  315. if currentLine == 0 && numberOfLines == 1 {
  316. self.heightTextView?.constant = 45
  317. } else if currentLine >= 4 {
  318. self.heightTextView?.constant = 95.0
  319. } else if currentLine < 4 && numberOfLines < 5 {
  320. self.heightTextView?.constant = textView.contentSize.height
  321. }
  322. }
  323. }
  324. }
  325. func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  326. return 1
  327. }
  328. func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  329. return selectedFile as QLPreviewItem
  330. }
  331. private func buildAppearance(_ contact: Contact, _ viewVc: UIView) {
  332. viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
  333. let buttonClose = UIButton(type: .system)
  334. buttonClose.setImage(UIImage(systemName: "xmark.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 40)), for: .normal)
  335. buttonClose.tintColor = .gray.withAlphaComponent(0.8)
  336. buttonClose.imageView?.contentMode = .scaleAspectFit
  337. buttonClose.clipsToBounds = true
  338. buttonClose.addTarget(self, action: #selector(backAction), for: .touchUpInside)
  339. viewVc.addSubview(buttonClose)
  340. buttonClose.translatesAutoresizingMaskIntoConstraints = false
  341. NSLayoutConstraint.activate([
  342. buttonClose.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 20.0),
  343. buttonClose.topAnchor.constraint(equalTo: viewVc.topAnchor, constant: 20.0),
  344. buttonClose.widthAnchor.constraint(equalToConstant: 40.0),
  345. buttonClose.heightAnchor.constraint(equalToConstant: 40.0),
  346. ])
  347. let containerView = UIView()
  348. containerView.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
  349. viewVc.addSubview(containerView)
  350. containerView.translatesAutoresizingMaskIntoConstraints = false
  351. NSLayoutConstraint.activate([
  352. containerView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
  353. containerView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
  354. containerView.heightAnchor.constraint(equalToConstant: 55)
  355. ])
  356. containerBottomConstraint = containerView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor)
  357. containerBottomConstraint?.isActive = true
  358. let containerTo = UIView()
  359. containerView.addSubview(containerTo)
  360. containerTo.translatesAutoresizingMaskIntoConstraints = false
  361. containerTo.layer.cornerRadius = 8
  362. containerTo.clipsToBounds = true
  363. containerTo.backgroundColor = .darkGray
  364. NSLayoutConstraint.activate([
  365. containerTo.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
  366. containerTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
  367. containerTo.heightAnchor.constraint(equalToConstant: 30),
  368. containerTo.widthAnchor.constraint(greaterThanOrEqualToConstant: 50)
  369. ])
  370. let textTo = UILabel()
  371. containerTo.addSubview(textTo)
  372. textTo.text = contact.name
  373. textTo.textColor = .white
  374. textTo.font = .systemFont(ofSize: 15)
  375. textTo.textAlignment = .center
  376. textTo.translatesAutoresizingMaskIntoConstraints = false
  377. NSLayoutConstraint.activate([
  378. textTo.leadingAnchor.constraint(equalTo: containerTo.leadingAnchor, constant: 10),
  379. textTo.trailingAnchor.constraint(equalTo: containerTo.trailingAnchor, constant: -10),
  380. textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
  381. ])
  382. let buttonTo = UIButton(type: .system)
  383. buttonTo.setImage(UIImage(systemName: "paperplane.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 35)), for: .normal)
  384. buttonTo.tintColor = .systemBlue
  385. buttonTo.addTarget(self, action: #selector(sendAction), for: .touchUpInside)
  386. containerView.addSubview(buttonTo)
  387. buttonTo.translatesAutoresizingMaskIntoConstraints = false
  388. NSLayoutConstraint.activate([
  389. buttonTo.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
  390. buttonTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
  391. buttonTo.widthAnchor.constraint(equalToConstant: 35),
  392. buttonTo.heightAnchor.constraint(equalToConstant: 35),
  393. ])
  394. textView = UITextView()
  395. viewVc.addSubview(textView)
  396. textView.textColor = .white
  397. textView.font = .systemFont(ofSize: 17)
  398. textView.textContainerInset = UIEdgeInsets(top: 10.5, left: 15, bottom: 10.5, right: 15)
  399. textView.translatesAutoresizingMaskIntoConstraints = false
  400. textView.layer.cornerRadius = 22.5
  401. textView.clipsToBounds = true
  402. textView.layer.borderColor = UIColor.gray.cgColor
  403. textView.layer.borderWidth = 1
  404. textView.delegate = self
  405. NSLayoutConstraint.activate([
  406. textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
  407. textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
  408. textView.bottomAnchor.constraint(equalTo: containerView.topAnchor, constant: -20),
  409. ])
  410. heightTextView = textView.heightAnchor.constraint(equalToConstant: 45)
  411. heightTextView?.isActive = true
  412. }
  413. func handleSharedContent(_ contact: Contact) {
  414. guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { return }
  415. for attachment in extensionItem.attachments ?? [] {
  416. if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
  417. // Handle Text
  418. attachment.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { (textItem, error) in
  419. if let sharedText = textItem as? String {
  420. DispatchQueue.main.async { [self] in
  421. typeShareNum = TypeShare.text
  422. if let viewVc = vcHandleText.view {
  423. viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
  424. self.navigationItem.backButtonTitle = ""
  425. let sendButton = UIBarButtonItem(title: "Send", style: .plain, target: self, action: #selector(sendAction))
  426. sendButton.tintColor = .label
  427. vcHandleText.navigationItem.rightBarButtonItem = sendButton
  428. let containerTo = UIView()
  429. viewVc.addSubview(containerTo)
  430. containerTo.translatesAutoresizingMaskIntoConstraints = false
  431. NSLayoutConstraint.activate([
  432. containerTo.topAnchor.constraint(equalTo: viewVc.safeAreaLayoutGuide.topAnchor, constant: 8.0),
  433. containerTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
  434. containerTo.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
  435. containerTo.heightAnchor.constraint(equalToConstant: 44)
  436. ])
  437. containerTo.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .systemBackground : .gray
  438. let textTo = UILabel()
  439. containerTo.addSubview(textTo)
  440. textTo.translatesAutoresizingMaskIntoConstraints = false
  441. NSLayoutConstraint.activate([
  442. textTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 10.0),
  443. textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
  444. ])
  445. textTo.text = "To: \(contact.name)"
  446. textTo.font = .systemFont(ofSize: 13)
  447. textTo.textColor = .label
  448. textView = UITextView()
  449. textView.translatesAutoresizingMaskIntoConstraints = false
  450. textView.isScrollEnabled = true
  451. textView.text = sharedText
  452. textView.textColor = .label
  453. textView.font = .systemFont(ofSize: 16)
  454. textView.backgroundColor = .clear
  455. textView.delegate = self
  456. viewVc.addSubview(textView)
  457. NSLayoutConstraint.activate([
  458. textView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
  459. textView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
  460. textView.topAnchor.constraint(equalTo: containerTo.bottomAnchor, constant: 5)
  461. ])
  462. textViewBottomConstraint = textView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor, constant: -20)
  463. textViewBottomConstraint?.isActive = true
  464. self.navigationController?.pushViewController(vcHandleText, animated: true)
  465. }
  466. }
  467. }
  468. }
  469. return
  470. } else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
  471. // Handle Image
  472. attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (imageItem, error) in
  473. if let imageURL = imageItem as? URL {
  474. DispatchQueue.main.async { [self] in
  475. typeShareNum = TypeShare.image
  476. selectedImage = imageURL
  477. if let viewVc = vcHandleImage.view {
  478. let imageView = UIImageView()
  479. imageView.image = UIImage(contentsOfFile: imageURL.path)
  480. imageView.contentMode = .scaleAspectFit
  481. imageView.clipsToBounds = true
  482. viewVc.addSubview(imageView)
  483. imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
  484. buildAppearance(contact, viewVc)
  485. vcHandleImage.modalPresentationStyle = .fullScreen
  486. self.navigationController?.present(vcHandleImage, animated: true)
  487. }
  488. }
  489. }
  490. }
  491. return
  492. } else if attachment.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
  493. // Handle Video
  494. attachment.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { (videoItem, error) in
  495. if let videoURL = videoItem as? URL {
  496. DispatchQueue.main.async { [self] in
  497. typeShareNum = TypeShare.video
  498. selectedVideo = videoURL
  499. if let viewVc = vcHandleVideo.view {
  500. previewView = VideoPreviewView(frame: CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 190))
  501. viewVc.addSubview(previewView!)
  502. previewView!.configure(with: videoURL)
  503. buildAppearance(contact, viewVc)
  504. vcHandleVideo.modalPresentationStyle = .fullScreen
  505. self.navigationController?.present(vcHandleVideo, animated: true)
  506. }
  507. }
  508. }
  509. }
  510. return
  511. } else if attachment.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
  512. // Handle Other Files
  513. attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
  514. if let fileURL = fileItem as? URL {
  515. DispatchQueue.main.async { [self] in
  516. typeShareNum = TypeShare.file
  517. selectedFile = fileURL
  518. if let viewVc = vcHandleFile.view {
  519. vcHandleFile.addChild(previewController)
  520. previewController.dataSource = self
  521. previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
  522. previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  523. viewVc.addSubview(previewController.view)
  524. previewController.didMove(toParent: vcHandleFile)
  525. buildAppearance(contact, viewVc)
  526. vcHandleFile.modalPresentationStyle = .fullScreen
  527. self.navigationController?.present(vcHandleFile, animated: true)
  528. }
  529. }
  530. } else {
  531. attachment.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
  532. if let url = urlData as? URL {
  533. DispatchQueue.main.async { [self] in
  534. typeShareNum = TypeShare.file
  535. selectedFile = url
  536. if let viewVc = vcHandleFile.view {
  537. vcHandleFile.addChild(previewController)
  538. previewController.dataSource = self
  539. previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
  540. previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  541. viewVc.addSubview(previewController.view)
  542. previewController.didMove(toParent: vcHandleFile)
  543. buildAppearance(contact, viewVc)
  544. vcHandleFile.modalPresentationStyle = .fullScreen
  545. self.navigationController?.present(vcHandleFile, animated: true)
  546. }
  547. }
  548. }
  549. }
  550. }
  551. }
  552. return
  553. }
  554. }
  555. }
  556. }
  557. struct Contact {
  558. let id: String
  559. let name: String
  560. let profileImage: UIImage?
  561. let imageId: String
  562. let typeContact: String
  563. }
  564. class TypeShare {
  565. static let text = 1
  566. static let image = 2
  567. static let video = 3
  568. static let file = 4
  569. }
  570. class VideoPreviewView: UIView {
  571. private var player: AVPlayer?
  572. private var playerLayer: AVPlayerLayer?
  573. private var isPlaying = false
  574. private let playPauseButton: UIButton = {
  575. let button = UIButton(type: .system)
  576. button.setImage(UIImage(systemName: "play.fill"), for: .normal)
  577. button.tintColor = .white
  578. button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  579. button.layer.cornerRadius = 25
  580. button.clipsToBounds = true
  581. return button
  582. }()
  583. override init(frame: CGRect) {
  584. super.init(frame: frame)
  585. setupUI()
  586. }
  587. required init?(coder: NSCoder) {
  588. super.init(coder: coder)
  589. setupUI()
  590. }
  591. private func setupUI() {
  592. playPauseButton.frame = CGRect(x: (bounds.width - 50) / 2, y: (bounds.height - 50) / 2, width: 50, height: 50)
  593. playPauseButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
  594. addSubview(playPauseButton)
  595. }
  596. func configure(with url: URL) {
  597. // Setup AVPlayer
  598. player = AVPlayer(url: url)
  599. playerLayer = AVPlayerLayer(player: player)
  600. playerLayer?.frame = bounds
  601. playerLayer?.videoGravity = .resizeAspect
  602. if let playerLayer = playerLayer {
  603. layer.insertSublayer(playerLayer, below: playPauseButton.layer)
  604. }
  605. // Observe when video ends
  606. NotificationCenter.default.addObserver(self, selector: #selector(videoDidEnd), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
  607. }
  608. @objc private func playPauseTapped() {
  609. guard let player = player else { return }
  610. if isPlaying {
  611. player.pause()
  612. playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
  613. } else {
  614. player.play()
  615. playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  616. }
  617. isPlaying.toggle()
  618. }
  619. @objc private func videoDidEnd() {
  620. guard let player = player else { return }
  621. // Replay video from start
  622. player.seek(to: .zero)
  623. player.play()
  624. playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  625. isPlaying = true
  626. }
  627. func stopVideo() {
  628. player?.pause()
  629. player?.seek(to: .zero)
  630. playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
  631. isPlaying = false
  632. }
  633. deinit {
  634. NotificationCenter.default.removeObserver(self)
  635. }
  636. }
  637. class ContactCell: UITableViewCell {
  638. let profileImageView = UIImageView()
  639. let nameLabel = UILabel()
  640. override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  641. super.init(style: style, reuseIdentifier: reuseIdentifier)
  642. setupUI()
  643. }
  644. required init?(coder: NSCoder) {
  645. fatalError("init(coder:) has not been implemented")
  646. }
  647. func setupUI() {
  648. profileImageView.translatesAutoresizingMaskIntoConstraints = false
  649. profileImageView.layer.cornerRadius = 25
  650. profileImageView.clipsToBounds = true
  651. profileImageView.contentMode = .center
  652. nameLabel.translatesAutoresizingMaskIntoConstraints = false
  653. nameLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
  654. contentView.addSubview(profileImageView)
  655. contentView.addSubview(nameLabel)
  656. NSLayoutConstraint.activate([
  657. profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
  658. profileImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
  659. profileImageView.widthAnchor.constraint(equalToConstant: 50),
  660. profileImageView.heightAnchor.constraint(equalToConstant: 50),
  661. nameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 15),
  662. nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
  663. ])
  664. }
  665. func configure(with contact: Contact) {
  666. nameLabel.text = contact.name
  667. profileImageView.image = contact.profileImage ?? UIImage(systemName: "person.circle")
  668. if !contact.imageId.isEmpty {
  669. profileImageView.contentMode = .scaleAspectFill
  670. }
  671. }
  672. }