終于有時(shí)間繼續(xù)寫我的文章了,這段時(shí)間在趕學(xué)校的軟件課程設(shè)計(jì),可算弄完了!
下面繼續(xù)我們的創(chuàng)造之旅~
本篇文章你會學(xué)到
- 用KVO方法優(yōu)化鍵盤彈出動畫
- 將同步下載消息改為異步,以減輕主線程的壓力。
- 實(shí)現(xiàn)app登錄、注冊的功能
首先下載本章源代碼:
百度網(wǎng)盤地址
在上一章結(jié)尾我提到:
我們的app在鍵盤彈出時(shí)有一些問題: -
在我們點(diǎn)出鍵盤時(shí)會遮擋消息:
iOS Simulator Screen Shot 2015年9月8日 下午4.14.55.png - 鍵盤彈出時(shí)把tableView拉到底部會有一個(gè)很難看的空白:
iOS Simulator Screen Shot 2015年9月8日 下午4.15.21.png
下面我們來解決它,我們需要在鍵盤彈出時(shí)修改tableView的一些屬性和約束條件,所以我們需要在鍵盤彈出時(shí)得到通知,要做到這個(gè),我們要使用KVO(Key-Value Observing)方法。
在viewDidLoad()中的結(jié)尾添加以下代碼來添加鍵值監(jiān)控:
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
notificationCenter.addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
首先獲取通知中心的實(shí)例,然后添加兩個(gè)觀察者,第一個(gè)用來監(jiān)控UIKeyboardWillShowNotification
鍵值的變化,這是系統(tǒng)提供的鍵值,當(dāng)鍵盤將要彈出時(shí)會改變;第二個(gè)監(jiān)控 UIKeyboardDidShowNotification
,同樣地,這也是系統(tǒng)提供的,當(dāng)鍵盤完全彈出時(shí)會改變。
當(dāng)這兩個(gè)鍵值改變時(shí),會向通知中心發(fā)送通知,然后由我們自定義的兩個(gè)selector
方法處理通知,下面定義這兩個(gè)方法。
首先第一個(gè)方法:
func keyboardWillShow(notification: NSNotification) {
let userInfo = notification.userInfo as NSDictionary!
let frameNew = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let insetNewBottom = tableView.convertRect(frameNew, fromView: nil).height
let insetOld = tableView.contentInset
let insetChange = insetNewBottom - insetOld.bottom
let overflow = tableView.contentSize.height - (tableView.frame.height-insetOld.top-insetOld.bottom)
let duration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
let animations: (() -> Void) = {
if !(self.tableView.tracking || self.tableView.decelerating) {
// 根據(jù)鍵盤位置調(diào)整Inset
if overflow > 0 {
self.tableView.contentOffset.y += insetChange
if self.tableView.contentOffset.y < -insetOld.top {
self.tableView.contentOffset.y = -insetOld.top
}
} else if insetChange > -overflow {
self.tableView.contentOffset.y += insetChange + overflow
}
}
}
if duration > 0 {
let options = UIViewAnimationOptions(UInt((userInfo[UIKeyboardAnimationCurveUserInfoKey] as! NSNumber).integerValue << 16)) // http://stackoverflow.com/a/18873820/242933
UIView.animateWithDuration(duration, delay: 0, options: options, animations: animations, completion: nil)
} else {
animations()
}
}
很難懂?不著急,我們一步一步解釋這些代碼!
首先取出通知的userifno,鍵盤的所有屬性都在這里面,他是一個(gè)字典類型的數(shù)據(jù):
let userInfo = notification.userInfo as NSDictionary!
然后通過UIKeyboardFrameEndUserInfoKey
key取出鍵盤的位置、大小信息,也就是frame,并將其的參考view設(shè)置為tableView,記錄下它的高度
let frameNew = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let insetNewBottom = tableView.convertRect(frameNew, fromView: nil).height
然后我們需要計(jì)算一些數(shù)據(jù):
let insetOld = tableView.contentInset
let insetChange = insetNewBottom - insetOld.bottom
let overflow = tableView.contentSize.height - (tableView.frame.height-insetOld.top-insetOld.bottom)
insetChange指的是那部分呢?我畫出一個(gè)圖大家就明白了:
tableview的
contentInset
所指的是所圖的紅框部分。overflow
指的是所有消息的總高度和鍵盤彈出前contentInset的差值,實(shí)際上就是沒有顯示部分的高度,也就是溢出的部分。然后通過
UIKeyboardAnimationDurationUserInfoKey
key來得到鍵盤彈出動畫的持續(xù)時(shí)間,設(shè)置自定義的動畫閉包:
let duration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
let animations: (() -> Void) = {
if !(self.tableView.tracking || self.tableView.decelerating) {
// 根據(jù)鍵盤位置調(diào)整Inset
if overflow > 0 {
self.tableView.contentOffset.y += insetChange
if self.tableView.contentOffset.y < -insetOld.top {
self.tableView.contentOffset.y = -insetOld.top
}
} else if insetChange > -overflow {
self.tableView.contentOffset.y += insetChange + overflow
}
}
}
我們看一下動畫閉包內(nèi)部做了些什么。
首先判斷tableView的滾動是否停止了,如果沒有停止?jié)L動就不做任何事情。
tableView的滾動有兩種情況:
- 手指點(diǎn)擊tableView,開始滾動,即
tracking
- 手指抬起,tableView還會有一段減速滾動,也就是
decelerating
if !(self.tableView.tracking || self.tableView.decelerating){
.....
.....
}
如果溢出大于0,則將tableView當(dāng)前位置contentOffset
向下移動,也就對應(yīng)著手指向上拖動insetChange
的高度,這樣可以保證消息和鍵盤同時(shí)向上移動,但是如果滾動之后仍然是負(fù)值,且超出insetOld.top的距離,也就是導(dǎo)航欄的高度,就把tableView的當(dāng)前位置設(shè)置成屏幕之上一個(gè)導(dǎo)航欄的高度。
如果溢出是負(fù)值,但是絕對值小于insetChange
,則contenOffset.y
增加兩者的差值。
當(dāng)時(shí)長大于0時(shí)真正執(zhí)行我們的動畫閉包,否則就即時(shí)執(zhí)行閉包:
if duration > 0 {
let options = UIViewAnimationOptions(UInt((userInfo[UIKeyboardAnimationCurveUserInfoKey] as! NSNumber).integerValue << 16)) // http://stackoverflow.com/a/18873820/242933
UIView.animateWithDuration(duration, delay: 0, options: options, animations: animations, completion: nil)
} else {
animations()
}
其中要注意的是,我們的動畫曲線要和鍵盤彈出動畫的曲線相同,所以要用 UIKeyboardAnimationCurveUserInfoKey
key得到曲線信息,這里的類型轉(zhuǎn)換比較麻煩,要進(jìn)行左移16的位運(yùn)算,因?yàn)闆]有對應(yīng)的 as類型轉(zhuǎn)換可用,只能用最底層的方式。
為什么要這樣呢,其實(shí)我也不知道。。我也是查來的= =
stackoverflow
第二個(gè)方法,是用來防止出現(xiàn)底下的白邊,原理就是限制顯示出的高度,將底部切掉一部分,也就是將contenInset.bottom
值變大一些,變大為鍵盤的高度:
func keyboardDidShow(notification: NSNotification) {
let userInfo = notification.userInfo as NSDictionary!
let frameNew = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let insetNewBottom = tableView.convertRect(frameNew, fromView: nil).height
//根據(jù)鍵盤高度設(shè)置Inset
let contentOffsetY = tableView.contentOffset.y
tableView.contentInset.bottom = insetNewBottom
tableView.scrollIndicatorInsets.bottom = insetNewBottom
// 優(yōu)化,防止鍵盤消失后tableview有跳躍
if self.tableView.tracking || self.tableView.decelerating {
tableView.contentOffset.y = contentOffsetY
}
}
這樣就好了,運(yùn)行一下,是不是感覺舒服多了?
好的,下面我們解決下一個(gè)問題,在我們打開app的時(shí)候,會看到控制臺顯示如下內(nèi)容:
2015-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.
意思是有一個(gè)長運(yùn)行時(shí)間的操作在主線程執(zhí)行,由于主線程主要用于UI顯示,所以如果有其他占用cpu的線程也在其中運(yùn)行的話會使得UI顯示變得很卡。
雖然沒有什么感覺,但是如果我們?nèi)タ碿pu的負(fù)荷圖的話,如下圖所示:
會看到一個(gè)瞬間cpu負(fù)荷暴漲到了32%!這樣很不酷對不對?
我們的解決辦法就是,將這個(gè)占用cpu很多使用量的操作放在另一個(gè)線程中,但首先我們要找到這是哪個(gè)操作,細(xì)心的你一定注意到,當(dāng)加載聊天界面的時(shí)候會比較慢,沒錯(cuò)就是那個(gè)操作在作怪!
所以呢,我們對initData()方法進(jìn)行一些優(yōu)化。
首先改變我們從Parse服務(wù)器下載數(shù)據(jù)的方法
query.findObjects()
,這是同步下載數(shù)據(jù),會占據(jù)我們很大一部分cpu負(fù)載,所以我們要改為異步下載,也就是放到其他線程執(zhí)行,將以下代碼修改一下:
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 < 120{
messages[section].append(message)
}else{
section++
messages.append([message])
}
currentDate = message.sentDate
index++
}
修改為以下使用findObjectsInBackgroundWithBlock
的版本:
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 < 120{
self.messages[section].append(message)
}else{
section++
self.messages.append([message])
}
currentDate = message.sentDate
index++
}
}
}else{
println("error \(error?.userInfo)")
}
}
由于這是異步下載,所以tableView仍然會繼續(xù)加載cell而不會去管messages里有沒有值,這時(shí)一定會崩潰,所以為了防止這種情況發(fā)生,我們要首先給messages賦一個(gè)歡迎消息,在方法開頭加上這一行代碼:
messages = [[Message(incoming: true, text: "你好,請叫我靈靈,我是主人的貼身小助手!", sentDate: NSDate())]]
然后運(yùn)行一下,同時(shí)看一下cpu的負(fù)荷率表:
僅有7%了!干的漂亮!
下面我們來為我們的app增加一個(gè)登錄的功能,因?yàn)闆]有辦法去區(qū)分聊天信息,所有人的聊天信息都是共享的,真正的聊天app可不會是這樣的。
要做到這個(gè),我們要為我們數(shù)據(jù)庫上的聊天消息類增加一個(gè)新屬性:
User類是Parse默認(rèn)的用戶類,我們的類型用指針,指向用戶類,將信息與用戶進(jìn)行綁定,這樣就能知道該條信息屬于哪個(gè)用戶了。
幸運(yùn)的是Parse已經(jīng)提供了登錄的視圖控制器,同樣還有注冊的視圖控制器:
PFLogInViewController和PFSignUpViewController
雖然它本身的語言是英文,但是我在初始項(xiàng)目里對他們進(jìn)行了一下漢化修改,其實(shí)有更好的辦法進(jìn)行國際化,但這個(gè)只是為了演示。
首先我們創(chuàng)建一個(gè)歡迎頁面:
還有登錄頁面,注冊頁面:
都加上
import ParseUI
在LogInViewController.swift中的viewDidLoad()方法里添加以下代碼來自定義logo:
self.logInView?.logo = UIImageView(image: UIImage(named: "logo"))
同樣地,在SignUpViewController.swift中的viewDidLoad()方法里添加以下代碼:
self.signInView?.logo = UIImageView(image: UIImage(named: "logo"))
在WelcomeViewController.swift增加import模塊:
import Parse
import ParseUI
使WelcomeViewController遵循PFSignUpViewControllerDelegate和
PFLogInViewControllerDelegate代理:
class WelcomeViewController: UIViewController,PFSignUpViewControllerDelegate,PFLogInViewControllerDelegate{
}
增加屬性,登錄視圖控制器和注冊視圖控制器,還有歡迎界面的logo
和welcomeLabel
用來顯示logo和歡迎語:
var loginVC:LogInViewController!
var signUpVC:SignUpViewController!
var logo:UIImageView!
var welcomeLabel:UILabel!
我們來實(shí)現(xiàn)一些代理方法,首先是登錄的代理方法:
func logInViewController(logInController: PFLogInViewController, shouldBeginLogInWithUsername username: String, password: String) -> Bool {
if (!username.isEmpty && !password.isEmpty )
{
return true
}
UIAlertView(title: "缺少信息", message: "請補(bǔ)全缺少的信息", delegate: self, cancelButtonTitle:"確定").show()
return false
}
func logInViewController(logInController: PFLogInViewController, didLogInUser user: PFUser) {
self.dismissViewControllerAnimated(true, completion: nil)
}
func logInViewController(logInController: PFLogInViewController, didFailToLogInWithError error: NSError?) {
println("登錄錯(cuò)誤")
}
第一個(gè)方法是執(zhí)行我們自定義的用戶名密碼的合法性檢查方法;第二個(gè)是在登錄之后執(zhí)行,可以通過user參數(shù)知道登錄的是哪個(gè)用戶;第三個(gè)是如果登錄出現(xiàn)錯(cuò)誤,錯(cuò)誤信息可以在這里找到。
同樣地,實(shí)現(xiàn)注冊相應(yīng)的三個(gè)方法:
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: "請補(bǔ)全缺少的信息", 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()中配置一下歡迎界面:
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()方法中實(shí)現(xiàn)歡迎頁面邏輯,當(dāng)已經(jīng)登錄時(shí),顯示歡迎語歡迎某某某
,然后2s后進(jìn)入聊天界面,否則顯示未登錄
,進(jìn)入登錄界面:
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)
}
}
}
定義這個(gè)延時(shí)方法,在import下面:
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()
}
}
運(yùn)行之前還有一步,就是在AppDelegate.swift的application()方法里修改我們的初始視圖控制器:
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
}
還有一件事,我們要在讀取數(shù)據(jù)的時(shí)候只讀取當(dāng)前登錄用戶的信息,而不是全部,所以我們要加上一個(gè)限制,在query.findObjectsInBackgroundWithBlock
執(zhí)行前加上以下代碼:
if let user = PFUser.currentUser(){
query.whereKey("createdBy", equalTo: user)
messages = [[Message(incoming: true, text: "\(user.username!)你好,請叫我靈靈,我是主人的貼身小助手!", sentDate: NSDate())]]
}
同樣地,我們保存消息的時(shí)候,將當(dāng)前用戶賦值給createdBy
屬性,修改一下saveMessage()方法:
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)")
}
}
}
至此我們的登錄注冊功能就集成進(jìn)我們的app了,當(dāng)然這只是一個(gè)演示,為了演示如何用ParseUI庫實(shí)現(xiàn)登錄功能,并沒有太多的自定義,更復(fù)雜的應(yīng)用這里先不進(jìn)行擴(kuò)展了。
到此我們的app已經(jīng)有一些正式的樣子了,下一章還會對其進(jìn)行功能的擴(kuò)充和優(yōu)化!請持續(xù)關(guān)注!
本章完成源代碼下載
如果我的文章對你有幫助,請點(diǎn)一下喜歡,大家的支持是我繼續(xù)寫作的動力!