「iOS」通過CoreLocation Framework重新了解多界面傳值以及MVC架構
文章目錄
- 「iOS」通過CoreLocation Framework重新了解多界面傳值以及MVC架構
- 前言
- CoreLocation了解
- 根據需求建模
- 設計屬性
- 方法設計
- 協議傳值
- Block傳值
- KVO
- Notification通知方式
- 總結
- 參考文章
前言
在這個學期的前段時間進行了MVC的相關學習,并且使用MVC完成了知乎日報奧的項目,加上學習的一些博客,開始對MVC這個架構有著更加深刻的理解,也體會到了MVC架構之中的缺點,這篇文章就是利用CoreLocation 這個原生關于定位的架構,來總結MVC的一些使用技巧和理解。
CoreLocation了解
根據需求建模
我們可以先來認識CoreLocation
實現定位的相關內容,我們知道CoreLocation
是一個關于位置定位的庫,我們分析一下相關的功能,要想實現定位首先就需要一個位置類來對位置進行描述,屬于它的屬性應該有經緯度,海拔等相關信息;一個地標類來描述所處位置的城市街道;一個管理器來控制位置的變更,以及位置的獲取;一個解析器根據當前的位置信息轉化為地標。那么邏輯如此,對應的圖片關系如下
我們可以在CoreLocation Framework
之中找到了對應的這幾個類的相關屬性,這里我簡單的給出這些類之中的部分頭文件
CLLocation
:
@interface CLLocation : NSObject <NSCopying, NSSecureCoding> @property(readonly, nonatomic) CLLocationCoordinate2D coordinate;//返回當前位置的坐標。
@property(readonly, nonatomic) CLLocationDistance altitude;// 返回位置的高度,正值表示海平面上,負值表示海平面下。- (instancetype)initWithLatitude:(CLLocationDegrees)latitudelongitude:(CLLocationDegrees)longitude;//初始化指定經緯度的位置。@end
CLPlacemark
:
@interface CLPlacemark : NSObject <NSCopying, NSSecureCoding>// 通過已有地標初始化新地標并復制其數據
- (instancetype)initWithPlacemark:(CLPlacemark *) placemark;// 獲取與地標相關聯的地理位置信息
@property (nonatomic, readonly, copy, nullable) CLLocation *location;// 城市名稱(如Cupertino)
@property (nonatomic, readonly, copy, nullable) NSString *locality;
......
// 一系列地標相關的具體屬性,用于更詳細地描述位置信息
CLLocationManager
:
@interface CLLocationManager : NSObject
// 獲取調用應用的當前授權狀態
@property (nonatomic, readonly) CLAuthorizationStatus authorizationStatus API_AVAILABLE(ios(14.0), macos(11.0), watchos(7.0), tvos(14.0));// 獲取最后接收到的位置信息,在接收到位置前為nil
@property(readonly, nonatomic, copy, nullable) CLLocation *location;// 判斷用戶是否啟用了位置服務
+ (BOOL)locationServicesEnabled API_AVAILABLE(ios(4.0), macos(10.7));// 開始更新位置信息
- (void)startUpdatingLocation API_AVAILABLE(watchos(3.0)) API_UNAVAILABLE(tvos);// 停止更新位置信息
- (void)stopUpdatingLocation;@end
CLGeocoder
:
// 定義地理編碼完成后的回調處理塊,CLPlacemarks按可信度從高到低排列
@interface CLGeocoder : NSObject
typedef void (^CLGeocodeCompletionHandler)(NSArray< CLPlacemark *> * __nullable placemarks, NSError * __nullable error);@interface CLGeocoder : NSObject
// 判斷是否正在進行地理編碼操作
@property (nonatomic, readonly, getter=isGeocoding) BOOL geocoding;// 反向地理編碼請求,根據給定位置獲取對應的地標信息
- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;// 用地址字符串進行地理編碼(默認無區域和首選語言環境設置)
- (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;
@end
CLLocationManagerDelegate
:
@protocol CLLocationManagerDelegate<NSObject>- (void)locationManager:(CLLocationManager *)managerdidUpdateLocations:(NSArray<CLLocation *> *)locations;@end
設計屬性
我們可以從這些類的設計學習一些MVC架構的設計思想
從剛剛的頭文件我們可以看到,其實頭文件之中的許多屬性是被標注為只讀,標志為只讀其實對于解耦合實現規范代碼有著很大的幫助,比如CLLocationManager
之中的location
屬性,因為這個位置管理器只是用來管理這個當前的位置,對于外部的使用者來說,只需要在適當的時候進行訪問而不是修改。對應數據的更新和維護其實就是這個類內部的負責。
我們就可以提煉出一個設計準則:外部使用者只要負責讀取數據,具體的數據更新則是由提供者來完成
這種設計思想其實很清晰的將層次分開了,我們這樣不僅成功保護了相關數據的安全,也能進一步的減少相關屬性值的濫用。
具體的操作大佬的博客歸結為:
- 業務類中的屬性設計為只讀。使用者只能通過屬性來讀取數據。而由業務類中的方法內部來更新這些屬性的值。
- 數據模型類中的屬性定義最好也設置為只讀,因為數據模型的建立是在業務類方法內部完成并通過通知或者異步回調的方式交給使用者。而不應該交由使用者來創建和更新。
- 數據模型類一般提供一個帶有所有屬性的init初始化方法,而初始化后這些屬性原則上是不能被再次改變,所以應該設置為只讀屬性。
這里的類全部指的是暴露在頭文件之中的屬性聲明
至于對于類內容,我們需要在內部(即.m文件)修改相關的聲明
@interface CLLocationManager ()@property(nonatomic, copy, nullable) CLLocation *location;
@end//也可以改用以下形式
@implementation CLLocationManager {CLLocation *_location;
}@end
方法設計
協議傳值
當我們類設計結束之后,隨之而來的就是類方法的設計類。無論如何,我們的業務模型最后總是會走向網絡請求或者數據庫調用這種需要,在獲取操作之后再進行異步的操作。在這里CoreLocation
的框架,使用的是Delegate和Blockzhe這兩種方式進行回調
先看CLLocationManager定義的屬性
@property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;
這個協議實現了
@protocol CLLocationManagerDelegate<NSObject>@optional
- (void)locationManager:(CLLocationManager *)managerdidUpdateLocations:(NSArray<CLLocation *> *)locations API_AVAILABLE(ios(6.0), macos(10.9));@end
當位置管理器對象更新了當前的位置后就會調用delegate屬性所指對象的didUpdateLocations方法來通知。
這就產生了幾個問題:
-
誰來創建M層的位置管理對象?
C層,這個其實很簡單,控制器是負責控制和協調M層,所以C層具有負責創建并持有M層對象的責任,C層也是一個使用觀察者。 -
M層如何來實現實時的更新和停止更新?
對于位置管理器之中,有以下兩個方法
- (void)startUpdatingLocation API_AVAILABLE(watchos(3.0)) __TVOS_PROHIBITED;- (void)stopUpdatingLocation;
好像是在tableView也有相似的兩個方法beginUpdates
和endUpdates
,通過方法通知tableView的數據源發生更新進而更新cell。位置管理器之中的這兩個方法通過通知管理器,位置類的數據(即M層)在內部發生變化。至于這個兩個方法是如何實現位置管理器之中持有的位置類的變化,就是內部實現的內容,相當于一個黑盒,我們作為M層的使用者不需要知道里面的實現原理。
-
誰來負責調用M層提供的那些方法?
答案是: 控制器C層。因為控制器既然負責M層對象的構建,那他當然也是負責M層方法的調用了。 -
誰來觀察M層的數據變化通知并進行相應的處理?
答案是: 控制器C層。因為C層既然負責調用M層所提供的方法,那么他理所當然的也要負責對方法的返回以及更新進行處理。在這里我們的C層控制器需要實現CLLocationManagerDelegate接口,并賦值給位置管理器對象的delegate屬性。
這里引用大佬博客之中的內容
我們知道MVC結構中,C層是負責協調和調度M和V層的一個非常關鍵的角色。而C和M以及V之間的交互協調方式用的最多的也是通過Delegate這種模式,Delegate這種模式并不局限在M和C之間,同樣也可以應用在V和C之間。Delegate的本質其實是一種雙方之間通信的接口,而通過接口來進行通信則可以最大限度的減少對象之間交互的耦合性。
Block傳值
除了用Delegate外,我們還可以用Block回調這種方式來實現方法調用的異步通知處理。標準格式如下:
typedef void (^BlockHandler)(id obj, NSError * error);
在地標解析器CLGeocoder之中,采用的就是block回調這種方式來實現異步通知的。我們來看看類的部分定義:
typedef void (^CLGeocodeCompletionHandler)(NSArray< CLPlacemark *> * __nullable placemarks, NSError * __nullable error);// 反向地理編碼請求,根據給定位置獲取對應的地標信息
- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;// 用地址字符串進行地理編碼(默認無區域和首選語言環境設置)
- (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;
從反向地理編碼請求可以看出來,我們根據一個CLLocation
方向解碼出一個CLPlacemark
的對象,用一個block來完成內容的回調
//VC中的某個點擊按鈕事件:-(void)ClickHandle:(UIButton*)sender
{sender.userInteractionEnabled = NO;__weak XXXVC *weakSelf = self;//geocoder也可以是XXXVC里面的一個屬性,從而可以避免重復建立CLGeocoder *geocoder = [CLGeocoder new];//假設知道了某個位置對象location[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray< CLPlacemark *> * placemarks, NSError * error)){if (weakSelf == nil)return;sender.userInteractionEnabled = YES;if (error == nil){//處理placemarks對象}else{//處理錯誤}}];
}
這里的block回調,其實在沒有異步的情況下(即讀取本地庫)是可以不需要寫的,但是在我們實際的編寫過程當中,還是盡可能的遵循統一的相關的模式,有時候需求是會改變的,如果我們這個操作要改為異步操作的話,那么代碼需要整段修改,還包括C層的代碼,修改起來很麻煩。那么不如就在一開始就使用block進行回調,有統一的格式以及便于理解的優點。
KVO
上面簡單展示了Delegate與Block機制用于實現M層數據的更新,前面介紹了這兩個機制的優點,下面引用博客的內容,概括這兩者的缺點,順帶引入對應的KVO機制監聽的內容:
Delegate的方式必須要事先定義出一個接口協議來,并且調用者和實現者都需要按照這個接口規則來進行通知和數據處理交互,這樣無形中就產生了一定的耦合性。也就是二者之間還是具有隱式的依賴形式。不利于擴展和進行完全自定義處理。
block方式的缺點則是使用不好則會產生循環引用的問題從而產生內存泄露,另外就是用block機制在出錯后難以調試以及難以進行問題跟蹤。 而且block機制其實也是需要在調用者和實現之間預先定義一個標準的BlockHandler接口來進行交互和處理。block機制還有一個缺陷是會在代碼中產生多重嵌套,從而影響代碼的美觀和可讀性。
Delegate和block方式雖然都是一種觀察者實現,但卻不是標準和經典的觀察者模式。因為這兩種模式是無法實現多觀察者的。也就是說當數據更新而進行通知時,只能有一個觀察者進行監聽和處理,不能實現多個觀察者的通知更新處理。
可惜的是在CoreLocation Framework并不支持KVO之中方式,下面是大佬假設其支持KVO寫出的相關代碼。
//再次申明的是CCLocationManager是不支持KVO來監聽位置變化的,這里只是一個假設支持的話的使用方法。@interface AppDelegate@property(nonatomic, strong) CLLocationManager *locationManager;
@end@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {self.locationManager = [CLLocationManager new];[self.locationManager startUpdatingLocation]; //開始監聽位置變化return YES;
}
@end//第一個頁面
@implementation VC1-(void)viewWillAppear:(BOOL)animated
{[ [UIApplication sharedApplication].delegate.locationManager addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew context:NULL];
}-(void)viewWillDisappear:(BOOL)animated
{[ [UIApplication sharedApplication].delegate.locationManager removeObserver:self forKeyPath:@"location" ];}- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{//這里處理位置變化時的邏輯。
}
@end//第二個頁面
@implementation VC2-(void)viewWillAppear:(BOOL)animated
{[ [UIApplication sharedApplication].delegate.locationManager addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew context:NULL];
}-(void)viewWillDisappear:(BOOL)animated
{[ [UIApplication sharedApplication].delegate.locationManager removeObserver:self forKeyPath:@"location" ];}- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{//這里處理位置變化時的邏輯。
}
@end//.. 其他頁面
接下來分析一下在什么情況下使用KVO:
- 最顯而易見的,當我這個數據更新可能會引起多個依賴該對象的對象更新時使用KVO
- 當某個對象,在對應流程之中會創建多個副本,而且在這個副本的狀態會不斷產生變化,當副本增多的情況下,我們就需要使用KVO機制來根據新的狀態來處理。
下面是我們使用多副本且不使用KVO的相關流程
使用KVO+單例,KVO在這里的意義就是,通知這個單例屬性狀態已經被改變,進行對應的更新
Notification通知方式
KVO模式實現了一種對屬性變化的通知觀察機制。而且這種機制由系統來完成,缺點就是他只是對屬性的變化進行觀察,而不能對某些異步方法調用進行通知處理。而如果我們想要正真的實現觀察者模式而不局限于屬性呢?答案就是iOS的NSNotificationCenter。
但是這個模式對應的也存在一些缺點,就使用通知的代碼較為松散,在一定程度上,不利于程序的解讀。看前面的內容,通過Delegate或者block時來設計業務層方法的回調時,可以很清楚的知道業務調用方法和實現機制的上下文,因為這些東西在代碼定義里面就已經固化了,我們必須額外的去學習和了解哪些業務層的方法需要添加觀察者哪些不需要,而且代碼中不管在什么時候需要都要在初始化時添加一段代碼上去。通知處理邏輯的可讀寫性以及代碼的可讀性也比較差。
總結
其實這篇文章寫著寫著,和前面寫的五大傳值方法其實有些重合,主要就是當時在學習傳值技巧的時候還沒有學到MVC架構,之前的學習還是停留在較為表面的,通過這次學習加深了對傳值和MVC架構結合的理解
參考文章
論MVVM偽框架結構和MVC中M的實現機制
控制層的設計方法
模型層設計方法