千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(三)(下篇)
本文篇幅较长,预计阅读时长1-2h。
这是《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成》的第三篇-下篇:《APP 内部流程与逻辑》
系列文章可参考:
《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(一)》:Demo演示与IM设计
《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(二)》:UI设计与搭建
《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(三)》:APP 内部流程与逻辑(上)
《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成(四)》:服务端搭建与总结
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 添加好友
添加好友的基本思路是:
- 如果查询本地联系人信息,已经是好友了,则直接打开会话页面;
- 如果本地将对方作为陌生人记录,则将陌生人标记修改为好友标记,并添加会话条目到会话列表页,然后打开会话页面;
- 否则查询业务服务器,确认对方是合法注册用户,然后保存联系人信息,并添加会话条目到会话列表页,打开会话页面。
编辑 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 创建群组
创建群组的基本思路:
- 查询本地联系人信息,如果已经是群组成员,则直接打开会话页面;
- 否则查询业务服务器,确认目标是合法群组,然后保存群组联系人信息,并添加会话条目到会话列表页,打开会话页面。
编辑 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 加入群组
加入群组的基本思路是:
- 如果查询本地联系人信息,如果已经加入群组了,则直接打开会话页面;
- 否则查询业务服务器,确认目标是合法群组,然后保存群组联系人信息,并添加会话条目到会话列表页,打开会话页面。
编辑 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 创建房间
创建房间的基本思路是:
- 查询本地联系人信息,如果本地保存有对应的房间联系人信息,则直接打开会话页面;
- 否则查询业务服务器,确认对方是合法房间,然后保存房间联系人信息,并添加会话条目到会话列表页,打开会话页面。
编辑 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 加入房间
加入房间的基本思路是:
- 查询本地联系人信息,如果本地保存有对应的房间联系人信息,则直接打开会话页面;
- 否则查询业务服务器,确认对方是合法房间,然后保存房间联系人信息,并添加会话条目到会话列表页,打开会话页面。
编辑 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() 先将收到的消息保存入数据库,然后发起一个主线程异步操作:
- 检查会话列表中是否已有对应会话
- 若当前不存在对应会话,则添加新会话和新联系人,然后更新联系人信息
- 若当前存在对应会话,则更新会话列表页中,对应条目的未读状态,以及最新聊天信息
- 若当前存在对应会话,且当前为对应的会话页面,则更新聊天信息列表,然后显示新消息标记
编辑 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() 先将收到的消息保存入数据库,然后发起一个主线程异步操作:
- 检查会话列表中是否已有对应会话
- 若当前不存在对应会话,则不做任何处理
- 若当前存在对应会话,且当前为对应的会话页面,则更新聊天信息列表,加入系统通知,但不显示新消息标记
编辑 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 联系人信息页面
最后,是联系人信息页面。
本页面和用户信息页面比较大的区别有两点:
- 用户信息页默认就能编辑,而联系人信息页,只有群组和房间的信息才能编辑,其他用户的信息不可被编辑
- 联系人信息页不含“退出登录”按钮
与用户信息页面类似,第二篇其实已经将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/rtm-teaching-demo/tree/main/rtm-imdemo/iOS/IMDemo
下篇,我们将进入服务端部分的开发。
若有收获,可以留下你的赞和收藏。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本