前言
Clubhouse是一個新的社交網路應用程式,提供了實時音頻聊天互動方式,給用戶創造了打破由社會圈層壁壘所導致的資訊傳播和人際鏈接壁壘的可能性,Clubhouse通常被昵稱為“硅谷最熱門的初創企業”,將自己定位為一個“獨家”和“另類”社交網路,吸引了各種名人和只想互相交談的人,
App Store 下載地址
Github開源下載地址
開發環境
- 開發工具:Xcode12 真機運行
- 開發語言:Swift
- SDK:ARtcKit_iOS
效果展示

核心框架
platform :ios, '9.0'
use_frameworks!
target 'anyHouse-iOS' do
#anyRTC 音視頻庫
pod 'ARtcKit_iOS', '~> 4.1.4.1'
#anyRTC 實時訊息庫
pod 'ARtmKit_iOS', '~> 1.0.1.4'
end
專案檔案目錄結構

功能目錄:
Main:
①ARMainViewController:主頁面,房間串列;
②ARMineViewController:我的,包含修改昵稱、隱私協議、版本資訊等等;
③ARCreateRoomViewController:創建房間,包含創建公開/私密房間、添加話題,
Audio:
①ARAudioViewController:語音房間,包含語音聊天、上下麥等功能;
②ARMicViewController:請求連麥串列;
③ARReportViewController:舉報功能,
專案部分功能模塊詳解
登錄、我的、首頁

- 首頁
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let avatar = Int(UserDefaults.string(forKey: .avatar) ?? "1")! - 1
avatarButton.setImage(UIImage(named: headListArr![avatar] as! String), for: .normal)
let arr = UserDefaults.standard.array(forKey: blacklistIdentifier)
arr?.count ?? 0 > 0 ? (blackList.addObjects(from: arr!)) : nil
tableView.tableFooterView = UIView()
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 120
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
[weak self] () -> Void in
guard let weakself = self else {return}
weakself.index = 1
weakself.requestRoomList()
})
}
@objc func createPlaceholder() {
placeholderView.showPlaceholderView(self.tableView, placeholderImageName: "icon_add", placeholderTitle: "可以嘗試下拉重繪或者創建房間") {
self.tableView.mj_header?.beginRefreshing()
}
placeholderView.backgroundColor = UIColor.clear
}
創建房間、添加話題

- 創建房間邏輯:
@IBAction func didClickTopicButton(_ sender: Any) {
passwordTextField.resignFirstResponder()
let alertVc = ARAlertTextViewController(title: "添加話題 \n ", message: "比如發生在身邊的趣事", preferredStyle: .alert)
alertVc.updateTextView(text: topic)
let cancelAction = UIAlertAction (title: "取消" , style: .cancel , handler: nil )
let okAction = UIAlertAction (title: "設定話題" , style: . default , handler: {
action in
if !self.stringAllIsEmpty(string: alertVc.textView.text) {
self.topic = alertVc.textView.text ?? ""
self.updateTopic()
}
})
alertVc.addAction(cancelAction)
alertVc.addAction(okAction)
present(alertVc, animated: true, completion: nil)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
alertVc.textView.becomeFirstResponder()
}
}
func updateTopic() {
if isPrivate == 0 {
(topic.count == 0) ? (topicLabel.text = publicText) : (topicLabel.text = String(format: "%@:“%@”", publicText,topic))
} else {
(topic.count == 0) ? (topicLabel.text = passwordText) : (topicLabel.text = String(format: "%@:“%@”", passwordText,topic))
}
}
@IBAction func didClickButton(_ sender: UIButton) {
if sender.tag != isPrivate {
isPrivate = sender.tag
passwordTextField.resignFirstResponder()
updateTopic()
if isPrivate == 0 {
//公開
passwordView.isHidden = true
padding.constant = 0
publicButton.backgroundColor = UIColor(hexString: "#DFE2EE")
passwordButton.backgroundColor = UIColor.white
} else {
//私密
passwordView.isHidden = false
padding.constant = 47
passwordButton.backgroundColor = UIColor(hexString: "#DFE2EE")
publicButton.backgroundColor = UIColor.white
}
}
}
- 重寫UIAlertController實作添加話題:
class ARAlertTextViewController : UIAlertController, UITextViewDelegate {
public var textView : UITextView!
private var tipLabel: UILabel!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let contentView = UIView()
let controller = UIViewController()
controller.view = contentView
textView = UITextView()
textView.delegate = self
textView.layer.masksToBounds = true
textView.layer.cornerRadius = 5
contentView.addSubview(textView)
textView.snp.makeConstraints({ (make) in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 15, bottom: 16, right: 15))
})
tipLabel = UILabel.init()
tipLabel.text = "還剩輸入60個字符"
tipLabel.textColor = UIColor(hexString: "#999999")
tipLabel.font = UIFont.init(name: "PingFang SC", size: 12)
tipLabel.textAlignment = .center
textView.addSubview(tipLabel)
tipLabel.snp.makeConstraints({ (make) in
make.bottom.equalTo(textView.snp_bottom).offset(80)
make.centerX.equalToSuperview()
make.width.equalTo(100)
make.height.equalTo(15)
})
//super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.setValue(controller, forKey: "contentViewController")
}
func updateTextView(text: String!) {
textView.text = text
tipLabel.text = String(format: "還剩輸入%d個字符", 60 - text.count)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textViewDidChange(_ textView: UITextView) {
if textView.text?.count ?? 0 > 60 {
textView.text = String(textView.text.prefix(60))
}
tipLabel.text = String(format: "還剩輸入%d個字符", 60 - textView.text.count)
}
}
語音房間、互動連麥

- 核心代碼:
func initializeEngine() {
// init ARtcEngineKit
rtcKit = ARtcEngineKit.sharedEngine(withAppId: UserDefaults.string(forKey: .appId)!, delegate: self)
rtcKit.setAudioProfile(.musicHighQuality, scenario: .gameStreaming)
//開啟音頻AI降噪
let dic1: NSDictionary = ["Cmd": "SetAudioAiNoise", "Enable": 1]
rtcKit.setParameters(getJSONStringFromDictionary(dictionary: dic1))
rtcKit.setChannelProfile(.liveBroadcasting)
if infoModel!.isBroadcaster {
rtcKit.setClientRole(.broadcaster)
}
rtcKit.enableAudioVolumeIndication(500, smooth: 3, report_vad: true)
//init ARtmKit
rtmEngine = ARtmKit.init(appId: UserDefaults.string(forKey: .appId)!, delegate: self)
rtmEngine.login(byToken: infoModel?.rtmToken, user: UserDefaults.string(forKey: .uid) ?? "0") { [weak self](errorCode) in
self?.rtmChannel = self?.rtmEngine.createChannel(withId: (self?.infoModel?.roomId)!, delegate: self)
self?.rtmChannel?.join(completion: { (errorCode) in
})
}
}
- 音頻檢測
//提示頻道內誰正在說話、說話者音量及本地用戶是否在說話的回呼
func rtcEngine(_ engine: ARtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [ARtcAudioVolumeInfo], totalVolume: Int) {
for speakInfo in speakers {
if speakInfo.volume > 3 {
for index in 0..<modelArr[0].count {
let micModel = modelArr[0][index]
if speakInfo.uid == micModel.uid || (speakInfo.uid == "0" && micModel.uid == UserDefaults.string(forKey: .uid)){
let indexPath: NSIndexPath = NSIndexPath(row: index, section: 0)
let cell: ARAudioViewCell? = collectionView.cellForItem(at: indexPath as IndexPath) as? ARAudioViewCell
cell?.startAnimation()
break
}
}
}
}
}
- 上下麥
private func becomBroadcaster(role: ARClientRole) {
//切換角色
rtcKit.setClientRole(role)
if role == .audience {
//下麥
audioButton.isHidden = true
audioButton.isSelected = false
micButton.isHidden = false
micButton.isSelected = false
rtcKit.enableLocalAudio(true)
for index in 0..<modelArr[0].count {
let micModel = modelArr[0][index]
if micModel.uid == UserDefaults.string(forKey: .uid) {
modelArr[0].remove(at: index)
modelArr[1].append(micModel)
collectionView.reloadData()
break
}
}
Drop.down("您已成為聽眾", state: .color(UIColor(hexString: "#4BAB63")), duration: 1)
} else {
//上麥
audioButton.isHidden = false
micButton.isHidden = true
}
}
協議、屏蔽、舉報功能

- 為應對蘋果審核機制,故而添加協議、屏蔽、舉報等功能模塊,
RTM相關信令
json:key =action value Int
例如: {“action”:1} toID:發送物件
| Key | Value | 說明 | http |
|---|---|---|---|
| action userName avatar | 1 userName(String) 1(Int) | 舉手 (toID為主持人) | updateUserStatus status =1 |
| action | 2 | 邀請聽眾上臺 (toID為該聽眾) | updateUserStatus status =-1 |
| action | 3 userName(String) | 聽眾拒絕邀請(toID為主持人) | updateUserStatus status = 0 |
| action userName | 4 | 同意邀請(toID為主持人) | updateUserStatus status =2 |
| action | 5 | 主持人關閉該發言者的麥克風(toID為該聽眾) | |
| action | 6 | 主持人將該發言者設定為聽眾(下臺)(toID為該聽眾) | updateUserStatus status =0 |
| action | 7 | 取消舉手(toID為主持人) | updateUserStatus status = 0 |
| action | 8 | 主持人正常離開(發送頻道訊息) | leaveRoom |
| action userName avatar | 9 userName(String) 1(Int) | 加入rtm頻道發送個人資訊(發送頻道訊息) |
加入頻道發送頻道訊息,用于其他人顯示
{“avatar”:1,userName:“lili”}
結束語
本專案并沒有完全復原ClubHouse,專案中還存在一些bug和待完善的功能點,僅供參考,歡迎大家fork,有不足之處歡迎大家指出issues,
最后再貼一下 Github開源下載地址 ,如果覺得不錯,希望點個star~
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/278497.html
標籤:其他
上一篇:Appium-Get Performance Data(獲取性能資料)
下一篇:UI組件
