##やりたいこと
個人でマッチングアプリを開発していた時に、トーク画面が必要になりました。
Swiftのトーク画面といえば色々なライブラリがありますが、使い方がよくわからなかったので、一から自分で作成することにしました。
##下準備
共通で使う色とLabelのカスタムクラスを定義します。
//トーク画面の背景の色
let commonBackgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)
//LINEのトーク画面の自分が送った文章の背景色
let greenColor = UIColor(red: 0.52, green: 0.89, blue: 0.29, alpha: 1.0)
//自分のテキストと相手のテキストを表示するためのラベル
class messageLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
self.numberOfLines = 0
self.layer.masksToBounds = true
self.layer.cornerRadius = 10.0
self.textAlignment = .left
self.textColor = UIColor.black
self.font = UIFont.systemFont(ofSize: 16.0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
サーバーから取得してきたタイムスタンプを、ユーザーに表示する日付に変換する関数を作ります
func unixToString(unix: Int) -> String {
let dateUnix: TimeInterval = TimeInterval(unix)
let date = Date(timeIntervalSince1970: dateUnix)
// NSDate型を日時文字列に変換するためのNSDateFormatterを生成
let formatter = DateFormatter()
formatter.dateFormat = "MM月dd日 HH:mm" //年は不要
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") as Locale
let dateStr: String = formatter.string(from: date)
return dateStr
}
##トーク画面のViewControllerの作成
import UIKit
import Alamofire
final class ChatDetailViewController: UIViewController {
let chatDetailView = ChatDetailView()
var isKeyBoardOpen: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = []
navigationItem.title = "相手の名前"
self.navigationController?.navigationBar.isTranslucent = false
chatDetailView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height - (self.navigationController?.navigationBar.frame.height)! - UIApplication.shared.statusBarFrame.height)
view.addSubview(chatDetailView)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
getData()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
@objc func send() {
//サーバーへメッセージを送信
//送信に成功したら画面上にメッセージを追加する
self.chatDetailView.setText(text: "こんばんは",timeStamp: 1000000001,mine: true)
}
func getData() {
//ここでAPIを叩いてテキストを取得
//相手が送ったメッセージを表示
self.chatDetailView.setText(text: "こんにちは",timeStamp: 1000000000,mine: false)
//自分が送ったメッセージを表示
self.chatDetailView.setText(text: "こんばんは",timeStamp: 1000000001,mine: true)
}
@objc private func keyboardWillShow(_ notification: Notification) {
guard let userInfo = notification.userInfo as? [String: Any] else {
return
}
guard let keyboardInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
return
}
guard let _ = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {
return
}
if !isKeyBoardOpen {
isKeyBoardOpen = true
//ここでレイアウトを調整する
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
if isKeyBoardOpen {
isKeyBoardOpen = false
//ここでレイアウトを調整する
}
}
}
extension UIScrollView {
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//scrollViewをタップした時にキーボードが閉じられるようにする
self.next?.touchesBegan(touches, with: event)
}
}
##左スワイプで元の画面に戻る
せっかくなので、LINEっぽくトーク画面から左スワイプで前の画面に戻るように拡張しましょう。
extension ChatDetailViewController {
func setSwipeBack() {
let target = self.navigationController?.value(forKey: "_cachedInteractionController")
let recognizer = UIPanGestureRecognizer(target: target, action: Selector(("handleNavigationTransition:")))
self.view.addGestureRecognizer(recognizer)
}
}
##トーク画面のView
必要な部品は、縦にするクロールする画面、送信ボタン、テキスト入力欄、メッセージのラベル、時刻表示のラベルです。
import Foundation
import UIKit
final class ChatDetailView: UIView {
var scrollView: UIScrollView = UIScrollView()
let textField = UITextField()
let checkButton = UIButton()
let sendButton: UIButton = {
//送信ボタンを作成
let button = UIButton()
button.setTitle("送信", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = orangeColor
button.titleLabel!.font = UIFont.boldSystemFont(ofSize: 19.0)
button.layer.masksToBounds = true
button.layer.cornerRadius = 20.0
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
required override init(frame:CGRect){
super.init(frame:frame)
addSubview(scrollView)
addSubview(sendButton)
addSubview(textField)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
makeScrollView()
sendButton.topAnchor.constraint(equalTo: self.textField.topAnchor, constant: 0).isActive = true
sendButton.leftAnchor.constraint(equalTo: self.textField.rightAnchor, constant: 10.0).isActive = true
sendButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10.0).isActive = true
sendButton.bottomAnchor.constraint(equalTo: self.textField.bottomAnchor, constant: 0.0).isActive = true
textField.translatesAutoresizingMaskIntoConstraints = false
textField.backgroundColor = commonBackgroundColor
textField.topAnchor.constraint(equalTo: self.scrollView.bottomAnchor, constant: 10.0).isActive = true
textField.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10.0).isActive = true
textField.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -100.0).isActive = true
scrollView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0).isActive = true
scrollView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0.0).isActive = true
scrollView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0.0).isActive = true
}
func makeScrollView() {
//scrollViewを作成する
scrollView.contentSize = CGSize(width: self.frame.width, height: 10)
scrollView.bounces = false
scrollView.indicatorStyle = .default
scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = commonBackgroundColor
scrollView.keyboardDismissMode = .onDrag
}
func setText(text: String, timeStamp: Int, mine: Bool) {
let height = scrollView.contentSize.height
let label = messageLabel(frame: CGRect())
label.text = text
//自分のテキストは右側 相手のテキストは左側に表示する
if mine {
label.frame = CGRect(x: self.frame.size.width/2-50, y: height, width: self.frame.size.width/2 + 40, height: 100)
label.backgroundColor = greenColor
}
else {
label.frame = CGRect(x: 10, y: height, width: self.frame.size.width/2 + 40, height: 100)
label.backgroundColor = .white
}
label.sizeToFit()
//メッセージを表示するlabelにpaddingを設ける
label.frame.size.height += 20.0
label.frame.size.width += 10.0
scrollView.addSubview(label)
let dayLabel = UILabel()
//先ほど定義した関数を用いて日付を表示
dayLabel.text = unixToString(unix: timeStamp)
dayLabel.numberOfLines = 1
dayLabel.backgroundColor = commonBackgroundColor
dayLabel.textColor = UIColor.black
if mine {
dayLabel.textAlignment = .right
dayLabel.frame = CGRect(x: 0, y: height, width: self.frame.size.width/2 - 60, height: 40)
}
else {
dayLabel.textAlignment = .left
dayLabel.frame = CGRect(x: self.frame.size.width/2 + 60, y: height, width: self.frame.size.width/2 - 20, height: 40)
}
dayLabel.font = UIFont.systemFont(ofSize: 11.0)
scrollView.addSubview(dayLabel)
//テキストが追加されるたびに、スクロールビューの中身の高さを伸ばしていく
scrollView.contentSize.height = height + label.frame.size.height + 10
}
}
setText関数では、第二引数のmineで自分のメッセージか相手のメッセージかを区別しています。自分のメッセージか相手のメッセージかによってラベルの背景色と位置と、時刻の表示場所が変わります。
##実際の表示
##まとめと課題
このままではキーボードが出された時に、送信ボタンをテキスト入力欄が隠れてしまいます。この辺りの情報は適宜追加していきたいと思います。IQKeyboardManagerなどのライブラリを使うと早いですね。
あとは吹き出しに見られるような突き出る部分をコードだけで書くのは辛いですね。
何より最大の問題はラベルのインスタンスが作られすぎて、メモリリークが起きる懸念があることです。インスタンスの再利用を考えるとtableViewを使うのが最良の方法でしょう。
とりあえずトーク画面っぽいものを作りたい人は参考にしてもらえたら嬉しいです。