포스트

Chat app (3)

Chat 기능 구현

우선 BarbuttonItem을 만들어 준다.

1
2
3
let newConversationBarButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(handleNewChat))

navigationItem.rightBarButtonItem = newConversationBarButton

Tableview 구현은 pass

Cell 생성.

이부분도 딱히 서술할게 없어서 pass

simulator_screenshot_945E1987-9FEF-4B74-A7D0-06F512CA5687

구현하면 이렇게 나온다

5개 나오는건

현재 개수를 5개로 해두었기 때문.

ChatVC 생성을 한다.

이때 특이한적음 UICollectionViewController 라는 것.

그리고 대화 셀 을 만들어준다.

ChatVC의 특이점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
extension ChatViewController: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        
        return .init(top: 15, left: 0, bottom: 15, right: 0)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
        let cell = ChatCollectionViewCell(frame: frame)
        cell.configure()
        cell.layoutIfNeeded()
        
        let targetSize = CGSize(width: view.frame.width, height: 1000)
        let estimeSize = cell.systemLayoutSizeFitting(targetSize)
        
        return .init(width: view.frame.width, height: estimeSize.height)
    }
}

// Cell
func configure(text: String) {
        bubbleLeftAnchor.isActive = true
        dateLeftAnchor.isActive = true
        
        textView.text = text
    }

섹션의 인셋을 설정하고,

셀의 크기를 동적으로 설정한다. 이게 포인트

  1. 임시로 ChatCollectionViewCell 객체를 생성하고, 필요한 구성 작업을 수행
  2. layoutIfNeeded()를 호출하여 레이아웃 업데이트
  3. systemLayoutSizeFitting 메서드를 사용하여 셀의 콘텐츠에 맞는 크기를 계산
  4. targetSize는 레이아웃을 계산할 목표 크기. 여기서는 높이를 1000으로 설정하여 셀의 실제 높이를 측정할 수 있게 함
  5. estimeSize는 실제 콘텐츠에 맞는 셀의 크기
  6. 최종적으로 계산된 셀의 크기를 반환

simulator_screenshot_A2906A40-108C-4668-AC3D-7334CB70BF88

결과는 다음과 같다.

InputView 설정

우선 UITextView를 사용하는데, 이것도 커스텀을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class InputTextView: UITextView {
    
    let placeHolderLabel = CustomLabel(text: "Type a message...", labelColor: .lightGray)
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        
        backgroundColor = #colorLiteral(red: 0.9656843543, green: 0.9657825828, blue: 0.9688259959, alpha: 1)
        layer.cornerRadius = 20
        isScrollEnabled = false
        
        addSubview(placeHolderLabel)
        placeHolderLabel.centerY(inView: self, leftAnchor: leftAnchor, rightAnchor: rightAnchor, paddingLeft: 8)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

크게 특이점은 없다

단지 이걸 사용하는 ChatVC에 특이점이 존재

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ChatViewController: UICollectionViewController {
    
    // MARK: - Properties
    private let reuseIdentifier = "ChatCollectionViewCell"
    private var messages: [String] = [
        "Here's sample data",
        "this the second line with more than one line",
        "Just wanna add more text for testing or where ever, and thats it for this lessson, Cool"
    ]
    
    private lazy var customInputView: CustomInputView = { //added
        let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
        let iv = CustomInputView(frame: frame)
        return iv
    }()
    
    // MARK: - Lifecycle
    init() {
        super.init(collectionViewLayout: UICollectionViewFlowLayout())
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureUI()
    }
    
    override var inputAccessoryView: UIView? {
        get { return customInputView }
    }
    
    override var canBecomeFirstResponder: Bool {
        return true
    }
    
    // MARK: - Helpers
    private func configureUI() {
        collectionView.backgroundColor = .white
        
        collectionView.register(ChatCollectionViewCell.self
                                , forCellWithReuseIdentifier: reuseIdentifier)
    }
}

바로 이녀석들이다

1
2
3
4
5
6
7
override var inputAccessoryView: UIView? {
        get { return customInputView }
    }
    
override var canBecomeFirstResponder: Bool {
        return true
    }

inputAcceryView

inputAccessoryView는 UIView 타입의 속성으로, 키보드가 나타날 때 함께 표시될 커스텀 뷰를 반환 inputAccessoryView는 읽기 전용 속성으로, get 접근자를 사용하여 customInputView를 반환

canBecomeFirstResponder

canBecomeFirstResponder는 Bool 타입의 속성으로, 뷰 컨트롤러가 첫 번째 응답자가 될 수 있는지 여부를 반환 true를 반환하면, 이 뷰 컨트롤러가 첫 번째 응답자가 되어 키보드와 입력 액세서리 뷰를 표시 기본적으로 UIViewController는 첫 번째 응답자가 될 수 없으므로, 이를 재정의하여 가능하게 한다.

그리고 inputview의 내용도 추가 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CustomInputView: UIView {
    
    // MARK: - Properties
    let inputTextView = InputTextView()
    
    // MARK: - Lifecycle
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .white
        autoresizingMask = .flexibleHeight // added
        
        addSubview(inputTextView)
        inputTextView.anchor(top: topAnchor, left: leftAnchor, bottom: safeAreaLayoutGuide.bottomAnchor, right: rightAnchor, paddingTop: 12, paddingLeft: 8, paddingBottom: 5, paddingRight: 8)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var intrinsicContentSize: CGSize { // added
        
        return .zero
    }
    
    // MARK: - Helpers
    
}

autoresizingMask

autoresizingMask는 iOS에서 UIView가 부모 뷰의 크기가 변경될 때 자신의 크기나 위치를 자동으로 조정할 수 있도록 설정하는 속성 autoresizingMask는 UIView.AutoresizingMask 타입으로, 여러 옵션을 조합하여 사용할 수 있다. 이 속성을 설정하면 뷰가 부모 뷰의 크기 변화에 적응할 수 있다.

intrinsicContentSize

intrinsicContentSize는 레이아웃 엔진이 뷰의 크기를 계산할 때 사용 예를 들어, UILabel은 텍스트의 길이에 따라 크기가 결정되므로, UILabel의 intrinsicContentSize는 텍스트의 크기를 반영 한다 기본적으로, UIView의 intrinsicContentSize는 CGSize.zero를 반환 이 의미는 뷰가 자체적으로 크기를 결정하지 않으며, 외부 레이아웃 제약 조건에 따라 크기가 결정된다는 것이다.

그리고 TextView에 NotificationCenter를 추가해준다.

입력의 변화에 대해 전달할 녀석이다.

여기선 placeholder를 숨기기위해 사용한다.

1
2
3
4
5
NotificationCenter.default.addObserver(self, selector: #selector(handleTextDidChange), name: UITextView.textDidChangeNotification, object: nil)

@objc func handleTextDidChange() {
        placeHolderLabel.isHidden = !text.isEmpty
    }

Jun-06-2024 18-15-54

작동확인.

또한

1
2
3
4
5
extension UITextView {
    func paddingView() {
        self.textContainerInset = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12)
    }
}

paddingView 함수를 만들면서 글자입력시 text가 textview에 바짝 붙지않게 조정해두었다.

send버튼 만들기

버튼 구성에는 특이한게 없으나 어제도 비슷한게 있어 적는다

1
2
3
4
5
6
7
protocol CustomInputViewDelegate: AnyObject {
    func inputView(_ view: CustomInputView, wantUploadMessage message: String)
}

@objc func handlePostButton() {
        delegate?.inputView(self, wantUploadMessage: inputTextView.text)
    }

해당 뷰에서 데이터를 전달할 text를 저렇게 delegate를 통해서 한다는것.

ChatVC에서 해당 delegate를 채택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 private lazy var customInputView: CustomInputView = {
        let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
        let iv = CustomInputView(frame: frame)
        iv.delegate = self // added
        return iv
    }()

extension ChatViewController: CustomInputViewDelegate {
    func inputView(_ view: CustomInputView, wantUploadMessage message: String) {
        print(message)
    }
    
    
}

simulator_screenshot_CEC4A24B-A8DC-4042-9976-507B129E3194

기본 뼈대 완성.

우선은 메세지를 보내기 했을때 추가가 되는지 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
func clearTextView() {
        inputTextView.text = ""
        inputTextView.placeHolderLabel.isHidden = false
    }

extension ChatViewController: CustomInputViewDelegate {
    func inputView(_ view: CustomInputView, wantUploadMessage message: String) {
        print(message)
        messages.append(message) // added
        view.clearTextView() // added
        collectionView.reloadData() // added
    }
}

Jun-06-2024 19-02-12

확인 완료.

이때 꼭 버튼이아니라 뒤에 있는 빨간색 배경을 눌렀을때도 되게 하기위해서 gesture를 추가

1
2
3
4
5
6
7
private let postBackgroundColor: CustomImageView = {
        let tap = UITapGestureRecognizer(target: self, action: #selector(handlePostButton))
        let iv = CustomImageView(width: 40, height: 40, backgroundColor: .red, cornerRadius: 20)
        iv.isUserInteractionEnabled = true
        iv.addGestureRecognizer(tap)
        return iv
    }()
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.