快速实现的 iOS 平台简易聊天软件

2,249 阅读5分钟
原文链接: yrq110.me

在逛论坛与看公众号文章时看到了铺天盖地的wilddog广告,由于没接触过这种东东,而且看官网很简洁清爽,符合个人口味,遂想写个简单的IM软件来实践一下。



先上代码:Dogchat_Swift

上学期课余时间写的,实现了一些简单的功能,主要是用wilddog的服务器与其API实现的:

  • 私聊(文字, 定位)
  • 群聊(文字, 图片)
  • 通讯录
  • 消息推送
  • 其他功能

测试时用两台测试机与模拟器进行群聊,一台测试机与模拟器进行私聊与定位,模拟器定位用手动设置的location,响应都挺快的。

环境:

  • OS X 10.11.6
  • Xcode 7.3
  • Swift

准备工作

运行代码时需要在ConstantValue.swift文件中将WilddogURL的值改成你自己的wilddog数据库地址。

在AppDelegate里修改根视图可直接跳过登录界面。

方便起见,直接使用wilddog的身份认证功能添加用户。

登录

file:LoginViewController.swift

使用Wilddog对象的authUser方法登录,用刚才添加的账号即可,不过要修改一下authUserLogin方法中的transmitAccountMsg函数参数,影响登陆后数据的获取。

登录界面只缺一个美工系列:

用户登录:

func authUserLogin(user:String,password:String){
    let myRootRef = Wilddog(url:WilddogURL)
    myRootRef.authUser(user, password: password) {
        error, authData in
        if error != nil {
            print(error)
            let alertController = UIAlertController(title: "错误", message: "账号或密码错误", preferredStyle: .Alert)
            let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
            alertController.addAction(defaultAction)
            self.presentViewController(alertController, animated: true, completion: nil)
        } else {
            let mainVC = ViewController()
            mainVC.transmitAccountMsg(user)
            let nvc=UINavigationController(rootViewController:mainVC)
            self.presentViewController(nvc, animated: true, completion: nil)

数据存储

后台数据,相关操作使用的wilddog提供的api:

  • PrivateMsg:私聊与定位数据
  • UserAccount:用户信息
  • UserMsg:群聊数据

群聊

file:ViewController.swift

在AppDelegate里修改根视图可直接跳过登录界面。

登录进去后的界面就是群聊界面,聊天内容所有成员可见。

有个方块旋转的加载动画,加载后如图:

群聊数据初始化:

func msgInitialAdd(){
    let myRootRef = Wilddog(url:WilddogURL)
    let userRef = myRootRef.childByAppendingPath("UserMsg")
    userRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
        let count = snapshot.value.count
        var i = 0
        self.observerHandle = userRef.queryOrderedByKey().observeEventType(.ChildAdded, withBlock: { snapshot in
            if snapshot.value != nil{
                self.dic[snapshot.key] = snapshot.value as? Dictionary
                self.chatMsgArray.append(snapshot.key)
                i=i+1
                if i == count {
                    self.loadingView.hidden = true
                    self.timeAnime.tupleToAnime(self.parse.parseStr(self.dic[snapshot.key]!["time"]!))
                    userRef.removeAllObservers()
                    self.initialAdds = false
                    self.tableView!.reloadData()

发送图片(ImagePickerControllerDelegate的委托方法):

func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject])
        self.dismissViewControllerAnimated(true, completion: nil)
        if let image = info[UIImagePickerControllerOriginalImage] as? UIImage
            print("select photo")
            let newImageThumbnail = imageCompressForWidth(image, targetWidth: 100)
            let data = UIImageJPEGRepresentation(newImageThumbnail, 1.0)!
            thumbnailImageStr = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
            let str = "image:" + thumbnailImageStr
            let myRootRef = Wilddog(url:WilddogURL)
            let msgRef = myRootRef.childByAppendingPath("UserMsg")
            msgRef.removeObserverWithHandle(self.observerHandle)
            let Ref = msgRef.childByAutoId()
            let msg = ["ID":self.userID,"nickname":self.userNickName,"message":str,"time":self.getTime()]
            Ref.setValue(msg)

后台数据存储:

可以输入文字聊天,可以选择相册中的图片并发送,图片转成base64码存储。

背景是个动画,至于这个后面说。

通讯录

file:ContactsViewController.swift

点击群聊界面左上角的contact进入通讯录,读取wilddog端的UserAccount数据:

获取通讯录数据:

func transmitAccountMsg(mail:String,id:String,nickname:String){
    print("Enter Contacts View")
    self.userID = id
    self.userNickname = nickname
    self.userArray = Array()
    let myRootRef = Wilddog(url:WilddogURL)
    let accountRef = myRootRef.childByAppendingPath("UserAccount")
    accountRef.queryOrderedByChild("mail").observeSingleEventOfType(.Value, withBlock: { snapshot in
        for i in (snapshot.value as! Dictionary>) {
            if i.1["mail"]! != mail {
                self.userArray.append(i.1["nickname"]!)
                self.mailArray.append(i.1["mail"]!)

后台数据存储:

私聊

file:SingleChatViewController.swift

点击通讯录中的任一条目即可开始私聊。

聊天:

私聊数据初始化:

func initMessage(){
    let userRef = myRootRef.childByAppendingPath("PrivateMsg"+"/"+self.userNickname+"/"+self.title!)
    userRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
        if snapshot.value != nil{
            print("Get msg!")
            self.msgPath = "PrivateMsg"+"/"+self.userNickname+"/"+self.title!
            self.refreshMessage()
            let count = snapshot.value.count
            var i = 0
            userRef.queryOrderedByKey().observeEventType(.ChildAdded, withBlock: { snapshot in
                self.dic[snapshot.key] = snapshot.value as? Dictionary
                self.chatMsgArray.append(snapshot.key)
                i += 1
                if i == count {
                    userRef.removeAllObservers()
                    self.tableView!.reloadData()
                    self.initLoad = false
        }else {
            self.initMessageAgain()
   print("init successed,\(self.msgPath)")

后台数据存储:

共享定位:

存在bug,还没fix,时好时坏。

自身位置:

func MyLocationInit(){
        if !CLLocationManager.locationServicesEnabled() {
            print("定位服务当前可能尚未打开,请设置打开!")
        if (CLLocationManager.authorizationStatus() == CLAuthorizationStatus.NotDetermined){
            locationManager.requestWhenInUseAuthorization()
        }else if(CLLocationManager.authorizationStatus() == CLAuthorizationStatus.AuthorizedWhenInUse){
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            let distance:CLLocationDistance = 2.0
            locationManager.distanceFilter = distance
            locationManager.startUpdatingLocation()
        mapView.userTrackingMode = MKUserTrackingMode.Follow

他人位置:

func otherLocationInit(){
    print("OtherPositon is \(OtherPostion)")
    let objectAnnotation = MKPointAnnotation()
    let lati = (OtherPostion["latitude"]! as NSString).doubleValue
    let longi = (OtherPostion["longitude"]! as NSString).doubleValue
    objectAnnotation.coordinate = CLLocation(latitude: lati,
        longitude: longi).coordinate
    objectAnnotation.title = "\(OtherName)的位置"
    objectAnnotation.subtitle = "subtitle"
    self.mapView.addAnnotation(objectAnnotation)
    mapSetRegion(longi, latitude: lati, isMy: false)
    func mapSetRegion(longitude:Double,latitude:Double,isMy:Bool){
    let latDelta = 0.05
    let longDelta = 0.05
    let currentLocationSpan:MKCoordinateSpan = MKCoordinateSpanMake(latDelta, longDelta)
    let center:CLLocation = CLLocation(latitude: latitude, longitude: longitude)
    let currentRegion:MKCoordinateRegion = MKCoordinateRegion(center: center.coordinate,
        span: currentLocationSpan)
    if isMy {
        (self.longitude,self.latitude) = (center.coordinate.longitude,center.coordinate.latitude)
    mapView.setRegion(currentRegion, animated: true)

消息推送

身边暂时没真机没法截图,以前测试过是正常的,如图:

消息推送:

func newMsgNotifi(title:String?,body:String?){
    var msgBody:String!
    if body!.hasPrefix("image:") {
        msgBody = "发来一张图片"
    }else{
        msgBody = ":" + body!
    var notification = UILocalNotification()
    notification.timeZone = NSTimeZone.localTimeZone()
    notification.alertTitle = title
    notification.alertBody = title! + msgBody
    notification.alertAction = "OK"
    notification.soundName = UILocalNotificationDefaultSoundName
    notification.applicationIconBadgeNumber = 1
    var userInfo:[NSObject : AnyObject] = [NSObject : AnyObject]()
    userInfo["kLocalNotificationID"] = "LocalNotificationID"
    userInfo["key"] = "Attention Please"
    notification.userInfo = userInfo
    UIApplication.sharedApplication().presentLocalNotificationNow(notification)

其它

在群聊界面中右上角的那个Other按钮点进去是这样的:

一个七巧板view,本来是个OC库,加个bridge改个调用函数就行了。忘了以前哪找的了,原作者是yuancan,作为附加功能的一个集合。

放了些有趣的东东,选择相册图片生成字符画,手写板,字符随机出现的文章,按钮动画等等。

关于主界面的那个背景动画

因为发送聊天记录时数据都会包含时间字段,我就想根据这个时间分析出季节、节日、早晚、天气等信息,然后通过动画展示出来。不过水平有限,只实现出了最简单的效果,太阳和月亮与它们出现的高度代表一天中的时刻,背景色代表季节,节日解析出来了不过还没处理,天气也还没写获取的相关代码。具体解析和动画代码在ParseDateStringClass.swiftResponseToAnimeView.swift中。

1.上传聊天记录时保存时间:

func getTime() -> String {
  let date = NSDate()
  let timeFormatter = NSDateFormatter()
  timeFormatter.dateFormat = "yyy-MM-dd.HH:mm:ss"
  let strNowTime = timeFormatter.stringFromDate(date) as String
  return strNowTime

2.过滤时间字符串:

func parseStr(str:String) -> (Int,Int,String,Int){
        let onceSplit = str.characters.split{$0 == "."}.map(String.init)
        let month = String(Int(onceSplit[0].characters.split{$0 == "-"}.map(String.init)[1]
            )!)
        let day = String(Int(onceSplit[0].characters.split{$0 == "-"}.map(String.init)[2])!)
        let date = month + "-" + day
        let dayTime = Int(onceSplit[1].characters.split{$0 == ":"}.map(String.init)[0])
        var season : Season
        var daytime : DayTimePeriod
        var weather : Weather = Weather.Sunny
        var festival : Festival
        switch Int(month)! {
        case 12,1,2 : season = Season.Winter
        case 3...5 : season = Season.Spring
        case 6...8 : season = Season.Summer
        case 9...11 : season = Season.Autumn
        default : season = Season.Spring
        switch dayTime! {
        case 6...9: daytime = DayTimePeriod.morning
        case 10...14: daytime = DayTimePeriod.noon
        case 15...18: daytime = DayTimePeriod.afternoon
        case 19...22: daytime = DayTimePeriod.evening
        case 22,23,24,0...5: daytime = DayTimePeriod.night
        default: daytime = DayTimePeriod.morning
        switch day {
        case "1-1": festival = Festival.NewYearsDay
        case "3-8": festival = Festival.WomensDay
        case "3-12": festival = Festival.ArborDay
        case "5-1": festival = Festival.LaborDay
        case "5-4": festival = Festival.YouthDay
        case "6-1": festival = Festival.ChildrensDay
        case "10-1": festival = Festival.NationalDay
        case "12-31": festival = Festival.NewYearsEve
        default: festival = Festival.NewYearsDay
        return (season.rawValue,daytime.rawValue,festival.rawValue,weather.rawValue)

3.转换为动画:

func tupleToAnime(tuple:(Int,Int,String,Int))
    if cloudIsFired == false {
        cloudTimer.fire()
        cloudIsFired = true
    seasonNo = tuple.0
    daytimeNo = tuple.1
    festivalNo = tuple.2
    weatherNo = tuple.3
    self.sunAndMoonWithTime(daytimeNo)

本来设置的是背景动画随着当前屏幕中tableview最底部显示的聊天记录的时间而变化的,就是拨动列表时这个动画是随着聊天记录的时间渐变的,不过也有些bug,暂时禁用了,一直显示一个固定时间的动画。

最后

不得不说的是这个云的名儿起的的确很奔放。。。