Help us understand the problem. What is going on with this article?

Swiftでソケット通信するチャットアプリ

More than 5 years have passed since last update.

move

Swiftでソケット通信するチャットアプリを作ってみた。
iOSにはSIOSocketを、バックエンドにはSocket.ioを使用。

Github

https://github.com/ytakzk/swift-socket-example

環境

iOS

Swift
SIOSocket https://github.com/MegaBits/SIOSocket

バックエンド

Node.js
Express https://github.com/strongloop/express/stargazers
socket.io https://github.com/Automattic/socket.io
mongodb http://www.mongodb.org/
mongoose https://github.com/LearnBoost/mongoose

あれこれ

iOSでソケット通信するためのライブラリにはSocketRocketなど色々あるがSIOSocketがSwiftと相性良さそう。

ソケット通信の部分以外にも、
キーボードを出すとTexiViewが隠れないようTableViewが上部にずれたり
TextViewが改行で可変になったり
Autolayoutで画面外に隠したcontainerViewをアニメーションで出し入れするなど
何かしら役に立つ部分があると思う。

バックエンドはもうちょっときれいに書きたかったけど今回は面倒なので割愛。
Expressも使う必要なかったけど今後の拡張を考えて使用している。

ソースコード(iOS)

ViewController
import UIKit

class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var inputAreaView: UIView!
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textViewConstraintHeight: NSLayoutConstraint!
@IBOutlet weak var sendButton: UIButton!
@IBOutlet weak var settingsButton: UIButton!

@IBOutlet weak var settingsView: UIView!
@IBOutlet weak var settingsViewConstraintMarginTop: NSLayoutConstraint!
var settingsViewController:SettingsViewController! = nil
var settingsViewIsDisplayed = false

var messages: Array<MessageModel>?
var socket:SIOSocket! = nil

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    messages = []

    // ここがソケット通信するところ
    SIOSocket.socketWithHost("ws://localhost:3000", response:  { (_socket: SIOSocket!) in
        self.socket = _socket

        self.socket.onConnect = {() in
            println("connected")
            self.socket.emit("message init", args: [])
        }

        self.socket.onReconnect = { (attempts: Int) in

        }

        self.socket.onDisconnect = {() in
            println("disconnected")
        }

        // メッセージを受信
        self.socket.on("message send", callback:{(data:[AnyObject]!)  in
            let dic = data[0] as NSDictionary
            let model = MessageModel(_name: dic["name"] as String, _message: dic["message"] as String)
            self.messages?.append(model)
            self.tableView.reloadData()
        })

        // メッセージの初期化
        self.socket.on("message init", callback:{(data:[AnyObject]!)  in
            let arr = data[0] as NSArray
            for var i = 0; i < arr.count; i++ {
                let dic = arr[i] as NSDictionary
                let model = MessageModel(_name: dic["name"] as String, _message: dic["message"] as String)
                self.messages?.append(model)
            }
            self.tableView.reloadData()
            // tableviewのスクロールを一番下まで動かす
            UIView.animateWithDuration(0.2, delay: 3.0, options: nil, animations: {}, completion: {(finished) -> Void in
                let indexPath = NSIndexPath(forRow:(self.tableView.numberOfRowsInSection(0) as Int - 1), inSection: self.tableView.numberOfSections()-1 as Int)
                self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: true)

            })
        })
    })

    // SettingsViewControllerを取得
    self.settingsViewController = self.childViewControllers[0] as SettingsViewController
    //SettingsViewControllerを閉じた時
    self.settingsViewController.closeMe = {
        self.moveSettingsView()
        self.settingsViewIsDisplayed = false
    }

    // tableViewの高さを可変にする
    self.tableView.estimatedRowHeight = 49
    self.tableView.rowHeight = UITableViewAutomaticDimension

    // textviewのスクロールのバグ修正
    textView.scrollEnabled = false;
    textView.scrollEnabled = true;

    // 装飾周り
    textView.layer.borderColor = UIColor(white: 0.5, alpha: 0.2).CGColor
    textView.layer.borderWidth = 0.5
    var inputViewLayer = CALayer()
    inputViewLayer.frame = CGRect(x: 0, y: 0, width: inputAreaView.frame.width, height: 1)
    inputViewLayer.backgroundColor = UIColor(white: 0.5, alpha: 0.3).CGColor
    inputAreaView.layer.addSublayer(inputViewLayer)
    sendButton.layer.cornerRadius = 2.0

    // イベント系
    sendButton.addTarget(self, action: "sended:", forControlEvents: UIControlEvents.TouchUpInside)
    settingsButton.addTarget(self, action: "settingsPressed:", forControlEvents: UIControlEvents.TouchUpInside)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillAppear:"), name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}

override func viewDidAppear(animated: Bool) {
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - TableViewDelegate
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int  {
    return messages!.count
}

func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath:NSIndexPath!) -> UITableViewCell! {
    var cell: MessageCell = self.tableView.dequeueReusableCellWithIdentifier("MessageCell", forIndexPath: indexPath) as MessageCell
    let message = messages![indexPath.row]
    cell.setContent(message)
    return cell
}

func tableView(tableView: UITableView?, didSelectRowAtIndexPath indexPath:NSIndexPath!) {

}

// MARK: - textFieldDelegate
func textViewDidBeginEditing(textView: UITextView) {
}

func textViewDidEndEditing(textView: UITextView) {
    textView.resignFirstResponder()
}

func textViewShouldEndEditing(textView: UITextView) -> Bool {
    textView.resignFirstResponder()
    return true
}

func textViewDidChange(textView: UITextView) {
    // autolayoutのconstraintを変更してtextviewの高さを可変にする
    let maxHeight:CGFloat = 60.0
    let size = textView.sizeThatFits(textView.frame.size)
    if (textView.frame.height < maxHeight) {
        self.textViewConstraintHeight.constant = size.height
    }
}

func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    return true
}

// MARK: - keyboardNotification
func keyboardWillAppear(notification: NSNotification) {
    // settingsViewが表示されている時(TexiField)
    if (settingsViewIsDisplayed) {
        return
    }

    // キーボードの高さだけ全体にオフセットを与える
    var rect:NSValue
    var duration: NSTimeInterval
    if let userInfo = notification.userInfo as? Dictionary<String,AnyObject> {
        rect = userInfo["UIKeyboardFrameEndUserInfoKey"] as NSValue
        duration = userInfo["UIKeyboardAnimationDurationUserInfoKey"] as NSTimeInterval

        let transform = CGAffineTransformMakeTranslation(0, -rect.CGRectValue().size.height)

        UIView.animateWithDuration(duration, delay: 0.0, options: UIViewAnimationOptions.CurveEaseOut,
            animations: {() -> Void in
                self.view.transform = transform
            }, completion: {(finished) in
        })
    }
}

func keyboardWillHide(notification: NSNotification) {
    // settingsViewが表示されている時(TexiField)
    if (settingsViewIsDisplayed) {
        return
    }

    // 与えたオフセットを取り除く
    var duration: NSTimeInterval
    if let userInfo = notification.userInfo as? Dictionary<String,AnyObject> {
        duration = userInfo["UIKeyboardAnimationDurationUserInfoKey"] as NSTimeInterval

        UIView.animateWithDuration(duration, delay: 0.0, options: UIViewAnimationOptions.CurveEaseOut,
            animations: {() -> Void in
                self.view.transform = CGAffineTransformIdentity
            }, completion: {(finished) in
        })
    }
}

// MARK: - Button Event
func sended(sender: UIButton!) {
    if !MyUtils().stringHasContent(textView.text) {
        return
    }
    let username = (MyUtils().stringHasContent(MyUtils().username)) ? MyUtils().username! : "Mr. Unknown"

    // ソケットにemitする
    let model = NSDictionary(dictionary: ["name": username, "message": textView.text, "date": convertDateToStr(NSDate())]);
    socket.emit("message send", args:[model] as SIOParameterArray)

    // texiviewの高さを元に戻す
    textView.text = nil
    let size = textView.sizeThatFits(textView.frame.size)
    self.textViewConstraintHeight.constant = size.height

    textView.resignFirstResponder()
}

private func convertDateToStr(date:NSDate) -> String {
    let dateFormatter = NSDateFormatter()
    dateFormatter.locale = NSLocale(localeIdentifier: "en_US")
    dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
    return dateFormatter.stringFromDate(date)
}

func settingsPressed(sender: UIButton!) {
    settingsViewIsDisplayed = true
    moveSettingsView()
    self.settingsViewController.textField.becomeFirstResponder()
}

func moveSettingsView() {
    var offset:CGFloat = 0.0

    if (self.settingsViewConstraintMarginTop.constant == 0) {
        offset = self.settingsView.frame.height
    } else {
        offset = 0.0
    }

    self.view.removeConstraint(self.settingsViewConstraintMarginTop)

    self.settingsViewConstraintMarginTop = NSLayoutConstraint(
        item: self.view!, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal,
        toItem: self.settingsView, attribute: NSLayoutAttribute.Top,
        multiplier: 1, constant: offset)

    self.view.addConstraint(self.settingsViewConstraintMarginTop)

    UIView.animateWithDuration(0.24,
        delay: 0.0,
        options: UIViewAnimationOptions.CurveEaseIn,
        animations: {() -> Void in
            self.view.layoutIfNeeded()
        },
        completion: nil)
}


}

ソースコード(バックエンド: Node.js)

app.js
#!/usr/bin/env node
var express = require('express'); 
var debug = require('debug')('socket-example');
var app = express();
var mongoose = require('mongoose');

app.set('port', process.env.PORT || 3000);

//mongoose
var Schema = mongoose.Schema;
var MessagesSchema = new Schema({
    name: String,
    message: String,
    date: Date
});
mongoose.model('messages', MessagesSchema);
mongoose.connect('mongodb://localhost/onechat');
var Messages = mongoose.model('messages');

//socket.io
var http = require('http').Server(app);
var io = require('socket.io')(http);
io.on('connection', function(socket) {

    socket.on('disconnect', function() {
        console.log('user disconnected');
    });

    socket.on('message init', function(data) {
        console.log('message init');

        Messages.find({}).limit(100).exec(function(err, data){
            socket.emit('message init', data);
        });
    });

    socket.on('message send', function(data) {
        console.log('message send');

        data.date = Date.parse(data.date);
        var message = new Messages(data);
        message.save(function(err, message) {
            if (err) return console.error(err);
            io.emit('message send', message);
        });
    });
});

http.listen(app.get('port'), function() {
    console.log('listening on *:' + http.address().port);
});
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away