본문 바로가기
아이폰 개발/Swift

swift socket io 예제

by 인생여희 2021. 3. 31.

swift socket io 예제

 

유저리스트

 

채팅방

 

스토리보드

 

 

✅사용 기술

MainQue

CustomCell xib file

Dictionary

Socket IO

클로저

prepareForSegue

UITextViewDelegate

UIGestureRecognizerDelegate

timer

notificationCenter

UIView.animate

키보드 델리게이트 메소드

 

 소켓 매니저 클래스

 

SocketIOManager.swift

 

[1]소캣연결시도

[2]소캣연결종료

[3]유저채팅방에 연결

[4]유저 채팅방에서 삭제

[5]메시지발송

[6]유저 입장, 퇴장 타이핑 유무 등록

[7]notificationcenter

 

import UIKit
import SocketIO

class SocketIOManager: NSObject {

    static let sharedInstance = SocketIOManager()
    
    //서버에서 메시지를 주고받을 수 있게 해주는 Socket.IO의 기본 클래스
    var manager = SocketManager(socketURL: URL(string: "localhost...:3000")!, config: [.log(true) , .compress])
//
    var socket:SocketIOClient!
    
    //클라이언트 소캣 초기화
    override init() {
        super.init()
        
        socket = self.manager.socket(forNamespace: "/")
        
        print("소켓 초기화 완료")
        
    }
    
    //MARK: 소켓 연결 시도
    func establishConnection() {
        
        socket.connect()

        print("소켓 연결 시도")
    }

    //MARK: 소켓 연결 종료
    func closeConnection() {
        
        socket.disconnect()

        print("소켓 연결 종료")
    }

    
    //MARK: 유저 채팅방에 연결
    func connectToServerWithNickname(nickname:String ,
                         completeHandler:(
                            @escaping ([[String:AnyObject]]) -> Void
                            )
                        )
    {
 
        //서버에 유저 아이디 전송
        socket.emit("connectUser", nickname)
 
        //서버에서 송신한 데이터 받기
        socket.on("userList") { (dataArray, ack) in
            completeHandler(dataArray[0] as! [[String:AnyObject]])
        }
        
        //유저들 입장, 퇴장 듣기
        listenForOtherMessage()
    }
        
    //MARK: 유저 채팅방에서 삭제
    func exitChatWithNickname(nickname:String, completeHandler: ()-> Void) {
        socket.emit("exitUser", nickname)
        completeHandler()
    }
    
    //MARK: 메시지 발송
    func sendMessage(message:String , withNickname nickname: String) {
        socket.emit("chatMessage" , nickname, message)
    }
    
    func getChatMessage(completHandler : ( @escaping
                                    ([String: AnyObject]) -> Void
                                  )
                
                )
    {
        
        socket.on("newChatMessage") { (dataArray, ack) in
            var msgDictionary = [String:AnyObject]()
            msgDictionary["nickname"] = dataArray[0] as! String as AnyObject
            msgDictionary["message"] = dataArray[1] as! String as AnyObject
            msgDictionary["date"] = dataArray[2] as! String as AnyObject
            
            completHandler(msgDictionary)
        }
        
    }

    /*
     두 개의 새로운 메시지(“ userConnectUpdate ”, “ userExitUpdate ”)를 듣기 위한 새로운 메소드를 구현할 것이다.
     전자는 새로운 유저의 닉네임이 서버에 전달되고 나서 연결될 때 서버에 의해 보내지는 것이다.
     반면 두 번째는 유저가 앱을 종료할 때나 유저가 Exit 버튼을 눌러서 유저리스트에서 완전히 삭제될 때 보내진다.
     */
    //MARK: 유저 입장, 퇴장, 타이핑유무 등록
    private func listenForOtherMessage(){
        //입장 - 유저 전체 리턴받음
        socket.on("userConnectUpdate") { (dataArray, ack) in
            
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "userWasConnectedNotification"), object: dataArray[0] as! [String:AnyObject])
        }
        
        //퇴장 - 퇴장한 유저명 리턴받음
        socket.on("userExitUpdate") { (dataArray, ack) in
           
            NotificationCenter.default.post(name: NSNotification.Name("userWasDisconnectedNotification"), object: dataArray[0] as! String)
        }
        
        //타이핑 유무
        socket.on("userTypingUpdate") { (dataArray, ack) in
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "userTypingNotification"), object: dataArray[0] as? [String:AnyObject])
        }
        
        //ChatViewController.swift - viewDidLoad(_:)  -위 세 알림 관찰 메소드 작성 필요
    }
    
    //MARK:- 유저 타이핑 유무..
/*
     현재 메시지를 치고 있는 유저의 닉네임을 라벨에 보여주는 것이고 아무도 아무것도 치고 있지 않으면 이를 숨기는 기능이다.
     이를 가능하게 하기 위해서 우리는 한 유저가 타이핑을 시작하거나 멈출때 서버에 알릴 것이다.
     그리고 그 결과로 우리는 타이핑하고 있는 모든 유저의 딕셔너리를 받게 된다.
*/
    func sendStartTypingMessage(nickName: String){
        socket.emit("startType", nickName)
    }
    
    func sendStopTypingMessage(nickName:String){
        socket.emit("stopType", nickName)
    }
}

 

 

 유저리스트 테이블 뷰 클래스

 

ViewController.swift

 

[1]viewWillAppear -화면 UI 설정

[2]viewDidAppear - 닉네임 팝업 설정

[3]테이블뷰 설정

[4]채팅방 나가기

[5]Navigation - segue

 

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    //테이블 뷰
    @IBOutlet weak var tblUserList: UITableView!
   
    //딕셔너리가 담긴 배열
    var users = [[String : AnyObject]]()
    
    //유저 닉네임
    var nickname: String!
    
    //UI 설정 상태
    var configurationOK = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    //MARK: viewWillAppear -화면 UI 설정
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        if !configurationOK{
            configureNavigationBar()
            configureTableView()
            configurationOK = true
        }
    }
    
    //MARK: viewDidAppear - 닉네임 팝업 설정
    override func viewDidAppear(_ animated: Bool) {
        
        super.viewDidAppear(animated)
        
        if nickname == nil {
            askForNickname()
        }
    }
    
    
    //MARK: CUSTOM METHODE
    func configureNavigationBar(){
        navigationItem.title = "socketChat"
    }
    func configureTableView() {
        self.tblUserList.delegate = self
        self.tblUserList.dataSource = self
        self.tblUserList.register(UINib(nibName: "UserCell", bundle: nil), forCellReuseIdentifier: "idCellUser")
        self.tblUserList.isHidden = true
        tblUserList.tableFooterView = UIView(frame: .zero)
    }
    
    //앱이 켜졌을 때 유저가 nickname을 타입해넣을 수 있는 textfield와 함께 alert constroller를 보여주는 메소드
    func askForNickname() {
        
        let alertController = UIAlertController(title: "SocketChat", message: "닉네임을 입력하세요:", preferredStyle: .alert)

        alertController.addTextField { (tf) in
            tf.placeholder = "nicName"
        }
        
        let OKAction = UIAlertAction(title: "OK", style: .default) { (action) in
            
            let textField = alertController.textFields![0]
            if textField.text?.count == 0 {
                //팝업창에 아무값을 입력안했으면 다시 호출
                self.askForNickname()
            }else{
                
                //유저 닉네임
                self.nickname = textField.text
                
                //소켓 연결 + 유저 닉네임
                SocketIOManager.sharedInstance.connectToServerWithNickname(nickname: self.nickname) { (userList) in
                    
                    //테이블뷰 DATA SOURCE 갱신
                    DispatchQueue.main.async {
                        if userList.count != 0{
                            self.users = userList
                            self.tblUserList.reloadData()
                            self.tblUserList.isHidden = false
                        }
                    }
                    

                }
                
            }

        }
        
        alertController.addAction(OKAction)
        present(alertController, animated: true, completion: nil)
    }
    
    //MARK:- 채팅방 나가기
    @IBAction func exitChat(_ sender: Any) {
        
        SocketIOManager.sharedInstance.exitChatWithNickname(nickname: nickname) {
            
            DispatchQueue.main.async {
                self.nickname = nil
                self.users.removeAll()
                self.tblUserList.isHidden = true
                self.askForNickname()
            }
            
        }
        
    }
    
    //MARK:- Navigation - segue
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        
        if let identifire = segue.identifier {
            if identifire == "idSegueJoinChat" {
                let chatViewController = segue.destination as! ChatViewController
                //채팅뷰 컨트롤러에 닉네임 할당
                chatViewController.nickname = self.nickname
            }
        }
        
    }
    
    
    //MARK:- TableView Delegate Method
    
    //MARK:- cell 개수
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    

    
    //MARK:- cell 구성 - 유저이름, 연결상태
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "idCellUser", for: indexPath) as! UserCell
    
        
        cell.textLabel?.text = users[indexPath.row]["nickname"] as? String
        cell.detailTextLabel?.text = (users[indexPath.row]["isConnected"] as! Bool) ? "online" : "offline"
        cell.detailTextLabel?.textColor = (users[indexPath.row]["isConnected"] as! Bool) ? .green : .red
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 44.0
    }
    
    

}

 

 

 채팅방 테이블 뷰 클래스

 

ChatViewController.swift

 

[1]NotificationCenter예제

[2]키보드 관련 메소드

[3]timer

[4]UIView animation

 

import UIKit

class ChatViewController: UIViewController , UITableViewDelegate, UITableViewDataSource , UITextViewDelegate , UIGestureRecognizerDelegate{

    
    //상태 메시지
    @IBOutlet weak var lblOtherUserActivityStatus: UILabel!
    
    //텍스트 뷰
    @IBOutlet weak var tvMessageEditor: UITextView!
    
    //테이블 뷰
    @IBOutlet weak var tblChat: UITableView!
    
    //뉴스 배너
    @IBOutlet weak var lblNewsBanner: UILabel!
    
    //닉네임
    var nickname : String!
    
    //채팅 메시지 데이터
    var chatMessage = [[String: AnyObject]]()
    
    //@IBOutlet weak var conBottomEditor: NSLayoutConstraint!
    
    var bannerLabelTimer : Timer!
    
    // MARK: - viewDidLoad
    override func viewDidLoad() {
        super.viewDidLoad()

        //유저가 들어왔을때
        NotificationCenter.default.addObserver(self, selector: #selector(handleConnectedUserUpdateNotification(notification:)),
            name: NSNotification.Name(rawValue: "userWasConnectedNotification"),
            object: nil)
        
        //유저 퇴장했을때
        NotificationCenter.default.addObserver(self, selector: #selector(handleDisconnectedUserUpdateNotification(notification:)),
            name: NSNotification.Name(rawValue: "userWasDisconnectedNotification"),
            object: nil)
        
        //유저가 타이핑 할때
        NotificationCenter.default.addObserver(self, selector: #selector(handleUserTypingNotification(notification:)),
            name: NSNotification.Name(rawValue: "userTypingNotification"),
            object: nil)
        
        
    //키보드 보임 관찰자
    NotificationCenter.default.addObserver(self,
                             selector: #selector(handleKeyboardDidShowNotification(notification:)),
                            name: UIResponder.keyboardDidShowNotification,
                            object: nil)

    //키보드 숨김 관찰자
    NotificationCenter.default.addObserver(self,
                            selector:
                            #selector(handleKeyboardDidHideNotification(notification:)),
                            name: UIResponder.keyboardDidHideNotification,
                            object: nil)

        //제스쳐 이벤트
        let swipGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        swipGestureRecognizer.direction = .down
        swipGestureRecognizer.delegate = self
        view.addGestureRecognizer(swipGestureRecognizer)
    }
    
    
    
    
    // MARK: - viewWillAppear - uisetting
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        configTableView()
        configureBannerLabel()
        configureOtherUserActivityLabel()
        
        tvMessageEditor.delegate = self
    }
    
    // MARK:viewDidAppear - getMessgae
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        //새로운 메시지를 받기 위한 로직
        SocketIOManager.sharedInstance.getChatMessage { (messageInfo) in
            
            DispatchQueue.main.async {
                self.chatMessage.append(messageInfo)
                self.tblChat.reloadData()
                //self.scrollToBottom()
            }
            
        }
        
    }
    
    
    // MARK: - Custom tblChat - Method
    func configTableView(){
        
        //테이블 뷰 델리게이트 설정
        tblChat.delegate = self
        tblChat.dataSource = self
        
        //테이블 셀 등록
        tblChat.register(UINib(nibName: "ChatCell", bundle: nil), forCellReuseIdentifier: "idCellChat")
        tblChat.estimatedRowHeight = 90.0            //예상되는 높이 값
        tblChat.rowHeight = UITableView.automaticDimension //각 행 높이 다르게
        tblChat.tableFooterView = UIView(frame: .zero)
        
        //https://m.blog.naver.com/PostView.nhn?blogId=jdub7138&logNo=220963701224&proxyReferer=https:%2F%2Fwww.google.com%2F
    }
    
    //배너 둥글게 + 알파값 설정 0
    func configureBannerLabel(){
        lblNewsBanner.layer.cornerRadius = 15.0
        lblNewsBanner.clipsToBounds = true
        lblNewsBanner.alpha = 0.0
    }
    
    //다른유저 상태 메시지
    func configureOtherUserActivityLabel() {
        lblOtherUserActivityStatus.isHidden = true
        lblOtherUserActivityStatus.text = ""
    }
    
    
    // MARK: - TableView delegate Method
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return chatMessage.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
        let cell = tableView.dequeueReusableCell(withIdentifier: "idCellChat", for: indexPath) as! ChatCell
        
        let curChatMsg = chatMessage[indexPath.row]
        let senderNickName = curChatMsg["nickname"] as! String
        let msg = curChatMsg["message"] as! String
        let date = curChatMsg["date"] as! String
        
        //내가 보낸 메시지 - 우측 정렬
        if senderNickName == nickname {
            cell.lblChatMessage.textAlignment = .right
            cell.lblMessageDetails.textAlignment = .right
            cell.lblChatMessage.textColor = lblNewsBanner.backgroundColor
        }
        
        
        cell.lblChatMessage.text = msg
        cell.lblMessageDetails.text = "by \(senderNickName) -  \(date)"
        cell.lblChatMessage.textColor = .darkGray
        
        return cell
    }
    
    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
    }
    */
    
    //MARK: - sendMessage
    @IBAction func sendMessage(_ sender: Any) {
        
        if tvMessageEditor.text.count > 0 {
            //메시지 서버에 발송
            SocketIOManager.sharedInstance.sendMessage(message: self.tvMessageEditor.text!, withNickname: nickname)
            
            tvMessageEditor.text = ""
            tvMessageEditor.resignFirstResponder()
        }
        
    }
    
    
    //MARK: - NOTI HANDLER METHOD
    //유저 타이핑 유무
    @objc func handleUserTypingNotification(notification: NSNotification){
        if let typingUserDictionary = notification.object as? [String:AnyObject] {
            
            var names = ""
            var totalTypingUser = 0
            
            for (typingUser,_) in typingUserDictionary {
                
                if typingUser != nickname {
                    names = (names == "") ? typingUser : "\(names) ,  \(typingUser)"
                    totalTypingUser += 1
                }
            }
            
            if totalTypingUser > 0 {
                let verb = (totalTypingUser == 1) ? "is" : "are"
                lblOtherUserActivityStatus.text = "\(names) \(verb) now typing a msg.."
                lblOtherUserActivityStatus.isHidden = false
            }
            else{
                lblOtherUserActivityStatus.isHidden = true
            }
            
        }
    }
    
    //유저 입장
    @objc func handleConnectedUserUpdateNotification(notification: NSNotification){
        
        let connectedUserInfo = notification.object as! [String:AnyObject]
        let connectedUserNickname = connectedUserInfo["nickname"] as? String
        lblNewsBanner.text = "User \(connectedUserNickname!) was connted"
        
        //배너 호출
        showBannerLabelAnimated()
    }
    
    //유저 퇴장
    @objc func handleDisconnectedUserUpdateNotification(notification: NSNotification){
        let disconnectedUserNickname = notification.object as! String
        lblNewsBanner.text = "User \(disconnectedUserNickname) has left"
        
        //배너 호출
        showBannerLabelAnimated()
    }
    
    //MARK:- 배너 애니메이션
    func showBannerLabelAnimated(){
        UIView.animate(withDuration: 0.75) {
            self.lblNewsBanner.alpha = 1.0
            
        } completion: { [self] (_) in
            
            //2초뒤 배너 숨기기
            self.bannerLabelTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(hideBannerLabel), userInfo: nil, repeats: false)
            
        }

    }
    
    //배너 숨기기
   @objc func hideBannerLabel(){
        
        //타이머 종료
        if bannerLabelTimer != nil {
            bannerLabelTimer.invalidate()
            bannerLabelTimer = nil
        }
        
        UIView.animate(withDuration: 0.75) {
            
            self.lblNewsBanner.alpha = 0.0
            
        } completion: { (_) in
            
        }
    }
    
    //MARK:- 키보드 Delegate
    
    //키보드 보임
    @objc func handleKeyboardDidShowNotification(notification: NSNotification) {
        if let userInfo = notification.userInfo{
            
            if let keyboardFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue{
                
                //conBottomEditor.constant = keyboardFrame.size.height
                //view.layoutIfNeeded()
            }
            
        }
    }
    
    //숨김
    @objc func handleKeyboardDidHideNotification(notification: NSNotification) {
        //conBottomEditor.constant = 5
        //view.layoutIfNeeded()
    }
    
    //키보드 내림 제스쳐 이벤트
   @objc func dismissKeyboard() {
        if tvMessageEditor.isFirstResponder{
            tvMessageEditor.resignFirstResponder()
            SocketIOManager.sharedInstance.sendStopTypingMessage(nickName: nickname)
        }
    }
    
    
    // MARK: UITextViewDelegate Methods
    
    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        SocketIOManager.sharedInstance.sendStartTypingMessage(nickName: nickname)
        
        return true
    }
    
    // MARK: UIGestureRecognizerDelegate Methods
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

 

 

 CELL - BaseCell.swift

class BaseCell: UITableViewCell {

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        
        separatorInset = .zero
        preservesSuperviewLayoutMargins = false
        layoutMargins = .zero
        layoutIfNeeded()
        
        selectionStyle = .none
    }

    
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

 

 

 UserCell.swift

import UIKit

class UserCell: BaseCell {

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
}

 

 ChatCell.swift

import UIKit

class ChatCell: BaseCell {

    //메시지
    @IBOutlet weak var lblChatMessage: UILabel!
    
    //이름 + 날짜
    @IBOutlet weak var lblMessageDetails: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
}

 

 

 AppDelegate.swift

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    //앱이 active될 때마다 서버와 연결할 것이고, 앱이 background로 들어갈때 연결을 끊을 것이다
    //앱이 포그라운드로 전환
    func applicationDidBecomeActive(_ application: UIApplication) {
        SocketIOManager.sharedInstance.establishConnection()
    }
    
    //앱이 백그라운드로 전환
    func applicationDidEnterBackground(_ application: UIApplication) {
        SocketIOManager.sharedInstance.closeConnection()
    }

}

 

socketios.zip
0.22MB

 

 

✅ 참고

https://nsios.tistory.com/28