// // Authorize.swift // DLSDK // // Created by osell on 2023/8/3. // import UIKit import CoreLocation import Photos import AVFoundation import UserNotifications import AdSupport import AppTrackingTransparency import URLNavigator // MARK: - AuthorizeType /// 权限类型枚举 @objc(DLAuthorizeType) public enum AuthorizeType: Int { /// 使用时定位,Info.plist需配置NSLocationWhenInUseUsageDescription case locationWhenInUse = 1 /// 后台定位,Info.plist需配置NSLocationAlwaysUsageDescription和NSLocationAlwaysAndWhenInUseUsageDescription case locationAlways = 2 /// 麦克风,需启用Microphone子模块,Info.plist需配置NSMicrophoneUsageDescription case microphone = 3 /// 相册,Info.plist需配置NSPhotoLibraryUsageDescription case photoLibrary = 4 /// 照相机,Info.plist需配置NSCameraUsageDescription case camera = 5 /// 联系人,需启用Contacts子模块,Info.plist需配置NSContactsUsageDescription case contacts = 6 /// 日历,需启用Calendar子模块,Info.plist需配置NSCalendarsUsageDescription case calendars = 7 /// 提醒,需启用Calendar子模块,Info.plist需配置NSRemindersUsageDescription case reminders = 8 /// 音乐,需启用AppleMusic子模块,Info.plist需配置NSAppleMusicUsageDescription case appleMusic = 9 /// 通知,远程推送需打开Push Notifications开关和Background Modes的Remote notifications开关 case notifications = 10 /// 广告跟踪,需启用Tracking子模块,Info.plist需配置NSUserTrackingUsageDescription case tracking = 11 } // MARK: - AuthorizeStatus /// 权限状态枚举 @objc(DLAuthorizeStatus) public enum AuthorizeStatus: Int { /// 未确认 case notDetermined = 0 /// 受限制 case restricted = 1 /// 被拒绝 case denied = 2 /// 已授权 case authorized = 3 } // MARK: - AuthorizeProtocol /// 权限授权协议 @objc(DLAuthorizeProtocol) public protocol AuthorizeProtocol { /// 查询权限状态,必须实现。某些权限会阻塞当前线程,建议异步查询,如通知 func authorizeStatus() -> AuthorizeStatus /// 执行权限授权,主线程回调,必须实现 func authorize(_ completion: ((AuthorizeStatus) -> Void)?) /// 异步查询权限状态,当前线程回调,可选实现。某些权限建议异步查询,不会阻塞当前线程,如通知 @objc optional func authorizeStatus(_ completion: ((AuthorizeStatus) -> Void)?) } // MARK: - AuthorizeManager /// 权限管理器。由于打包上传ipa时会自动检查隐私库并提供Info.plist描述,所以默认关闭隐私库声明 /// @objc(DLAuthorizeManager) @objcMembers public class AuthorizeManager: NSObject { private static var managers: [AuthorizeType: AuthorizeProtocol] = [:] private static var blocks: [AuthorizeType: () -> AuthorizeProtocol] = [:] /// 获取指定类型的权限管理器单例,部分权限未启用时返回nil public static func manager(type: AuthorizeType) -> AuthorizeProtocol? { if let manager = managers[type] { return manager } guard let manager = factory(type: type) else { return nil } managers[type] = manager return manager } /// 注册指定类型的权限管理器创建句柄,用于动态扩展权限类型 public static func registerAuthorize(_ type: AuthorizeType, withBlock block: @escaping () -> AuthorizeProtocol) { blocks[type] = block } private static func factory(type: AuthorizeType) -> AuthorizeProtocol? { if let block = blocks[type] { return block() } switch type { case .locationWhenInUse: return AuthorizeLocation(isAlways: false) case .locationAlways: return AuthorizeLocation(isAlways: true) case .photoLibrary: return AuthorizePhotoLibrary() case .camera: return AuthorizeCamera() case .notifications: return AuthorizeNotifications() case .microphone: return AuthorizeMicrophone() case .tracking: return AuthorizeTracking() default: return nil } } static func openAppSetting(title: String, message: String) { let alertVC = UIAlertController.init(title: title, message: message, preferredStyle: .alert) let cancelAction = UIAlertAction.init(title: "取消", style: .cancel) let setAction = UIAlertAction.init(title: "去设置", style: .default) { action in guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return } if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) } } alertVC.addAction(cancelAction) alertVC.addAction(setAction) UIViewController.topMost?.present(alertVC, animated: true, completion: nil) } } // MARK: - AuthorizeLocation /// 定位授权 private class AuthorizeLocation: NSObject, AuthorizeProtocol, CLLocationManagerDelegate { private lazy var locationManager: CLLocationManager = { let result = CLLocationManager() result.delegate = self return result }() private var completionBlock: ((AuthorizeStatus) -> Void)? private var changeIgnored: Bool = false private var isAlways: Bool = false init(isAlways: Bool) { super.init() self.isAlways = isAlways } func authorizeStatus() -> AuthorizeStatus { // 定位功能未打开时返回Denied,可自行调用[CLLocationManager locationServicesEnabled]判断 let status = CLLocationManager.authorizationStatus() switch status { case .restricted: return .restricted case .denied: return .denied case .authorizedAlways: return .authorized case .authorizedWhenInUse: if isAlways { let isAuthorized = UserDefaults.standard.object(forKey: "DLAuthorizeLocation") return isAuthorized != nil ? .denied : .notDetermined } else { return .authorized } default: return .notDetermined } } func authorize(_ completion: ((AuthorizeStatus) -> Void)?) { completionBlock = completion if isAlways { locationManager.requestAlwaysAuthorization() // 标记已请求授权 UserDefaults.standard.set(NSNumber(value: 1), forKey: "DLAuthorizeLocation") UserDefaults.standard.synchronize() } else { locationManager.requestWhenInUseAuthorization() } } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { // 如果请求always权限且当前已经是WhenInUse权限,系统会先回调此方法一次,忽略之 if isAlways && !changeIgnored { if status == .notDetermined || status == .authorizedWhenInUse { changeIgnored = true return } } // 主线程回调,仅一次 if completionBlock != nil { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.completionBlock?(self.authorizeStatus()) self.completionBlock = nil } } } } // MARK: - AuthorizePhotoLibrary /// 相册授权 private class AuthorizePhotoLibrary: NSObject, AuthorizeProtocol { func authorizeStatus() -> AuthorizeStatus { let status = PHPhotoLibrary.authorizationStatus() switch status { case .restricted: return .restricted case .denied: return .denied case .authorized: return .authorized default: return .notDetermined } } func authorize(_ completion: ((AuthorizeStatus) -> Void)?) { PHPhotoLibrary.requestAuthorization { status in if completion != nil { DispatchQueue.main.async { [weak self] in guard let self = self else { return } completion?(self.authorizeStatus()) } } } } } // MARK: - AuthorizeCamera /// 照相机授权 private class AuthorizeCamera: NSObject, AuthorizeProtocol { func authorizeStatus() -> AuthorizeStatus { // 模拟器不支持照相机,返回受限制 let mediaType = AVMediaType.video guard let device = AVCaptureDevice.default(for: mediaType) else { return .restricted } if !device.hasMediaType(mediaType) { return .restricted } let status = AVCaptureDevice.authorizationStatus(for: mediaType) switch status { case .restricted: return .restricted case .denied: return .denied case .authorized: return .authorized default: return .notDetermined } } func authorize(_ completion: ((AuthorizeStatus) -> Void)?) { AVCaptureDevice.requestAccess(for: .video) { granted in if completion != nil { DispatchQueue.main.async { [weak self] in guard let self = self else { return } completion?(self.authorizeStatus()) } } } } } // MARK: - AuthorizeNotifications /// 通知授权 private class AuthorizeNotifications: NSObject, AuthorizeProtocol { func authorizeStatus() -> AuthorizeStatus { var status: AuthorizeStatus = .notDetermined // 由于查询授权为异步方法,此处使用信号量阻塞当前线程,同步返回查询结果 let semaphore = DispatchSemaphore(value: 0) UNUserNotificationCenter.current().getNotificationSettings { settings in switch settings.authorizationStatus { case .denied: status = .denied case .authorized: status = .authorized case .provisional: status = .authorized default: status = .notDetermined } semaphore.signal() } semaphore.wait() return status } func authorizeStatus(_ completion: ((AuthorizeStatus) -> Void)?) { UNUserNotificationCenter.current().getNotificationSettings { settings in var status: AuthorizeStatus = .notDetermined switch settings.authorizationStatus { case .denied: status = .denied case .authorized: status = .authorized case .provisional: status = .authorized default: status = .notDetermined } if completion != nil { DispatchQueue.main.async { completion?(status) } } } } func authorize(_ completion: ((AuthorizeStatus) -> Void)?) { let options: UNAuthorizationOptions = [.badge, .sound, .alert] UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in let status: AuthorizeStatus = granted ? .authorized : .denied if completion != nil { DispatchQueue.main.async { completion?(status) } } } UIApplication.shared.registerForRemoteNotifications() } } // MARK: - AuthorizeMicrophone /// 麦克风授权 private class AuthorizeMicrophone: NSObject, AuthorizeProtocol { func authorizeStatus() -> AuthorizeStatus { let status = AVAudioSession.sharedInstance().recordPermission switch status { case .denied: return .denied case .granted: return .authorized default: return .notDetermined } } func authorize(_ completion: ((AuthorizeStatus) -> Void)?) { AVAudioSession.sharedInstance().requestRecordPermission { granted in let status: AuthorizeStatus = granted ? .authorized : .denied if completion != nil { DispatchQueue.main.async { completion?(status) } } } } } // MARK: - AuthorizeTracking /// IDFA授权,iOS14+使用AppTrackingTransparency,其它使用AdSupport class AuthorizeTracking: NSObject, AuthorizeProtocol { func authorizeStatus() -> AuthorizeStatus { if #available(iOS 14.0, *) { let status = ATTrackingManager.trackingAuthorizationStatus switch status { case .restricted: return .restricted case .denied: return .denied case .authorized: return .authorized default: return .notDetermined } } else { return ASIdentifierManager.shared().isAdvertisingTrackingEnabled ? .authorized : .denied } } func authorize(_ completion: ((AuthorizeStatus) -> Void)?) { if #available(iOS 14.0, *) { ATTrackingManager.requestTrackingAuthorization { status in if completion != nil { DispatchQueue.main.async { [weak self] in guard let self = self else { return } completion?(self.authorizeStatus()) } } } } else { if completion != nil { DispatchQueue.main.async { [weak self] in guard let self = self else { return } completion?(self.authorizeStatus()) } } } } }