SwiftでLINEのようなトーク画面をライブラリ無しで作ろうとした

##やりたいこと
個人でマッチングアプリを開発していた時に、トーク画面が必要になりました。

Swiftのトーク画面といえば色々なライブラリがありますが、使い方がよくわからなかったので、一から自分で作成することにしました。

##下準備
共通で使う色とLabelのカスタムクラスを定義します。

```UI.swift
  //トーク画面の背景の色
  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")
    }    
 }
```
サーバーから取得してきたタイムスタンプを、ユーザーに表示する日付に変換する関数を作ります

```Common.swift

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の作成

```ChatViewController.swift
 
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っぽくトーク画面から左スワイプで前の画面に戻るように拡張しましょう。

```ChatDetailViewController.swift
   
   extension ChatDetailViewController {
     func setSwipeBack() {
        let target = self.navigationController?.value(forKey: "_cachedInteractionController")
        let recognizer = UIPanGestureRecognizer(target: target, action: Selector(("handleNavigationTransition:")))
        self.view.addGestureRecognizer(recognizer)
     }

   }

```


##トーク画面のView
必要な部品は、縦にするクロールする画面、送信ボタン、テキスト入力欄、メッセージのラベル、時刻表示のラベルです。

```ChatDetailView.swift

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で自分のメッセージか相手のメッセージかを区別しています。自分のメッセージか相手のメッセージかによってラベルの背景色と位置と、時刻の表示場所が変わります。

##実際の表示

<img width="400" alt="IMG_6210.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240900/20a4421d-2ef7-69f3-d550-d3823fd2ca0b.jpeg">


##まとめと課題
このままではキーボードが出された時に、送信ボタンをテキスト入力欄が隠れてしまいます。この辺りの情報は適宜追加していきたいと思います。IQKeyboardManagerなどのライブラリを使うと早いですね。

あとは吹き出しに見られるような突き出る部分をコードだけで書くのは辛いですね。

何より最大の問題はラベルのインスタンスが作られすぎて、メモリリークが起きる懸念があることです。インスタンスの再利用を考えるとtableViewを使うのが最良の方法でしょう。

とりあえずトーク画面っぽいものを作りたい人は参考にしてもらえたら嬉しいです。