123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884 |
- //
- // ShareViewController.swift
- // AppBuilderShare
- //
- // Created by Qindi on 11/02/25.
- //
- import UIKit
- import Social
- import UniformTypeIdentifiers
- import AVFoundation
- import QuickLook
- class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UITextViewDelegate, QLPreviewControllerDataSource {
- let tableView = UITableView()
- let searchBar = UISearchBar()
- var contacts: [Contact] = []
- var filteredContacts: [Contact] = []
- var selectedContact: Contact!
- private var textViewBottomConstraint: NSLayoutConstraint?
- private var containerBottomConstraint: NSLayoutConstraint?
- private var heightTextView: NSLayoutConstraint?
- let vcHandleText = UIViewController()
- let vcHandleImage = UIViewController()
- let vcHandleVideo = UIViewController()
- let vcHandleFile = UIViewController()
- var textView = UITextView()
- var typeShareNum = 0
- var selectedImage: URL!
- var selectedVideo: URL!
- var selectedFile: URL!
- var selectedImageTypeImage: UIImage!
- private var previewView: VideoPreviewView?
- let previewController = QLPreviewController()
- let nameGroupShare = "group.nexilis.share"
- override func viewDidLoad() {
- super.viewDidLoad()
- loadCustomContacts()
- registerKeyboardNotifications()
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self) // Remove observers when view controller deallocates
- }
-
- override func viewDidAppear(_ animated: Bool) {
- setupUI()
- }
-
- func setupUI() {
- let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelAction))
- cancelButton.tintColor = .label
- self.navigationItem.leftBarButtonItem = cancelButton
- // Search Bar (Right)
- searchBar.placeholder = "Search"
- searchBar.delegate = self
- searchBar.sizeToFit()
- self.navigationItem.titleView = searchBar
-
- self.navigationController?.navigationBar.backgroundColor = .systemBackground
- self.navigationController?.navigationBar.tintColor = .label
- // TableView Setup
- tableView.translatesAutoresizingMaskIntoConstraints = false
- tableView.delegate = self
- tableView.dataSource = self
- tableView.register(ContactCell.self, forCellReuseIdentifier: "ContactCell")
- tableView.separatorStyle = .singleLine
- view.addSubview(tableView)
- // Auto Layout Constraints
- NSLayoutConstraint.activate([
- tableView.topAnchor.constraint(equalTo: view.topAnchor),
- tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- ])
- }
- func loadCustomContacts() {
- if let userDefaults = UserDefaults(suiteName: nameGroupShare),
- let value = userDefaults.string(forKey: "shareContacts") {
- if let jsonData = value.data(using: .utf8) {
- do {
- if let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] {
- for json in jsonArray {
- let id = json["id"] as? String ?? ""
- let name = json["name"] as? String ?? ""
- let imageId = json["image"] as? String ?? ""
- let type = json["type"] as? Int ?? 0
- var profileImage = type == 0 ? UIImage(systemName: "person.fill") : UIImage(systemName: "bubble.right.fill")
- if !imageId.isEmpty {
- if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
- let sharedFileURL = appGroupURL.appendingPathComponent(imageId)
- if FileManager.default.fileExists(atPath: sharedFileURL.path) {
- profileImage = UIImage(contentsOfFile: sharedFileURL.path)
- }
- }
- }
- contacts.append(Contact(id: id, name: name, profileImage: profileImage, imageId: imageId, typeContact: "\(type)"))
- }
- filteredContacts = contacts
- tableView.reloadData()
- }
- } catch {
- print("Error parsing JSON: \(error)")
- }
- }
- }
- }
- // TableView DataSource Methods
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return filteredContacts.count
- }
-
- func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return 60
- }
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
- cell.separatorInset = UIEdgeInsets(top: 0, left: 65, bottom: 0, right: 25)
- let contact = filteredContacts[indexPath.row]
- cell.configure(with: contact)
- return cell
- }
- // Handle Contact Selection
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- tableView.deselectRow(at: indexPath, animated: true)
- selectedContact = filteredContacts[indexPath.row]
- handleSharedContent(selectedContact)
- }
- // Cancel Button Action
- @objc func cancelAction() {
- self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
- }
-
- @objc func sendAction() {
- if let userDefaults = UserDefaults(suiteName: nameGroupShare) {
- do {
- var dataShared: [String: Any] = [:]
- dataShared["typeShare"] = typeShareNum
- dataShared["typeContact"] = selectedContact.typeContact
- dataShared["idContact"] = selectedContact.id
- dataShared["data"] = textView.text
- if typeShareNum == TypeShare.image {
- let compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent : "SS_Image")"
- let thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent : "SS_Image")"
- if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
- let sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
- let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
- if selectedImage != nil {
- try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
- try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.5)?.write(to: sharedImageURL)
- } else {
- try? selectedImageTypeImage?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
- try? selectedImageTypeImage?.jpegData(compressionQuality: 0.5)?.write(to: sharedImageURL)
- }
- }
- dataShared["thumb"] = thumbName
- dataShared["image"] = compressedImageName
- let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
- if let jsonString = String(data: jsonData, encoding: .utf8) {
- userDefaults.set(jsonString, forKey: "sharedItem")
- userDefaults.synchronize()
- let notificationName = "realtimeShareExtensionNexilis" as CFString
- CFNotificationCenterPostNotification(
- CFNotificationCenterGetDarwinNotifyCenter(),
- CFNotificationName(notificationName),
- nil,
- nil,
- true
- )
- }
- } else if typeShareNum == TypeShare.video {
- let dataVideo = try? Data(contentsOf: selectedVideo)
- if let dataVideotoCompress = dataVideo {
- let sizeInKB = Double(dataVideotoCompress.count) / 1024.0
- let sizeOfVideo = sizeInKB / 1024.0
- if (sizeOfVideo > 10.0) {
- let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
- compressVideo(inputURL: selectedVideo,
- outputURL: compressedURL) { exportSession in
- guard let session = exportSession else {
- return
- }
-
- switch session.status {
- case .unknown:
- break
- case .waiting:
- break
- case .exporting:
- break
- case .completed:
- guard let compressedData = try? Data(contentsOf: compressedURL) else {
- return
- }
- self.sendVideoToMainApp(compressedData, dataShared)
- case .failed:
- break
- case .cancelled:
- break
- @unknown default:
- break
- }
- }
- return
- } else {
- self.sendVideoToMainApp(dataVideotoCompress, dataShared)
- }
- }
- } else if typeShareNum == TypeShare.file || typeShareNum == TypeShare.audio {
- let fileName = selectedFile.lastPathComponent
- if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
- let sharedFileURL = appGroupURL.appendingPathComponent(fileName)
- try? Data(contentsOf: selectedFile).write(to: sharedFileURL)
- }
- dataShared[typeShareNum == TypeShare.audio ? "audio" : "file"] = fileName
- let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
- if let jsonString = String(data: jsonData, encoding: .utf8) {
- userDefaults.set(jsonString, forKey: "sharedItem")
- userDefaults.synchronize()
- let notificationName = "realtimeShareExtensionNexilis" as CFString
- CFNotificationCenterPostNotification(
- CFNotificationCenterGetDarwinNotifyCenter(),
- CFNotificationName(notificationName),
- nil,
- nil,
- true
- )
- }
- } else {
- let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
- if let jsonString = String(data: jsonData, encoding: .utf8) {
- userDefaults.set(jsonString, forKey: "sharedItem")
- userDefaults.synchronize()
- let notificationName = "realtimeShareExtensionNexilis" as CFString
- CFNotificationCenterPostNotification(
- CFNotificationCenterGetDarwinNotifyCenter(),
- CFNotificationName(notificationName),
- nil,
- nil,
- true
- )
- }
- }
- } catch {
-
- }
- }
- self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
- }
-
- private func sendVideoToMainApp(_ data: Data, _ dataShared: [String: Any]) {
- do {
- var dataShared = dataShared
- let originalVideoName = self.selectedVideo.lastPathComponent
- let renamedVideoName = "Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName)"
- let thumbName = "THUMB_Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName)"
- if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
- let sharedVideoURL = appGroupURL.appendingPathComponent(renamedVideoName)
- let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
- try? data.write(to: sharedVideoURL)
- let asset = AVURLAsset(url: self.selectedVideo, options: nil)
- let imgGenerator = AVAssetImageGenerator(asset: asset)
- imgGenerator.appliesPreferredTrackTransform = true
- let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
- let thumbnail = UIImage(cgImage: cgImage)
- try? thumbnail.jpegData(compressionQuality: 1.0)?.write(to: sharedThumbURL)
- dataShared["thumb"] = thumbName
- dataShared["video"] = renamedVideoName
- let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
- if let jsonString = String(data: jsonData, encoding: .utf8) {
- let userDefaults = UserDefaults(suiteName: nameGroupShare)
- userDefaults!.set(jsonString, forKey: "sharedItem")
- userDefaults!.synchronize()
- let notificationName = "realtimeShareExtensionNexilis" as CFString
- CFNotificationCenterPostNotification(
- CFNotificationCenterGetDarwinNotifyCenter(),
- CFNotificationName(notificationName),
- nil,
- nil,
- true
- )
- }
- }
- } catch {
-
- }
- }
-
- func compressVideo(inputURL: URL,
- outputURL: URL,
- handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
- let urlAsset = AVURLAsset(url: inputURL, options: nil)
- guard let exportSession = AVAssetExportSession(asset: urlAsset,
- presetName: AVAssetExportPresetMediumQuality) else {
- handler(nil)
-
- return
- }
-
- exportSession.outputURL = outputURL
- exportSession.outputFileType = .mp4
- exportSession.shouldOptimizeForNetworkUse = true
- exportSession.exportAsynchronously {
- handler(exportSession)
- }
- }
-
- @objc func backAction() {
- if let previewView = previewView {
- previewView.stopVideo()
- }
- self.dismiss(animated: false, completion: nil)
- }
- // SearchBar Delegate Methods
- func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
- if searchText.isEmpty {
- filteredContacts = contacts
- } else {
- filteredContacts = contacts.filter { $0.name.lowercased().contains(searchText.lowercased()) }
- }
- tableView.reloadData()
- }
-
- private func registerKeyboardNotifications() {
- NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
- }
- @objc private func keyboardWillShow(_ notification: Notification) {
- if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
- let keyboardHeight = keyboardFrame.height
- let info:NSDictionary = notification.userInfo! as NSDictionary
- let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
- updateTextViewBottomConstraint(-keyboardHeight - 20, duration)
- }
- }
- @objc private func keyboardWillHide(_ notification: Notification) {
- let info:NSDictionary = notification.userInfo! as NSDictionary
- let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
- updateTextViewBottomConstraint(-20, duration) // Reset bottom constraint
- }
- private func updateTextViewBottomConstraint(_ constant: CGFloat, _ duration: CGFloat) {
- if typeShareNum == TypeShare.text {
- textViewBottomConstraint?.constant = constant
- } else {
- containerBottomConstraint?.constant = constant + 20
- }
- UIView.animate(withDuration: TimeInterval(duration)) { // Smooth animation
- self.view.layoutIfNeeded()
- }
- }
-
- func textViewDidChange(_ textView: UITextView) {
- vcHandleText.navigationItem.rightBarButtonItem?.isEnabled = !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
- }
-
- func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
- if text == "\n" {
- textView.resignFirstResponder()
- return false
- }
- return true
- }
-
- func textViewDidChangeSelection(_ textView: UITextView) {
- if typeShareNum == TypeShare.text {
- return
- }
- let cursorPosition = textView.caretRect(for: textView.selectedTextRange!.start).origin
- let doubleCurrentLine = cursorPosition.y / textView.font!.lineHeight
- if doubleCurrentLine.isFinite {
- let currentLine = Int(doubleCurrentLine)
- UIView.animate(withDuration: 0.3) {
- let numberOfLines = textView.textContainer.lineBreakMode == .byWordWrapping ? Int(textView.contentSize.height / textView.font!.lineHeight) - 1 : 1
- if currentLine == 0 && numberOfLines == 1 {
- self.heightTextView?.constant = 45
- } else if currentLine >= 4 {
- self.heightTextView?.constant = 95.0
- } else if currentLine < 4 && numberOfLines < 5 {
- self.heightTextView?.constant = textView.contentSize.height
- }
- }
- }
- }
-
- func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
- return 1
- }
-
- func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
- return selectedFile as QLPreviewItem
- }
-
- private func buildAppearance(_ contact: Contact, _ viewVc: UIView) {
- viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
- let buttonClose = UIButton(type: .system)
- buttonClose.setImage(UIImage(systemName: "xmark.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 40)), for: .normal)
- buttonClose.tintColor = .gray.withAlphaComponent(0.8)
- buttonClose.imageView?.contentMode = .scaleAspectFit
- buttonClose.clipsToBounds = true
- buttonClose.addTarget(self, action: #selector(backAction), for: .touchUpInside)
- viewVc.addSubview(buttonClose)
- buttonClose.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- buttonClose.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 20.0),
- buttonClose.topAnchor.constraint(equalTo: viewVc.topAnchor, constant: 20.0),
- buttonClose.widthAnchor.constraint(equalToConstant: 40.0),
- buttonClose.heightAnchor.constraint(equalToConstant: 40.0),
- ])
-
- let containerView = UIView()
- containerView.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
- viewVc.addSubview(containerView)
- containerView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- containerView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
- containerView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
- containerView.heightAnchor.constraint(equalToConstant: 55)
- ])
- containerBottomConstraint = containerView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor)
- containerBottomConstraint?.isActive = true
-
- let containerTo = UIView()
- containerView.addSubview(containerTo)
- containerTo.translatesAutoresizingMaskIntoConstraints = false
- containerTo.layer.cornerRadius = 8
- containerTo.clipsToBounds = true
- containerTo.backgroundColor = .darkGray
- NSLayoutConstraint.activate([
- containerTo.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
- containerTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
- containerTo.heightAnchor.constraint(equalToConstant: 30),
- containerTo.widthAnchor.constraint(greaterThanOrEqualToConstant: 50)
- ])
-
- let textTo = UILabel()
- containerTo.addSubview(textTo)
- textTo.text = contact.name
- textTo.textColor = .white
- textTo.font = .systemFont(ofSize: 15)
- textTo.textAlignment = .center
- textTo.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- textTo.leadingAnchor.constraint(equalTo: containerTo.leadingAnchor, constant: 10),
- textTo.trailingAnchor.constraint(equalTo: containerTo.trailingAnchor, constant: -10),
- textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
- ])
-
- let buttonTo = UIButton(type: .system)
- buttonTo.setImage(UIImage(systemName: "paperplane.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 35)), for: .normal)
- buttonTo.tintColor = .systemBlue
- buttonTo.addTarget(self, action: #selector(sendAction), for: .touchUpInside)
- containerView.addSubview(buttonTo)
- buttonTo.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- buttonTo.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
- buttonTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
- buttonTo.widthAnchor.constraint(equalToConstant: 35),
- buttonTo.heightAnchor.constraint(equalToConstant: 35),
- ])
-
- textView = UITextView()
- viewVc.addSubview(textView)
- textView.textColor = .white
- textView.font = .systemFont(ofSize: 17)
- textView.textContainerInset = UIEdgeInsets(top: 10.5, left: 15, bottom: 10.5, right: 15)
- textView.translatesAutoresizingMaskIntoConstraints = false
- textView.layer.cornerRadius = 22.5
- textView.clipsToBounds = true
- textView.layer.borderColor = UIColor.gray.cgColor
- textView.layer.borderWidth = 1
- textView.delegate = self
- NSLayoutConstraint.activate([
- textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
- textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
- textView.bottomAnchor.constraint(equalTo: containerView.topAnchor, constant: -20),
- ])
- heightTextView = textView.heightAnchor.constraint(equalToConstant: 45)
- heightTextView?.isActive = true
- }
-
- func handleSharedContent(_ contact: Contact) {
- guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { return }
- for attachment in extensionItem.attachments ?? [] {
- if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
- // Handle Text
- attachment.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { (textItem, error) in
- if let sharedText = textItem as? String {
- DispatchQueue.main.async { [self] in
- typeShareNum = TypeShare.text
- if let viewVc = vcHandleText.view {
- viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
- self.navigationItem.backButtonTitle = ""
- let sendButton = UIBarButtonItem(title: "Send", style: .plain, target: self, action: #selector(sendAction))
- sendButton.tintColor = .label
- vcHandleText.navigationItem.rightBarButtonItem = sendButton
-
- let containerTo = UIView()
- viewVc.addSubview(containerTo)
- containerTo.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- containerTo.topAnchor.constraint(equalTo: viewVc.safeAreaLayoutGuide.topAnchor, constant: 8.0),
- containerTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
- containerTo.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
- containerTo.heightAnchor.constraint(equalToConstant: 44)
- ])
- containerTo.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .systemBackground : .gray
-
- let textTo = UILabel()
- containerTo.addSubview(textTo)
- textTo.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- textTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 10.0),
- textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
- ])
- textTo.text = "To: \(contact.name)"
- textTo.font = .systemFont(ofSize: 13)
- textTo.textColor = .label
-
- textView = UITextView()
- textView.translatesAutoresizingMaskIntoConstraints = false
- textView.isScrollEnabled = true
- textView.text = sharedText
- textView.textColor = .label
- textView.font = .systemFont(ofSize: 16)
- textView.backgroundColor = .clear
- textView.delegate = self
- viewVc.addSubview(textView)
- NSLayoutConstraint.activate([
- textView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
- textView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
- textView.topAnchor.constraint(equalTo: containerTo.bottomAnchor, constant: 5)
- ])
- textViewBottomConstraint = textView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor, constant: -20)
- textViewBottomConstraint?.isActive = true
-
- self.navigationController?.pushViewController(vcHandleText, animated: true)
- }
- }
- }
- }
- return
- } else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
- // Handle Image
- attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (imageItem, error) in
- if let imageURL = imageItem as? URL {
- DispatchQueue.main.async { [self] in
- typeShareNum = TypeShare.image
- selectedImage = imageURL
- if let viewVc = vcHandleImage.view {
- let imageView = UIImageView()
- imageView.image = UIImage(contentsOfFile: imageURL.path)
- imageView.contentMode = .scaleAspectFit
- imageView.clipsToBounds = true
- viewVc.addSubview(imageView)
- imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
-
- buildAppearance(contact, viewVc)
-
- vcHandleImage.modalPresentationStyle = .fullScreen
- self.navigationController?.present(vcHandleImage, animated: true)
- }
- }
- } else if let image = imageItem as? UIImage {
- DispatchQueue.main.async { [self] in
- typeShareNum = TypeShare.image
- selectedImageTypeImage = image
- if let viewVc = vcHandleImage.view {
- let imageView = UIImageView()
- imageView.image = image
- imageView.contentMode = .scaleAspectFit
- imageView.clipsToBounds = true
- viewVc.addSubview(imageView)
- imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
-
- buildAppearance(contact, viewVc)
-
- vcHandleImage.modalPresentationStyle = .fullScreen
- self.navigationController?.present(vcHandleImage, animated: true)
- }
- }
- }
- }
- return
- } else if attachment.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
- // Handle Video
- attachment.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { (videoItem, error) in
- if let videoURL = videoItem as? URL {
- DispatchQueue.main.async { [self] in
- typeShareNum = TypeShare.video
- selectedVideo = videoURL
- if let viewVc = vcHandleVideo.view {
- previewView = VideoPreviewView(frame: CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 190))
- viewVc.addSubview(previewView!)
- previewView!.configure(with: videoURL)
-
- buildAppearance(contact, viewVc)
-
- vcHandleVideo.modalPresentationStyle = .fullScreen
- self.navigationController?.present(vcHandleVideo, animated: true)
- }
- }
- }
- }
- return
- } else if isSupportedAudioFormat(attachment: attachment) {
- attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
- if let fileURL = fileItem as? URL {
- self.getExactAudioDuration(url: fileURL) { durationFormatted in
- let alert = UIAlertController(title: "Send to: \(contact.name)?", message: "File size: \(self.getExactFileSize(url: fileURL)) KB\nDuration: \(durationFormatted)", preferredStyle: .alert)
- alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
- alert.addAction(UIAlertAction(title: "Send", style: .default, handler: {[self] _ in
- typeShareNum = TypeShare.audio
- selectedFile = fileURL
- sendAction()
- }))
-
- DispatchQueue.main.async {
- self.navigationController?.present(alert, animated: true, completion: nil)
- }
- }
- }
- }
- } else if attachment.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
- // Handle Other Files
- attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
- if let fileURL = fileItem as? URL {
- DispatchQueue.main.async { [self] in
- typeShareNum = TypeShare.file
- selectedFile = fileURL
- if let viewVc = vcHandleFile.view {
- vcHandleFile.addChild(previewController)
- previewController.dataSource = self
- previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
- previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- viewVc.addSubview(previewController.view)
- previewController.didMove(toParent: vcHandleFile)
-
- buildAppearance(contact, viewVc)
-
- vcHandleFile.modalPresentationStyle = .fullScreen
- self.navigationController?.present(vcHandleFile, animated: true)
- }
- }
- } else {
- attachment.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
- if let url = urlData as? URL {
- DispatchQueue.main.async { [self] in
- typeShareNum = TypeShare.file
- selectedFile = url
- if let viewVc = vcHandleFile.view {
- vcHandleFile.addChild(previewController)
- previewController.dataSource = self
- previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
- previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- viewVc.addSubview(previewController.view)
- previewController.didMove(toParent: vcHandleFile)
-
- buildAppearance(contact, viewVc)
-
- vcHandleFile.modalPresentationStyle = .fullScreen
- self.navigationController?.present(vcHandleFile, animated: true)
- }
- }
- }
- }
- }
- }
- return
- }
- }
- }
-
- func isSupportedAudioFormat(attachment: NSItemProvider) -> Bool {
- var supportedAudioTypes: [UTType] = [
- .mpeg4Audio,
- .mp3,
- .aiff,
- .wav,
- .appleProtectedMPEG4Audio
- ]
-
- if let flacType = UTType(filenameExtension: "flac") {
- supportedAudioTypes.append(flacType)
- }
- if let oggType = UTType(filenameExtension: "ogg") {
- supportedAudioTypes.append(oggType)
- }
- if let cafType = UTType(filenameExtension: "caf") {
- supportedAudioTypes.append(cafType)
- }
- if let opusType = UTType(filenameExtension: "opus") {
- supportedAudioTypes.append(opusType)
- }
-
- return supportedAudioTypes.contains { attachment.hasItemConformingToTypeIdentifier($0.identifier) }
- }
- func getExactFileSize(url: URL) -> Int64 {
- do {
- let fileData = try Data(contentsOf: url)
- let fileSizeInBytes = Int64(fileData.count)
- return (fileSizeInBytes + 1023) / 1000 // Using 1000 instead of 1024
- } catch {
- print("Error reading file size: \(error)")
- }
- return 0
- }
-
- func getExactAudioDuration(url: URL, completion: @escaping (String) -> Void) {
- let asset = AVURLAsset(url: url)
- asset.loadValuesAsynchronously(forKeys: ["duration"]) {
- DispatchQueue.main.async {
- let durationSeconds = CMTimeGetSeconds(asset.duration)
- // Apply rounding logic like WhatsApp
- let roundedDuration = Int(durationSeconds.rounded(.up))
- let minutes = roundedDuration / 60
- let seconds = roundedDuration % 60
- let formattedDuration = String(format: "%d:%02d", minutes, seconds)
- completion(formattedDuration)
- }
- }
- }
- }
- struct Contact {
- let id: String
- let name: String
- let profileImage: UIImage?
- let imageId: String
- let typeContact: String
- }
- class TypeShare {
- static let text = 1
- static let image = 2
- static let video = 3
- static let file = 4
- static let audio = 5
- }
- class VideoPreviewView: UIView {
-
- private var player: AVPlayer?
- private var playerLayer: AVPlayerLayer?
- private var isPlaying = false
-
- private let playPauseButton: UIButton = {
- let button = UIButton(type: .system)
- button.setImage(UIImage(systemName: "play.fill"), for: .normal)
- button.tintColor = .white
- button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
- button.layer.cornerRadius = 25
- button.clipsToBounds = true
- return button
- }()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
- setupUI()
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupUI()
- }
-
- private func setupUI() {
- playPauseButton.frame = CGRect(x: (bounds.width - 50) / 2, y: (bounds.height - 50) / 2, width: 50, height: 50)
- playPauseButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
- addSubview(playPauseButton)
- }
-
- func configure(with url: URL) {
- // Setup AVPlayer
- player = AVPlayer(url: url)
- playerLayer = AVPlayerLayer(player: player)
- playerLayer?.frame = bounds
- playerLayer?.videoGravity = .resizeAspect
- if let playerLayer = playerLayer {
- layer.insertSublayer(playerLayer, below: playPauseButton.layer)
- }
-
- // Observe when video ends
- NotificationCenter.default.addObserver(self, selector: #selector(videoDidEnd), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
- }
-
- @objc private func playPauseTapped() {
- guard let player = player else { return }
-
- if isPlaying {
- player.pause()
- playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
- } else {
- player.play()
- playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
- }
-
- isPlaying.toggle()
- }
-
- @objc private func videoDidEnd() {
- guard let player = player else { return }
-
- // Replay video from start
- player.seek(to: .zero)
- player.play()
- playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
- isPlaying = true
- }
-
- func stopVideo() {
- player?.pause()
- player?.seek(to: .zero)
- playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
- isPlaying = false
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
- }
- class ContactCell: UITableViewCell {
-
- let profileImageView = UIImageView()
- let nameLabel = UILabel()
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
- setupUI()
- }
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- func setupUI() {
- profileImageView.translatesAutoresizingMaskIntoConstraints = false
- profileImageView.layer.cornerRadius = 25
- profileImageView.clipsToBounds = true
- profileImageView.contentMode = .center
- nameLabel.translatesAutoresizingMaskIntoConstraints = false
- nameLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
- contentView.addSubview(profileImageView)
- contentView.addSubview(nameLabel)
- NSLayoutConstraint.activate([
- profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
- profileImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
- profileImageView.widthAnchor.constraint(equalToConstant: 50),
- profileImageView.heightAnchor.constraint(equalToConstant: 50),
- nameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 15),
- nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
- ])
- }
- func configure(with contact: Contact) {
- nameLabel.text = contact.name
- profileImageView.image = contact.profileImage ?? UIImage(systemName: "person.circle")
- if !contact.imageId.isEmpty {
- profileImageView.contentMode = .scaleAspectFill
- }
- }
- }
|