終於有時間繼續寫我的文章了,這段時間在趕學校的軟體課程設計,可算弄完了!
下面繼續我們的創造之旅~
本篇文章你會學到
- 用KVO方法優化鍵盤彈出動畫
- 將同步下載訊息改為非同步,以減輕主執行緒的壓力。
- 實現app登入、註冊的功能
首先下載本章原始碼:
百度網盤地址
在上一章結尾我提到:
我們的app在鍵盤彈出時有一些問題: - 在我們點出鍵盤時會遮擋訊息:
iOS Simulator Screen Shot 2015年9月8日 下午4.14.55.png
- 鍵盤彈出時把tableView拉到底部會有一個很難看的空白:
iOS Simulator Screen Shot 2015年9月8日 下午4.15.21.png
下面我們來解決它,我們需要在鍵盤彈出時修改tableView的一些屬性和約束條件,所以我們需要在鍵盤彈出時得到通知,要做到這個,我們要使用KVO(Key-Value Observing)方法。
在viewDidLoad()中的結尾新增以下程式碼來新增鍵值監控:123let notificationCenter = NSNotificationCenter.defaultCenter()notificationCenter.addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)notificationCenter.addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)首先獲取通知中心的例項,然後新增兩個觀察者,第一個用來監控
UIKeyboardWillShowNotification
鍵值的變化,這是系統提供的鍵值,當鍵盤將要彈出時會改變;第二個監控UIKeyboardDidShowNotification
,同樣地,這也是系統提供的,當鍵盤完全彈出時會改變。
當這兩個鍵值改變時,會向通知中心傳送通知,然後由我們自定義的兩個selector
方法處理通知,下面定義這兩個方法。
首先第一個方法:12345678910111213141516171819202122func keyboardWillShow(notification: NSNotification) {let userInfo = notification.userInfo as NSDictionary!let frameNew = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()let insetNewBottom = tableView.convertRect(frameNew, fromView: nil).heightlet insetOld = tableView.contentInsetlet insetChange = insetNewBottom - insetOld.bottomlet overflow = tableView.contentSize.height - (tableView.frame.height-insetOld.top-insetOld.bottom)let duration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValuelet animations: (() -> Void) = {if !(self.tableView.tracking || self.tableView.decelerating) {// 根據鍵盤位置調整Insetif overflow > 0 {self.tableView.contentOffset.y += insetChangeif self.tableView.contentOffset.y -overflow {self.tableView.contentOffset.y += insetChange + overflow}}}if duration > 0 {let options = UIViewAnimationOptions(UInt((userInfo[UIKeyboardAnimationCurveUserInfoKey] as! NSNumber).integerValue很難懂?不著急,我們一步一步解釋這些程式碼!
首先取出通知的userifno,鍵盤的所有屬性都在這裡面,他是一個字典型別的資料:1let userInfo = notification.userInfo as NSDictionary!然後通過
UIKeyboardFrameEndUserInfoKey
key取出鍵盤的位置、大小資訊,也就是frame,並將其的參考view設定為tableView,記錄下它的高度12let frameNew = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()let insetNewBottom = tableView.convertRect(frameNew, fromView: nil).height然後我們需要計算一些資料:
123let insetOld = tableView.contentInsetlet insetChange = insetNewBottom - insetOld.bottomlet overflow = tableView.contentSize.height - (tableView.frame.height-insetOld.top-insetOld.bottom)insetChange指的是那部分呢?我畫出一個圖大家就明白了:
tableview的contentInset
所指的是所圖的紅框部分。
overflow
指的是所有訊息的總高度和鍵盤彈出前contentInset的差值,實際上就是沒有顯示部分的高度,也就是溢位的部分。
然後通過UIKeyboardAnimationDurationUserInfoKey
key來得到鍵盤彈出動畫的持續時間,設定自定義的動畫閉包:
1 2 3 4 5 6 7 8 9 10 11 |
let duration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue let animations: (() -> Void) = { if !(self.tableView.tracking || self.tableView.decelerating) { // 根據鍵盤位置調整Inset if overflow > 0 { self.tableView.contentOffset.y += insetChange if self.tableView.contentOffset.y -overflow { self.tableView.contentOffset.y += insetChange + overflow } } } |
我們看一下動畫閉包內部做了些什麼。
首先判斷tableView的滾動是否停止了,如果沒有停止滾動就不做任何事情。
tableView的滾動有兩種情況:
- 手指點選tableView,開始滾動,即
tracking
- 手指抬起,tableView還會有一段減速滾動,也就是
decelerating
1234if !(self.tableView.tracking || self.tableView.decelerating){..........}
如果溢位大於0,則將tableView當前位置contentOffset
向下移動,也就對應著手指向上拖動insetChange
的高度,這樣可以保證訊息和鍵盤同時向上移動,但是如果滾動之後仍然是負值,且超出insetOld.top的距離,也就是導航欄的高度,就把tableView的當前位置設定成螢幕之上一個導航欄的高度。
如果溢位是負值,但是絕對值小於insetChange
,則contenOffset.y
增加兩者的差值。
當時長大於0時真正執行我們的動畫閉包,否則就即時執行閉包:
12if duration > 0 {let options = UIViewAnimationOptions(UInt((userInfo[UIKeyboardAnimationCurveUserInfoKey] as! NSNumber).integerValue
其中要注意的是,我們的動畫曲線要和鍵盤彈出動畫的曲線相同,所以要用UIKeyboardAnimationCurveUserInfoKey
key得到曲線資訊,這裡的型別轉換比較麻煩,要進行左移16的位運算,因為沒有對應的 as型別轉換可用,只能用最底層的方式。
為什麼要這樣呢,其實我也不知道。。我也是查來的= =
stackoverflow
第二個方法,是用來防止出現底下的白邊,原理就是限制顯示出的高度,將底部切掉一部分,也就是將contenInset.bottom
值變大一些,變大為鍵盤的高度:
1234567891011121314func keyboardDidShow(notification: NSNotification) {let userInfo = notification.userInfo as NSDictionary!let frameNew = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()let insetNewBottom = tableView.convertRect(frameNew, fromView: nil).height//根據鍵盤高度設定Insetlet contentOffsetY = tableView.contentOffset.ytableView.contentInset.bottom = insetNewBottomtableView.scrollIndicatorInsets.bottom = insetNewBottom// 優化,防止鍵盤消失後tableview有跳躍if self.tableView.tracking || self.tableView.decelerating {tableView.contentOffset.y = contentOffsetY}}
這樣就好了,執行一下,是不是感覺舒服多了?
好的,下面我們解決下一個問題,在我們開啟app的時候,會看到控制檯顯示如下內容:
122015-09-14 21:16:24.951 TuringChatMachine[820:36384] Warning: A long-running operation is being executed on the main thread.Break on warnBlockingOperationOnMainThread() to debug.
意思是有一個長執行時間的操作在主執行緒執行,由於主執行緒主要用於UI顯示,所以如果有其他佔用cpu的執行緒也在其中執行的話會使得UI顯示變得很卡。
雖然沒有什麼感覺,但是如果我們去看cpu的負荷圖的話,如下圖所示:
會看到一個瞬間cpu負荷暴漲到了32%!這樣很不酷對不對?
我們的解決辦法就是,將這個佔用cpu很多使用量的操作放在另一個執行緒中,但首先我們要找到這是哪個操作,細心的你一定注意到,當載入聊天介面的時候會比較慢,沒錯就是那個操作在作怪!
所以呢,我們對initData()方法進行一些優化。
首先改變我們從Parse伺服器下載資料的方法query.findObjects()
,這是同步下載資料,會佔據我們很大一部分cpu負載,所以我們要改為非同步下載,也就是放到其他執行緒執行,將以下程式碼修改一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
for object in query.findObjects() as! [PFObject]{ let message = Message(incoming: object[incomingKey] as! Bool, text: object[textKey] as! String, sentDate: object[sentDateKey] as! NSDate) if let url = object[urlKey] as? String{ message.url = url } if index == 0{ currentDate = message.sentDate } let timeInterval = message.sentDate.timeIntervalSinceDate(currentDate!) if timeInterval |
修改為以下使用findObjectsInBackgroundWithBlock
的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in if error == nil { if objects!.count > 0 { for object in objects as! [PFObject] { if index == objects!.count - 1{ dispatch_async(dispatch_get_main_queue(), { () -> Void in self.tableView.reloadData() }) } let message = Message(incoming: object["incoming"] as! Bool, text: object["text"] as! String, sentDate: object["sentDate"] as! NSDate) if let url = object["url"] as? String{ message.url = url } if index == 0{ currentDate = message.sentDate } let timeInterval = message.sentDate.timeIntervalSinceDate(currentDate!) if timeInterval |
由於這是非同步下載,所以tableView仍然會繼續載入cell而不會去管messages裡有沒有值,這時一定會崩潰,所以為了防止這種情況發生,我們要首先給messages賦一個歡迎訊息,在方法開頭加上這一行程式碼:
1 |
messages = [[Message(incoming: true, text: "你好,請叫我靈靈,我是主人的貼身小助手!", sentDate: NSDate())]] |
然後執行一下,同時看一下cpu的負荷率表:
僅有7%了!乾的漂亮!
下面我們來為我們的app增加一個登入的功能,因為沒有辦法去區分聊天資訊,所有人的聊天資訊都是共享的,真正的聊天app可不會是這樣的。
要做到這個,我們要為我們資料庫上的聊天訊息類增加一個新屬性:
)
User類是Parse預設的使用者類,我們的型別用指標,指向使用者類,將資訊與使用者進行繫結,這樣就能知道該條資訊屬於哪個使用者了。
幸運的是Parse已經提供了登入的檢視控制器,同樣還有註冊的檢視控制器:
PFLogInViewController和PFSignUpViewController
雖然它本身的語言是英文,但是我在初始專案裡對他們進行了一下漢化修改,其實有更好的辦法進行國際化,但這個只是為了演示。
首先我們建立一個歡迎頁面:
還有登入頁面,註冊頁面:
都加上
1 |
import ParseUI |
在LogInViewController.swift中的viewDidLoad()方法裡新增以下程式碼來自定義logo:
1 |
self.logInView?.logo = UIImageView(image: UIImage(named: "logo")) |
同樣地,在SignUpViewController.swift中的viewDidLoad()方法裡新增以下程式碼:
1 |
self.signInView?.logo = UIImageView(image: UIImage(named: "logo")) |
在WelcomeViewController.swift增加import模組:
1 2 |
import Parse import ParseUI |
使WelcomeViewController遵循PFSignUpViewControllerDelegate和
PFLogInViewControllerDelegate代理:
1 2 3 |
class WelcomeViewController: UIViewController,PFSignUpViewControllerDelegate,PFLogInViewControllerDelegate{ } |
增加屬性,登入檢視控制器和註冊檢視控制器,還有歡迎介面的logo
和welcomeLabel
用來顯示logo和歡迎語:
1 2 3 4 |
var loginVC:LogInViewController! var signUpVC:SignUpViewController! var logo:UIImageView! var welcomeLabel:UILabel! |
我們來實現一些代理方法,首先是登入的代理方法:
1 2 |
func logInViewController(logInController: PFLogInViewController, shouldBeginLogInWithUsername username: String, password: String) -> Bool { if (!username.isEmpty |
第一個方法是執行我們自定義的使用者名稱密碼的合法性檢查方法;第二個是在登入之後執行,可以通過user引數知道登入的是哪個使用者;第三個是如果登入出現錯誤,錯誤資訊可以在這裡找到。
同樣地,實現註冊相應的三個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func signUpViewController(signUpController: PFSignUpViewController, shouldBeginSignUp info: [NSObject : AnyObject]) -> Bool { var infomationComplete = true for key in info.values { var field = key as! String if (field.isEmpty){ infomationComplete = false break } } if (!infomationComplete){ UIAlertView(title: "缺少資訊", message: "請補全缺少的資訊", delegate: self, cancelButtonTitle:"確定").show() return false } return true } func signUpViewController(signUpController: PFSignUpViewController, didSignUpUser user: PFUser) { self.dismissViewControllerAnimated(true, completion: nil) } func signUpViewController(signUpController: PFSignUpViewController, didFailToSignUpWithError error: NSError?) { println("註冊失敗") } |
下面我們在viewDidLoad()中配置一下歡迎介面:
1 2 3 4 5 6 7 8 9 10 11 |
view.backgroundColor = UIColor.whiteColor() self.navigationController?.navigationBarHidden = true logo = UIImageView(image: UIImage(named: "logo")) logo.center = CGPoint(x: view.center.x, y: view.center.y - 50) welcomeLabel = UILabel(frame: CGRect(x: view.center.x - 150/2, y: view.center.y + 20, width: 150, height: 50)) welcomeLabel.font = UIFont.systemFontOfSize(22) welcomeLabel.textColor = UIColor(red:0.11, green:0.55, blue:0.86, alpha:1) welcomeLabel.textAlignment = .Center view.addSubview(welcomeLabel) view.addSubview(logo) |
我們在viewWillAppear()方法中實現歡迎頁面邏輯,當已經登入時,顯示歡迎語歡迎某某某
,然後2s後進入聊天介面,否則顯示未登入
,進入登入介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
override func viewWillAppear(animated: Bool) { if (PFUser.currentUser() != nil){ self.welcomeLabel.text = "歡迎 (PFUser.currentUser()!.username!)!" delay(seconds: 2.0, { () -> () in var chatVC = ChatViewController() chatVC.title = "靈靈" var naviVC = UINavigationController(rootViewController: chatVC) self.presentViewController(naviVC, animated: true, completion: nil) }) }else{ self.welcomeLabel.text = "未登入" delay(seconds: 2.0) { () -> () in self.loginVC = LogInViewController() self.loginVC.delegate = self self.signUpVC = SignUpViewController() self.signUpVC.delegate = self self.loginVC.signUpController = self.signUpVC self.presentViewController(self.loginVC, animated: true, completion: nil) } } } |
定義這個延時方法,在import下面:
1 2 3 4 5 6 7 |
func delay(#seconds: Double, completion:()->()) { let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64( Double(NSEC_PER_SEC) * seconds )) dispatch_after(popTime, dispatch_get_main_queue()) { completion() } } |
執行之前還有一步,就是在AppDelegate.swift的application()方法裡修改我們的初始檢視控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Override point for customization after application launch. Parse.setApplicationId("CYdFL9mvG8jHqc4ZA5PJsWMInBbMMun0XCoqnHgf", clientKey: "6tGOC1uIKeYp5glvJE6MXZOWG9pmLtMuIUdh2Yzo") var welcomeVC:WelcomeViewController = WelcomeViewController() UINavigationBar.appearance().tintColor = UIColor.whiteColor() UINavigationBar.appearance().barTintColor = UIColor(red: 0.05, green: 0.47, blue: 0.91, alpha: 1.0) UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()] UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.LightContent let frame = UIScreen.mainScreen().bounds window = UIWindow(frame: frame) window!.rootViewController = welcomeVC window!.makeKeyAndVisible() return true } |
還有一件事,我們要在讀取資料的時候只讀取當前登入使用者的資訊,而不是全部,所以我們要加上一個限制,在query.findObjectsInBackgroundWithBlock
執行前加上以下程式碼:
1 2 3 4 |
if let user = PFUser.currentUser(){ query.whereKey("createdBy", equalTo: user) messages = [[Message(incoming: true, text: "(user.username!)你好,請叫我靈靈,我是主人的貼身小助手!", sentDate: NSDate())]] } |
同樣地,我們儲存訊息的時候,將當前使用者賦值給createdBy
屬性,修改一下saveMessage()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func saveMessage(message:Message){ var saveObject = PFObject(className: "Messages") saveObject["incoming"] = message.incoming saveObject["text"] = message.text saveObject["sentDate"] = message.sentDate saveObject["url"] = message.url var user = PFUser.currentUser() saveObject["createdBy"] = user saveObject.saveEventually { (success, error) -> Void in if success{ println("訊息儲存成功!") }else{ println("訊息儲存失敗! (error)") } } } |
至此我們的登入註冊功能就整合進我們的app了,當然這只是一個演示,為了演示如何用ParseUI庫實現登入功能,並沒有太多的自定義,更復雜的應用這裡先不進行擴充套件了。
到此我們的app已經有一些正式的樣子了,下一章還會對其進行功能的擴充和優化!請持續關注!
本章完成原始碼下載
如果我的文章對你有幫助,請點一下喜歡,大家的支援是我繼續寫作的動力!