关于im:千亿级IM独立开发指南全球即时通讯全套代码4小时速成三

39次阅读

共计 88103 个字符,预计需要花费 221 分钟才能阅读完成。

本文篇幅较长,共计 19352 字,预计浏览时长 1 -2h。

这是《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成》的第三篇:《APP 外部流程与逻辑》
系列文章可参考:
《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成(一)》:Demo 演示与 IM 设计
《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成(二)》:UI 设计与搭建
《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成》4:服务端搭建与总结(行将凋谢)

三、APP 外部流程与逻辑

1. 数据库操作

作为即时通讯的 App,将会有很多的数据须要本地存储。比方联系人的信息,聊天的历史音讯。假如如果每次都是从网上实时拉取对话的历史聊天记录,当一个延绵不绝的会话超过 1 个月以上时,比方,你和你好友的会话,那海量的聊天记录,不经工夫漫长,而且还会让用户的带宽苦楚不已~~

所以,咱们将应用数据库保留联系人信息和聊天记录。
对于数据库的抉择,一贯是够用就行,第三方依赖越少越好。刚好 iOS 自带 SQLite,作为数据库的新手,咱们决定间接应用 SQLite,至于 SQLite.swift 这个出名的第三方库,目前感觉至多在这个 Demo 中,没有应用的必要。

1.1 增加 SQLite 库

关上我的项目属性页面,抉择“TARGETS”->“IMDemo”->“General”:

点击页面上“Frameworks, Libraries, and Embendded Content”栏下方的“+”号,关上框架与库增加窗口:

在搜寻框内输出“SQLite”,抉择“libsqlite3.tbd”,点击“Add”按钮进行增加。
增加结束后,“Frameworks, Libraries, and Embendded Content”一栏显示为:

1.2 初始化数据库

增加 swift 代码文件 DBOperator.swift,编辑代码如下:

class DBOperator {
    
    var db: OpaquePointer?
    
    init() {db = nil}
    
    deinit {
        if db != nil {sqlite3_close(db)
        }
    }
    
    func openDatabase(userId:Int64) {//-- TODO}
}

因为每个用户的数据须要隔离,所以最简略的形式便是每个用户应用一个独立的数据库。因而咱们无奈在 App 一起动的时候便关上数据库,而须要等到登录胜利后,获取到了用户惟一的 userId,才可晓得,该为谁关上数据库,应该关上那个数据库。

因而,咱们实现 openDatabase() 的代码如下:

func openDatabase(userId:Int64) {let DocumentsPath =  NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first!
        
        var databasePath = DocumentsPath + "/user_\(userId)"
        try! FileManager.default.createDirectory(at: URL(string: "file://" + databasePath)!, withIntermediateDirectories: true, attributes: nil)
        
        databasePath += "/database.sql3"
        
        let requireCreateTables = !(FileManager.default.fileExists(atPath: databasePath))
        
        if sqlite3_open(databasePath, &db) == SQLITE_OK {
            if requireCreateTables {createContactTable()
            }
        } else {print("Open database at" + databasePath + "failed.")
        }
    }
    
    private func createContactTable() {//-- TODO}

每次关上数据库时,将先查看指标数据库是否存在,不存在则进行创立。

咱们设计联系人信息表如下:

CREATE TABLE IF NOT EXISTS Contact(
  kind int not null,
  xid bigint not null,
  xname varchar(255) not null,
  nickname varchar(255) not null,
  imgUrl varchar(255) not null,
  imgPath varchar(255) not null,
  info varchar(255) not null,
  unique(kind, xid)
)

历史音讯表如下:

CREATE TABLE IF NOT EXISTS message(
    kind int not null,
    xid bigint not null,
    senderUid bigint not null,
    isCmd tinyint not null default 0,
    mid bigint not null,
    message varchar(255) not null,
    mtime bigint not null,
    unique(kind, xid, senderUid, mid)
)

此外,如果历史音讯十分多,一次拉取不完,则须要分段拉取。而如果在分段拉取的过程中,用户退出,便会造成拉取的中断。而此时如果一段时间后,用户再登录,则必然须要先拉取最新的历史音讯,而不是从上次中断处持续拉取。而如果在拉取到上次中断处之前,用户再次退出,则拉取将再次进行。而此时,历史音讯连续性上的空洞便产生了。
为了防止历史音讯连续性上的空洞造成的音讯遗留,咱们还须要第三个表,来贮存历史音讯的中断地位,和中断的拉取方向。于是咱们减少历史音讯检查点数据表如下:

CREATE TABLE IF NOT EXISTS checkpoint(
    kind int not null,
    xid bigint not null,
    ts bigint not null,
    desc int not null,
    unique(kind, xid, ts, desc)
)

因为很多时候,咱们晓得须要执行的 SQL 必然会胜利,而且咱们不关怀其状态和返回值。
因而,咱们减少 executeSQL() 函数如下:

    private func executeSQL(sql: String, printError: Bool = true) -> Bool {if (sqlite3_exec(db, sql.cString(using: String.Encoding.utf8)!, nil, nil, nil) == SQLITE_OK) {return true} else {if printError, let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
            return false
        }
    }

最初,实现 createContactTable() 函数如下:

    private func createContactTable() {
        
        //-- Contact Kind:0: 非联系人用户;1: 联系人用户;2: 群组;3: 房间。let contactSQL = """
    CREATE TABLE IF NOT EXISTS Contact(
        kind int not null,
        xid bigint not null,
        xname varchar(255) not null,
        nickname varchar(255) not null,
        imgUrl varchar(255) not null,
        imgPath varchar(255) not null,
        info varchar(255) not null,
        unique(kind, xid)
    )
"""let messageSQL ="""
        CREATE TABLE IF NOT EXISTS message(
            kind int not null,
            xid bigint not null,
            senderUid bigint not null,
            isCmd tinyint not null default 0,
            mid bigint not null,
            message varchar(255) not null,
            mtime bigint not null,
            unique(kind, xid, senderUid, mid)
        )
"""let historyCheckpointSQL ="""
        CREATE TABLE IF NOT EXISTS checkpoint(
            kind int not null,
            xid bigint not null,
            ts bigint not null,
            desc int not null,
            unique(kind, xid, ts, desc)
        )
"""
        
        _ = executeSQL(sql: contactSQL)
        _ = executeSQL(sql: messageSQL)
        _ = executeSQL(sql: historyCheckpointSQL)
    }

而后编辑 IMCenter.swift,退出对 DBOperator 的援用:

class IMCenter {
    ... ...
    static var db = DBOperator()
    ... ...
}

1.3 数据库的其余操作

咱们增加 IMDemoApp 所需的数据库其余操作如下,具体性能请参见函数正文:

    //-- 保留新收到的聊天音讯
    func insertChatMessage(type:Int, xid:Int64, sender:Int64, mid:Int64, message:String, mtime:Int64, printError: Bool = true) -> Bool {
        let sql = """
        insert into message (kind, xid, senderUid, mid, message, mtime) values
            (\(type), \(xid), \(sender), \(mid), '\(message)', \(mtime))
"""
        
        objc_sync_enter(self)
        let status = executeSQL(sql:sql, printError: printError)
        objc_sync_exit(self)
        
        return status
    }
    
    //-- 保留新收到的零碎告诉
    func insertChatCmd(type:Int, xid:Int64, sender:Int64, mid:Int64, message:String, mtime:Int64, printError: Bool = true) -> Bool {
        let sql = """
        insert into message (kind, xid, senderUid, mid, message, mtime, isCmd) values
            (\(type), \(xid), \(sender), \(mid), '\(message)', \(mtime), 1)
"""
        objc_sync_enter(self)
        let status = executeSQL(sql:sql, printError: printError)
        objc_sync_exit(self)
        
        return status
    }
    
    //-- 插入历史音讯检查点
    func insertCheckPoint(type:Int, xid:Int64, ts:Int64, desc:Bool) {
        let sql = """
        insert into checkpoint (kind, xid, ts, desc) values
            (\(type), \(xid), \(ts), \(desc ? 1 : 0))
"""
        objc_sync_enter(self)
        _ = executeSQL(sql: sql)
        objc_sync_exit(self)
    }
    
    //-- 更新头像本地存储信息
    func updateImageStoreInfo(type: Int, xid: Int64, filePath: String) {let sql = "update Contact set imgPath='\(filePath)'where kind=\(type) and xid=\(xid)"
        
        objc_sync_enter(self)
        _ = executeSQL(sql: sql)
        objc_sync_exit(self)
    }
    
    //-- 保留新的联系人信息
    private func insertNewContact(contact: ContactInfo, printError: Bool = true) -> Bool {
        let sql = """
        insert into Contact (kind, xid, xname, nickname, imgUrl, imgPath, info) values (\(contact.kind), \(contact.xid), '\(contact.xname)', '\(contact.nickname)',
            '\(contact.imageUrl)', '\(contact.imagePath)', '\(contact.showInfo)')
"""

        return executeSQL(sql: sql, printError: printError)
    }
    
    //-- 保留新的联系人信息
    func storeNewContact(contact: ContactInfo) {let updateSQL = """update Contact set nickname='\(contact.nickname)', imgUrl='\(contact.imageUrl)',
        imgPath='\(contact.imagePath)', info='\(contact.showInfo)'
        where kind=\(contact.kind) and xid=\(contact.xid)
"""
        
        objc_sync_enter(self)
        if (insertNewContact(contact: contact, printError: false)) {} else if (sqlite3_exec(db, updateSQL.cString(using: String.Encoding.utf8)!, nil, nil, nil) == SQLITE_OK) { } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        objc_sync_exit(self)
    }
    
    //-- 当增加好友时,如果对方曾经作为陌生人被保留在联系人数据表中,则批改陌生人状态为好友状态
    func changeStrangerToFriend(xid:Int64) {let sql = """update Contact set kind=\(ContactKind.Friend.rawValue) where kind=\(ContactKind.Stranger.rawValue) and xid=\(xid)"""
        objc_sync_enter(self)
        _ = executeSQL(sql:sql)
        objc_sync_exit(self)
    }
    
    //-- 更新全剧惟一的联系人注册名称
    func updateXname(contact: ContactInfo) {let sql = """update Contact set xname='\(contact.xname)'where kind=\(contact.kind) and xid=\(contact.xid)"""
        objc_sync_enter(self)
        if (sqlite3_exec(db, sql.cString(using: String.Encoding.utf8)!, nil, nil, nil) == SQLITE_OK) {} else if (insertNewContact(contact: contact, printError: false)) { } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        objc_sync_exit(self)
    }
    
    //-- 更新联系人公开信息:蕴含 昵称 / 展现名、头像地址、头像本地存储门路、用户签名 / 群组布告
    func updatePublicInfo(contact: ContactInfo) {let sql = """update Contact set nickname='\(contact.nickname)', imgUrl='\(contact.imageUrl)', imgPath='\(contact.imagePath)', info='\(contact.showInfo)'where kind=\(contact.kind) and xid=\(contact.xid)"""
        
        objc_sync_enter(self)
        if (sqlite3_exec(db, sql.cString(using: String.Encoding.utf8)!, nil, nil, nil) == SQLITE_OK) {} else if (insertNewContact(contact: contact, printError: false)) { } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        objc_sync_exit(self)
    }
    
    //-- 从数据库获取所有联系人信息,陌生人除外
    func loadContentInfos() -> [ContactInfo] {
        
        let sql = """select kind, xid, xname, nickname, imgUrl, imgPath, info from Contact where kind <> 0"""
        var result: [ContactInfo] = []
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {let info = ContactInfo()
                
                info.kind = Int(sqlite3_column_int(statement, 0))
                info.xid = sqlite3_column_int64(statement, 1)
                
                var chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 2))
                info.xname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 3))
                info.nickname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 4))
                info.imageUrl = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 5))
                info.imagePath = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 6))
                info.showInfo = String.init(cString: chars!)
                
                result.append(info)
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)
        return result
    }
    
    //-- 按类别从数据库获取所有陌联系人和好友信息
    func loadAllUserContactInfos() -> [Int64:ContactInfo] {let sql = """select kind, xid, xname, nickname, imgUrl, imgPath, info from Contact where kind in (0, 1)"""
        var result: [Int64:ContactInfo] = [:]
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {let info = ContactInfo()
                
                info.kind = Int(sqlite3_column_int(statement, 0))
                info.xid = sqlite3_column_int64(statement, 1)
                
                var chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 2))
                info.xname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 3))
                info.nickname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 4))
                info.imageUrl = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 5))
                info.imagePath = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 6))
                info.showInfo = String.init(cString: chars!)
                
                result[info.xid] = info
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)
        return result
    }
    
    //-- 从数据库获取特定联系人的信息
    func loadContentInfo(type:Int, uid:Int64) -> ContactInfo? {let sql = """select xname, nickname, imgUrl, imgPath, info from Contact where kind=\(type) and xid=\(uid)"""
        var contact: ContactInfo? = nil
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {let info = ContactInfo()
                
                info.kind = type
                info.xid = uid
                
                var chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 0))
                info.xname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 1))
                info.nickname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 2))
                info.imageUrl = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 3))
                info.imagePath = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 4))
                info.showInfo = String.init(cString: chars!)
                
                contact = info
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)
        return contact
    }
    
    //-- 从数据库获取特定联系人的信息
    func loadContentInfo(type:Int, xname:String) -> ContactInfo? {let sql = """select xid, nickname, imgUrl, imgPath, info from Contact where kind=\(type) and xname='\(xname)'"""
        var contact: ContactInfo? = nil
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {let info = ContactInfo()
                
                info.kind = type
                info.xname = xname
                
                info.xid = sqlite3_column_int64(statement, 0)
                
                var chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 1))
                info.nickname = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 2))
                info.imageUrl = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 3))
                info.imagePath = String.init(cString: chars!)
                
                chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 4))
                info.showInfo = String.init(cString: chars!)
                
                contact = info
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)
        return contact
    }
    
    //-- 从数据库获取特定联系人在本地存储的最新一条历史音讯
    private func loadLastMessage(type: Int, xid: Int64) -> LastMessage {let sql = "select mid, message, mtime from message where kind=\(type) and xid=\(xid) and isCmd=0"
        var lastMessage = LastMessage()
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {lastMessage.mid = sqlite3_column_int64(statement, 0)
                
                let chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 1))
                lastMessage.message = String.init(cString: chars!)

                lastMessage.timestamp = sqlite3_column_int64(statement, 2)
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)
        
        return lastMessage
    }
    
    //-- 从数据库获取指定的联系人在本地存储的最新一条历史音讯,并将后果以会话列表的模式返回
    func loadLastMessage(contactList: [ContactInfo]) -> [SessionItem] {var sessions = [SessionItem]()
        
        for contact in contactList {let sessionItem = SessionItem(contact: contact)
            sessionItem.lastMessage = loadLastMessage(type: contact.kind, xid: contact.xid)
            sessions.append(sessionItem)
        }

        return sessions
    }
    
    //-- 从数据库获取指定联系人在本地保留的所有历史聊天记录
    func loadAllMessages(contact:ContactInfo) -> [ChatMessage] {let sql = """select senderUid, mid, message, mtime, isCmd from message where kind=\(contact.kind) and xid=\(contact.xid) order by mtime asc"""
        
        var messages = [ChatMessage]()
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {let senderUid = sqlite3_column_int64(statement, 0)
                let mid = sqlite3_column_int64(statement, 1)
                
                let chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, 2))
                let messageContent = String.init(cString: chars!)

                let mtime = sqlite3_column_int64(statement, 3)
                
                let message = ChatMessage(sender: senderUid, mid: mid, mtime: mtime, message: messageContent)
                if sqlite3_column_int(statement, 4) != 0 {message.isChat = false}
                
                messages.append(message)
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)

        return messages
    }
    
    //-- 获取指定联系人所有的历史音讯检查点信息
    func loadAllHistoryMessageCheckpoints(contact:ContactInfo) -> [HistoryCheckpoint] {let sql = """select ts, desc from checkpoint where kind=\(contact.kind) and xid=\(contact.xid)"""
        
        var checkpoints = [HistoryCheckpoint]()
        var statement: OpaquePointer? = nil
        
        objc_sync_enter(self)
        
        if sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &statement, nil) == SQLITE_OK {while sqlite3_step(statement) == SQLITE_ROW {let checkpoint = HistoryCheckpoint()
                
                checkpoint.ts = sqlite3_column_int64(statement, 0)
                checkpoint.desc = (sqlite3_column_int(statement, 1) > 0) ? true : false
                
                checkpoints.append(checkpoint)
            }
        } else {if let error = String(validatingUTF8:sqlite3_errmsg(db)) {print("SQL execute failed. Error: \(error)")
            }
        }
        
        sqlite3_finalize(statement)
        objc_sync_exit(self)

        return checkpoints
    }
    
    //-- 革除指定联系人所有的历史音讯检查点信息
    func clearAllHistoryMessageCheckpoints(contact:ContactInfo) {let sql = """delete from checkpoint where kind=\(contact.kind) and xid=\(contact.xid)"""
        
        objc_sync_enter(self)
        _ = executeSQL(sql: sql)
        objc_sync_exit(self)
    }

并于 IMCenter.swift 中退出对历史音讯检查点类型的定义:

class HistoryCheckpoint {
    var ts:Int64 = 0
    var desc = false
}

以上便是数据库相干的全副操作。

2. 网络交互流程

为了确保 App 的用户对平台厂商的透明度,让云平台厂商无奈白嫖,确保云平台的使用者安心,所以云上曲率的 IM 即时通讯服务须要 App 的开发者确认 App 用户的合法性。因而,咱们须要搭建一个本人的服务器,进行用户的确认,以及用户的注册治理。
同时,因为咱们采纳的是用户名注册,而非云上曲率 IM 即时通讯服务所应用的数字 ID,因而,咱们也须要一个简略的服务器,做一下用户名到数字 ID 的转换。
鉴于以上剖析,咱们的 App 须要连贯两个服务,一个是云上曲率 IM 即时通讯服务,另外一个是咱们本人的小型服务。与云上曲率 IM 即时通讯服务交互的局部,由云上曲率对应的 SDK 实现,咱们无需关怀。剩下的就是与咱们本人的服务进行交互。

依据之前 UI 的设计和交互,初步梳理了一下,咱们须要咱们本人的服务器提供以下五个接口:

  1. 登陆接口
    确认 App 本身用户的合法性。如果通过验证,返回用户的数字 ID,和即时通讯服务的登陆 token,以便遇上曲率的 SDK 进行即时通讯服务的登录。
  2. 注册接口
    注册用户,如果注册失常,则返回用户的数字 ID,和即时通讯服务的登陆 token,以便遇上曲率的 SDK 进行即时通讯服务的登录。
  3. 创立群组接口
    确认群组名称惟一,创立群组,并将创建者退出群组。
  4. 创立房间接口
    确认房间名称惟一,创立房间,并将创建者退出房间。
  5. 查问信息接口
    通过用户 / 群组 / 房间的惟一名称查问对应的数字 ID;通过用户 / 群组 / 房间惟一的数字 ID 查问对应的惟一名称。

3. 集成云上曲率即时通讯 iOS SDK

云上曲率即时通讯分成 RTM(Real-Time Message,实时音讯)、IMLib、IMKit 三类。其中 RTM 次要面对实时信令,同时也是 IMLib 和 IMKit 是的根底。
本篇咱们将采纳 RTM SDK,而面向游戏的 IMlib 和面向社交软件的 IMKit 将另篇再说。

3.1 RTM SDK 的获取

首先来到云上曲率官网,首页下拉至底部,能够看到 GitHub 的图标,来到云上曲率的 GitHub 仓库 HighRAS。

在搜寻框搜寻 objc,会呈现两个 rtm-client-sdk。

其中一个是蕴含 RTC 模块,另外一个不蕴含 RTC 模块。
本次咱们抉择应用不蕴含 RTC 模块的 SDK,后续文章演示 RTC 性能时,再选用蕴含 RTC 的 SDK。
点击链接,咱们来到 rtm-client-sdk-objc 的我的项目页,点击右侧 Release 标签

进入发布页面

间接抉择最新公布版本,点击下载。

3.2 RTM SDK 的编译

下载解压后,咱们进入 SDK 源码目录,点击 RtmWorkSpace.xcworkspace,关上 SDK 我的项目工程。
抉择编译指标为:Rtm -> Any iOS Device (arm64, armv7, armv7s)

点击 Shift + Command + K,清理以后工程。清理结束后,点击 Command + B,进行构建。
构建实现后,在 XCode 左侧我的项目浏览器中,关上 Rtm 我的项目,找到 Products,开展。在 Rtm 上关上右键菜单:

抉择“Show in Finder”,关上 Release-iphoneos 文件夹:

这就是咱们须要集成的 RTM SDK。

3.3. RTM SDK 的集成
将上节生成的 Rtm.framework 文件夹拖入 IMDemoApp 工程,弹出对话,如图抉择:

点击“Finish”。而后咱们找到下载下来的 RTM SDK 源码目录,关上 Test 目录。将 RTMAudioManager 文件夹拖入拖入 IMDemoApp 工程。此时仍旧弹出对话框,按上图抉择。
采纳之前增加 SQLite3 的办法,增加 libresolv.9.tbd。增加完如图所示:

在本视图,抉择 TARGETS -> Build Settings,在搜寻框中搜寻“Other Linker Flags”:

为“Other Linker Flags”增加值“-ObjC”。增加结束如下图所示:

因为 RTM ObjC SDK 采纳 Objective-C++ 实现,所以咱们还需增加一个包装文件和一个.mm 文件。
咱们间接增加一个 Objective-C 文件:

取名 RTMWrapper,文件类型抉择空文件:

当保留的时候,XCode 会问你,是否要创立桥接头文件,抉择创立:

此时咱们的工程文件如图所示:

编辑 IMDemo-Bridging-Header.h,退出对 <Rtm/Rtm.h> 的援用:

#import <Foundation/Foundation.h>
#import <Rtm/Rtm.h>

将 RTMWrapper.m 改名为 RTMWrapper.mm,改后代码如下:

#import <Foundation/Foundation.h>
#import "IMDemo-Bridging-Header.h"

此时便集成结束。
留神:因为咱们方才只做了 iphone 的 SDK,没有制作模拟器的 SDK,因而后续调试仅能在真机上调试。如需模拟器反对,可自行查阅相干文档。

3.4 应用 RTM SDK 的框架代码

编辑 IMCenter.swift,减少对 RTMClient 的援用:

class IMCenter {
    ... ...
    static var client: RTMClient? = nil
    ... ...
}

因为 RTMClient 会有很多的事件,咱们如果须要解决对应的事件,比方收到新的音讯,那咱们须要一个事件告诉的机制。RTMClient 在创立时,便会要求咱们提供一个实现了 RTM 协定的类。除了个别必须实现的接口外,其余仅用实现咱们关怀的事件接口即可。
减少 IMEventProcessor.swift,编辑内容如下:

import Foundation

@objcMembers public class IMEventProcessor: NSObject, RTMProtocol {public func rtmReloginWillStart(_ client: RTMClient, reloginCount: Int32) -> Bool {return true}
    
    public func rtmReloginCompleted(_ client: RTMClient, reloginCount: Int32, reloginResult: Bool, error: FPNError) {//-- Do nothings}
    
    public func rtmConnectClose(_ client: RTMClient) {
        DispatchQueue.main.async {
            IMCenter.viewSharedInfo.brokenInfo = "RTM 链接已敞开!"
            IMCenter.viewSharedInfo.currentPage = .LoginView
        }
    }
    
    public func rtmKickout(_ client: RTMClient) {
        DispatchQueue.main.async {
            IMCenter.viewSharedInfo.brokenInfo = "账号已在其余中央登陆!"
            IMCenter.viewSharedInfo.currentPage = .LoginView
        }
    }
    
    
    public func rtmPushP2PChatMessage(_ client: RTMClient, message: RTMMessage?) {//-- TODO}
    
    public func rtmPushGroupChatMessage(_ client: RTMClient, message: RTMMessage?) {//-- TODO}
    
    public func rtmPushRoomChatMessage(_ client: RTMClient, message: RTMMessage?) {//-- TODO}
    
    public func rtmPushGroupChatCmd(_ client: RTMClient, message: RTMMessage?) {//-- TODO}
    
    public func rtmPushRoomChatCmd(_ client: RTMClient, message: RTMMessage?) {//-- TODO}
}

其中 rtmReloginWillStart() 和 rtmReloginCompleted() 是网络中断时,主动重连机制开始于实现的告诉。咱们简略疏忽即可。

rtmConnectClose() 是链接齐全断开,且主动重连被禁止 / 敞开状态下的事件回调,比如说,用户退出登录。
rtmKickout() 是,当 RTM 多端登录未开启时,同一账号在另外的设施上登录,将以后设施的用户踢下线的告诉。
rtmPushP2PChatMessage()、rtmPushGroupChatMessage()、rtmPushRoomChatMessage()则是 P2P、群组、房间聊天的音讯告诉,而 rtmPushGroupChatCmd()、rtmPushRoomChatCmd() 则是群组和房间的指令 / 信令的告诉。

最初,批改 IMCenter.swift,减少对 IMEventProcessor 的持有:

class IMCenter {
    ... ...
    static var imEventProcessor: IMEventProcessor = IMEventProcessor()
    ... ...
}

到此为止,除了创立 RTMClient 实例外,RTM iOS SDK 的接入和应用相干的框架代码曾经全副搭建结束。

3.5 注册云上曲率即时通信服务账号

登录云上曲率官网,点击右上角“收费试用”。
注册结束后,咱们来到控制台页面。如果仍旧在首页,能够通过点击右上角“控制台”,进入控制台页面。
在控制台左侧,“控制台概览”中,抉择“实时信令”条目:

点击“创立我的项目”按钮,填写我的项目信息,实现我的项目创立。之后进入我的项目控制台,在左侧列表中,抉择“服务配置”:

而后在右侧便可看到我的项目的根本信息。

其中“项目编号”、“服务端 SDK 接入点”是下篇“服务器搭建与总结”须要记录和配置的内容。在这里,咱们记下客户端 SDK 接入点(2.7.0 版本及之后):rtm-nx-front.ilivedata.com:13321。

留神:国际版和国内版,接入点是不同的。

4. 拜访咱们本人的用户服务器

因为要向 RTM 登陆,首先须要咱们本人的业务服务器确认用户无效,并通过 RTM 服务端 SDK 链接 RTM 服务端,获取对应用户的 token,返回给客户端。而后客户端用这个 token,向 RTM 服务集群登陆。因而咱们先回到与咱们本人的服务器交互下面来。
首先,假如咱们本人的服务器 IP 地址为 43.138.12.11,监听端口为 13601,编辑 Config.swift 文件,退出以下代码:

class IMDemoConfig {
    static let RTMEndpoint = "rtm-nx-front.ilivedata.com:13321"
    static let IMDemoServerEndpoint = "43.138.12.11:13601"
}

而后增加 BizClient.swift,编辑框架代码如下:

import Foundation
import UIKit

class BizClient {class func login(username: String, password: String, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}
    
    class func register(username: String, password: String, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}
    
    class func createGroup(uniqueName: String, completedAction: @escaping (_ gid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}
    
    class func joinGroup(uniqueGroupName: String, completedAction: @escaping (_ gid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}

    class func dropGroup(uniqueName: String, gid:String, completedAction: @escaping () -> Void, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}
    
    class func createRoom(uniqueName: String, completedAction: @escaping (_ rid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}
    
    class func dropRoom(uniqueName: String, rid:Int64, completedAction: @escaping () -> Void, errorAction: @escaping (_ message: String) -> Void) {//-- TODO}
}

为了简略起见,在没有 SSL 证书的状况下,咱们选用 HTTP 协定和咱们的服务器进行连贯。

5. 登录流程

5.1 发送登陆申请

首先是登陆。App LoginView 将获取到用户的注册名和明码,而后调用 BizClient 的 login()函数,拜访咱们的服务器。咱们本人的服务器如果确认是咱们本人的非法用户,则将通过 RTM 服务端 SDK 向 RTM 服务器获取用户的登录 token,并将登陆 token 和用户 ID,以及咱们作为 RTM 客户的我的项目 ID 一并返回,供 App 外部 RTMClient 进行登录。为了简略起见,咱们采纳 GET 形式。编辑 BizClient.swift,增加如下代码:

struct UserLoginResponse: Codable {
    var pid: Int64
    var uid: Int64
    var token: String
}

struct FpnnErrorResponse: Codable {
    var code: Int
    var ex: String
}

class BizClient {private class func checkUserChanged(loginName: String) {let oldName = IMCenter.fetchUserProfile(key: "username")
        var changed = false
        
        if oldName.isEmpty {IMCenter.storeUserProfile(key: "username", value: loginName)
            changed = false
        }
        
        if oldName != loginName {IMCenter.storeUserProfile(key: "username", value: loginName)
            changed = true
        }
        
        if changed {IMCenter.storeUserProfile(key: "nickname", value: "")
            IMCenter.storeUserProfile(key: "showInfo", value: "")
        }
    }
    
    class func createIMClient(userLoginInfo: UserLoginResponse, errorAction: @escaping (_ message: String) -> Void) {let client = RTMClient(endpoint: IMDemoConfig.RTMEndpoint, projectId: userLoginInfo.pid, userId: userLoginInfo.uid, delegate: IMCenter.imEventProcessor, config: nil, autoRelogin: true)

        IMCenter.client = client

        client?.login(withToken: userLoginInfo.token, language: nil, attribute: nil, timeout: 20, success: {IMCenter.RTMLoginSuccess()
        }, connectFail: { error in
            if (error != nil) {errorAction(error!.ex)
            } else {errorAction("未知谬误!")
            }
        })
    }
    
    class func urlEncode(string: String) -> String {let encodeUrlString = string.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)
        return encodeUrlString ?? ""
    }
    
    class func login(username: String, password: String, errorAction: @escaping (_ message: String) -> Void) {let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/userLogin?username=" + urlEncode(string: username) + "&pwd=" + urlEncode(string: password))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{(data, response, error) in
            
            if error != nil {errorAction("连贯谬误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {let json = try JSONDecoder().decode(UserLoginResponse.self, from: data!)
                    
                    checkUserChanged(loginName: username)
                    createIMClient(userLoginInfo: json, errorAction: errorAction)
                       
                } catch {
                    do {let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()}
}

login() 是 App 向咱们本人服务器发送用户登录申请,并获取回应的接口,因为下篇咱们将决定应用 FPNN 框架疾速搭建咱们的 HTTP/HTTPS 服务,因而应用 FPNN HTTP/HTTPS 拜访的标准:http(s)://<domain(ip)>[:port]/service/<interface>[?…]
咱们本次要拜访的接口为 userLogin,所以咱们须要向 http://43.138.12.11:13601/ser… 的 Url 发出请求。
此外,因为在异常情况下,FPNN 框架会返回 FPNN 异样,所以咱们也须要增加 FPNN 异样响应构造:FpnnErrorResponse。
urlEncode() 则将中文等 uri 不平安字符进行本义。
当登录胜利后,checkUserChanged() 查看以后登录用户是否是上次登录用户。如果不是,则意味着本地存储的用户相干信息须要更新。以前的须要清理,新的须要从新获取(放到后续解决,不在 checkUserChanged()函数里)。
而后创立 RTMClient 实例,调用 RTMClient.login()接口,进行登录。
为了代码的顺利编译,咱们在 IMCenter.swift 中增加框架代码:

    class func RTMLoginSuccess() {//-- TODO: ....}

5.2 为登录胜利后流程的筹备

当 RTMClient.login()接口传入的回调函数 IMCenter.RTMLoginSuccess() 被触发的那一刻起,第二步就开始了。

第二步蕴含以下并发内容:

  1. 关上用户对应的数据库,加载联系人和最近一条聊天记录,初始化联系人页面和会话页面。
  2. 监察室否有新的回话,有则拉取会话信息,以及最新 10 条聊天记录。而后增加到联系人列表页和会话页面。
  3. 获取未读信息,拉取对应会话最新 10 条聊天记录,并更新会话列表页,将未读会话置顶,并增加未读标记。

因为期间咱们须要重复屡次查问用户信息,尽管都是通过数字 ID 查问注册名称(RTM 新音讯告诉、新会话、群组 / 房间中的陌生人等),但思考到后续在退出群组,退出房间时,也须要依据群组和房间的惟一名反查对应的数字 ID。因而,咱们设计 lookup 接口如下:
输出:用户 ID 列表(能够为空)、群组 ID 列表(能够为空)、房间 ID 列表(能够为空)、用户注册名列表(能够为空)、群组惟一名称列表(能够为空)、房间惟一名称列表(能够为空)
输入:以 用户注册名为 key,用户 ID 为值的字典;以 群组惟一名为 key,群组 ID 为值的字典、以 房间惟一名为 key,房间 ID 为值的字典。
因为咱们采纳 HTTP 协定,因而采纳 Json 会比拟不便。此外,因为 Json 的个性,咱们无奈退出以整型数字 ID 为 key 的字典类型。如果冀望减少,能够通过将整型的数字 ID 变为字符串的变通形式,或者间接采纳 FPNN 协定进行通信(本 Demo 对应演示服务器即用 FPNN 框架进行开发,且 RTMClient SDK 是基于 FPNN SDK 进行的下层开发)。
此外,因为批量查问时,查问数据量可能较大,因而咱们采纳 POST 形式,而不是 GET 形式。

批改 BizClient.swift,减少代码如下:

struct LookupResponse: Codable {var users: [String:Int64]
    var groups: [String:Int64]
    var rooms: [String:Int64]
}

class BizClient {
    ... ...
    
    private class func appendStringArray(str: inout String, array: [String], withKey: String) -> Void {str.append("\"")
        str.append(withKey)
        str.append("\":[")
        
        var requireComma = false
        
        for item in array {
            if requireComma {str.append(",\"")
            } else {
                requireComma = true
                str.append("\"")
            }
            
            str.append(item)
            str.append("\"")
        }
        
        str.append("]")
    }
    
    private class func appendInt64Array(str: inout String, array: [Int64], withKey: String) -> Void {str.append("\"")
        str.append(withKey)
        str.append("\":[")
        
        var requireComma = false
        
        for item in array {
            if requireComma {str.append(",")
            } else {requireComma = true}
            
            str.append(String(item))
        }
        
        str.append("]")
    }
    
    class func lookup(users:[String]?, groups:[String]?, rooms:[String]?, uids:[Int64]?, gids: [Int64]?, rids: [Int64]?, completedAction: @escaping (_ response: LookupResponse) -> Void, errorAction: @escaping (_ info: String) -> Void) {
        
        var requireComma = false
        var postJson = "{"
        
        if users != nil {appendStringArray(str: &postJson, array: users!, withKey: "users")
            requireComma = true
        }
        
        if groups != nil {
            if requireComma {postJson.append(",")
            } else {requireComma = true}
            appendStringArray(str: &postJson, array: groups!, withKey: "groups")
        }
        
        if rooms != nil {
            if requireComma {postJson.append(",")
            } else {requireComma = true}
            appendStringArray(str: &postJson, array: rooms!, withKey: "rooms")
        }
        
        if uids != nil {
            if requireComma {postJson.append(",")
            } else {requireComma = true}
            appendInt64Array(str: &postJson, array: uids!, withKey: "uids")
        }
        
        if gids != nil {
            if requireComma {postJson.append(",")
            } else {requireComma = true}
            appendInt64Array(str: &postJson, array: gids!, withKey: "gids")
        }
        
        if rids != nil {
            if requireComma {postJson.append(",")
            } else {requireComma = true}
            appendInt64Array(str: &postJson, array: rids!, withKey: "rids")
        }
        
        postJson.append("}")
        
        let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/lookup")!
        var request = URLRequest(url: url)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        
        request.httpBody = postJson.data(using: .utf8)    // postJson.percentEncoded()
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data, error == nil else {errorAction("连贯谬误: \(error!.localizedDescription)")
                return
            }

            if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
                if response != nil {errorAction("Error! Response: \(response!)")
                } else {errorAction("Error! Response code: \(httpStatus.statusCode)")
                }
                return
            }

            // let responseString = String(data: data, encoding: .utf8)
            
            do {let json = try JSONDecoder().decode(LookupResponse.self, from: data)
                completedAction(json)
            } catch {
                do {let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data)
                    errorAction(json.ex)
                } catch {errorAction("Error during JSON serialization: \(error.localizedDescription)")
                }
            }
        }
        task.resume()}
}

代码基本上无需多说,整体流程就是:筹备 json 数据,而后像咱们本人的服务器发送查问申请,而后解析服务器返回的数据。

5.3. 登录胜利后执行的流程

登录胜利之后,RTM 便会触发 IMCenter.RTMLoginSuccess() 的回调。该回调次要解决 5 件事:

  1. 关上登录用户对应的数据库;
  2. 查看并更新以后登录用户的用户信息
  3. 依据本地数据库的记录,筹备好会话列表和联系人列表的根本数据,并查看更新联系人信息
  4. 查看是否有本地未记录的新的会话

编辑 IMCenter.swift,减少 RTMLoginSuccess() 根本代码如下:

    class func sortSessions(sessions: [SessionItem]) -> [SessionItem] {
        if sessions.count <= 1 {return sessions}
        
        return sessions.sorted(by: { (s1, s2) -> Bool in
            
            if s1.lastMessage.unread && !s2.lastMessage.unread {return true}
            
            if !s1.lastMessage.unread && s2.lastMessage.unread {return false}
            
            if s1.lastMessage.timestamp > s2.lastMessage.timestamp {return true}
            
            if s1.lastMessage.timestamp == s2.lastMessage.timestamp {return s1.lastMessage.mid > s2.lastMessage.mid}
            return false
        })
    }
    
    class func RTMLoginSuccess() {db.openDatabase(userId: IMCenter.client!.userId)
        
        querySelfInfo()
        
        let contacts = db.loadContentInfos()
        var sessions = db.loadLastMessage(contactList: contacts)
        sessions = IMCenter.sortSessions(sessions: sessions)
        
        let contactList = prepareContactList(contacts:contacts)
        
        DispatchQueue.main.async {
            IMCenter.viewSharedInfo.sessions = sessions
            IMCenter.viewSharedInfo.contactList = contactList
            IMCenter.viewSharedInfo.currentPage = .SessionView
            
            DispatchQueue.global(qos: .default).async {checkContactsUpdate(contactList: contactList)
            }
        }
        
        checkNewSessions(contacts: contacts)
        
        for contact in contacts {
            if contact.imagePath.isEmpty && contact.imageUrl.isEmpty == false {downloadImage(contactInfo: contact)
            }
        }
    }

其中 querySelfInfo() 查看并更新登录用户本身的信息,以确保和服务端同步。因为查看本身信息是一个网络操作,为了优化登录登录胜利后,到会话页面展示前的等待时间,querySelfInfo() 被设计成采纳异步并发执行。相干代码如下:

struct OpenInfoData: Codable {
    var nickname: String
    var imageUrl: String
    var showInfo: String
}

... ...

class IMCenter {
    ... ...
    
    private class func decodeOpenInfo(contact: inout ContactInfo, json: String) -> Void {
        do {let info = try JSONDecoder().decode(OpenInfoData.self, from: json.data(using: .utf8)!)
            contact.nickname = info.nickname
            contact.imageUrl = info.imageUrl
            contact.showInfo = info.showInfo
        } catch {print("Error during JSON serialization:" + json)
        }
    }
    
    private class func storeImage(type: Int, xid: Int64, image: Data) -> String? {
        
        let uid: Int64 = IMCenter.client!.userId
        var path = NSHomeDirectory() + "/Documents/user_\(uid)/"
        var relativePath = "user_\(uid)/"
        
        switch type {
        case 1:
            path.append("user/")
            relativePath.append("user/")
        case 2:
            path.append("group/")
            relativePath.append("group/")
        case 3:
            path.append("room/")
            relativePath.append("room/")
        default:
            //-- 陌生人,或者本人
            path.append("user/")
            relativePath.append("user/")
        }
        
        try! FileManager.default.createDirectory(at: URL(string: "file://" + path)!, withIntermediateDirectories: true, attributes: nil)

        let filePath = path + String(xid) + ".img"
        relativePath += String(xid) + ".img"
        do {try image.write(to: URL(fileURLWithPath: filePath))
            return relativePath
            
        } catch {return nil}
    }
    
    private class func updateViewsImageUrl(contactInfo: ContactInfo, newPath: String) {if let contacts = IMCenter.viewSharedInfo.contactList[contactInfo.kind] {
            for idx in 0..<contacts.count {if contacts[idx].kind == contactInfo.kind && contacts[idx].xid == contactInfo.xid {contacts[idx].imagePath = newPath
                }
            }
        }
    }
    
    private class func downloadImage(contactInfo: ContactInfo, completedAction: @escaping (_ path:String, _ contactInfo: ContactInfo)->Void, failedAction: @escaping()->Void) {
        
        if contactInfo.imageUrl.isEmpty {failedAction()
            return
        }
        
        URLSession.shared.dataTask(with: URL(string:contactInfo.imageUrl)!) { data, response, error in
            guard
                let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
                let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
                let data = data, error == nil
                else {print("Download image error:\(String(describing: error))")
                    failedAction()
                    return
                }
            
            if let path = IMCenter.storeImage(type: contactInfo.kind, xid: contactInfo.xid, image: data) {completedAction(path, contactInfo)
            } else {failedAction()
            }
            
            }.resume()}
    
    private class func downloadImage(contactInfo: ContactInfo) {
        downloadImage(contactInfo: contactInfo, completedAction: {(path, contactInfo) in
            
            IMCenter.db.updateImageStoreInfo(type: contactInfo.kind, xid: contactInfo.xid, filePath: path)
            
            DispatchQueue.main.async {updateViewsImageUrl(contactInfo: contactInfo, newPath: path) }
        }, failedAction: {})
    }
    
    class func querySelfInfo() {
        
        IMCenter.client!.getUserInfo(withTimeout: 0, success: {
            infoAnswer in
            
            if let openInfo = infoAnswer?.openInfo {var contact = ContactInfo(type: 0, xid: IMCenter.client!.userId)
                decodeOpenInfo(contact: &contact, json: openInfo)
            
                IMCenter.storeUserProfile(key: "nickname", value: contact.nickname)
                IMCenter.storeUserProfile(key: "showInfo", value: contact.showInfo)
                
                let username = IMCenter.fetchUserProfile(key: "username")
                IMCenter.storeUserProfile(key: "\(username)-image-url", value: contact.imageUrl)

                downloadImage(contactInfo: contact, completedAction: {(path, contactInfo) in
                    
                    let username = IMCenter.fetchUserProfile(key: "username")
                    IMCenter.storeUserProfile(key: "\(username)-image", value: path)
                
                }, failedAction:{})
            }
            
        }, fail: {
            errorAnswer in
            
            if errorAnswer?.code == 200010 {sleep(2)
                querySelfInfo()}
        })
    }
    
    ... ...
}

其中,在 querySelfInfo() 函数中,错误码 200010 是达到了 RTM 我的项目的频率限度,所以咱们要做一个简略期待,而后重试。对于付费客户,频率限度等是能够提交工单进行批改的。个别收费应用,除非像本教程异步和并发操作极多,否则通常状况下,是很难触发 RTM 的频率限度的。

而后,登录胜利的回调中,prepareContactList() 负责筹备好联系人列表的数据。相干代码如下:

    private class func sortContacts(contacts: [ContactInfo]) -> [ContactInfo] {
        if contacts.count <= 1 {return contacts}
        
        return contacts.sorted(by: { (c1, c2) -> Bool in
            
            if c1.nickname < c2.nickname {return true}
            
            if c1.nickname == c2.nickname {
                if c1.xname < c2.xname {return true}
                
                if c1.xname == c2.xname {return c1.xid < c2.xid}
            }
            return false
        })
    }

    private class func prepareContactList(contacts:[ContactInfo]) -> [Int: [ContactInfo]] {var contactList: [Int: [ContactInfo]] = [:]
        contactList[ContactKind.Friend.rawValue] = [ContactInfo]()
        contactList[ContactKind.Group.rawValue] = [ContactInfo]()
        contactList[ContactKind.Room.rawValue] = [ContactInfo]()
        
        for contact in contacts {
            if contact.kind == ContactKind.Friend.rawValue {contactList[ContactKind.Friend.rawValue]?.append(contact)
            } else if contact.kind == ContactKind.Group.rawValue {contactList[ContactKind.Group.rawValue]?.append(contact)
            } else if contact.kind == ContactKind.Room.rawValue {contactList[ContactKind.Room.rawValue]?.append(contact)
            }
        }
        
        for idx in ContactKind.Friend.rawValue...ContactKind.Room.rawValue {if contactList[idx]!.count > 1 {let tmp = contactList[idx]!
                contactList[idx] = sortContacts(contacts: tmp)
            }
        }

        return contactList
    }

而后 RTMLoginSuccess() 中,在筹备好根底的会话列表信息和联系人列表信息后,触发一个主线程的异步并发工作(RTMLoginSuccess()以后是在异步线程中被调用的),跟新会话列表信息和联系人列表所用数据,并疏导加载会话列表视图后,在主线程中再次登程一个异步的非主线程并发网络工作 checkContactsUpdate(),更新联系人列表的信息,和服务器放弃同步。checkContactsUpdate() 相干代码如下:

    private class func decodeAttributeAnswer(type: Int, attriAnswer: RTMAttriAnswer) -> [ContactInfo] {var contacts = [ContactInfo]()
        
        for (key, value) in  attriAnswer.atttriDictionary {
            if let keyStr = key as? String, let jsonStr = value as? String {
                if jsonStr.isEmpty {continue}
                
                var contact = ContactInfo(type: type, xid: Int64(keyStr)!)
                decodeOpenInfo(contact: &contact, json: jsonStr)
                contacts.append(contact)
            }
        }

        return contacts
    }

    private class func syncCheckSessionsInfoUpdate(type: Int, contacts:[ContactInfo]) {var queryIds = [NSNumber]()
        for contact in contacts {queryIds.append(NSNumber(value: contact.xid))
        }
        
        var attriAnswer: RTMAttriAnswer? = nil
        if type == ContactKind.Friend.rawValue {attriAnswer = IMCenter.client!.getUserOpenInfo(queryIds, timeout: 0)
        } else if type == ContactKind.Group.rawValue {attriAnswer = IMCenter.client!.getGroupsOpenInfo(withId: queryIds, timeout: 0)
        } else if type == ContactKind.Room.rawValue {attriAnswer = IMCenter.client!.getRoomsOpenInfo(withId: queryIds, timeout: 0)
        } else {return}
        
        if attriAnswer != nil {let contacts = decodeAttributeAnswer(type: type, attriAnswer: attriAnswer!)
            
            DispatchQueue.main.async {
                for contact in contacts {updateContactCustomInfos(contact: contact)
                }
            }
        }
    }    

    private class func updateContactXname(contact: ContactInfo) {if let contacts = IMCenter.viewSharedInfo.contactList[contact.kind] {
            for user in contacts {
                if contact.xid == user.xid {
                    if user.xname != contact.xname {
                        user.xname = contact.xname
                        db.updateXname(contact: user)
                    }
                }
            }
        }
    }
    
    private class func updateContactCustomInfos(contact: ContactInfo) {if let contacts = IMCenter.viewSharedInfo.contactList[contact.kind] {
            for user in contacts {
                if contact.xid == user.xid {
                    
                    if user.imageUrl != contact.imageUrl {user.imagePath = ""}
                        
                    user.nickname = contact.nickname
                    user.imageUrl = contact.imageUrl
                    user.showInfo = contact.showInfo

                    db.updatePublicInfo(contact: user)
                    downloadImage(contactInfo: user)
                }
            }
        }
    }

    private class func checkContactsUpdate(type:Int, contacts:[ContactInfo]) {var contactList = [Int:[ContactInfo]]()
        contactList[type] = contacts
        checkContactsUpdate(contactList: contactList)
    }

    private class func checkContactsUpdate(contactList:[Int:[ContactInfo]]) {
        
        //-- 查问 xname
        var uids = [Int64]()
        var gids = [Int64]()
        var rids = [Int64]()
        
        if let contacts = contactList[ContactKind.Friend.rawValue] {
            for contact in contacts {uids.append(contact.xid)
            }
        }
        
        if let contacts = contactList[ContactKind.Group.rawValue] {
            for contact in contacts {gids.append(contact.xid)
            }
        }
        
        if let contacts = contactList[ContactKind.Room.rawValue] {
            for contact in contacts {rids.append(contact.xid)
            }
        }
        
        BizClient.lookup(users: nil, groups: nil, rooms: nil, uids: uids, gids: gids, rids: rids, completedAction: {
            lookupData in
            
            var friends = [ContactInfo]()
            for (key, value) in lookupData.users {let contact = ContactInfo(type: ContactKind.Friend.rawValue, xid: value)
                contact.xname = key
                
                friends.append(contact)
            }
            
            var groups = [ContactInfo]()
            for (key, value) in lookupData.groups {let contact = ContactInfo(type: ContactKind.Group.rawValue, xid: value)
                contact.xname = key
                
                groups.append(contact)
            }
            
            var rooms = [ContactInfo]()
            for (key, value) in lookupData.rooms {let contact = ContactInfo(type: ContactKind.Room.rawValue, xid: value)
                contact.xname = key
                
                rooms.append(contact)
            }
            
            DispatchQueue.main.async {
                for user in friends {updateContactXname(contact: user)
                }
                for group in groups {updateContactXname(contact: group)
                }
                for room in rooms {updateContactXname(contact: room)
                }
            }
        }, errorAction: {_ in})
        
        //-- 查问展现信息
        if let contacts = contactList[ContactKind.Friend.rawValue] {
            if contacts.count < 100 {syncCheckSessionsInfoUpdate(type: ContactKind.Friend.rawValue, contacts: contacts)
            } else {var queryContacts = [ContactInfo]()
                
                for contact in contacts {queryContacts.append(contact)
                    if queryContacts.count == 99 {syncCheckSessionsInfoUpdate(type: ContactKind.Friend.rawValue, contacts: queryContacts)
                        queryContacts.removeAll()}
                }
                
                if queryContacts.count > 0 {syncCheckSessionsInfoUpdate(type: ContactKind.Friend.rawValue, contacts: queryContacts)
                }
            }
        }
        
        if let contacts = contactList[ContactKind.Group.rawValue] {
            if contacts.count < 100 {syncCheckSessionsInfoUpdate(type: ContactKind.Group.rawValue, contacts: contacts)
            } else {var queryContacts = [ContactInfo]()
                
                for contact in contacts {queryContacts.append(contact)
                    if queryContacts.count == 99 {syncCheckSessionsInfoUpdate(type: ContactKind.Group.rawValue, contacts: queryContacts)
                        queryContacts.removeAll()}
                }
                
                if queryContacts.count > 0 {syncCheckSessionsInfoUpdate(type: ContactKind.Group.rawValue, contacts: queryContacts)
                }
            }
        }
        
        if let contacts = contactList[ContactKind.Room.rawValue] {
            if contacts.count < 100 {syncCheckSessionsInfoUpdate(type: ContactKind.Room.rawValue, contacts: contacts)
            } else {var queryContacts = [ContactInfo]()
                
                for contact in contacts {queryContacts.append(contact)
                    if queryContacts.count == 99 {syncCheckSessionsInfoUpdate(type: ContactKind.Room.rawValue, contacts: queryContacts)
                        queryContacts.removeAll()}
                }
                
                if queryContacts.count > 0 {syncCheckSessionsInfoUpdate(type: ContactKind.Room.rawValue, contacts: queryContacts)
                }
            }
        }
    }

因为 RTM 为了防止滥用,限度了 getUserOpenInfo、getGroupsOpenInfo、getRoomsOpenInfo 几个接口每次最多仅查问 100 个联系人的公开信息,所以在 checkContactsUpdate() 中,对超过 100 个的联系人列表,做了分段分批查问的解决。

最初,是查看登录用户是否有新会话。比方在离线期间,其余用户向以后登陆用户发动的会话。而 checkNewSessions() 又别离做了以下几件事件:

  1. 从 RTM 获取以后登录用户的所有会话
  2. 获取并更新每个会话的信息
  3. 在更新每个绘画的信息后,查看是否存在未读音讯

因为更新会话信息是网络操作,而且在 checkNewSessions() 中,P2P 和群组会话是并发异步更新的。如果在会话信息更新完之前就异步开始查看未读音讯,会导致前面的流程异样简单。
本着教学演示的目标,简化流程起见,咱们决定等 P2P 和群组会话两个异步流程都实现后,再启动未读音讯的查看。于是,咱们利用 class 的生命流程能够等价为一个三段状态机的原理,引入新的辅助类型 AsyncTask:

class AsyncTask {var action: () -> Void
    
    init(action: @escaping ()->Void) {self.action = action}
    deinit {action()
    }
    
    func npAction() {}
}

当 AsyncTask 实例析构时,咱们须要的动作便会开始执行。于是 checkNewSessions() 相干代码如下:

    private class func appendContactsForSessionView(contacts: [ContactInfo]) {if contacts.isEmpty { return}
        
        var oldSessions = IMCenter.viewSharedInfo.sessions
        
        for contact in contacts {let sessionItem = SessionItem(contact: contact)
            oldSessions.append(sessionItem)
        }
        
        IMCenter.viewSharedInfo.sessions = sortSessions(sessions: oldSessions)
    }
    
    private class func appendContactsForContactView(contacts: [ContactInfo]) {if contacts.isEmpty { return}
        
        var oldContactList = IMCenter.viewSharedInfo.contactList
        
        for contact in contacts {oldContactList[contact.kind]?.append(contact)
        }
        
        for idx in ContactKind.Friend.rawValue...ContactKind.Room.rawValue {if oldContactList[idx]!.count > 1 {let tmp = oldContactList[idx]!
                oldContactList[idx] = sortContacts(contacts: tmp)
            }
        }
        
        IMCenter.viewSharedInfo.contactList = oldContactList
    }

    private class func getAllSessions(success: @escaping (_ answer: RTMP2pGroupMemberAnswer?)->Void) {
        
        client!.getAllSessions(withTimeout: 0, success: success, fail: {
            
            errorAnswer in
            
            if errorAnswer?.code == 200010 {sleep(2)
                getAllSessions(success: success)
            }
            
        })
    }
    
    private class func checkNewSessions(contacts: [ContactInfo]) -> Void {let asyncTask = AsyncTask(action: { IMCenter.checkUnreadMessage() })
        
        getAllSessions(success: { sessionInfos in
            guard sessionInfos != nil else {return}
            
            var localUids = Set<Int64>()
            var localGids = Set<Int64>()
            
            for index in 0..<contacts.count {let info = contacts[index]
                if info.kind == ContactKind.Friend.rawValue {localUids.insert(info.xid)
                } else if info.kind == ContactKind.Group.rawValue {localGids.insert(info.xid)
                }
            }
            
            var newUids = [NSNumber]()
            var newGids = [NSNumber]()
            
            for v in sessionInfos!.p2pArray {
                if let uid = v as? NSNumber {if localUids.contains(uid.int64Value) == false {newUids.append(uid)
                    }
                }
            }
            
            for v in sessionInfos!.groupArray {
                if let gid = v as? NSNumber {if localGids.contains(gid.int64Value) == false {newGids.append(gid)
                    }
                }
            }
            
            if newUids.isEmpty == false {fetchNewP2PSessions(uids: newUids, asyncTask: asyncTask)
            }
            
            if newGids.isEmpty == false {fetchNewGroupSessions(gids: newGids, asyncTask: asyncTask)
            }
            
        })
    }

    private class func addNewSessions(contacts: [ContactInfo], asyncTask: AsyncTask) {if contacts.isEmpty { return}
        
        for contact in contacts {db.storeNewContact(contact: contact)
        }
        
        DispatchQueue.main.async {IMCenter.appendContactsForSessionView(contacts: contacts)
            IMCenter.appendContactsForContactView(contacts: contacts)
            
            DispatchQueue.global(qos: .default).async {asyncTask.npAction()
                
                for contact in contacts {downloadImage(contactInfo: contact)
                }
                checkContactsUpdate(type: contacts.first!.kind, contacts: contacts)
            }
        }
    }

    private class func decodeNewSession(type: Int, xids:[NSNumber], attriAnswer: RTMAttriAnswer, asyncTask: AsyncTask) {
        
        let dic = attriAnswer.atttriDictionary
        var contacts = [ContactInfo]()
            
        for number in xids {
            let xid = number.int64Value
            if let info = dic[String(xid)] {
                if let openInfo = info as? String {
                    if openInfo.isEmpty {contacts.append(ContactInfo(type: type, xid: xid))
                    } else {var contact = ContactInfo(type:type, xid: xid)
                        decodeOpenInfo(contact: &contact, json: openInfo)
                        contacts.append(contact)
                    }
                } else {contacts.append(ContactInfo(type: type, xid: xid))
                }
                
            } else {contacts.append(ContactInfo(type: type, xid: xid))
            }
        }
            
        addNewSessions(contacts: contacts, asyncTask: asyncTask)
    }

    private class func fetchNewP2PSessions(uids: [NSNumber], asyncTask: AsyncTask) -> Void {
        IMCenter.client?.getUserOpenInfo(uids, timeout: 0, success: {
            attriAnswer in
            
            if attriAnswer != nil {decodeNewSession(type: ContactKind.Friend.rawValue, xids:uids, attriAnswer: attriAnswer!, asyncTask: asyncTask)
            }
            
        }, fail: {
            errorAnswer in
            
            if errorAnswer?.code == 200010 {sleep(2)
                fetchNewP2PSessions(uids: uids, asyncTask: asyncTask)
            }
        })
    }
    
    private class func fetchNewGroupSessions(gids: [NSNumber], asyncTask: AsyncTask) -> Void {
        IMCenter.client?.getGroupsOpenInfo(withId: gids, timeout: 0, success: {
            attriAnswer in
            
            if attriAnswer != nil {decodeNewSession(type: ContactKind.Group.rawValue, xids:gids, attriAnswer: attriAnswer!, asyncTask: asyncTask)
            }
    
        }, fail: {
            errorAnswer in
            
            if errorAnswer?.code == 200010 {sleep(2)
                fetchNewGroupSessions(gids: gids, asyncTask: asyncTask)
            }
        })
    }

最初,获取未读音讯的次要流程就是依据 RTMClient.getUnreadMessages() 接口返回的数据,找到会话列表中对应的条目,加上未读标记,而后对于有未读音讯的会话,拉取最新的历史音讯,并将最新的一条,显示在会话列表页对应条目联系人显示名称的下方。
checkUnreadMessage() 流程相干代码如下:

private class func updateUnreadStatus(p2pUids: [Int64], groupIds: [Int64]) {
        
        DispatchQueue.main.async {
            var sessions = IMCenter.viewSharedInfo.sessions
            
            for uid in p2pUids {
                for session in sessions {
                    if session.contact.kind == ContactKind.Friend.rawValue
                        && session.contact.xid == uid {session.lastMessage.unread = true}
                }
            }
            
            for gid in groupIds {
                for session in sessions {
                    if session.contact.kind == ContactKind.Group.rawValue
                        && session.contact.xid == gid {session.lastMessage.unread = true}
                }
            }
            
            sessions = sortSessions(sessions: sessions)
            IMCenter.viewSharedInfo.sessions = sessions
        }
    }
    
    
    class func checkUnreadMessage() {
        IMCenter.client!.getUnreadMessages(withClear: true, timeout: 0, success: {
            unreadArrays in
            
            guard unreadArrays != nil else {return}
            
            var p2pIds = [Int64]()
            var groupIds = [Int64]()
            
            for v in unreadArrays!.p2pArray {
                if let uid = v as? NSNumber {p2pIds.append(uid.int64Value)
                }
            }
            
            for v in unreadArrays!.groupArray {
                if let gid = v as? NSNumber {groupIds.append(gid.int64Value)
                }
            }
            
            updateUnreadStatus(p2pUids: p2pIds, groupIds: groupIds)
            fetchUnreadMessage(p2pUids: p2pIds, groupIds: groupIds)
            
        }, fail: {
            error in
            if error?.code == 200010 {sleep(2)
                checkUnreadMessage()} else {print("RTM: Get unread chat message faield. Error info: \(String(describing: error?.ex))")
            }
        })
    }
    
    private class func syncFetchUnreadP2PChat(uid:Int64) {let answer = IMCenter.client?.getP2PHistoryMessageChat(withUserId: NSNumber(value:uid), desc: true, num: NSNumber(value: 10), begin: nil, end: nil, lastid: nil, timeout: 0)
         
        var insertCheckoutPoint = true
        if let unreads = answer?.history.messageArray {
            for rtmMessage in unreads {
                //-- 暂不思考二进制音讯,和开启主动翻译后的翻译音讯
                if IMCenter.db.insertChatMessage(type: ContactKind.Friend.rawValue, xid: uid, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: rtmMessage.stringMessage, mtime: rtmMessage.modifiedTime) == false {
                    insertCheckoutPoint = false
                    break
                }
            }
            
            if unreads.count > 0 {
                
                //-- Insert check point
                if insertCheckoutPoint {
                    let rtmMessage = unreads.last!
                    IMCenter.db.insertCheckPoint(type: ContactKind.Friend.rawValue, xid: uid, ts:rtmMessage.modifiedTime, desc:true)
                }
                
                //-- Update unread info for SessionsView
                DispatchQueue.main.async {
                    var sessions = IMCenter.viewSharedInfo.sessions
                    
                    for session in sessions {
                        if session.contact.kind == ContactKind.Friend.rawValue
                            && session.contact.xid == uid {
                            session.lastMessage.unread = true
                            session.lastMessage.message = unreads.first!.stringMessage
                            break
                        }
                    }
                    
                    sessions = sortSessions(sessions: sessions)
                    IMCenter.viewSharedInfo.sessions = sessions
                }
            }
        }
    }
    
    private class func syncFetchUnreadGroupChat(gid:Int64) {let answer = IMCenter.client?.getGroupHistoryMessageChat(withGroupId: NSNumber(value:gid), desc: true, num: NSNumber(value: 10), begin: nil, end: nil, lastid: nil, timeout: 0)
        
        var insertCheckoutPoint = true
        var lastMessage = LastMessage()
        if let unreads = answer?.history.messageArray {
            for rtmMessage in unreads {
                //-- 暂不思考二进制音讯,和开启主动翻译后的翻译音讯
                if rtmMessage.messageType == 30 {if IMCenter.db.insertChatMessage(type: ContactKind.Group.rawValue, xid: gid, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: rtmMessage.stringMessage, mtime: rtmMessage.modifiedTime) == false {
                        insertCheckoutPoint = false
                        break
                    } else {
                        lastMessage.message = rtmMessage.stringMessage
                        lastMessage.mid = rtmMessage.messageId
                        lastMessage.timestamp = rtmMessage.modifiedTime
                        lastMessage.unread = true
                    }
                } else {if IMCenter.db.insertChatCmd(type: ContactKind.Group.rawValue, xid: gid, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: rtmMessage.stringMessage, mtime: rtmMessage.modifiedTime) == false {
                        insertCheckoutPoint = false
                        break
                    }
                }
            }
            if unreads.count > 0 {
                
                //-- Insert check point
                if insertCheckoutPoint {
                    let rtmMessage = unreads.last!
                    IMCenter.db.insertCheckPoint(type: ContactKind.Group.rawValue, xid: gid, ts:rtmMessage.modifiedTime, desc:true)
                }
                
                if lastMessage.unread {
                    DispatchQueue.main.async {
                        var sessions = IMCenter.viewSharedInfo.sessions
                        
                        for session in sessions {
                            if session.contact.kind == ContactKind.Group.rawValue
                                && session.contact.xid == gid {
                                session.lastMessage = lastMessage
                                break
                            }
                        }
                        
                        sessions = sortSessions(sessions: sessions)
                        IMCenter.viewSharedInfo.sessions = sessions
                    }
                }
            }
        }
    }
        
    private class func fetchUnreadMessage(p2pUids: [Int64], groupIds: [Int64]) {
        
        for uid in p2pUids {syncFetchUnreadP2PChat(uid: uid)
        }
        
        for gid in groupIds {syncFetchUnreadGroupChat(gid: gid)
        }
    }

因为房间用户离线即视为退出,所以不存在未读一说。只有 P2P 会话,和群组,存在未读音讯。
在 syncFetchUnreadP2PChat() 和 syncFetchUnreadGroupChat() 中,本着拉取一条是一次网络调用,拉取 10 条也是一次网络调用,白拉白不拉的精力,咱们对每个有未读音讯的会话,一次性拉取 10 条最新历史音讯,而后顺次插入数据库。如果对应的音讯在数据库中曾经存在,则意味着咱们曾经从工夫上接上了数据库中上次保留的最新的音讯。因而,后续的插入将被跳过。但如果没有,则意味着咱们本次拉取到的数据,和上次数据库保留的最新数据之间未在工夫上产生链接,其间可能存在有历史音讯未被获取。即历史音讯空洞。为了后续在对话页面显示时能填补这些空洞,咱们网数据库中写入响应的历史音讯检查点 checkPoint。

此时,登陆流程就曾经齐全开发结束。上面将会对对接 SwiftUI 的显示界面。

5.4. UI 显示对接

批改 LoginView.swift,批改 userLogin() 函数如下:

    func userLogin(){
        
        if username.isEmpty {
            
            self.alertTitle = "有效输出"
            self.errorMessage = "用户名不能为空!"
            self.showAlert = true
            
            return
        }
        
        if password.isEmpty {
            
            self.alertTitle = "有效输出"
            self.errorMessage = "用户明码不能为空!"
            self.showAlert = true
            
            return
        }
        
        self.showLoginingHint = true
        
        BizClient.login(username: username, password: password, errorAction: {(message) in
            
            self.showLoginingHint = false
            self.errorMessage = message
            self.loginFailed = true
        })
    }

6. 注册流程

有了登录流程的教训,注册流程其实十分相似。
编辑 BizClient.swift,批改 register() 函数代码如下:

class func register(username: String, password: String, errorAction: @escaping (_ message: String) -> Void) {let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/userRegister?username=" + urlEncode(string: username) + "&pwd=" + urlEncode(string: password))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{(data, response, error) in
            
            if error != nil {errorAction("连贯谬误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {let json = try JSONDecoder().decode(UserLoginResponse.self, from: data!)
                    
                    checkUserChanged(loginName: username)
                    createIMClient(userLoginInfo: json, errorAction: errorAction)
                       
                } catch {
                    do {let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()}

批改 RegisterView.swift,批改 userRegister() 函数如下:

func userRegister(){
        
        if username.isEmpty {
            self.alertTitle = "有效输出"
            self.errorMessage = "用户名不能为空!"
            self.showAlert = true
            
            return
        }
        
        if password.isEmpty {
            self.alertTitle = "有效输出"
            self.errorMessage = "用户明码不能为空!"
            self.showAlert = true
            
            return
        }
        
        if passwordAgain.isEmpty {
            self.alertTitle = "有效输出"
            self.errorMessage = "确认明码不能为空!"
            self.showAlert = true
            
            return
        }
        
        if password != passwordAgain {
            self.alertTitle = "有效输出"
            self.errorMessage = "确认明码不匹配!"
            self.showAlert = true
            
            return
        }

        self.showLoginingHint = true
        
        BizClient.register(username: username, password: password, errorAction: {(message) in
                    
                    self.showLoginingHint = false
                    self.errorMessage = message
                    self.loginFailed = true
                })
    }

注册 + 登陆流程,实现!

7. 会话窗口音讯展现

进入会话窗口前,须要先筹备好本地已保留的历史音讯。而后显示窗口界面的同时,异步线程开始拉取最新的历史音讯,并且查看之前的历史音讯是否有缺失,有缺失就会继续填补,直到填补实现,或者用户退出等引发的填补中断。

编辑 IMCenter.swift,退出 showDialogueView() 函数:

    class func showDialogueView(contact: ContactInfo) {
        
        //-- 仅能在主线程中调用
        IMCenter.viewSharedInfo.targetContact = contact
        
        IMCenter.viewSharedInfo.strangerContacts.removeAll()
        
        IMCenter.prepareDialogueMesssageInfos(contact: contact)
        
        IMCenter.viewSharedInfo.lastPage = IMCenter.viewSharedInfo.currentPage
        
        IMCenter.viewSharedInfo.currentPage = .DialogueView
    }

并批改 ContactItemView.swift 中 onTapGesture 行为为:

        ... ...

        .onTapGesture {IMCenter.showDialogueView(contact: contactInfo)
        }

        ... ...

showDialogueView() 次要清理上一个(如果存在)会话的相干数据,并调用 prepareDialogueMesssageInfos() 函数,筹备新的会话数据,而后显示会话窗口界面。

7.1 准备会话窗口数据

prepareDialogueMesssageInfos() 函数先加载本地保留的历史数据,并按工夫排序;而后启动两个异步线程,一个清理未同步的未知联系人信息,一个查看并一直填充历史音讯。

prepareDialogueMesssageInfos() 代码如下:

    private class func soreChatMessages(messages: [ChatMessage]) -> [ChatMessage] {
        if messages.count <= 1 {return messages}
        
        return messages.sorted(by: { (m1, m2) -> Bool in
            if m1.mtime < m2.mtime {return true}
            
            if m1.mtime == m2.mtime {
                if m1.mid < m2.mid {return true}
            }
            return false
        })
    }


    class func prepareDialogueMesssageInfos(contact:ContactInfo) {
        
        if contact.kind == ContactKind.Room.rawValue {DispatchQueue.global(qos: .default).async {IMCenter.client!.enterRoom(withId: NSNumber(value: contact.xid), timeout: 0, success: {
                    
                    DispatchQueue.main.sync {sendCmd(contact: contact, message: "\(getSelfDispalyName()) 进入房间")
                    }

                }, fail: {_ in})
            }
        }
        
        let chatMessages = IMCenter.db.loadAllMessages(contact:contact)
        IMCenter.viewSharedInfo.dialogueMesssages = soreChatMessages(messages: chatMessages)
        
        DispatchQueue.global(qos: .default).async {let unknownContacts = pickupUnknownContacts(messages:chatMessages)
            cleanUnknownContacts(unknownContacts: unknownContacts)
        }
        
        DispatchQueue.global(qos: .default).async {refillHistoryMessage(contact:contact)
        }
    }

7.2 发送零碎告诉

当点击的联系人为房间类型时,为了简略起见,间接通过客户端进入房间。这时,咱们须要通知房间中的其余用户,谁进入房间了。因而,咱们在这里用 RTM 预约义的 Cmd 类型音讯发送零碎告诉:

    private class func sendGroupCmd(contact:ContactInfo, message:String) {IMCenter.client!.sendGroupCmdMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {_ in}, fail: {_ in})
    }
    
    private class func sendRoomCmd(contact:ContactInfo, message:String) {IMCenter.client!.sendRoomCmdMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {_ in}, fail: {_ in})
    }

    class func sendCmd(contact:ContactInfo, message:String) {
        if contact.kind == ContactKind.Group.rawValue {sendGroupCmd(contact:contact, message:message)
        }else if contact.kind == ContactKind.Room.rawValue {sendRoomCmd(contact:contact, message:message)
        } else {return}
        
        objc_sync_enter(locker)
        let mid = fakeMid
        fakeMid += 1
        objc_sync_exit(locker)
        
        let curr = Date().timeIntervalSince1970 * 1000
        
        let chatMessage = ChatMessage(sender: IMCenter.client!.userId, mid: mid, mtime: Int64(curr), message: message)
        chatMessage.isChat = false
        
        var chats = IMCenter.viewSharedInfo.dialogueMesssages
        chats.append(chatMessage)
        
        IMCenter.viewSharedInfo.dialogueMesssages = chats
    }

因为用户发送音讯是网络操作,而网络操作可能会失败,比方断网时。因而咱们无奈等服务器确认后,再将用户发送的内容显示到界面上。但 RTM 只有在返回后,能力获取到音讯的 messageId。但显示音讯须要应用 MessageId 初始化 ChatMessage 对象。于是咱们采纳了当年 QQ 的办法:间接本地显示(这办法当初微信也还在用)。为此,咱们引入了一个虚伪的 MessageId:fakeMid:Int64 进行代替。

然而在其余状况下,比方批改信息或者其余状况,也须要发送零碎告诉。而且这些零碎告诉往往是在网络操作实现后,在其余线程内异步触发的。那就存在着 fakeMid 被并发读写的状况。于是咱们须要增加一个锁对象 class Locker,并用其进行同步。
于是在 IMCenter.swift 中,减少以下代码:

... ...

class Locker {}

... ...

class IMCenter {
    
    ... ...
    
    static var locker = Locker()
    
    ... ...
    
    static var fakeMid:Int64 = 1
    
    ... ...
}

7.3 同步未知联系人
在一个群组,或者房间中,RTM 零碎推送过去的音讯,只有发送人惟一数字 ID,而其它的展现信息,则须要咱们本人获取。无论是从咱们本人的服务器上获取,还是从 RTM 服务器上获取。
此外,在获取的过程中,可能因为用户退出等起因,获取过程被中断,此时,App 本地的数据中也将存在未知联系人。所以 pickupUnknownContacts() 便是从本地音讯中筛查出未知联系人,而后交给 cleanUnknownContacts() 进行信息更新。
相干代码如下:

    private class func syncQueryUsersInfos(type: Int, contacts:[ContactInfo]) {var queryIds = [NSNumber]()
        for contact in contacts {queryIds.append(NSNumber(value: contact.xid))
        }
        
        let attriAnswer = IMCenter.client!.getUserOpenInfo(queryIds, timeout: 0)
        let strangers = decodeAttributeAnswer(type: type, attriAnswer: attriAnswer)
        
        DispatchQueue.main.async {
            for stranger in strangers {if let contact = IMCenter.viewSharedInfo.strangerContacts[stranger.xid] {
                    contact.nickname = stranger.nickname
                    contact.imageUrl = stranger.imageUrl
                    contact.showInfo = stranger.showInfo
                } else {IMCenter.viewSharedInfo.strangerContacts[stranger.xid] = stranger
                }
            }
        }
    }

    private class func pickupUnknownContacts(messages:[ChatMessage]) -> [ContactInfo] {var uids: Set<Int64> = []
        for msg in messages {
            if msg.sender != IMCenter.client!.userId {uids.insert(msg.sender)
            }
        }
        
        var contacts = [ContactInfo]()
        if uids.isEmpty == false {let allUsers = IMCenter.db.loadAllUserContactInfos()
            
            for uid in uids {if let contact = allUsers[uid] {
                    if contact.nickname.isEmpty || contact.imageUrl.isEmpty {contacts.append(contact)
                    }
                } else {contacts.append(ContactInfo(xid: uid))
                }
            }
        }
        
        return contacts
    }

    private class func cleanUnknownContacts(unknownContacts:[ContactInfo]) {
        
        //-- 查问 xname
        var uids = [Int64]()
        for contact in unknownContacts {uids.append(contact.xid)
        }
        BizClient.lookup(users: nil, groups: nil, rooms: nil, uids: uids, gids: nil, rids: nil, completedAction: {
            lookupData in
            
            var strangers = [ContactInfo]()
            for (key, value) in lookupData.users {let contact = ContactInfo(type: ContactKind.Stranger.rawValue, xid: value)
                contact.xname = key
                
                strangers.append(contact)
            }
            
            DispatchQueue.main.async {
                for stranger in strangers {if let contact = IMCenter.viewSharedInfo.strangerContacts[stranger.xid] {contact.xname = stranger.xname} else {IMCenter.viewSharedInfo.strangerContacts[stranger.xid] = stranger
                    }
                }
            }
        }, errorAction: {_ in})
        
        //-- 查问展现信息
        if unknownContacts.count < 100 {syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: unknownContacts)
        } else {var queryContacts = [ContactInfo]()
            
            for contact in unknownContacts {queryContacts.append(contact)
                if queryContacts.count == 99 {syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: queryContacts)
                    queryContacts.removeAll()}
            }
            
            if queryContacts.count > 0 {syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: queryContacts)
            }
        }
    }

7.4 填补空缺的历史音讯

最初一步,是填补历史音讯空缺。填补历史音讯空缺其实也很简略。先从数据库中获取曾经保留的历史音讯检查点,按从新到旧进行排序。而后从新到旧,以 10 条为单位(RTM 的默认限度),降序拉取以后会话的历史音讯,而后逐条保留。一旦须要保留的历史音讯曾经在数据库中存在,则意味着曾经填补了历史音讯的最新一段空缺。则查看对应范畴内是否存在历史音讯检查点,如果存在则删除,不存在则以最新的历史音讯检查点开始,持续从新到旧,以降序拉取历史音讯。
如果这之间发现有未知联系人,则启动新的异步线程进行同步。
填补空缺历史音讯的 refillHistoryMessage() 函数及相干代码如下:

    private class func sortHistoryCheckpoint(checkpoints:[HistoryCheckpoint]) -> [HistoryCheckpoint] {
        if checkpoints.count < 2 {return checkpoints}
        
        return checkpoints.sorted(by: { (c1, c2) -> Bool in
            if c1.ts > c2.ts {return true}
            if c1.ts == c2.ts {return c1.desc}
            return false
        })
    }

    private class func refillHistoryMessage(contact:ContactInfo) {
        var historyAnswer: RTMHistoryMessageAnswer? = nil
        var begin: Int64 = 0
        var end: Int64 = 0
        var lastId: Int64 = 0
        
        let fetchCount = 10
        let nsXid = NSNumber(value: contact.xid)
        let nsCount = NSNumber(value: fetchCount)
        
        var checkpoints = db.loadAllHistoryMessageCheckpoints(contact:contact)
        checkpoints = sortHistoryCheckpoint(checkpoints: checkpoints)
        
        while (true)
        {
            if contact.kind == ContactKind.Friend.rawValue {historyAnswer = IMCenter.client!.getP2PHistoryMessageChat(withUserId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
            } else if contact.kind == ContactKind.Group.rawValue {historyAnswer = IMCenter.client!.getGroupHistoryMessageChat(withGroupId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
            } else if contact.kind == ContactKind.Room.rawValue {historyAnswer = IMCenter.client!.getRoomHistoryMessageChat(withRoomId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
            } else {return}
            
            if historyAnswer != nil && historyAnswer!.error.code == 0 {var chatMessages = [ChatMessage]()
                for message in historyAnswer!.history.messageArray {
                    
                    if message.messageType == 30 {if IMCenter.db.insertChatMessage(type: contact.kind, xid: contact.xid, sender: message.fromUid, mid: message.messageId, message: message.stringMessage, mtime: message.modifiedTime, printError: false) == false {break}
                    } else {if IMCenter.db.insertChatCmd(type: contact.kind, xid: contact.xid, sender: message.fromUid, mid: message.messageId, message: message.stringMessage, mtime: message.modifiedTime, printError: false) == false {break}
                    }
                    
                    let chatMsg = ChatMessage(sender: message.fromUid, mid: message.messageId, mtime: message.modifiedTime, message: message.stringMessage)
                    
                    if message.messageType != 30 {chatMsg.isChat = false}
                    
                    chatMessages.append(chatMsg)
                }
                
                if chatMessages.count > 0 {DispatchQueue.global(qos: .default).async {let unknownContacts = pickupUnknownContacts(messages:chatMessages)
                        cleanUnknownContacts(unknownContacts: unknownContacts)
                    }
                    
                    var continueLoading = true

                    DispatchQueue.main.sync {
                        if IMCenter.viewSharedInfo.targetContact == nil
                            || IMCenter.viewSharedInfo.targetContact!.xid != contact.xid
                            || IMCenter.viewSharedInfo.targetContact!.kind != contact.kind {continueLoading = false} else {
                            var oldDialogues = IMCenter.viewSharedInfo.dialogueMesssages
                            
                            for chatMsg in chatMessages {oldDialogues.append(chatMsg)
                            }
                            
                            oldDialogues = soreChatMessages(messages: oldDialogues)
                            IMCenter.viewSharedInfo.dialogueMesssages = oldDialogues
                        }
                    }

                    if continueLoading == false {IMCenter.db.insertCheckPoint(type: contact.kind, xid: contact.xid, ts:historyAnswer!.history.end, desc:true)
                        return
                    }
                }
                
                if historyAnswer!.history.messageArray.count < fetchCount {IMCenter.db.clearAllHistoryMessageCheckpoints(contact: contact)
                    return
                }
                
                if chatMessages.count == fetchCount {
                    
                    begin = historyAnswer!.history.begin
                    end = historyAnswer!.history.end
                    lastId = historyAnswer!.history.lastid
                    
                } else {while (true) {if checkpoints.count == 0 { return}
                        
                        if checkpoints.first!.ts >= end {checkpoints.removeFirst()
                        } else {
                            begin = 0
                            end = Int64(checkpoints.first!.ts)
                            lastId = 0
                            
                            break
                        }
                    }
                }
            } else if historyAnswer != nil && historyAnswer!.error.code == 200010 {
                var continueLoading = true
                DispatchQueue.main.sync {
                    if IMCenter.viewSharedInfo.targetContact == nil
                        || IMCenter.viewSharedInfo.targetContact!.xid != contact.xid
                        || IMCenter.viewSharedInfo.targetContact!.kind != contact.kind {continueLoading = false}
                }
                
                if continueLoading == false {IMCenter.db.insertCheckPoint(type: contact.kind, xid: contact.xid, ts:end, desc:true)
                    return
                }
                
                sleep(2)
            } else {return}
        }
    }

至此,会话窗口的历史信息处理结束。

7.5 音讯的发送

在咱们开始批改会话窗口的视图界背后,咱们还的解决音讯的发送行为。毕竟会话窗口不仅须要展示会话内容,还须要提供发送音讯的能力。
sendMessage() 其实与 sendCmd() 高度相似。毕竟 sendMessge() 实质上是应用的 RTM 预约义的 chat 类型音讯。chat 类型音讯与 cmd 音讯类型的不同之处在于,如果 chat 的内容是单纯的文本聊天内容,而非 json、xml 等结构化的信息,则能够间接开启 RTM 的文本主动翻译和文本主动审核两个性能。除此之外,其余没有差异。

    private class func sendP2PMessage(contact:ContactInfo, message:String) {IMCenter.client!.sendP2PMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {
            answer in
            _ = IMCenter.db.insertChatMessage(type: ContactKind.Friend.rawValue, xid: contact.xid, sender: IMCenter.client!.userId, mid: answer.messageId, message: message, mtime: answer.mtime)
        }, fail: {
            _ in
            //-- 这里应该在 UI 上显示红色圆形底叹号,但 IMDemo 为演示目标,这里从略
        })
    }
    
    private class func sendGroupMessage(contact:ContactInfo, message:String) {IMCenter.client!.sendGroupMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {
            answer in
            _ = IMCenter.db.insertChatMessage(type: ContactKind.Group.rawValue, xid: contact.xid, sender: IMCenter.client!.userId, mid: answer.messageId, message: message, mtime: answer.mtime)
        }, fail: {
            _ in
            //-- 这里应该在 UI 上显示红色圆形底叹号,但 IMDemo 为演示目标,这里从略
        })
    }
    
    private class func sendRoomMessage(contact:ContactInfo, message:String) {IMCenter.client!.sendRoomMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: {
            answer in
            _ = IMCenter.db.insertChatMessage(type: ContactKind.Room.rawValue, xid: contact.xid, sender: IMCenter.client!.userId, mid: answer.messageId, message: message, mtime: answer.mtime)
        }, fail: {
            _ in
            //-- 这里应该在 UI 上显示红色圆形底叹号,但 IMDemo 为演示目标,这里从略
        })
    }

    class func sendMessage(contact:ContactInfo, message:String) {
        if contact.kind == ContactKind.Friend.rawValue {sendP2PMessage(contact:contact, message:message)
        } else if contact.kind == ContactKind.Group.rawValue {sendGroupMessage(contact:contact, message:message)
        }else if contact.kind == ContactKind.Room.rawValue {sendRoomMessage(contact:contact, message:message)
        } else {return}
        
        objc_sync_enter(locker)
        let mid = fakeMid
        fakeMid += 1
        objc_sync_exit(locker)
        
        let curr = Date().timeIntervalSince1970 * 1000
        
        let chatMessage = ChatMessage(sender: IMCenter.client!.userId, mid: mid, mtime: Int64(curr), message: message)
        
        var chats = IMCenter.viewSharedInfo.dialogueMesssages
        chats.append(chatMessage)
        
        IMCenter.viewSharedInfo.dialogueMesssages = chats
    }

7.6 实现之前占位的辅助性能

在最初和会话窗口页面关联起来之前,第二篇中,会话窗口须要的一个性能 findContact(),过后仅实现了一个空函数用于占位。此时,咱们须要先将其欠缺。

函数的根本流程是,当初好友列表中查找,找到返回。如果不存在,则在记录的陌生人中查找。如果还找不到,查看是不是以后登录用户本身。如果不是,记录新的陌生人信息,并调用 cleanUnknownContacts() 同步联系人信息。

编辑 IMCenter.swift,批改 findContact() 函数代码如下:

class func findContact(chatMessage: ChatMessage) -> ContactInfo {
        //-- 好友列表中查找
        for contact in IMCenter.viewSharedInfo.contactList[ContactKind.Friend.rawValue]! {
            if contact.xid == chatMessage.sender {return contact}
        }
        
        //-- 陌生人中查找
        if let contact = IMCenter.viewSharedInfo.strangerContacts[chatMessage.sender] {return contact}
        
        let contact = ContactInfo(type: ContactKind.Stranger.rawValue, xid: chatMessage.sender)
        
        if chatMessage.sender == IMCenter.client!.userId {let username = IMCenter.fetchUserProfile(key: "username")
            contact.imageUrl = IMCenter.fetchUserProfile(key: "\(username)-image-url")
            contact.imagePath = IMCenter.fetchUserProfile(key: "\(username)-image")
        } else {IMCenter.viewSharedInfo.strangerContacts[chatMessage.sender] = contact
            
            DispatchQueue.global(qos: .default).async {var unknownContacts = [ContactInfo]()
                unknownContacts.append(contact)
                cleanUnknownContacts(unknownContacts: unknownContacts)
            }
        }
        
        return contact
    }

7.7 界面 UI 同步批改
最初,咱们将性能和页面关联起来。
编辑 DialogueView.swift,首先是 DialogueHeaderView 视图,减少函数 updateSessionState(),并批改返回按钮的 onTapGesture 行为如下:

struct DialogueHeaderView: View {
    
    ... ...

    func updateSessionState() {
        for session in IMCenter.viewSharedInfo.sessions {
            if session.contact.kind == IMCenter.viewSharedInfo.targetContact!.kind && session.contact.xid == IMCenter.viewSharedInfo.targetContact!.xid {
                session.lastMessage.unread = false
                
                for idx in 0..<IMCenter.viewSharedInfo.dialogueMesssages.count {
                    let realIdx = IMCenter.viewSharedInfo.dialogueMesssages.count - 1 - idx
                    if IMCenter.viewSharedInfo.dialogueMesssages[realIdx].isChat {let message = IMCenter.viewSharedInfo.dialogueMesssages[realIdx]
                        session.lastMessage.message = message.message
                        session.lastMessage.mid = message.mid
                        session.lastMessage.timestamp = message.mtime
                        
                        let oldSessions = IMCenter.viewSharedInfo.sessions
                        IMCenter.viewSharedInfo.sessions = IMCenter.sortSessions(sessions: oldSessions)
                        
                        return
                    }
                }
                
                session.lastMessage.message = ""
                session.lastMessage.mid = 0
                session.lastMessage.timestamp = 0
                
                let oldSessions = IMCenter.viewSharedInfo.sessions
                IMCenter.viewSharedInfo.sessions = IMCenter.sortSessions(sessions: oldSessions)
                
                return
            }
        }
    }
    
    var body: some View {
        HStack {Image("button_back")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:IMDemoUIConfig.navigationIconEdgeLen, height: IMDemoUIConfig.navigationIconEdgeLen, alignment: .center)
                .padding((IMDemoUIConfig.topNavigationHight - IMDemoUIConfig.navigationIconEdgeLen)/2)
                .onTapGesture {updateSessionState()
                    
                    IMCenter.viewSharedInfo.currentPage = IMCenter.viewSharedInfo.lastPage
                }
 
            ... ...
        }
    }
}

函数 updateSessionState() 用于在返回联系人列表或者会话列表界背后,去除会话页面中,以后会话可能的未读音讯状态,以及更新联系人名称下显示的最新一条聊天音讯。

之后是 DialogueFooterView 视图,增加对发送性能的调用:

struct DialogueFooterView: View {
 
    ... ...
    
    var body: some View {
        HStack {
            ... ...
            
            Image("button_send")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:IMDemoUIConfig.navigationIconEdgeLen, height: IMDemoUIConfig.navigationIconEdgeLen, alignment: .center)
                .padding((IMDemoUIConfig.topNavigationHight - IMDemoUIConfig.navigationIconEdgeLen)/2)
                .onTapGesture {
                    
                    IMCenter.viewSharedInfo.newMessageReceived = false
                    
                    if self.message.isEmpty {
                        viewInfo.newestMessage = IMCenter.viewSharedInfo.dialogueMesssages.last!
                        hideKeyboard()
                        return
                    }
                    
                    IMCenter.sendMessage(contact: contact, message: self.message)
                    self.message = ""
                    
                    hideKeyboard()
                    viewInfo.newestMessage = IMCenter.viewSharedInfo.dialogueMesssages.last!
                }
        }
    }
}

最初是 DialogueView 视图,修改对以后用户 ID 的援用:

struct DialogueView: View {
    
    ... ...
    
    init() {
        self.contact = IMCenter.viewSharedInfo.targetContact!
        self.selfId = IMCenter.client!.userId
        self.viewInfo = IMCenter.viewSharedInfo
        self.contactForInfoPage = self.contact
    }
    
    ... ...
}

到此,会话视图便开发结束。

8. 菜单事件

接下来,便是对菜单的点击产生响应。

8.1 增加好友

增加好友的基本思路是:

  1. 如果查问本地联系人信息,曾经是好友了,则间接关上会话页面;
  2. 如果本地将对方作为陌生人记录,则将陌生人标记批改为好友标记,并增加会话条目到会话列表页,而后关上会话页面;
  3. 否则查问业务服务器,确认对方是非法注册用户,而后保留联系人信息,并增加会话条目到会话列表页,关上会话页面。

编辑 IMCenter.swift,增加 入口函数 addFriendInMainThread() 及相干性能函数:

    class func addNewSessionByMenuActionInMainThread(contact: ContactInfo) {var contacts = [ContactInfo]()
        contacts.append(contact)
        
        IMCenter.appendContactsForSessionView(contacts: contacts)
        IMCenter.appendContactsForContactView(contacts: contacts)
        
        DispatchQueue.global(qos: .default).async {downloadImage(contactInfo: contact)
            checkContactsUpdate(type: contact.kind, contacts: contacts)
        }
    }

    class func addFriendInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {var contact = db.loadContentInfo(type: ContactKind.Friend.rawValue, xname: xname)
        if contact != nil {existAction(contact!)
            showDialogueView(contact: contact!)
            return
        } else {contact = db.loadContentInfo(type: ContactKind.Stranger.rawValue, xname: xname)
            if contact != nil {existAction(contact!)
                db.changeStrangerToFriend(xid: contact!.xid)
                addNewSessionByMenuActionInMainThread(contact: contact!)
                showDialogueView(contact: contact!)
                return
            }
        }
        
        var users = [String]()
        users.append(xname)
        
        BizClient.lookup(users: users, groups: nil, rooms: nil, uids: nil, gids: nil, rids: nil, completedAction: {
            respon in
            if let uid = respon.users[xname] {let contact = ContactInfo(type: ContactKind.Friend.rawValue, uniqueId: uid, uniqueName: xname, nickname: "")
                
                DispatchQueue.main.async {successAction(contact)
                }
                return
            } else {var errInfo = ErrorInfo()
                errInfo.title = "用户不存在"
                errInfo.desc = "被增加的用户尚未注册!"
                
                DispatchQueue.main.async {errorAction(errInfo)
                }
            }
        }, errorAction: { errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "增加好友失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {errorAction(errInfo)
            }
        })
    }

批改 MenuActionView.swift,编辑 AddFriendView 视图,增加 checkInput() 函数,并批改“增加”按钮响应如下:

struct AddFriendView: View {
    ... ...
    
    func checkInput() -> Bool {
        if username.isEmpty {let error = ErrorInfo(title: "有效的输出", desc: "用户名不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        ... ...
        
            Button("增加") {if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.addFriendInMainThread(xname: self.username, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
        
        ... ...
    }
}

此时,“增加好友”菜单性能实现。

8.2 创立群组
创立群组的基本思路:

  1. 查问本地联系人信息,如果曾经是群组成员,则间接关上会话页面;
  2. 否则查问业务服务器,确认指标是非法群组,而后保留群组联系人信息,并增加会话条目到会话列表页,关上会话页面。

编辑 BizClient.swift,实现 createGroup() 函数:

... ...

struct CreateGroupResponse: Codable {var gid: Int64}

... ...

class BizClient {
    
    ... ...
    
    class func createGroup(uniqueName: String, completedAction: @escaping (_ gid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/createGroup?uid=\(IMCenter.client!.userId)&group=" + urlEncode(string: uniqueName))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{(data, response, error) in
            
            if error != nil {errorAction("连贯谬误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {let json = try JSONDecoder().decode(CreateGroupResponse.self, from: data!)
                    completedAction(json.gid)
                } catch {
                    do {let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()}
    
    ... ...
}

编辑 IMCenter.swift,增加入口函数 createGroupInMainThread():

    class func createGroupInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {let contact = db.loadContentInfo(type: ContactKind.Group.rawValue, xname: xname)
        if contact != nil {existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        BizClient.createGroup(uniqueName: xname, completedAction: {
            gid in
            
            let contact = ContactInfo(type: ContactKind.Group.rawValue, uniqueId: gid, uniqueName: xname, nickname: "")
            
            DispatchQueue.main.async {successAction(contact)
            }
            
        }, errorAction: {
            errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "创立群组失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {errorAction(errInfo)
            }
        })
    }

批改 MenuActionView.swift,编辑 JoinGroupView 视图,增加 checkInput() 函数,并批改“创立”按钮响应如下:

struct CreateGroupView: View {
    
    ... ...

    func checkInput() -> Bool {
        if groupname.isEmpty {let error = ErrorInfo(title: "有效的输出", desc: "群组惟一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("创立") {if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.createGroupInMainThread(xname: self.groupname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
        
        ... ...
    }   
}

此时,“创立群组”菜单性能实现。

8.3 退出群组

退出群组的基本思路是:

  1. 如果查问本地联系人信息,如果曾经退出群组了,则间接关上会话页面;
  2. 否则查问业务服务器,确认指标是非法群组,而后保留群组联系人信息,并增加会话条目到会话列表页,关上会话页面。

编辑 BizClient.swift,实现 joinGroup() 函数:

    class func joinGroup(uniqueGroupName: String, completedAction: @escaping (_ gid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/joinGroup?uid=\(IMCenter.client!.userId)&group=" + urlEncode(string: uniqueGroupName))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{(data, response, error) in
            
            if error != nil {errorAction("连贯谬误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {let json = try JSONDecoder().decode(CreateGroupResponse.self, from: data!)
                    completedAction(json.gid)
                } catch {
                    do {let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()}

编辑 IMCenter.swift,增加 入口函数 joinGroupInMainThread():

    class func joinGroupInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {let contact = db.loadContentInfo(type: ContactKind.Group.rawValue, xname: xname)
        if contact != nil {existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        BizClient.joinGroup(uniqueGroupName: xname, completedAction: {
            gid in
            
            let contact = ContactInfo(type: ContactKind.Group.rawValue, uniqueId: gid, uniqueName: xname, nickname: "")
            
            DispatchQueue.main.async {successAction(contact)
            }
            
        }, errorAction: {
            errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "退出群组失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {errorAction(errInfo)
            }
        })
    }

批改 MenuActionView.swift,编辑 JoinGroupView 视图,增加 checkInput() 函数,并批改“退出”按钮响应如下:

struct JoinGroupView: View {
    
    ... ...
    
    func checkInput() -> Bool {
        if groupname.isEmpty {let error = ErrorInfo(title: "有效的输出", desc: "群组惟一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("退出") {if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.joinGroupInMainThread(xname: self.groupname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
        
        ... ...
    }   
}

此时,“退出群组”菜单性能实现。

8.4 创立房间

创立房间的基本思路是:

  1. 查问本地联系人信息,如果本地保留有对应的房间联系人信息,则间接关上会话页面;
  2. 否则查问业务服务器,确认对方是非法房间,而后保留房间联系人信息,并增加会话条目到会话列表页,关上会话页面。

编辑 BizClient.swift,实现 createRoom() 函数:

... ...

struct CreateRoomResponse: Codable {var rid: Int64}

... ...

class BizClient {
    
    ... ...

    class func createRoom(uniqueName: String, completedAction: @escaping (_ rid: Int64) -> Void, errorAction: @escaping (_ message: String) -> Void) {let url = URL(string:"http://" + IMDemoConfig.IMDemoServerEndpoint + "/service/createRoom?room=" + urlEncode(string: uniqueName))!
        
        let task = URLSession.shared.dataTask(with:url, completionHandler:{(data, response, error) in
            
            if error != nil {errorAction("连贯谬误: \(error!.localizedDescription)")
            }
            if response != nil {
                do {let json = try JSONDecoder().decode(CreateRoomResponse.self, from: data!)
                    completedAction(json.rid)
                } catch {
                    do {let json = try JSONDecoder().decode(FpnnErrorResponse.self, from: data!)
                        errorAction(json.ex)
                    } catch {errorAction("Error during JSON serialization: \(error.localizedDescription)")
                    }
                }
            }
        })
        
        task.resume()}
    
    ... ...
}

编辑 IMCenter.swift,增加 入口函数 createRoomInMainThread():

    class func createRoomInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {let contact = db.loadContentInfo(type: ContactKind.Room.rawValue, xname: xname)
        if contact != nil {existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        BizClient.createRoom(uniqueName: xname, completedAction: {
            rid in
            
            let contact = ContactInfo(type: ContactKind.Room.rawValue, uniqueId: rid, uniqueName: xname, nickname: "")
            
            DispatchQueue.main.async {successAction(contact)
            }
            
        }, errorAction: {
            errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "创立房间失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {errorAction(errInfo)
            }
        })
    }

批改 MenuActionView.swift,编辑 CreateRoomView 视图,增加 checkInput() 函数,并批改“创立”按钮响应如下:

struct CreateRoomView: View {
    
    ... ...
    
    func checkInput() -> Bool {
        if roomname.isEmpty {let error = ErrorInfo(title: "有效的输出", desc: "房间惟一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("创立") {if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.createRoomInMainThread(xname: self.roomname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
            
        ... ...
    }
}

此时,“创立房间”菜单性能实现。

8.5 退出房间

退出房间的基本思路是:

  1. 查问本地联系人信息,如果本地保留有对应的房间联系人信息,则间接关上会话页面;
  2. 否则查问业务服务器,确认对方是非法房间,而后保留房间联系人信息,并增加会话条目到会话列表页,关上会话页面。

编辑 IMCenter.swift,增加 入口函数 joinRoomInMainThread() 及相干性能函数:

    class func joinRoomInMainThread(xname: String, existAction: @escaping (_ contact: ContactInfo)->Void, successAction: @escaping (_ contact: ContactInfo)->Void, errorAction: @escaping (_ errorInfo: ErrorInfo)->Void) {let contact = db.loadContentInfo(type: ContactKind.Room.rawValue, xname: xname)
        if contact != nil {existAction(contact!)
            showDialogueView(contact: contact!)
            return
        }
        
        var rooms = [String]()
        rooms.append(xname)
        
        BizClient.lookup(users: nil, groups: nil, rooms: rooms, uids: nil, gids: nil, rids: nil, completedAction: {
            respon in
            if let rid = respon.rooms[xname] {let contact = ContactInfo(type: ContactKind.Room.rawValue, uniqueId: rid, uniqueName: xname, nickname: "")
                
                DispatchQueue.main.async {successAction(contact)
                }
                return
            } else {var errInfo = ErrorInfo()
                errInfo.title = "房间不存在"
                errInfo.desc = "房间尚未被创立!"
                
                DispatchQueue.main.async {errorAction(errInfo)
                }
            }
        }, errorAction: { errorMessage in
            
            var errInfo = ErrorInfo()
            
            errInfo.title = "进入房间失败"
            errInfo.desc = errorMessage
            
            DispatchQueue.main.async {errorAction(errInfo)
            }
        })
    }

批改 MenuActionView.swift,编辑 EnterRoomView 视图,增加 checkInput() 函数,并批改“退出”按钮响应如下:

struct EnterRoomView: View {
    
    ... ...
    
    func checkInput() -> Bool {
        if roomname.isEmpty {let error = ErrorInfo(title: "有效的输出", desc: "房间惟一名称不能为空!")
            IMCenter.errorInfo = error
            
            return false
        }
        return true
    }
    
    var body: some View {
        
        ... ...
        
            Button("退出") {if !checkInput() {
                    showError = true
                    return
                }
                
                showProcessing = true
                IMCenter.joinRoomInMainThread(xname: self.roomname, existAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                }, successAction: {
                    contact in
                    
                    showProcessing = false
                    IMCenter.viewSharedInfo.menuAction = .HideMode
                    
                    IMCenter.db.storeNewContact(contact: contact)
                    IMCenter.addNewSessionByMenuActionInMainThread(contact: contact)
                    IMCenter.showDialogueView(contact: contact)
                    
                }, errorAction: {
                    errorInfo in
                    
                    IMCenter.errorInfo = errorInfo
                    
                    showProcessing = false
                    showError = true
                })
            }
            
        ... ...
    }
}

至此,菜单所有性能响应,增加结束。

9. 接管音讯

RTM 音讯品种其实十分丰盛,目前咱们可能收到的音讯有五类:P2P 聊天音讯、群组聊天音讯、房间聊天音讯、群组零碎告诉、房间零碎告诉。

为了简化起见,聊天音讯咱们对立解决。
处理函数 receiveNewNessage() 先将收到的音讯保留入数据库,而后发动一个主线程异步操作:

  1. 查看会话列表中是否已有对应会话
  2. 若以后不存在对应会话,则增加新会话和新联系人,而后更新联系人信息
  3. 若以后存在对应会话,则更新会话列表页中,对应条目标未读状态,以及最新聊天信息
  4. 若以后存在对应会话,且以后为对应的会话页面,则更新聊天信息列表,而后显示新音讯标记

编辑 IMCenter.swift,增加代码如下:

    private class func addNewSession(contact: ContactInfo, message: RTMMessage?) {db.storeNewContact(contact: contact)
        downloadImage(contactInfo: contact)
        
        var contacts = [ContactInfo]()
        contacts.append(contact)
        
        DispatchQueue.main.async {IMCenter.appendContactsForContactView(contacts: contacts)
            
            var oldSessions = IMCenter.viewSharedInfo.sessions
            
            let sessionItem = SessionItem(contact: contact)
            if message != nil {sessionItem.lastMessage.message = extraChatMessage(rtmMessage: message!)
                sessionItem.lastMessage.mid = message!.messageId
                sessionItem.lastMessage.timestamp = message!.modifiedTime
                sessionItem.lastMessage.unread = true
            }
            oldSessions.append(sessionItem)
            
            IMCenter.viewSharedInfo.sessions = sortSessions(sessions: oldSessions)
            
            DispatchQueue.global(qos: .default).async {checkContactsUpdate(type: contact.kind, contacts: contacts)
            }
        }
    }

    private class func extraChatMessage(rtmMessage: RTMMessage) -> String {
        
        if rtmMessage.translatedInfo.targetText.isEmpty == false {return rtmMessage.translatedInfo.targetText}
        
        if rtmMessage.translatedInfo.sourceText.isEmpty == false {return rtmMessage.translatedInfo.sourceText}
        
        if rtmMessage.stringMessage.isEmpty == false {return rtmMessage.stringMessage}
        
        return ""
    }

    class func receiveNewNessage(type:Int, rtmMessage: RTMMessage) {_ = db.insertChatMessage(type: type, xid: rtmMessage.toId, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: extraChatMessage(rtmMessage: rtmMessage), mtime: rtmMessage.modifiedTime)
        
        DispatchQueue.main.async {
            let sessions = IMCenter.viewSharedInfo.sessions
            
            DispatchQueue.global(qos: .default).async {
                for session in sessions {let matchXid = (session.contact.kind == ContactKind.Friend.rawValue) ? rtmMessage.fromUid : rtmMessage.toId
                    if session.contact.kind == type && session.contact.xid == matchXid {
                        //-- 已有的 session
                        session.lastMessage.mid = rtmMessage.messageId
                        session.lastMessage.message = extraChatMessage(rtmMessage: rtmMessage)
                        session.lastMessage.timestamp = rtmMessage.modifiedTime
                        session.lastMessage.unread = true
                        
                        DispatchQueue.main.async {let sessions2 = sortSessions(sessions: sessions)
                            IMCenter.viewSharedInfo.sessions = sessions2
                            
                            if let currContact = IMCenter.viewSharedInfo.targetContact {
                                if currContact.kind == type && currContact.xid == matchXid {
                                    
                                    var dialogueMesssages = IMCenter.viewSharedInfo.dialogueMesssages
                                    let chatMsg = ChatMessage(sender: rtmMessage.fromUid, mid: rtmMessage.messageId, mtime: rtmMessage.modifiedTime, message: extraChatMessage(rtmMessage: rtmMessage))
                                    dialogueMesssages.append(chatMsg)
                                    
                                    dialogueMesssages = soreChatMessages(messages: dialogueMesssages)
                                    
                                    IMCenter.viewSharedInfo.dialogueMesssages = dialogueMesssages
                                    IMCenter.viewSharedInfo.newMessageReceived = true
                                }
                            }
                        }
                        return
                    }
                }
                
                //-- new Session
                var newContact: ContactInfo? = nil
                if type == ContactKind.Group.rawValue || type == ContactKind.Room.rawValue {newContact = ContactInfo(type: type, uniqueId: rtmMessage.toId, uniqueName: "", nickname:"")
                } else {newContact = ContactInfo(type: type, uniqueId: rtmMessage.fromUid, uniqueName: "", nickname:"")
                }
                
                addNewSession(contact: newContact!, message: rtmMessage)
            }
        }
    }

同样为了简化起见,零碎告诉咱们也对立解决。不过相对而言,零碎告诉要简略许多。

处理函数 receiveNewChatCmd() 先将收到的音讯保留入数据库,而后发动一个主线程异步操作:

  1. 查看会话列表中是否已有对应会话
  2. 若以后不存在对应会话,则不做任何解决
  3. 若以后存在对应会话,且以后为对应的会话页面,则更新聊天信息列表,退出零碎告诉,但不显示新音讯标记

编辑 IMCenter.swift,增加代码如下:

    class func receiveNewChatCmd(type:Int, rtmMessage: RTMMessage) {_ = db.insertChatCmd(type: type, xid: rtmMessage.toId, sender: rtmMessage.fromUid, mid: rtmMessage.messageId, message: rtmMessage.stringMessage, mtime: rtmMessage.modifiedTime)
        
        DispatchQueue.main.async {
            
            //-- 更新以后对话列表信息
            if let currContact = IMCenter.viewSharedInfo.targetContact {
                if currContact.kind == type && currContact.xid == rtmMessage.toId {
                    
                    var dialogueMesssages = IMCenter.viewSharedInfo.dialogueMesssages
                    let chatMsg = ChatMessage(sender: rtmMessage.fromUid, mid: rtmMessage.messageId, mtime: rtmMessage.modifiedTime, message: rtmMessage.stringMessage)
                    chatMsg.isChat = false
                    dialogueMesssages.append(chatMsg)
                    
                    dialogueMesssages = soreChatMessages(messages: dialogueMesssages)
                    
                    IMCenter.viewSharedInfo.dialogueMesssages = dialogueMesssages
                    return
                }
            }
        }
    }

最初,咱们要和 RTMClient 的事件告诉进行关联。关上 IMEventProcessor.swift,编辑代码如下:

@objcMembers public class IMEventProcessor: NSObject, RTMProtocol {
    
    ... ...

    public func rtmPushP2PChatMessage(_ client: RTMClient, message: RTMMessage?) {IMCenter.receiveNewNessage(type: ContactKind.Friend.rawValue, rtmMessage: message!)
    }
    
    public func rtmPushGroupChatMessage(_ client: RTMClient, message: RTMMessage?) {IMCenter.receiveNewNessage(type: ContactKind.Group.rawValue, rtmMessage: message!)
    }
    
    public func rtmPushRoomChatMessage(_ client: RTMClient, message: RTMMessage?) {IMCenter.receiveNewNessage(type: ContactKind.Room.rawValue, rtmMessage: message!)
    }
    
    public func rtmPushGroupChatCmd(_ client: RTMClient, message: RTMMessage?) {
        if let msg = message {IMCenter.receiveNewChatCmd(type: ContactKind.Group.rawValue, rtmMessage: msg)
        }
    }
    
    public func rtmPushRoomChatCmd(_ client: RTMClient, message: RTMMessage?) {
        if let msg = message {IMCenter.receiveNewChatCmd(type: ContactKind.Room.rawValue, rtmMessage: msg)
        }
    }
}

至此,RTMClient 相干事件关联实现,接管音讯也同时解决实现。

10. 用户和群组、房间信息的查问和批改

用户信息页面在体现和性能上与联系人信息页面高度雷同:不仅展示内容简直雷同,而且操作形式完全相同。但苦于根底数据起源模式不同,以及类似的数据性质不同,所以简略起见,咱们分成两个独立的视图进行制作。

10.1 用户信息页面

用户信息页须要显示用户的头像、惟一数字 ID、注册名称、昵称、签名,以及“退出登录”的按钮,而在编辑模式下,须要显示用户的头像、惟一数字 ID、注册名称、昵称、头像的网络门路、签名。其中,在编辑模式时,“退出登录”的按钮将被暗藏,减少头像网络门路的显示,且仅有昵称、头像的网络门路、签名能够被编辑批改。
当用户在编辑模式下提交批改时,咱们须要核查批改是否无效(不为空),而后向服务器提交。如果头像网络门路被批改,则还须要下载新的头像,并存储到本地,而后更新用户信息页面上的头像。
于是编辑 IMCenter.swift,减少处理函数 updateUserProfile():

    class func updateUserProfile(nickname: String, imgUrl: String, showInfo: String, completedAction: @escaping (_ path:String)->Void) {let info = OpenInfoData(nickname: nickname, imageUrl: imgUrl, showInfo: showInfo)
        let jsonEncoder = JSONEncoder()
        let jsonData = try? jsonEncoder.encode(info)
        let jsonStr = String(data: jsonData!, encoding: .utf8)
        
        DispatchQueue.global(qos: .default).async {
            
            //-- 临时疏忽错误处理
            IMCenter.client!.setUserInfoWithOpenInfo(jsonStr, privteinfo: nil, timeout: 0, success:{}, fail: {_ in})
            
            let contact = ContactInfo()
            contact.kind = ContactKind.Friend.rawValue
            contact.xid = IMCenter.client!.userId
            contact.xname = IMCenter.fetchUserProfile(key: "username")
            contact.imageUrl = imgUrl
            
            downloadImage(contactInfo: contact, completedAction: {(path, contactInfo) in
                
                let username = IMCenter.fetchUserProfile(key: "username")
                
                IMCenter.storeUserProfile(key: "\(username)-image-url", value: imgUrl)
                IMCenter.storeUserProfile(key: "\(username)-image", value: path)
                IMCenter.storeUserProfile(key: "nickname", value: nickname)
                IMCenter.storeUserProfile(key: "showInfo", value: showInfo)
                
                DispatchQueue.main.async {completedAction(path)
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            }, failedAction:{IMCenter.storeUserProfile(key: "nickname", value: nickname)
                IMCenter.storeUserProfile(key: "showInfo", value: showInfo)

                DispatchQueue.main.async {completedAction("")
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            })
        }
    }

而后编辑批改 ProfileView 的 body 局部为:

    var body: some View {
        ZStack {
        VStack {
            if self.editMode == false {
                TopNavigationView(title: "我的信息", icon: "button_edit", buttonAction: {self.editMode = true}).frame(width: UIScreen.main.bounds.width, height: CGFloat(IMDemoUIConfig.topNavigationHight), alignment: .center)
            } else {
                TopNavigationView(title: "批改我的信息", icon: "button_ok", buttonAction: {
                    
                    self.editMode = false
                    self.viewInfo.inProcessing = true
                    
                    if self.newNickname.isEmpty {self.newNickname = self.nickname}
                    
                    if self.newImageUrl.isEmpty {self.newImageUrl = self.userImageUrl}
                    
                    if self.newShowInfo.isEmpty {self.newShowInfo = self.showInfo}
                    
                    IMCenter.updateUserProfile(nickname: self.newNickname, imgUrl: self.newImageUrl, showInfo: self.newShowInfo, completedAction: updateCallback)
                    
                }).frame(width: UIScreen.main.bounds.width, height: CGFloat(IMDemoUIConfig.topNavigationHight), alignment: .center)
            }
            
            
            Divider()
            
            Spacer()
            
            VStack {Spacer()
                
                if self.userImagePath.isEmpty {Image(IMDemoUIConfig.defaultIcon)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                        .padding()} else {Image(uiImage: IMCenter.loadUIIMage(path: self.userImagePath))
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                        .padding()}
                
                LazyVGrid(columns:[GridItem(.fixed(UIScreen.main.bounds.width * 0.4)), GridItem()]) {
                    HStack {Spacer()
                        Text("用户 ID:")
                            .padding()}
                    HStack {Text(String(IMCenter.client!.userId))
                            .padding()
                        Spacer()}
                    
                    
                    HStack {Spacer()
                        Text("用户名:")
                            .padding()}
                    HStack {Text(self.username)
                            .padding()
                        Spacer()}
                    
                    HStack {Spacer()
                        Text("用户昵称:")
                            .padding()}
                    HStack {
                        if self.editMode == false {
                            if self.nickname.isEmpty {Text(self.username)
                                    .padding()} else {Text(self.nickname)
                                    .padding()}
                            
                        } else {TextField(self.nickname.isEmpty ? "给本人取个昵称" : self.nickname, text: $newNickname)
                                .autocapitalization(.none)
                                .frame(width: UIScreen.main.bounds.width/3,
                                height: nil)
                                .padding()}
                        
                        Spacer()}
                    
                    if self.editMode {
                        HStack {Spacer()
                            Text("头像地址:")
                                .padding()}
                        HStack {TextField(self.userImageUrl.isEmpty ? "更改头像地址" : self.userImageUrl, text: $newImageUrl)
                                .autocapitalization(.none)
                                .frame(width: UIScreen.main.bounds.width/3,
                                height: nil)
                                .padding()
                            
                            Spacer()}
                    }
                    
                    HStack {Spacer()
                        Text("用户签名:")
                            .padding()}
                    HStack {
                        if self.editMode == false {TextEditor(text: $showInfo).disabled(true)
                                    .padding()} else {TextEditor(text: $showInfo)
                                .frame(width: UIScreen.main.bounds.width/3,
                                height: 80)
                                .ignoresSafeArea(.keyboard)
                                .padding()
                                .overlay(RoundedRectangle(cornerRadius: 8)
                                        .stroke(Color.secondary).opacity(0.5))
                        }
                        
                        Spacer()}
                }
                
                if self.editMode == false {Button("退出登录") {DispatchQueue.global(qos: .default).async {IMCenter.client!.closeConnect()
                        }
                    }
                    .frame(width: UIScreen.main.bounds.width/4,
                    height: nil)
                    .padding(10)
                    .foregroundColor(.white)
                    .background(.blue)
                    .cornerRadius(10)
                }
                
                Spacer()}
            
            Spacer()
            
            Divider()
            
            BottomNavigationView().frame(width: UIScreen.main.bounds.width, height: CGFloat(IMDemoUIConfig.bottomNavigationHight), alignment: .center)
        }
        .onTapGesture {hideKeyboard()
        }
         
            if self.viewInfo.inProcessing {ProcessingView(info: "更新中,请期待……")
            }
        }
    }

到此,用户信息页面批改实现。

10.2 联系人信息页面

最初,是联系人信息页面。
本页面和用户信息页面比拟大的区别有两点:

  1. 用户信息页默认就能编辑,而联系人信息页,只有群组和房间的信息能力编辑,其余用户的信息不可被编辑
  2. 联系人信息页不含“退出登录”按钮

与用户信息页面相似,第二篇其实曾经将 UI 相干的性能筹备的差不多了,咱们在此只需补上 UI 之外的相干解决。X 相干解决也与 updateUserProfile() 相似。编辑 IMCenter.swift,减少性能函数 updateGroupOrRoomProfile() 及相干辅助函数:

    class func genGroupOrRoomProfileChangedNotifyMessage(type: Int) -> String {
        
        if type == ContactKind.Group.rawValue {return "\(getSelfDispalyName()) 批改了本群信息"
        } else if type == ContactKind.Room.rawValue {return "\(getSelfDispalyName()) 批改了本房间信息"
        } else {return "\(getSelfDispalyName()) 批改了信息"
        }
    }
    
    class func updateGroupOrRoomProfile(contact: ContactInfo, orgImageUrl: String, completedAction: @escaping (_ path:String)->Void) {let info = OpenInfoData(nickname: contact.nickname, imageUrl: contact.imageUrl, showInfo: contact.showInfo)
        let jsonEncoder = JSONEncoder()
        let jsonData = try? jsonEncoder.encode(info)
        let jsonStr = String(data: jsonData!, encoding: .utf8)
        
        DispatchQueue.global(qos: .default).async {
            
            //-- 临时疏忽错误处理
            if contact.kind == ContactKind.Group.rawValue {IMCenter.client!.setGroupInfoWithId(NSNumber(value: contact.xid), openInfo: jsonStr, privateInfo: nil, timeout: 0, success: {}, fail: { _ in})
            } else if contact.kind == ContactKind.Room.rawValue {IMCenter.client!.setRoomInfoWithId(NSNumber(value: contact.xid), openInfo: jsonStr, privateInfo: nil, timeout: 0, success: {}, fail: { _ in})
            } else {return}
        
            if contact.imageUrl == orgImageUrl {
                
                DispatchQueue.main.async {completedAction("")
                    IMCenter.viewSharedInfo.inProcessing = false
                }
                return
            }
            
            downloadImage(contactInfo: contact, completedAction: {(path, contactInfo) in
                
                contact.imagePath = path
                IMCenter.db.updatePublicInfo(contact: contact)
                sendCmd(contact: contact, message: genGroupOrRoomProfileChangedNotifyMessage(type: contact.kind))
                
                DispatchQueue.main.async {completedAction(path)
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            }, failedAction:{IMCenter.db.updatePublicInfo(contact: contact)
                sendCmd(contact: contact, message: genGroupOrRoomProfileChangedNotifyMessage(type: contact.kind))
                
                DispatchQueue.main.async {completedAction("")
                    IMCenter.viewSharedInfo.inProcessing = false
                }
            })
        }
    }

而后编辑批改 ContactInfoView.swift,批改 ContactInfoView 视图 ContactInfoHeaderView 组件的 editAction 为:

                editAction: {
                    inEditing in
                    
                    self.editMode = inEditing
                    
                    
                    if inEditing == false {
                        self.viewInfo.inProcessing = true
                        
                        let newContact = ContactInfo()
                        newContact.kind = contact.kind
                        newContact.xid = contact.xid
                        newContact.xname = contact.xname
                        newContact.nickname = self.newNickname.isEmpty ? contact.nickname : self.newNickname
                        newContact.showInfo = self.newShowInfo.isEmpty ? contact.showInfo : self.newShowInfo
                        
                        if self.newImageUrl.isEmpty {newContact.imageUrl = self.contact.imageUrl} else {newContact.imageUrl = self.newImageUrl}
    
                        IMCenter.updateGroupOrRoomProfile(contact: newContact, orgImageUrl:self.contact.imageUrl, completedAction: updateCallback)
                    }
                }

至此,整个 Demo 的 iOS 端局部,便全副开发实现。残缺代码可参见:https://github.com/highras/rt…

下篇,咱们将进入服务端局部的开发。

若有播种,能够留下你的赞和珍藏。

正文完
 0