Created
November 13, 2025 09:21
-
-
Save dterekhov/979766fb7584da93eac47915515820e7 to your computer and use it in GitHub Desktop.
URLOpeningUtil: Correctly opens phone, email URLs in the app #uikit #swift-api #real-project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import UIKit | |
| import MessageUI | |
| public final class URLOpeningUtil: NSObject, Sendable { | |
| // MARK: - Validation | |
| public enum RegularExpressions: String { | |
| // swiftlint:disable line_length | |
| case phone = "^\\s*(?:\\+?(\\d{1,3}))?([-. (]*(\\d{3})[-. )]*)?((\\d{3})[-. ]*(\\d{2,4})(?:[-.x ]*(\\d+))?)\\s*$" | |
| case email = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" | |
| // swiftlint:enable line_length | |
| } | |
| public static func isValid(string: String, regex: RegularExpressions) -> Bool { | |
| let matches = string.range(of: regex.rawValue, options: .regularExpression) | |
| return matches != nil | |
| } | |
| // MARK: - Opening URL | |
| @MainActor | |
| @discardableResult | |
| public static func call(phoneNumber: String, phoneExtension: String? = nil) -> Bool { | |
| let phoneNumberWithExt = [phoneNumber, phoneExtension] | |
| .compactMap({ $0 }) | |
| .filter({ !$0.isEmpty }) | |
| .joined(separator: ",") | |
| guard isValid(string: phoneNumber, regex: .phone), | |
| let url = URL(string: "tel://\(onlyDigits(fromString: phoneNumberWithExt))"), | |
| UIApplication.shared.canOpenURL(url) else { return false } | |
| UIApplication.shared.open(url) | |
| return true | |
| } | |
| @MainActor | |
| @discardableResult | |
| public static func sendEmail(_ email: String) -> Bool { | |
| guard isValid(string: email, regex: .email), | |
| let url = URL(string: "mailto:\(email)"), | |
| UIApplication.shared.canOpenURL(url) else { return false } | |
| UIApplication.shared.open(url) | |
| return true | |
| } | |
| // MARK: - Private API | |
| private static func onlyDigits(fromString string: String) -> String { | |
| let commaChar = "," | |
| var allowedCharacters = CharacterSet.decimalDigits | |
| allowedCharacters.insert(charactersIn: commaChar) | |
| let filtredUnicodeScalars = string.unicodeScalars.filter { allowedCharacters.contains($0) } | |
| return String(String.UnicodeScalarView(filtredUnicodeScalars)) | |
| } | |
| } | |
| extension URLOpeningUtil: @MainActor MFMessageComposeViewControllerDelegate { | |
| // MARK: - Text sending | |
| private static let shared = URLOpeningUtil() | |
| @MainActor | |
| public func messageComposeViewController(_ controller: MFMessageComposeViewController, | |
| didFinishWith result: MessageComposeResult) { | |
| controller.presentingViewController?.dismiss(animated: true, completion: nil) | |
| } | |
| /// SMS or iMessage | |
| @MainActor | |
| @discardableResult public static func sendText(_ text: String?, | |
| to recipient: String, | |
| in presentingVC: UIViewController) -> Bool { | |
| guard MFMessageComposeViewController.canSendText() else { return false } | |
| let controller = MFMessageComposeViewController() | |
| controller.body = text | |
| controller.recipients = [recipient] | |
| controller.messageComposeDelegate = shared | |
| presentingVC.present(controller, animated: true, completion: nil) | |
| return true | |
| } | |
| // MARK: - Call or message | |
| @MainActor | |
| public static func callOrMessage(_ text: String?, | |
| phoneNumber: String, | |
| phoneExtension: String?, | |
| in presentingVC: UIViewController, | |
| failure: (() -> Void)?) { | |
| let alert = UIAlertController(title: "Contact", | |
| message: nil, | |
| preferredStyle: .alert) | |
| let callAction = UIAlertAction(title: "Call", style: .default) { _ in | |
| guard call(phoneNumber: phoneNumber, phoneExtension: phoneExtension) else { failure?(); return } | |
| } | |
| let messageAction = UIAlertAction(title: "Message", style: .default) { _ in | |
| guard sendText(text, to: phoneNumber, in: presentingVC) else { failure?(); return } | |
| } | |
| let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) | |
| alert.view.tintColor = UIColor.green | |
| alert.addAction(callAction) | |
| alert.addAction(messageAction) | |
| alert.addAction(cancelAction) | |
| presentingVC.present(alert, animated: true, completion: nil) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment