// // 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! 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.lastPathComponent)" let thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)" if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) { let sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName) let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName) try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL) try? UIImage(contentsOfFile: selectedImage.path)?.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) } } } } 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 } } }