一款記錄跑步應用,能記錄運動軌跡和運動過程中的數據,功能實現比較完整,15年年底做的上架項目,開發者賬號過期AppStore上已經搜不到了,不過一些Android應用市場還能找到Android的版本。
源碼放出來作為學習交流。見github。
應用截圖
應用大概長這個樣子,截取了部分界面
項目文件結構
項目的文件結構如下所示
界面相關的實現全在View里,其他文件是一些比較獨立的且和整個項目相關的功能的實現。
個人偏好是以功能模塊來劃分文件,如跑步相關的功能會專門放一個文件里,用戶相關的功能又會放另一個文件里??雌渌恍╅_源項目,比如以MVVM為架構的工程,會專門分為Model、ViewController、ViewModel三個文件夾,然后所有相關的文件無論功能劃分全堆到對應的文件里。這里會存在一個問題,當應用比較復雜的時候,每個文件夾下便會存在很多文件,從中去找要修改的文件很麻煩,而且修改同一個功能還要跨文件夾工作。
還有一種方式是文件夾按功能來劃分,然后在每個實現對應功能的文件下再按MVVM或者MVC架構的方式劃分對應的文件夾,看個人喜好。
界面的實現,個人習慣是將界面中關聯較緊密的部分,單獨抽出來作為一個獨立的控件,對外提供修改數據的接口,視圖控制器直接引用這個控件,并通過外部接口來修改該控件的顯示,而不用關注控件內部例如布局相關、UI設置的具體實現。有時候控件里面也會包含其他的控件。總之就是為了將對外無關的代碼實現封裝到類中,外部操作控件時只需要關注邏輯實現。
以前公司的項目,見過一個視圖控制器類,包含所有視圖的生成布局設置,和相關業務邏輯的處理,結果就是一個UIViewController類文件的代碼行數近3萬行,簡直是維護人員的噩夢。所以個人偏向將比較獨立的代碼單獨抽出為一個文件,無論是界面布局相關,或是業務流程處理相關,盡量保證每個文件的代碼在幾百行之內。
布局方式
大部分視圖控件使用了xib做自動布局,沒用storyboard做頁面間的跳轉,是因為大部分時間用筆記本做開發,小屏幕看storyboard一堆連在一起的界面體驗太差。當時開發的時候還是15年年底,iOS9的reference也才剛出,項目需要兼容iOS8。
有些視圖的布局,需要的運算很小,懶得專門新建個xib,就直接通過計算frame的方式來實現了。
有些使用了xib但需要做布局變化的地方,會引用布局約束NSLayoutConstraint,通過代碼方式對布局約束進行調整。
也有些地方嘗試使用純代碼定義NSLayoutConstraint的方式來實現自動布局,感受就是,實現的代碼量太大,不方便后續維護修改。
抽點代碼出來感受一下。。這段代碼其實在xib里面就是給一個視圖添加了一個上下左右邊緣的約束和對應的間距,界面上點幾下就能實現的事,換成代碼會要用好多行。所以項目里的布局能用xib的基本全用xib了,我懶。
...
[self addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0]];
...
若真想用代碼來實現自動布局,推薦一個框架Masonry,它對NSLayoutConstraint做了一層封裝,用一種更直觀更優雅的編碼方式來實現布局約束。這個框架有對應的Swift版本SnapKit,也是同一個團隊在進行維護。具體可查看官方文檔。
第三方庫
主要用到的第三方庫:
- 高德地圖SDK:用來做路徑繪制和定位
- ShareSDK:第三方分享和登錄
- FMDB:本地數據庫的操作
- AFNetworking:網絡請求
- 友盟SDK:做埋點統計和用戶反饋
使用到的第三方庫都放到了項目Vendors文件夾里,當時是直接將第三方的源碼和資源拖到項目里,并手動給項目target添加需要的系統庫。這并不是一種好的實踐方式,引用管理第三方還是推薦使用CocoaPods。當然,這種做法也有一種好處,就是方便別人把項目download下來之后什么都不用管直接就能跑起來了。:)
藍牙連接
應用可以通過藍牙連接外設獲取數據。我們有配套的運動內衣,穿戴上之后可收集心率。應用通過掃描附近藍牙設備并連接就可獲取到心率的數據。
整個藍牙相關的流程是標準的藍牙協議,iOS自帶相關框架<CoreBluetooth/CoreBluetooth.h>
專門處理這個流程。
代碼具體實現在 Bluetooth 文件夾下的 YSBluetoothConnect 類。
實現對應的代理來進行藍牙事件回調處理,如設備的連接、斷開,心率數據的獲取。
CBCentralManagerDelegate
CBPeripheralDelegate
心率數據貌似在藍牙設備中有固定的標準字段,代碼里面這個字段的常量。
static NSString *ServiceHeartRateUUIDStr = @"180D";
static NSString *CharacteristicHeartRateUUIDStr = @"2A37";
所以應該只要是能發送心率的藍牙設備都能給這個應用傳輸數據。代碼里面對藍牙設備名稱的前綴做了判斷處理,若想嘗試自行連其他藍牙設備可以把這個前綴判斷注釋掉。
有些界面需要有藍牙數據才能顯示,為了獲取到模擬的藍牙數據,直接將這個字段設為YES即可。
// 設為YES時模擬生成心率
static BOOL simulationMode = YES;
語音提示
Voice 文件夾下的 YSVoicePrompt類,運動時每公里提示,若連接心率設備時,會根據心率是否在特定的范圍做相應提示。提示可設置男聲女聲,資源文件在Audio.bundle里,音頻資源當時還是專門上淘寶找人做的。
數據庫操作
主要實現在 Database 文件下。FMDB對系統自帶的SQLite做了封裝,使得可以直接用OC的方式來操作數據庫。
數據庫主要保存用戶信息和每次跑步數據,例如每次跑步記錄的GPS路徑坐標,運動過程中收集到的心率數據。界面展示相關信息時需要從數據庫查詢。
系統的sqlite并非線程安全,多線程同時對sqlite進行操作極有可能造成崩潰。數據庫的讀寫比較耗時不應該放到主線程里,多線程操作可以放到FMDB的FMDatabaseQueue隊列里進行。
網絡請求
請求相關的處理實現在 Network 文件下。如用戶的注冊時的驗證碼,登錄注銷,云端數據同步和本地數據上傳等。具體接口可看 YSNetworkManager.h 文件注釋。
租用的服務器已過期,所以現在的請求基本都返回超時,只能看著接口自行腦補過程了。
地圖功能
整個應用最核心的功能就是和地圖相關的路徑記錄了。使用的是高德SDK。具體的功能實現在 Manager/Map 文件下。
主要的實現類為 YSMapManager
運動開始結束時調用的接口
- (void)startLocation;
- (void)endLocation;
MAMapView 為顯示地圖的視圖,用來顯示地圖和繪制運動路徑。
實現MAMapView的代理MAMapViewDelegate,定位實時更新時會收到對應的回調
- (void)mapView:(MAMapView *)mapView didUpdateUserLocation:(MAUserLocation *)userLocation updatingLocation:(BOOL)updatingLocation
回調參數里會包含新的定位的信息,將新獲取到的定位數據保存,并重新繪,即可看到實時的運動軌跡。
實時路徑的繪制具體可看代碼,也可看這篇文章,高德地圖實時路徑繪制代碼實現,忽略其他的代碼。
YSMapPaintFunc 類實現了將獲取到的定位點依次連成一條路徑顯示在地圖上。GPS定位有時候會存在一定的誤差,繪制到地圖上會顯示毛刺,實現的時候用了個小算法將這些毛刺點做了平滑處理,具體自己看代碼啦。
記錄路徑的過程中還需要計算一些其他的值,如總距離,平均速度,當前速度,配速等。這些東可以通過搜集到的定位數據和時間來進行計算。YSMapPaintFunc類用來做相應的計算。
計時器
Manager/Time 中實現。YSTimeManager 主要用了一個Runloop通過一定的時間間隔來不斷刷新界面和時間相關的信息。需要注意的是NSRunLoop在主線程和子線程使用的區別。
Manager/CaptchaTimer 中的實現為倒計時,用戶手機注冊時會發送驗證碼,發送驗證碼按鈕點擊之后會有一定的時間將按鈕置灰,以防止頻繁的發送驗證碼。
應用配置
Manager/Config 的YSConfigManager 類用來記錄應用的配置,通過系統的NSUserDefaults進行保存。需要配置的信息較少,也就語音提示選擇男聲或女聲,界面上時候顯示實時心率數據的面板,藍牙默認連接。配置的選項不多。
Models
Models文件加下有很多model,當時初衷是將一些相關的數據保存在一個類里,方便作為參數進行數據傳遞。如單次跑步相關的數據保存到一個model里,傳遞給專門展示這些數據的視圖做為界面的顯示?,F在回過頭看感覺處理得有點亂,也許不是個好的實踐,讀者請自行斟酌參考。
應用的界面實現
界面相關的代碼實現全在View文件夾下。
文件夾對應的功能實現
login
用戶相關功能的實現,如注冊登錄,修改重置密碼等。
Settings
設置界面,提示音設置,打開關閉心率面板,用戶反饋。
Calendar
tabBar左邊的日歷界面,當時想用UICollectionView來實現個日歷控件,后面調試的時候發現有些日期cell之間的左右間隔在設為0之后還是會出現細線,不得以只能強行用一個UIScrollView在上面自己貼UIView的方式來實現了這控件。性能還有待優化,左右滑動切換日期時有時會有卡頓現象。
Run
主要的幾個界面:
- YSRunViewController:tabBar中間的界面,顯示記錄的總體數據,開始運動的入口
- YSRunningRecordViewController:運動過程中界面,主要包含兩個視圖,顯示實時運動軌跡的地圖視圖,和不顯示地圖的視圖
- YSResultRecordView:運動結束后顯示結果的界面
User
tabBar右邊的用戶界面,顯示當前用戶信息,登錄注銷,修改用戶資料的入口。
SportRecord
記錄單次運動具體信息和數據分析的界面。
幾個文件實現的功能:
- Detail:詳情界面,此次運動的距、時間、速度等
- HeartRate:心率數據界面,分析運動全程心率在各個范圍內的比例
- Locus:運動的軌跡
- Pace:計算出每段路程的配速
- Share:社交分享,將軌跡生成圖片發送
General
一些可以單獨抽出來,或者幾個界面都會用到的控件。大部分也都是xib實現。
xib實現可復用控件時需要考慮一個問題,使用時是直接用在另一個xib中,還是在代碼中通過nibWithNibName的方式加載,兩種使用方式設定Custom Class的地方不一樣,具體參考源碼。
其他
項目的大體情況如上所述,具體的一些細節實現請自行查看源碼。:)
最后
源碼可以隨便使用,不用經過允許。有問題可提issues。
偶然發現騰訊部落小伙伴還在運營,關注數量還挺多,喜歡運動跑步的小伙伴可關注易瘦跑步騰訊部落,每天都會分享運動跑步相關的小知識。:)
完。