preface

Clubhouse is a new social networking application that provides real-time audio chat and interaction, creating the possibility for users to break down the barriers of information dissemination and interpersonal connection caused by social barriers. Clubhouse, often nicknamed “Silicon Valley’s Hottest start-up,” markets itself as an “exclusive” and “alternative” social network that attracts a variety of celebrities and people who just want to talk to each other.

App Store download address

Github open source download address

The development environment
  • Development tools: Xcode12 real machine run
  • Development language: Swift
  • The SDK: ARtcKit_iOS

Results show

The core framework

platform :ios, '9.0'
use_frameworks!

target 'anyHouse-iOS' do#anyRTC pod 'ARtcKit_iOS', '~ > 4.1.4.1'#anyRTC Real-Time messaging pod'ARtmKit_iOS', '~ > 1.0.1.4'
end
Copy the code

Project file directory structure

Functional Directory:

Main:

①ARMainViewController: home page, room list;

②ARMineViewController: mine, including change nickname, privacy protocol, version information, etc.

(3) ARCreateRoomViewController: create room, including creating public/private rooms, add topics.

Audio:

①ARAudioViewController: voice room, including voice chat, up and down mic and other functions;

②ARMicViewController: request list;

ARReportViewController: report function.

Detailed description of some functional modules of the project

Login, my, home page

  • Home page
    override func viewDidLoad(a) {
        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(a) {
        placeholderView.showPlaceholderView(self.tableView, placeholderImageName: "icon_add", placeholderTitle: "Try a drop-down refresh or create a room.") {
            self.tableView.mj_header?.beginRefreshing()
        }
        placeholderView.backgroundColor = UIColor.clear
    }
Copy the code
Create rooms and add topics

  • Create room logic:
    @IBAction func didClickTopicButton(_ sender: Any) {
        passwordTextField.resignFirstResponder()
        let alertVc = ARAlertTextViewController(title: "Add topic\n ", message: "Like funny things that happen around you.", preferredStyle: .alert)
        alertVc.updateTextView(text: topic)
        let cancelAction =  UIAlertAction (title:  "Cancel" , style: .cancel , handler:  nil )
        let okAction =  UIAlertAction (title:  "Set the topic" , 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(a) {
        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 {
                / / public
                passwordView.isHidden = true
                padding.constant = 0
                publicButton.backgroundColor = UIColor(hexString: "#DFE2EE")
                passwordButton.backgroundColor = UIColor.white
            } else {
                / / private
                passwordView.isHidden = false
                padding.constant = 47
                passwordButton.backgroundColor = UIColor(hexString: "#DFE2EE")
                publicButton.backgroundColor = UIColor.white
            }
        }
    }
Copy the code
  • Rewrite UIAlertController implementation to add topic:
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(a)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 characters left to enter."
        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: "Left to enter %d characters".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: "Left to enter %d characters".60 - textView.text.count)
    }
}
Copy the code
Voice room, interactive connection

  • Core code:
    func initializeEngine(a) {
        // init ARtcEngineKit
        rtcKit = ARtcEngineKit.sharedEngine(withAppId: UserDefaults.string(forKey: .appId)!, delegate: self)
        rtcKit.setAudioProfile(.musicHighQuality, scenario: .gameStreaming)
        
        // Enable audio AI noise reduction
        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}}})Copy the code
  • Audio test
	Callbacks that indicate who is talking in the channel, the volume of the speaker, and whether the local user is talking
    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
                    }
                }
            }
        }
    }
Copy the code
  • Fluctuation of wheat
    private func becomBroadcaster(role: ARClientRole) {
    	// Switch roles
        rtcKit.setClientRole(role)
        if role = = .audience {
            / / the wheat
            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("You have become an audience.", state: .color(UIColor(hexString: "#4BAB63")), duration: 1)}else {
            / / on wheat
            audioButton.isHidden = false
            micButton.isHidden = true}}Copy the code
Protocol, shielding, reporting functions

  • In order to cope with the Apple audit mechanism, the protocol, shielding, reporting and other functional modules are added.

RTM signaling

Json: key =action value Int

For example: {“action”:1} toID: send object

Key Value instructions http
action



userName



avatar
1



userName(String)



1(Int)
Show of hands (toID as moderator) updateUserStatus



status =1
action 2 Invite the audience to the stage (toID is the audience) updateUserStatus



status =-1
action 3



userName(String)
Audience refused invitation (toID as host) updateUserStatus



status = 0
action



userName

4 Agree to invite (toID as host) updateUserStatus



status =2
action 5 The moderator turns off the speaker’s microphone (toID for the listener)
action 6 The moderator sets the speaker as the listener (down) (toID is the listener) updateUserStatus



status =0
action 7 Cancel the show of hands (toID as moderator) updateUserStatus



status = 0
action 8 Moderator leaves normally (Sending channel messages) leaveRoom
action



userName



avatar
9



userName(String)



1(Int)
Join RTM channel to send personal information (Sending channel messages)

Join channel Sends channel messages for others to display

{“avatar”:1,userName:”lili”}

conclusion

This project has not completely restored the ClubHouse, and there are still some bugs and function points to be improved. For your information only, welcome to fork. You are welcome to point out issues if you have any shortcomings.

Finally, post the Github open source download address. If you feel good, I hope to click star~