写点什么

anyHouse-iOS 高仿 ClubHouse

发布于: 2021 年 04 月 20 日

前言

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            }        }    }
复制代码


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 实现添加话题:

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)}
复制代码


  • 重写 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:发送对象

加入频道发送频道消息,用于其他人显示

{"avatar":1,userName:"lili"}

结束语

本项目并没有完全复原 ClubHouse,项目中还存在一些 bug 和待完善的功能点。仅供参考,欢迎大家 fork。有不足之处欢迎大家指出issues

最后再贴一下 Github开源下载地址 。如果觉得不错,希望点个 star~

用户头像

实时交互,万物互联! 2020.08.10 加入

实时交互,万物互联,全球实时互动云服务商领跑者!

评论

发布
暂无评论
anyHouse-iOS 高仿ClubHouse