LINEのようなチャットUIをつくる

この記事は[Swift愛好会 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/love_swift)の15日目のものです！

チャット画面はチャットアプリだけではなくSNSのDM画面や、カスタマーサポートの画面など、様々なところで利用されることがあると思います。
今回はそのチャット画面の実装のサンプルをご紹介します。
間違っているところやベストプラクティスでない場合がありますので、
その際は是非コメントにお願い致します！

# 見た目
![image.png](https://qiita-image-store.s3.amazonaws.com/0/197742/eed74cba-20bb-0f75-3def-f6c8611b2038.png)

# ソースコード

以下の通りに作成しても、ナビゲーションバーはつきませんのでご了承ください⚠

```swift:ChatRoomViewController.swift

import UIKit

class ChatRoomViewController: UIViewController {
	
	@IBOutlet weak var tableView: UITableView!
	
	var bottomView: ChatRoomInputView! //画面下部に表示するテキストフィールドと送信ボタン
	
	var chats: [ChatEntity] = []
	
	override func viewDidLoad() {
		super.viewDidLoad()
		setupUI()
	}
	
	override func didReceiveMemoryWarning() {
		super.didReceiveMemoryWarning()
	}
	
	override func viewWillDisappear(_ animated: Bool) {
		super.viewWillDisappear(animated)
	}
	
	override func viewWillAppear(_ animated: Bool) {
		super.viewWillAppear(animated)
	}
	
	override var canBecomeFirstResponder: Bool {
		return true
	}
	
	override var inputAccessoryView: UIView? {
		return bottomView //通常はテキストフィールドのプロパティに設定しますが、画面を表示している間は常に表示したいため、ViewControllerのプロパティに設定します
	}
}

extension ChatRoomViewController {
	func setupUI() {
		self.view.backgroundColor = UIColor(red: 113/255, green: 148/255, blue: 194/255, alpha: 1)
		tableView.backgroundColor = UIColor(red: 113/255, green: 148/255, blue: 194/255, alpha: 1)
		
		tableView.separatorColor = UIColor.clear // セルを区切る線を見えなくする
		tableView.estimatedRowHeight = 10000 // セルが高さ以上になった場合バインバインという動きをするが、それを防ぐために大きな値を設定
		tableView.rowHeight = UITableViewAutomaticDimension // Contentに合わせたセルの高さに設定
		tableView.allowsSelection = false // 選択を不可にする
		tableView.keyboardDismissMode = .interactive // テーブルビューをキーボードをまたぐように下にスワイプした時にキーボードを閉じる
		
		tableView.register(UINib(nibName: "YourChatViewCell", bundle: nil), forCellReuseIdentifier: "YourChat")
		tableView.register(UINib(nibName: "MyChatViewCell", bundle: nil), forCellReuseIdentifier: "MyChat")
		
		self.bottomView = ChatRoomInputView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 70)) // 下部に表示するテキストフィールドを設定
		let chat1 = ChatEntity(text: "text1", time: "10:01", userType: .I)
		let chat2 = ChatEntity(text: "text2", time: "10:02", userType: .You)
		let chat3 = ChatEntity(text: "text3", time: "10:03", userType: .I)
		chats = [chat1, chat2, chat3]
		
	}
}

extension ChatRoomViewController: UITableViewDataSource {
	func numberOfSections(in tableView: UITableView) -> Int {
		return 1
	}
	
	func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return self.chats.count
	}
	
	func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		let chat = self.chats[indexPath.row]
		if chat.isMyChat() {
			let cell = tableView.dequeueReusableCell(withIdentifier: "MyChat") as! MyChatViewCell
			cell.clipsToBounds = true //bound外のものを表示しない
			// Todo: isRead
			cell.updateCell(text: chat.text, time: chat.time, isRead: true)
			return cell
		} else {
			let cell = tableView.dequeueReusableCell(withIdentifier: "YourChat") as! YourChatViewCell
			cell.clipsToBounds = true
			cell.updateCell(text: chat.text, time: chat.time)
			return cell
		}
	}
}

extension ChatRoomViewController: UITableViewDelegate {
	func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
		print(indexPath)
	}
	
	func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
		return 10
	}
}

```


```swift:ChatRoomInputView.swift
import UIKit

class ChatRoomInputView: UIView {
	
	@IBOutlet weak var chatTextField: UITextField!
	@IBOutlet weak var sendButton: UIButton!
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		self.setFromXib()
	}
	
	required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
		self.setFromXib()
	}
	
	func setFromXib() {
		let nib = UINib.init(nibName: "ChatRoomInputView", bundle: nil)
		let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
		self.addSubview(view)// Storyboardから読み込んだレイアウトでビューを重ねて表示
	}
}

```



```swift:ChatEntity.swift
import Foundation
import UIKit

public class ChatEntity {
	var text = ""
	var time = ""
	var userType: UserType
	
	public init(text: String, time: String, userType: UserType) {
		self.text = text
		self.time = time
		self.userType = userType
	}
	
	public func isMyChat() -> Bool {
		return userType == .I
	}
}

public enum UserType {
	case I
	case You
}
```

```swift:MyChatViewCell.swift
import UIKit

class MyChatViewCell: UITableViewCell {
	
	@IBOutlet weak var textView: UITextView!
	@IBOutlet weak var timeLabel: UILabel!
	@IBOutlet weak var readLabel: UILabel!
	
	@IBOutlet weak var textViewWidthConstraint: NSLayoutConstraint!
	
	override func awakeFromNib() {
		super.awakeFromNib()
		self.backgroundColor = UIColor.clear
		self.textView.layer.cornerRadius = 15 // 角を丸める
		addSubview(MyBalloonView(frame: CGRect(x: Int(frame.size.width+30), y: 0, width: 30, height: 30))) //吹き出しのようにするためにビューを重ねる
	}
	
	override func setSelected(_ selected: Bool, animated: Bool) {
		super.setSelected(selected, animated: animated)
	}
}

extension MyChatViewCell {
	func updateCell(text: String, time: String, isRead: Bool) {
		self.textView?.text = text
		self.timeLabel?.text = time
		self.readLabel?.isHidden = !isRead
		
		let frame = CGSize(width: self.frame.width - 8, height: CGFloat.greatestFiniteMagnitude)
		var rect = self.textView.sizeThatFits(frame)
		if(rect.width<30){
			rect.width=30
		}
		textViewWidthConstraint.constant = rect.width//テキストが短くても最小のビューの幅を30とする
	}
}
```

```swift:YourChatViewCell.swift
import UIKit

class YourChatViewCell: UITableViewCell {
	
	@IBOutlet weak var textView: UITextView!
	@IBOutlet weak var timeLabel: UILabel!
	@IBOutlet weak var textViewWidthConstraint: NSLayoutConstraint!
	
	override func awakeFromNib() {
		super.awakeFromNib()
		self.backgroundColor = UIColor.clear
		self.textView.layer.cornerRadius = 15// 角を丸める
		self.addSubview(YourBalloonView(frame: CGRect(x: textView.frame.minX-10, y: textView.frame.minY-10, width: 50, height: 50)))//吹き出しのようにするためにビューを重ねる
	}
	
	override func setSelected(_ selected: Bool, animated: Bool) {
		super.setSelected(selected, animated: animated)
		// Configure the view for the selected state
	}
	
}

extension YourChatViewCell {
	func updateCell(text: String, time: String) {
		self.textView?.text = text
		self.timeLabel?.text = time
		
		let frame = CGSize(width: self.frame.width, height: CGFloat.greatestFiniteMagnitude)
		var rect = self.textView.sizeThatFits(frame)
		if(rect.width<30){
			rect.width=30
		}
		textViewWidthConstraint.constant = rect.width//テキストが短くても最小のビューの幅を30とする
	}
}


```

```swift:MyBalloonView.swift
import UIKit

class MyBalloonView: UIView {
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		self.backgroundColor = UIColor.clear
	}
	
	required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
	}

	override func draw(_ rect: CGRect) {
		let line = UIBezierPath()//吹き出しの口部分を描画
		UIColor(red: 133/255, green: 226/255, blue: 73/255, alpha: 1.0).setFill()
		UIColor.clear.setStroke()
		line.move(to: CGPoint(x: 0, y: 10))
		line.addQuadCurve(to: CGPoint(x: 20, y: 0), controlPoint: CGPoint(x: 10, y: 20))
		line.addQuadCurve(to: CGPoint(x: 5, y: 20), controlPoint: CGPoint(x: 13, y: 30))
		line.close()
		line.fill()
		line.stroke()
	}
}

```

```swift:YourBalloonView.swift
import UIKit

class YourBalloonView: UIView {
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		self.backgroundColor = UIColor.clear
	}
	
	required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
	}
	
	override func draw(_ rect: CGRect) {
		let line = UIBezierPath()//吹き出しの口部分を描画
		UIColor.white.setFill()
		UIColor.clear.setStroke()
		line.move(to: CGPoint(x: 20, y: 15))
		line.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 5, y: 10))
		line.addQuadCurve(to: CGPoint(x: 10, y: 25), controlPoint: CGPoint(x: 0, y: 10))
		line.close()
		line.fill()
		line.stroke()
	}
}

```
# Xib
##MyChatViewCell.xib
ポイント
セルのCustom ClassにMyChatViewCellを設定します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/197742/ab2d3be3-559e-2581-d3e3-6cb1ed06676a.png)

##YourChatViewCell.xib
セルのCustom ClassにYourChatViewCellを設定します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/197742/4621a0c0-21a5-04d2-2d86-adf503144bd3.png)

##ChatRoomInputView.xib
![image.png](https://qiita-image-store.s3.amazonaws.com/0/197742/9195f357-869f-b945-505f-0a1e96896ee3.png)

#おわりに
長くなってしまいましたが、このようLINEのようなUIを作ることができます。
また、今回のソースコードはリポジトリとして[GitHubにあげています](https://github.com/shimokp/chat-ui-sample)
Swift愛好会に参加した事があるのは1回だけですがノリと勢いでアドベントカレンダーに参加してしまいました笑
できるだけ恥ずかしくないようにテーマをしっかりしたものに設定したつもりでしたが、
趣旨とそれてしまっていたらすみません🙌🏻
Xibのレイアウトについてはスクリーンショットで伝わるようにしたつもりですが、
もし不明な点があればぜひ聞いてください！

ちなみに、LINEの再現のプロジェクトを個人的に進めています（大したものではありませんが）
バグは結構ありますが、現状ではリアルタイムでのチャットとビデオ通話ができるようになっています。
もし興味がある方が居たら[こちら](https://github.com/shimokp/lime)もご覧ください！
（今回のソースに出ていなかったNavigationBarもこちらでは実装しています！）

最後までお読み頂きありがとうございました！！

明日16日目は@gotou015さんです！
よろしくお願いします😆