Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- combine
- clean architecture
- map
- 애니메이션
- 리펙토링
- Human interface guide
- collectionview
- RxCocoa
- UITextView
- rxswift
- uiscrollview
- ios
- uitableview
- 리펙터링
- tableView
- swiftUI
- ribs
- Clean Code
- UICollectionView
- Xcode
- HIG
- MVVM
- 리팩토링
- SWIFT
- Protocol
- Refactoring
- 스위프트
- Observable
- swift documentation
- 클린 코드
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 채팅 UI (UICollectionView를 사용하여 chatting UI 구현) 본문
UI 컴포넌트 (swift)
[iOS - swift] 채팅 UI (UICollectionView를 사용하여 chatting UI 구현)
jake-kim 2021. 10. 31. 21:01
구현 아이디어
- UICollectionView 사용하고 layout은 IUCollectionViewFlowLayout 인스턴스 사용
- UITextView를 갖는 커스텀 cell을 만든 후, 말풍선의 tipView는 CGMutablePath를 통해 그려주는 방식
커스텀 채팅 Cell 구현
class ChatMessageCell: BaseCollectionViewCell {
// 구현
}
- 필요한 모델을 nested로 정의
enum ChatType: CaseIterable {
case receive
case send
}
struct Model {
let message: String
let chatType: ChatType
}
var model: Model? {
didSet { bind() }
}
- UI 컴포넌트 초기화
let messaageTextView: UITextView = {
let view = UITextView()
view.font = .systemFont(ofSize: 18.0)
view.text = "Sample message"
view.textColor = .black
view.backgroundColor = .white
view.layer.cornerRadius = 15.0
view.layer.masksToBounds = false
view.isEditable = false
return view
}()
let profileImageView: UIImageView = {
let view = UIImageView(image: UIImage(named: "profile"))
view.layer.cornerRadius = view.bounds.width / 2
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.clear.cgColor
return view
}()
override func setupViews() {
super.setupViews()
contentView.addSubview(messaageTextView)
messaageTextView.translatesAutoresizingMaskIntoConstraints = false
messaageTextView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
messaageTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
contentView.addSubview(profileImageView)
profileImageView.translatesAutoresizingMaskIntoConstraints = false
profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
}
- bind() 메소드에서 사용되는 메소드
- message 텍스트 값을 받아서, View의 width크기를 계산 String의 extension으로 `getEstimatedFrame(with:)`구현
- receive인지 send인지 파악 후 말풍선의 tipView를 붙여야 하므로 UIView의 extension으로 `addTipViewToLeftTop(with:)`와 `addTipViewToRightBottom(with:)` 구현
// String값을 가지고 예상되는 frame크기를 return 하는 메소드 정의
extension String {
func getEstimatedFrame(with font: UIFont) -> CGRect {
let size = CGSize(width: UIScreen.main.bounds.width * 2/3, height: 1000)
let optionss = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: self).boundingRect(with: size, options: optionss, attributes: [.font: font], context: nil)
return estimatedFrame
}
}
// UIView에서 tipView를 왼쪽 상단 또는 오른쪽 하단에 표출하기 위한 메소드 정의
extension UIView {
func addTipViewToLeftTop(with color: UIColor?) {
let path = CGMutablePath()
path.move(to: CGPoint(x: -8, y: 16))
path.addLine(to: CGPoint(x: 12, y: 16))
path.addLine(to: CGPoint(x: 12, y: 2))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = color?.cgColor
layer.insertSublayer(shape, at: 0)
}
func addTipViewToRightBottom(with color: UIColor?) {
// frame 값을 얻기 위해서 layoutIfNeeded() 호출 (호출 안하면 width, height값 모두 0인 상태)
layoutIfNeeded()
print(frame)
let height = frame.height
let width = frame.width
let path = CGMutablePath()
path.move(to: CGPoint(x: width + 12, y: height - 18))
path.addLine(to: CGPoint(x: width - 8, y: height - 18))
path.addLine(to: CGPoint(x: width - 8, y: height - 4))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = color?.cgColor
layer.insertSublayer(shape, at: 0)
}
}
- bind() 메소드
private func bind() {
guard let model = model, let font = messaageTextView.font else {
return
}
messaageTextView.text = model.message
let estimatedFrame = model.message.getEstimatedFrame(with: font)
messaageTextView.widthAnchor.constraint(equalToConstant: estimatedFrame.width + 16).isActive = true
if case .receive = model.chatType {
messaageTextView.backgroundColor = .systemBlue
profileImageView.isHidden = true
messaageTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true
messaageTextView.addTipViewToRightBottom(with: messaageTextView.backgroundColor)
} else {
messaageTextView.backgroundColor = .white
profileImageView.isHidden = false
messaageTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16 + profileImageView.bounds.width).isActive = true
messaageTextView.addTipViewToLeftTop(with: messaageTextView.backgroundColor)
}
}
ViewController에서 사용
- UICollectionView 초기화 시, 정의한 Cell을 적용하여 사용
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 16.0
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.register(ChatMessageCell.self, forCellWithReuseIdentifier: "cell")
view.delegate = self
view.dataSource = self
view.backgroundColor = .lightGray
return view
}()
- collectionView 관련 delegate
var messages: [(message: String, chatType: ChatMessageCell.ChatType)] = []
...
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ChatMessageCell
let message = messages[indexPath.row]
cell.model = .init(message: message.message, chatType: message.chatType)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let estimatedFrame = messages[indexPath.row].message.getEstimatedFrame(with: .systemFont(ofSize: 18))
return CGSize(width: view.bounds.width, height: estimatedFrame.height + 20)
}
}
* 전체 소스 코드: https://github.com/JK0369/ExChat
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments