【Swift 5】我到底身在何方?我到底去到何處?
因為呢,個人不務正業太久了,又加上新冠肺炎疫情的緣故,這來寫寫這個有關定位 / 語系的文章。無論你身在何方 無論你去到何處,我都要定位的到你啦,怎麼有點恐怖情人的感覺…XD,主要是在工作上有用到,用來判斷該給什麼語系的資料,所以想寫個定位懶人包以後可以用,這次的文章比較多Code,話不多說,就讓我們繼續看下去。
作業環境
項目 | 版本 |
---|---|
CPU | Apple M1 |
macOS | Big Sur 11.4 |
Xcode | 12.5 |
完成的結果
設得資訊
事前準備
// MARK: - 自定義的Print
public func wwPrint<T>(_ msg: T, file: String = #file, method: String = #function, line: Int = #line) {
#if DEBUG
Swift.print("🚩 \((file as NSString).lastPathComponent):\(line) - \(method) \n\t ✅ \(msg)")
#endif
}
// MARK: - Utility (單例)
final class Utility: NSObject {
static let shared = Utility()
private override init() {}
}
// MARK: - typealias
extension Utility {
// 定位位置 => [GPS / SIM卡 / 首選語言 / 區域]
typealias LocationCountryCode = (GPS: String?, SIMs: [String], preferredLanguage: String?, region: String?)
// 語言資訊 => [語系-分支-地區]
typealias LanguageInfomation = (code: String.SubSequence?, script: String.SubSequence?, region: String.SubSequence?)
// 定位的相關資訊
typealias LocationInfomation = (location: CLLocation?, isAvailable: Bool)
}
// MARK: - Collection (override class function)
extension Collection {
/// [為Array加上安全取值特性 => nil](https://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings)
subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil }
}
取得裝置區域
- 取得裝置區域,也就是在『設定 -> 一般 -> 語言與地區 -> 地區』的資訊,目前是取得『TW』這個文字。地區代碼的標準是ISO 3166-1。
// MARK: - 我到底身在何方?我到底去到何處?
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
wwPrint(Locale._currentRegionCode()) // Optional("TW")
}
}
// MARK: - Locale (static function)
extension Locale {
/// 取得裝置區域
/// - 設定 -> 一般 -> 語言與地區 -> 地區 (TW)
/// - Returns: 區域辨識碼
static func _currentRegionCode() -> String? { return Locale.current.regionCode }
}
取得裝置語系
// MARK: - 我到底身在何方?我到底去到何處?
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
wwPrint(Locale._preferredLanguageInfomation()) // Optional((code: Optional("ja"), script: nil, region: Optional("TW")))
}
}
// MARK: - Locale (static function)
extension Locale {
/// 取得裝置區域
/// - 設定 -> 一般 -> 語言與地區 -> 地區 (TW)
/// - Returns: 區域辨識碼
static func _currentRegionCode() -> String? { return Locale.current.regionCode }
/// 取得首選語系 (第一個語系)
/// - 設定 -> 一般 -> 語言與地區 -> 偏好的語言順序 (zh-Hant-TW / [語系-分支-地區])
/// - Returns: [語系辨識碼](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html)
static func _preferredLanguage() -> String? {
guard let language = Locale.preferredLanguages.first else { return nil }
return language
}
/// 把完整的語系編碼分類
/// - zh-Hant-TW => [語系-分支-地區]
/// - Parameter language: 完整的語系文字
/// - Returns: Utility.LanguageInfomation?
static func _preferredLanguageInfomation(_ language: String? = Locale.preferredLanguages.first) -> Utility.LanguageInfomation? {
guard let preferredLanguage = language,
let languageInfos = Optional.some(preferredLanguage.split(separator: "-")),
languageInfos.count > 0
else {
return nil
}
var info: Utility.LanguageInfomation = (nil, nil, nil)
switch languageInfos.count {
case 1: info.code = languageInfos.first
case 2: info.code = languageInfos.first; info.region = languageInfos.last
case 3: info.code = languageInfos.first; info.region = languageInfos.last; info.script = languageInfos[safe: 1]
default: break
}
return info
}
}
取得SIM卡供應商的國籍碼
- 取得SIM卡供應商的國籍碼,因為在iOS 12之後有雙SIM卡的手機出現,所以會是個Array。利用CoreTelephony Framework所提供的功能來取得。
// MARK: - 我到底身在何方?我到底去到何處?
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
wwPrint(UIDevice._isoCountryCodes()) // ["TW", "TW"]
}
}
// MARK: - UIDevice (static function)
extension UIDevice {
/// [SIM卡供應商的國籍碼](https://stackoverflow.com/questions/18798398/how-to-get-the-users-country-calling-code-in-ios)
/// - iOS 12之後有雙SIM卡 => [ISO 3166-1](https://zh.wikipedia.org/wiki/ISO_3166-1)
/// - Returns: [String]
static func _isoCountryCodes() -> [String] {
var codeArray = [String]()
if let info = CTTelephonyNetworkInfo().serviceSubscriberCellularProviders {
for (_, value) in info { if let code = value.isoCountryCode { codeArray.append(code.uppercased()) } }
}
return codeArray
}
}
整合一下
- 接下來,把這些功能整合成一個function。
// MARK: - CLLocationManager (static function)
extension CLLocationManager {
/// 取得該裝置的國家地域碼 (不包含GPS定位)
/// - Returns: LocationCountryCode
static func _locationCountryCode() -> Utility.LocationCountryCode {
var code = Utility.LocationCountryCode(GPS: nil, SIMs: [], preferredLanguage: nil, region: nil)
code.SIMs = UIDevice._isoCountryCodes()
code.region = Locale._currentRegionCode()
if let codeWithLanguage = Locale._preferredLanguageInfomation()?.region { code.preferredLanguage = String(codeWithLanguage) }
return code
}
}
手機定位
定位權限
- 主要就是利用CoreLocation Framework來做處理。在info.plist中,加入NSLocationWhenInUseUsageDescription權限設定,來開啟定位的使用。
- 同時也加上CLLocationManagerDelegate
NSLocationWhenInUseUsageDescription
// MARK: - Utility (單例)
final class Utility: NSObject {
static let shared = Utility()
lazy var locationManager = { CLLocationManager._build(delegate: self) }()
private override init() {}
}
// MARK: - CLLocationManagerDelegate
extension Utility: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {}
func locationManager(_ manager: CLLocationManager, didFinishDeferredUpdatesWithError error: Error?) {}
}
// MARK: - CLLocationManager (static function)
extension CLLocationManager {
/// 定位授權Manager產生器
/// - Parameters:
/// - delegate: CLLocationManagerDelegate
/// - desiredAccuracy: 定位的精度
/// - distanceFilter: 過濾的距離
/// - Returns: CLLocationManager
static func _build(delegate: CLLocationManagerDelegate?, desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest, distanceFilter: CLLocationDistance = 1) -> CLLocationManager {
let locationManager = CLLocationManager()
locationManager.delegate = delegate
locationManager.desiredAccuracy = desiredAccuracy
locationManager.distanceFilter = distanceFilter
return locationManager
}
}
Notification
- 因為它並不是一定馬上可以取到的值,所以這裡利用NotificationCenter來傳送數值。
// MARK: - Notification (static function)
extension Notification {
/// String => Notification.Name
/// - Parameter name: key的名字
/// - Returns: Notification.Name
static func _name(_ name: String) -> Notification.Name { return Notification.Name(rawValue: name) }
/// NotificationName => Notification.Name
/// - Parameter name: key的名字 (enum)
/// - Returns: Notification.Name
static func _name(_ name: Utility.NotificationName) -> Notification.Name { return name.value }
}
// MARK: - NotificationCenter (class function)
extension NotificationCenter {
/// 註冊通知
/// - Parameters:
/// - name: 要註冊的Notification名稱
/// - queue: 執行的序列
/// - object: 接收的資料
/// - handler: 監聽到後要執行的動作
func _register(name: Notification.Name, queue: OperationQueue = .main, object: Any? = nil, handler: @escaping ((Notification) -> Void)) {
self.addObserver(forName: name, object: object, queue: queue) { (notification) in handler(notification) }
}
/// 發射通知
/// - Parameters:
/// - name: 要發射的Notification名稱
/// - object: 要傳送的資料
func _post(name: Notification.Name, object: Any? = nil) { self.post(name: name, object: object) }
/// 移除通知
/// - Parameters:
/// - observer: 要移除的位置
/// - name: 要移除的Notification名稱
/// - object: 接收的資料
func _remove(observer: Any, name: Notification.Name, object: Any? = nil) { self.removeObserver(observer, name: name, object: object) }
}
啟用定位服務
- 在這裡就可以試看看定位功能是不是可以正常使用?
// MARK: - 我到底身在何方?我到底去到何處?
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Utility.shared.locationManager._locationServicesAuthorizationStatus {
wwPrint("alwaysHandler")
} whenInUseHandler: {
wwPrint("whenInUseHandler")
} deniedHandler: {
wwPrint("deniedHandler")
}
}
}
// MARK: - CLLocationManager (class function)
extension CLLocationManager {
/// 定位授權的各種狀態處理
/// - requestWhenInUseAuthorization() => 詢問是否要開啟定位授權的提示視窗
/// - info.plist => NSLocationAlwaysAndWhenInUseUsageDescription
/// - Parameters:
/// - alwaysHandler: 選擇always後的動作
/// - whenInUseHandler: 選擇whenInUse後的動作
/// - deniedHandler: 選擇denied後的動作
func _locationServicesAuthorizationStatus(alwaysHandler: @escaping () -> Void, whenInUseHandler: @escaping () -> Void, deniedHandler: @escaping () -> Void) {
let authorizationStatus = CLLocationManager.authorizationStatus()
switch authorizationStatus {
case .notDetermined: requestWhenInUseAuthorization()
case .authorizedWhenInUse: whenInUseHandler()
case .authorizedAlways: alwaysHandler()
case .denied: deniedHandler()
case .restricted: deniedHandler()
@unknown default: fatalError()
}
}
}
定位協定
- 利用CLLocationManagerDelegate來取得相關的值,進而做處理。
// MARK: - enum
extension Utility {
/// 自訂錯誤
enum MyError: Error, LocalizedError {
var errorDescription: String { errorMessage() }
case notGeocodeLocation
/// 顯示錯誤說明
/// - Returns: String
private func errorMessage() -> String {
switch self {
case .notGeocodeLocation: return "地理編碼錯誤"
}
}
}
/// NotificationName
enum NotificationName {
/// 顯示真實的值
var value: Notification.Name { return notificationName() }
// 定位服務 (自定義)
case _locationServices
/// 顯示真實的值 => Notification.Name
func notificationName() -> Notification.Name {
switch self {
case ._locationServices: return Notification._name("_locationServices")
}
}
}
}
// MARK: - CLLocationManagerDelegate
extension Utility: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { countryCode(with: manager, locations: locations) }
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { locationManagerStatus(manager, status: status) }
func locationManager(_ manager: CLLocationManager, didFinishDeferredUpdatesWithError error: Error?) {
let code = CLLocationManager._locationCountryCode()
NotificationCenter.default._post(name: Notification._name(._locationServices), object: code)
}
}
定位權限的狀態處理
- LocationManager的狀態處理,當權限許可時,就可以更新位置,不然就停下來。
// MARK: - 定位相關 (CLLocationManager)
extension Utility {
/// LocationManager狀態處理
/// - Parameters:
/// - manager: CLLocationManager
/// - status: CLAuthorizationStatus
private func locationManagerStatus(_ manager: CLLocationManager, status: CLAuthorizationStatus) {
switch status {
case .authorizedAlways: manager.startUpdatingLocation()
case .authorizedWhenInUse: manager.startUpdatingLocation()
case .notDetermined: manager.stopUpdatingLocation()
case .denied: manager.stopUpdatingLocation()
case .restricted: manager.stopUpdatingLocation()
@unknown default: fatalError()
}
}
}
取得有效的位置資訊
- manager._locationInfomation():取得有效的位置資訊。
- _placemark():將坐標(25.04216003, 121.52873230) => CLPlacemark (地址)。
- _locationCountryCode:取得該裝置的國家地域碼 (不包含GPS定位)。
- 最後再利用NotificationCenter把自定義的NotificationName發送出去。
// MARK: - 定位相關 (CLLocationManager)
extension Utility {
/// 取得該裝置的國家地域碼
/// - Notification._name(._locationServices)
/// - Parameters:
/// - manager: CLLocationManager
/// - locations: [CLLocation]
private func countryCode(with manager: CLLocationManager, locations: [CLLocation]) {
var code = CLLocationManager._locationCountryCode()
guard manager._locationInfomation(with: locations).isAvailable,
let location = manager._locationInfomation(with: locations).location
else {
NotificationCenter.default._post(name: Notification._name(._locationServices), object: code); return
}
location.coordinate._placemark { (result) in
switch result {
case .failure(_): NotificationCenter.default._post(name: Notification._name(._locationServices), object: code)
case .success(let placemark):
manager.stopUpdatingLocation()
code.GPS = placemark.isoCountryCode
NotificationCenter.default._post(name: Notification._name(._locationServices), object: code)
}
}
}
}
// MARK: - CLLocationManager (class function)
extension CLLocationManager {
/// 處理位置的相關資料
/// - 最後一筆的有效位置 > 0
/// - Parameter locations: 取到的位置們
/// - Returns: Utility.LocationInfomation
func _locationInfomation(with locations: [CLLocation]) -> Utility.LocationInfomation {
guard let location = locations.last,
let isAvailable = Optional.some(location.horizontalAccuracy > 0)
else {
return (nil, false)
}
return (location, isAvailable)
}
}
// MARK: - CLLocationCoordinate2D (class function)
extension CLLocationCoordinate2D {
/// 將坐標(25.04216003, 121.52873230) => CLPlacemark (地址)
/// - Parameters:
/// - preferredLocale: 傳回的資料語系區域
/// - result: Result<CLPlacemark, Error>
func _placemark(preferredLocale: Locale = Locale(identifier: "zh_TW"), result: @escaping (Result<CLPlacemark, Error>) -> Void) {
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: preferredLocale) { placemarks, error in
if let error = error { result(.failure(error)); return }
if let placemark = placemarks?.first { result(.success(placemark)); return }
result(.failure(Utility.MyError.notGeocodeLocation))
}
}
}
使用方法
- 雖然寫起來有點複雜,但使用起來卻是超簡單的。
- 先註冊Notification,然後就可以用了。
// MARK: - 我到底身在何方?我到底去到何處?
final class ViewController: UIViewController {
@IBOutlet weak var resultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let notificationName = Notification._name(._locationServices)
NotificationCenter.default._register(name: notificationName) { notification in
self.resultLabel.text = notification.object.debugDescription
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Utility.shared.locationManager._locationServicesAuthorizationStatus {
wwPrint("alwaysHandler")
} whenInUseHandler: {
wwPrint("whenInUseHandler")
} deniedHandler: {
self.resultLabel.text = "\(CLLocationManager._locationCountryCode())"
}
}
}
範例程式碼下載
後記
- 原本還以為應該滿簡單的,不過使用起來還滿不錯用的,雖然寫起來滿複雜,但至少在使用上,能很靈活的去排定結果的前後順序,維護上應該也滿容易的。
- 寫註解也是滿累的,不過我可是個有良心的iOS打字工啊…XD