Skip to content

Instantly share code, notes, and snippets.

@dterekhov
Created November 13, 2025 09:21
Show Gist options
  • Select an option

  • Save dterekhov/979766fb7584da93eac47915515820e7 to your computer and use it in GitHub Desktop.

Select an option

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
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