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()
}
}
✅ 참고
'아이폰 개발 > Swift' 카테고리의 다른 글
swift -Realm 예제 1 - 단순 CRUD (0) | 2021.04.12 |
---|---|
swift alamofire 예제 (0) | 2021.04.05 |
swift fmdb 예제 1 - feat: tableview , typealias , 튜플, do catch (0) | 2021.03.30 |
swift sqlite 예제 feat : FileManager, Bundle.main, defer (0) | 2021.03.30 |
swift MVVM 패턴 예제 (0) | 2021.03.29 |