Swift4 Day92:APP 製作從0到1全記錄(上)
Swift4 / iOS11 / 2018.12.03/ Moving
介紹
幸運的加入了 AppWorks School 之後,經歷一連串學習 iOS 的基礎知識之後,開始了我的第二個個人專案:Moving健身運動教練
一開始要做自己的專案時,想了很久也不知道要做什麼,而且只有五週就要完成上架的時程壓力。
更重要的是個人專案作品的好壞,會影響到你最後能收到多少面試機會。
最後就從自己的生活中下手,想改善自己用 Youtube 健身的一些問題
- 希望能跳過解說專注在運動的部分
- 解決影片秒數與目標運動長度不同的問題
- 無法紀錄運動時間
決定好大方向想要做的 App 之後,要開始調查怎麼實作,因為很多想出來的功能其實自己也沒實作過,還有研究類似的 App 截長補短,把基本的 UI 跟想做的功能做成簡報,跟 AppWorks School 的大家簡報介紹自己要做的內容。那時候的介紹功能簡報長這樣跟現在實際成品差很多 😂
接著我們會用 Trello 去把要做的事情拆成小的功能,將要做的是排在不同的類別裡。每天早上開早會的時候,就會跟 iOS 班的大家討論,自己昨天做了什麼、今天要做什麼,有問題的話也可以在這時候提出來問我們的 Mentor Luke 大大討論。
在 Trello 上會有下列幾種分類方式:
Product Backlog:一開始所有要做的事,突然想要做的功能就放進去。
Sprint Backlog:其實看到 announcement 裡,都有紀錄每個 Sprint 的時間段,所以自己決定這個 Sprint 要做的事就會放進 Sprint Backlog裡。
Todo:會把這一兩天想做的事放在 Todo。
Doing:正在做的部分就會把卡移到這裡。
Done:做完的時候就會把自己的卡移到這。
Youtube Player 建置
一開始我最主要的功能是希望能播放 Youtube 、暫停 Youtube、切換 Youtube 的時間,所以我先去調查,怎麼樣做出類似的功能。所以我用了 YouTubePlayer-Swift 這個 framework 。
我作出了可以播放、暫停、切換影片的雛形,我就把它應用在我的 project 中了,但我後來才發現他有很多問題與限制是一開始難以發現的,像你放大影片 切回來 StatusBar 會消失、拉時間條無法偵測、狂按播放暫停會 crash、而且上方的 Youtube 頻道的名稱按下去會導到 Youtube 之類的問題。
後來我就讓videoView.isUserInteractionEnabled = false
用點擊Cell的方式去改變影片的狀態。
這個framework只有7顆星星,換成他 fork 的原始檔也是有相同問題。QQ
APP 資料結構設計與存取
點選第一頁運動頁面,就要去抓出所有這個運動部位的運動計劃,切換到第二個頁面 ListModel 中應該要有一個 ActionModel 的 Array ,存這個運動計畫的各個動作與動作的狀態。
一開始我只用一個名為 LocaolDatabase 的 class ,裝著所有本地端資料結構,大概像這樣,用 Singleton 方式設計。
即使沒有網路也可以看到運動內容與圖片,只是播放影片的時候會用 Reachability 去判斷網路行為,處理沒有網路時內容呈現。
BUT!
有一天發現其中一支影片不能連接了,我更新好之後卻要等 Apple 審核,覺得這樣太慢了,我就決定把我的 LocaolDatabase 的資料搬到 Firebase。
Struct 轉 JSON
首先將資料Encode,然後將 console 的內容複製到 Sublime ,把副檔名改成 JSON,或直接用 Swift File也能做 JSON 檔。在 Firebase選擇匯入 JSON 就不用重新輸入資料了。原本不知道還差點要手打勒 QQ
在每一個運動計畫加上 Category ,回到本地端 Filter 就能分好所有運動種類。
let jsonData = try? jsonEncoder.encode(LocaolDatabase.instance.getAbs())let jsonString = String(data: jsonData!, encoding: .utf8)!print(jsonString)
網路存取圖片
因為所有資料都透過 Firebase 傳遞,所以我將圖片存在 Firebase Storage後,再將 URL 存回 Firebase Database ,到本地的時候用 Kingfisher 套件解析圖片,也要處理更新前的圖片讀取的方法。
if summaryModel.videoImg.contains("https://firebasestorage") {guard let url = URL(string: summaryModel.videoImg) else { return }videoImg.kf.setImage(with: url)} else {videoImg.image = UIImage(named: summaryModel.videoImg)}
動畫介紹
看到這一頁先拆解一下要做的事吧
- 要辨別運動時間與休息時間,處理不同動畫,要能暫停
- 處理 tableview 的 reuse 機制,因為設計上動畫條是不在 model 上的
- 點選 cell 的時候,判斷現在 cell 與被點選的 cell 的關係,切換影片時切換也要處理其他所有 cell 的狀態
- 處理影片時間與目標影片時間不同
- 換動作的時候, 運動的cell 對齊影片,但當你按最後一個 cell 時該怎麼做
再來一個一個介紹🙋
要辨別運動時間與休息時間,處理不同動畫,要能暫停
大家想要更了解動畫可以參考,我們家志瑋的簡報。
簡單來說對我需求而言,就是 大家平常使用的 Core Animtion 沒有辦法暫停,只能執行與移除。在動畫一開始時,其實你的物件就在目標處了,眼睛所看到的動畫,都是假的,是 Presentation Layer 幫你畫的,所以你想知道現在畫面長到哪,要檢查的是 Presentation Layer。
後來我選擇用 UIViewPropertyAnimatior 做我的專案,可以暫停與 restart 。
要辨別運動時間與休息時間,其實把 property 寫在 ActionModel 裡,修改後讓 View 與 Model 維持一致就好。
處理 tableview 的 reuse 機制,因為設計上動畫條是不在 model 上的
我寫的 function actionViewWidthAnimate 中判斷兩個狀況,目前運動秒數與初始秒數相同,就從頭畫動畫。
如果有被 reuse,就在 willDisplay cell 的地方傳 UITableViewCell 回 actionViewWidthAnimate 去重畫,抓到現在的秒數與總時間的比例,讓他長到該在的地方,然後繼續長好長滿。 rest 也是用相同的方法做處理。
//actionLists[nowIndex].timesDescription 是初始秒數
//actionSec 是我運動倒數的秒數,如果相同就代表是剛開始,從頭開始畫if Int(actionLists[nowIndex].timesDescription) == actionSec
點選 cell 的時候,判斷現在 cell 與被點選的 cell 的關係,切換影片時切換也要處理其他所有 cell 的狀態
在 didSelectRowAt 中判斷,點選的 cell 是否是正在運動的 cell ,是的話就暫停或播放,不是的話就去判斷他們關係,還有相對應的畫面呈現。
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {if indexPath.row == nowIndex {if actionLists[nowIndex].playingOrPause == .playing {videoView.pause()} else {videoView.play()}} else if indexPath.row > nowIndex {moreThanNowIndex(indexPath: indexPath)} else if indexPath.row < nowIndex {lessThanNowIndex(indexPath: indexPath) }
}
處理影片時間與目標秒數不同的關係
我用 asyncAfter 讓現在的時間加上該 repeat 的秒數,如果時間到了還是在相同的影片,就會 seekTo 他原先的影片位置。
DispatchQueue.main.asyncAfter(deadline: .now() + youtubestopTime) {if compareIndex == self.nowIndex {self.videoView.seekTo(Float(self.actionLists[self.nowIndex].youtubeTime), seekAhead: true)
換動作的時候, 運動的cell 對齊影片,但當你按最後一個 cell 時該怎麼做
tableview 會不夠長,對吧?那就用 contentInset 讓他變長吧。
最長只要達到, TableView 全長減一個 cell 的高,雖然也可以用 row 距離底部多長去做變化,但我就直接讓他長最長啦。
let contentInsetHeight = Int(actionTableView.frame.height - cell.frame.height)if contentInsetNumber < contentInsetHeight {contentInsetNumber += contentInsetHeightactionTableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: CGFloat(contentInsetNumber), right: 0)
這個專案的 GitHub :
下集在這👉