From 2ab507a30f258a9f3b99f5376100e2c1c6852d51 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Thu, 4 Apr 2024 20:38:29 +0900 Subject: [PATCH 01/36] =?UTF-8?q?feat:=20ChatBubbleView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 16 ++ ChatBot/ChatBot/View/ChatBubbleView.swift | 172 ++++++++++++++++++++++ ChatBot/ChatBot/View/MessageView.swift | 8 + 3 files changed, 196 insertions(+) create mode 100644 ChatBot/ChatBot/View/ChatBubbleView.swift create mode 100644 ChatBot/ChatBot/View/MessageView.swift diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index 74bc8812..f06efd31 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 27EFEA7B2BBD7DC3004A3426 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA7A2BBD7DC3004A3426 /* MockURLSession.swift */; }; 27EFEA7D2BBD7DFD004A3426 /* NetworkTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA7C2BBD7DFC004A3426 /* NetworkTestable.swift */; }; 27EFEA7F2BBE44AA004A3426 /* NetworkURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA7E2BBE44AA004A3426 /* NetworkURL.swift */; }; + 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A652BBE938700354947 /* ChatBubbleView.swift */; }; + 89510A682BBE9BC400354947 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A672BBE9BC400354947 /* MessageView.swift */; }; 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A632BB4233C00C643D3 /* ResponseModel.swift */; }; 89A49A662BB426ED00C643D3 /* RequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A652BB426ED00C643D3 /* RequestModel.swift */; }; 89A49A682BB4272600C643D3 /* Choice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A672BB4272600C643D3 /* Choice.swift */; }; @@ -62,6 +64,8 @@ 27EFEA7A2BBD7DC3004A3426 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; 27EFEA7C2BBD7DFC004A3426 /* NetworkTestable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkTestable.swift; sourceTree = ""; }; 27EFEA7E2BBE44AA004A3426 /* NetworkURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkURL.swift; sourceTree = ""; }; + 89510A652BBE938700354947 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; + 89510A672BBE9BC400354947 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 89A49A632BB4233C00C643D3 /* ResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseModel.swift; sourceTree = ""; }; 89A49A652BB426ED00C643D3 /* RequestModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestModel.swift; sourceTree = ""; }; 89A49A672BB4272600C643D3 /* Choice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Choice.swift; sourceTree = ""; }; @@ -180,6 +184,15 @@ path = Mock; sourceTree = ""; }; + 89510A642BBE934E00354947 /* View */ = { + isa = PBXGroup; + children = ( + 89510A652BBE938700354947 /* ChatBubbleView.swift */, + 89510A672BBE9BC400354947 /* MessageView.swift */, + ); + path = View; + sourceTree = ""; + }; 89A49A622BB422DF00C643D3 /* Model */ = { isa = PBXGroup; children = ( @@ -232,6 +245,7 @@ isa = PBXGroup; children = ( 273615912BB2A496004C3F15 /* App */, + 89510A642BBE934E00354947 /* View */, 89A49A622BB422DF00C643D3 /* Model */, 27EFEA732BBD7BB0004A3426 /* ViewModel */, 273615922BB2A4A7004C3F15 /* Controller */, @@ -357,6 +371,7 @@ 27EFEA792BBD7DA1004A3426 /* MockDataSample.swift in Sources */, 27EFEA7F2BBE44AA004A3426 /* NetworkURL.swift in Sources */, B4B3E2C12B42D1BB00818B3C /* ChatBotViewController.swift in Sources */, + 89510A682BBE9BC400354947 /* MessageView.swift in Sources */, 27EFEA7D2BBD7DFD004A3426 /* NetworkTestable.swift in Sources */, B4B3E2BD2B42D1BB00818B3C /* AppDelegate.swift in Sources */, 27EFEA7B2BBD7DC3004A3426 /* MockURLSession.swift in Sources */, @@ -364,6 +379,7 @@ 27EFEA702BBD7B7D004A3426 /* JSONHandler.swift in Sources */, 27EFEA682BBD7AF3004A3426 /* APIType.swift in Sources */, 89A49A6A2BB4273700C643D3 /* Usage.swift in Sources */, + 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */, 27EFEA722BBD7B95004A3426 /* NetworkError.swift in Sources */, 27EFEA752BBD7BC0004A3426 /* ChatViewModel.swift in Sources */, 89A49A6C2BB4274B00C643D3 /* Message.swift in Sources */, diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift new file mode 100644 index 00000000..0c7a8676 --- /dev/null +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -0,0 +1,172 @@ +// +// ChatBubbleView.swift +// Swift_CoreGraphics +// +// Created by 강창현 on 4/4/24. +// + +import UIKit + +final class ChatBubbleView: UIView { + enum Role: String { + case system = "left" + case user = "right" + } + private var borderColor: UIColor = .clear { + didSet { + setNeedsDisplay() + } + } + + private var role: Role = .system { + didSet { + setNeedsDisplay() + } + } + + private let messageView = MessageView() + + init(role: Role) { + self.role = role + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + let bezierPath = UIBezierPath() + role == .system ? setLeftBubbleView(rect: rect, bezierPath: bezierPath) : setRightBubbleView(rect: rect, bezierPath: bezierPath) + bezierPath.close() + backgroundColor?.setFill() + borderColor.setStroke() + bezierPath.fill() + bezierPath.stroke() + } + + private func configureUI() { + self.addSubview(messageView) + messageView.translatesAutoresizingMaskIntoConstraints = false + + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + messageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + ]) + } + + func setRightBubbleView(rect: CGRect, bezierPath: UIBezierPath) { + self.backgroundColor = .lightGray + let bottom = rect.height + let right = rect.width + + bezierPath.move( + to: CGPoint(x: right - 22, y: bottom) + ) + bezierPath.addLine( + to: CGPoint(x: 17, y: bottom) + ) + bezierPath.addCurve( + to: CGPoint(x: 0, y: bottom - 18), + controlPoint1: CGPoint(x: 7.61, y: bottom), + controlPoint2: CGPoint(x: 0, y: bottom - 7.61) + ) + bezierPath.addLine( + to: CGPoint(x: 0, y: 17) + ) + bezierPath.addCurve( + to: CGPoint(x: 17, y: 0), + controlPoint1: CGPoint(x: 0, y: 7.61), + controlPoint2: CGPoint(x: 7.61, y: 0) + ) + bezierPath.addLine( + to: CGPoint(x: right - 21, y: 0) + ) + bezierPath.addCurve( + to: CGPoint(x: right - 4, y: 17), + controlPoint1: CGPoint(x: right - 11.61, y: 0), + controlPoint2: CGPoint(x: right - 4, y: 7.61) + ) + bezierPath.addLine( + to: CGPoint(x: right - 4, y: bottom - 11) + ) + bezierPath.addCurve( + to: CGPoint(x: right, y: bottom), + controlPoint1: CGPoint(x: right - 4, y: bottom - 1), + controlPoint2: CGPoint(x: right, y: bottom) + ) + bezierPath.addLine( + to: CGPoint(x: right + 0.05, y: bottom - 0.01) + ) + bezierPath.addCurve( + to: CGPoint(x: right - 11.04, y: bottom - 4.04), + controlPoint1: CGPoint(x: right - 4.07, y: bottom + 0.43), + controlPoint2: CGPoint(x: right - 8.16, y: bottom - 1.06) + ) + bezierPath.addCurve( + to: CGPoint(x: right - 22, y: bottom), + controlPoint1: CGPoint(x: right - 16, y: bottom), + controlPoint2: CGPoint(x: right - 19, y: bottom) + ) + + } + + func setLeftBubbleView(rect: CGRect, bezierPath: UIBezierPath) { + self.backgroundColor = .systemBlue + let bottom = rect.height + let right = rect.width + + bezierPath.move( + to: CGPoint(x: 22, y: bottom) + ) + bezierPath.addLine( + to: CGPoint(x: right - 17, y: bottom) + ) + bezierPath.addCurve( + to: CGPoint(x: right, y: bottom - 18), + controlPoint1: CGPoint(x: right - 7.61, y: bottom), + controlPoint2: CGPoint(x: right, y: bottom - 7.61) + ) + bezierPath.addLine( + to: CGPoint(x: right, y: 17) + ) + bezierPath.addCurve( + to: CGPoint(x: right - 17, y: 0), + controlPoint1: CGPoint(x: right, y: 7.61), + controlPoint2: CGPoint(x: right - 7.61, y: 0) + ) + bezierPath.addLine( + to: CGPoint(x: 21, y: 0) + ) + bezierPath.addCurve( + to: CGPoint(x: 4, y: 17), + controlPoint1: CGPoint(x: 11.61, y: 0), + controlPoint2: CGPoint(x: 4, y: 7.61) + ) + bezierPath.addLine( + to: CGPoint(x: 4, y: bottom - 11) + ) + bezierPath.addCurve( + to: CGPoint(x: 0, y: bottom), + controlPoint1: CGPoint(x: 4, y: bottom - 1), + controlPoint2: CGPoint(x: 0, y: bottom) + ) + bezierPath.addLine( + to: CGPoint(x: -0.05, y: bottom - 0.01) + ) + bezierPath.addCurve( + to: CGPoint(x: 11.04, y: bottom - 4.04), + controlPoint1: CGPoint(x: 4.07, y: bottom + 0.43), + controlPoint2: CGPoint(x: 8.16, y: bottom - 1.06) + ) + bezierPath.addCurve( + to: CGPoint(x: 22, y: bottom), + controlPoint1: CGPoint(x: 16, y: bottom), + controlPoint2: CGPoint(x: 19, y: bottom) + ) + } +} + diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageView.swift new file mode 100644 index 00000000..dccb8d1f --- /dev/null +++ b/ChatBot/ChatBot/View/MessageView.swift @@ -0,0 +1,8 @@ +// +// MessageView.swift +// ChatBot +// +// Created by 이보한 on 2024/4/4. +// + +import Foundation From 916816bb1eb7aeee25387b46361df650f0da25a6 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Thu, 4 Apr 2024 20:38:43 +0900 Subject: [PATCH 02/36] =?UTF-8?q?feat:=20MessageView=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/MessageView.swift | 57 +++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageView.swift index dccb8d1f..b6b44dd0 100644 --- a/ChatBot/ChatBot/View/MessageView.swift +++ b/ChatBot/ChatBot/View/MessageView.swift @@ -5,4 +5,59 @@ // Created by 이보한 on 2024/4/4. // -import Foundation +import UIKit + +final class MessageView: UILabel { + private var width: Double = 0.0 + private var height: Double = 0.0 + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + setupSize() + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + self.text = "" + self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColor = .white + self.textAlignment = .left + self.numberOfLines = 0 + } + + private func setupSize() { + guard let text = self.text as? NSString else { return } + + width = text.size( + withAttributes: [ + NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) + ] + ).width + + if width >= 150 { + width = 150 + } + + height = text.size( + withAttributes: [ + NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) + ] + ).height + + if width >= 150 { + height = CGFloat(Int(height) + self.numberOfLines * Int(height)) + } + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + self.heightAnchor.constraint(equalToConstant: height), + self.widthAnchor.constraint(equalToConstant: width), + ]) + } +} From 48aa667ff9f9d3063e1738068248095158cefed2 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Fri, 5 Apr 2024 14:14:45 +0900 Subject: [PATCH 03/36] =?UTF-8?q?feat:=20ChatBubbleView=20=EB=B0=8F=20Mess?= =?UTF-8?q?ageView=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ChatBotViewController.swift | 84 +++++++++++-------- ChatBot/ChatBot/View/ChatBubbleView.swift | 20 +++-- ChatBot/ChatBot/View/MessageView.swift | 26 +++--- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index 69e7a915..c3a1ccd3 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -9,44 +9,58 @@ import UIKit import Combine final class ChatBotViewController: UIViewController { - - private let chatBotViewModel: ChatViewModel = .init() - private var cancellable = Set() - private let input: PassthroughSubject = .init() - - override func viewDidLoad() { - super.viewDidLoad() - bind() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - input.send( - .sendButtonTapped( - message: Message( - role: "user", - content: "Compose a poem that explains the concept of recursion in programming." - ) - ) + var chatBubbleView = ChatBubbleView() + + private let chatBotViewModel: ChatViewModel = .init() + private var cancellable = Set() + private let input: PassthroughSubject = .init() + + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + view.addSubview(chatBubbleView) + setupConstraints() + bind() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + input.send( + .sendButtonTapped( + message: Message( + role: "user", + content: "Compose a poem that explains the concept of recursion in programming." ) - } + ) + ) + } } private extension ChatBotViewController { - func bind() { - let output = chatBotViewModel.transform( - input: input.eraseToAnyPublisher() - ) - output.sink { event in - switch event { - case .fetchChatResponseDidFail(let error): - print(error.localizedDescription) - case .fetchChatResponseDidSucceed(let response): - print(response.choices[0].message.content) - case .toggleSendButton(let isEnable): - print("\(isEnable)") - } - } - .store(in: &cancellable) + func setupConstraints() { + chatBubbleView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + chatBubbleView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + chatBubbleView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + ] + ) + } + + func bind() { + let output = chatBotViewModel.transform( + input: input.eraseToAnyPublisher() + ) + output.sink { event in + switch event { + case .fetchChatResponseDidFail(let error): + print(error.localizedDescription) + case .fetchChatResponseDidSucceed(let response): + print(response.choices[0].message.content) + case .toggleSendButton(let isEnable): + print("\(isEnable)") + } } + .store(in: &cancellable) + } } diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index 0c7a8676..2f45109c 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -25,10 +25,12 @@ final class ChatBubbleView: UIView { } private let messageView = MessageView() - - init(role: Role) { - self.role = role - super.init(frame: .zero) + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .clear + self.configureUI() + self.setupConstraints() } required init?(coder: NSCoder) { @@ -52,14 +54,18 @@ final class ChatBubbleView: UIView { } private func setupConstraints() { + NSLayoutConstraint.activate([ + self.widthAnchor.constraint(equalTo: messageView.widthAnchor, constant: 20), + self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), messageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), ]) } - func setRightBubbleView(rect: CGRect, bezierPath: UIBezierPath) { - self.backgroundColor = .lightGray + func setRightBubbleView(rect: CGRect,bezierPath: UIBezierPath) { + self.backgroundColor = .systemBlue + messageView.textColor = .white let bottom = rect.height let right = rect.width @@ -115,7 +121,7 @@ final class ChatBubbleView: UIView { } func setLeftBubbleView(rect: CGRect, bezierPath: UIBezierPath) { - self.backgroundColor = .systemBlue + self.backgroundColor = .systemGray5 let bottom = rect.height let right = rect.width diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageView.swift index b6b44dd0..27b7de3f 100644 --- a/ChatBot/ChatBot/View/MessageView.swift +++ b/ChatBot/ChatBot/View/MessageView.swift @@ -22,41 +22,35 @@ final class MessageView: UILabel { fatalError("init(coder:) has not been implemented") } + override func drawText(in rect: CGRect) { + let insets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 0) + super.drawText(in: rect.inset(by: insets)) + } + private func configureUI() { - self.text = "" + self.text = "In the land of code, there lies a path.Where functions call themselves in wrath.It's recursion, a looping scheme.A mesmerizing, recursive dream.A function's call, within its own embrace.A cycle of enchanting grace.Like a mirror reflecting back its view.Each iteration is born anew.Through recursive calls, the task is done.A pattern spun, a code web spun.Like a never-ending dance of thought.The function's journey, never caught.In programming's realm, recursion thrives.A concept of looping that endlessly drives.A journey within, a tale untold.In the mystic, recursive code's stronghold.So embrace the recursion, don't be shy.Let your functions dance, let them fly.In programming's poetry, let them sing.The beauty of recursion, a majestic thingIn the land of code, there lies a path.Where functions call themselves in wrath.It's recursion, a looping scheme.A mesmerizing, recursive dream.A function's call, within its own embrace.A cycle of enchanting grace.Like a mirror reflecting back its view.Each iteration is born anew.Through recursive calls, the task is done.A pattern spun, a code web spun.Like a never-ending dance of thought.The function's journey, never caught.In programming's realm, recursion thrives." self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = .white + self.backgroundColor = .clear self.textAlignment = .left self.numberOfLines = 0 } private func setupSize() { guard let text = self.text as? NSString else { return } - + width = text.size( withAttributes: [ NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) ] ).width - if width >= 150 { - width = 150 - } - - height = text.size( - withAttributes: [ - NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) - ] - ).height - - if width >= 150 { - height = CGFloat(Int(height) + self.numberOfLines * Int(height)) + if width >= UIScreen.main.bounds.width * 0.75 { + width = UIScreen.main.bounds.width * 0.75 } } private func setupConstraints() { NSLayoutConstraint.activate([ - self.heightAnchor.constraint(equalToConstant: height), self.widthAnchor.constraint(equalToConstant: width), ]) } From e21c6ef42a8a23c1b0e9587e39cbd5caaa41fac4 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sat, 6 Apr 2024 15:38:47 +0900 Subject: [PATCH 04/36] =?UTF-8?q?add:=20CollectionView=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatCell.swift | 8 ++++++++ ChatBot/ChatBot/View/ChatCollectionView.swift | 8 ++++++++ ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift | 8 ++++++++ 3 files changed, 24 insertions(+) create mode 100644 ChatBot/ChatBot/View/ChatCell.swift create mode 100644 ChatBot/ChatBot/View/ChatCollectionView.swift create mode 100644 ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift diff --git a/ChatBot/ChatBot/View/ChatCell.swift b/ChatBot/ChatBot/View/ChatCell.swift new file mode 100644 index 00000000..bcc6c3f0 --- /dev/null +++ b/ChatBot/ChatBot/View/ChatCell.swift @@ -0,0 +1,8 @@ +// +// ChatCell.swift +// ChatBot +// +// Created by 강창현 on 4/5/24. +// + +import Foundation diff --git a/ChatBot/ChatBot/View/ChatCollectionView.swift b/ChatBot/ChatBot/View/ChatCollectionView.swift new file mode 100644 index 00000000..fa371d9b --- /dev/null +++ b/ChatBot/ChatBot/View/ChatCollectionView.swift @@ -0,0 +1,8 @@ +// +// ChatCollectionView.swift +// ChatBot +// +// Created by 강창현 on 4/5/24. +// + +import Foundation diff --git a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift new file mode 100644 index 00000000..d1b411df --- /dev/null +++ b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift @@ -0,0 +1,8 @@ +// +// ChatCollectionViewDataSource.swift +// ChatBot +// +// Created by 강창현 on 4/5/24. +// + +import Foundation From 76d7e69e2f0ffa1e8eb32ad0449b89a573216d5c Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sat, 6 Apr 2024 15:39:31 +0900 Subject: [PATCH 05/36] =?UTF-8?q?refine:=20MockData=20=ED=8C=8C=EC=8B=B1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/Network/Mock/MockURLSession.swift | 34 ++++++++++++++++--- .../Model/Network/NetworkTestable.swift | 4 +-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/ChatBot/ChatBot/Model/Network/Mock/MockURLSession.swift b/ChatBot/ChatBot/Model/Network/Mock/MockURLSession.swift index 540ac77d..24bb0190 100644 --- a/ChatBot/ChatBot/Model/Network/Mock/MockURLSession.swift +++ b/ChatBot/ChatBot/Model/Network/Mock/MockURLSession.swift @@ -11,13 +11,39 @@ import Combine final class MockURLSession: URLSessionProtocol { typealias Response = AnyPublisher<(Data, URLResponse), NetworkError> - private let response: Response + private var response: Response? = { + let data: Data = JSONHandler.load(fileName: "MockData")! + let httpResponse = HTTPURLResponse( + url: URL(string: "www.naver.com")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! as URLResponse + let response = Just((data, httpResponse)) + .setFailureType(to: NetworkError.self) + .eraseToAnyPublisher() + return response + }() - init(response: Response) { - self.response = response + init(response: Response? = nil) { + self.response = response } - + + func makeMockResponse(fileName: String, statusCode: Int) -> Response { + let data: Data = JSONHandler.load(fileName: fileName)! + let httpResponse = HTTPURLResponse( + url: URL(string: "www.naver.com")!, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil + )! as URLResponse + let response = Just((data, httpResponse)) + .setFailureType(to: NetworkError.self) + .eraseToAnyPublisher() + return response + } func dataTaskPublisher(for request: URLRequest) -> AnyPublisher { + guard let response else { return Empty().eraseToAnyPublisher() } return response .tryMap { (data: Data, response: URLResponse) in try self.handleHTTPResponse(data: data, httpResponse: response) diff --git a/ChatBot/ChatBot/Model/Network/NetworkTestable.swift b/ChatBot/ChatBot/Model/Network/NetworkTestable.swift index f532cbac..ef507893 100644 --- a/ChatBot/ChatBot/Model/Network/NetworkTestable.swift +++ b/ChatBot/ChatBot/Model/Network/NetworkTestable.swift @@ -10,7 +10,7 @@ import Combine protocol NetworkTestable { func setMockSession(session: URLSessionProtocol) -> NetworkService - func makeMockURLSession(fileName: String, statusCode: Int) throws -> MockURLSession + func makeMockURLSession(fileName: String, statusCode: Int) throws -> URLSessionProtocol } extension NetworkTestable { @@ -18,7 +18,7 @@ extension NetworkTestable { return NetworkService(session: session) } - func makeMockURLSession(fileName: String, statusCode: Int) throws -> MockURLSession { + func makeMockURLSession(fileName: String, statusCode: Int) throws -> URLSessionProtocol { let data: Data = JSONHandler.load(fileName: fileName)! let httpResponse = HTTPURLResponse( url: URL(string: "www.naver.com")!, From 672954f8454b799698a0b8a53302f8e239823c72 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sat, 6 Apr 2024 15:39:57 +0900 Subject: [PATCH 06/36] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 12 +++ .../Controller/ChatBotViewController.swift | 45 +++++++--- ChatBot/ChatBot/Model/Message.swift | 2 +- .../ChatBot/Model/Request/RequestModel.swift | 2 +- ChatBot/ChatBot/View/ChatBubbleView.swift | 82 +++++++++++-------- ChatBot/ChatBot/View/ChatCell.swift | 61 +++++++++++++- ChatBot/ChatBot/View/ChatCollectionView.swift | 20 ++++- .../View/ChatCollectionViewDataSource.swift | 24 +++++- ChatBot/ChatBot/View/MessageView.swift | 27 +++--- ChatBot/ChatBot/ViewModel/ChatViewModel.swift | 15 ++-- 10 files changed, 221 insertions(+), 69 deletions(-) diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index f06efd31..ec75f211 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ 27EFEA7B2BBD7DC3004A3426 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA7A2BBD7DC3004A3426 /* MockURLSession.swift */; }; 27EFEA7D2BBD7DFD004A3426 /* NetworkTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA7C2BBD7DFC004A3426 /* NetworkTestable.swift */; }; 27EFEA7F2BBE44AA004A3426 /* NetworkURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA7E2BBE44AA004A3426 /* NetworkURL.swift */; }; + 27EFEA832BBFD813004A3426 /* ChatCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */; }; + 27EFEA852BBFD836004A3426 /* ChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA842BBFD836004A3426 /* ChatCell.swift */; }; + 27EFEA872BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */; }; 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A652BBE938700354947 /* ChatBubbleView.swift */; }; 89510A682BBE9BC400354947 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A672BBE9BC400354947 /* MessageView.swift */; }; 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A632BB4233C00C643D3 /* ResponseModel.swift */; }; @@ -64,6 +67,9 @@ 27EFEA7A2BBD7DC3004A3426 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; 27EFEA7C2BBD7DFC004A3426 /* NetworkTestable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkTestable.swift; sourceTree = ""; }; 27EFEA7E2BBE44AA004A3426 /* NetworkURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkURL.swift; sourceTree = ""; }; + 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionView.swift; sourceTree = ""; }; + 27EFEA842BBFD836004A3426 /* ChatCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCell.swift; sourceTree = ""; }; + 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewDataSource.swift; sourceTree = ""; }; 89510A652BBE938700354947 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; 89510A672BBE9BC400354947 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 89A49A632BB4233C00C643D3 /* ResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseModel.swift; sourceTree = ""; }; @@ -189,6 +195,9 @@ children = ( 89510A652BBE938700354947 /* ChatBubbleView.swift */, 89510A672BBE9BC400354947 /* MessageView.swift */, + 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */, + 27EFEA842BBFD836004A3426 /* ChatCell.swift */, + 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */, ); path = View; sourceTree = ""; @@ -373,6 +382,8 @@ B4B3E2C12B42D1BB00818B3C /* ChatBotViewController.swift in Sources */, 89510A682BBE9BC400354947 /* MessageView.swift in Sources */, 27EFEA7D2BBD7DFD004A3426 /* NetworkTestable.swift in Sources */, + 27EFEA872BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift in Sources */, + 27EFEA832BBFD813004A3426 /* ChatCollectionView.swift in Sources */, B4B3E2BD2B42D1BB00818B3C /* AppDelegate.swift in Sources */, 27EFEA7B2BBD7DC3004A3426 /* MockURLSession.swift in Sources */, 27EFEA6A2BBD7B17004A3426 /* HttpMethod.swift in Sources */, @@ -381,6 +392,7 @@ 89A49A6A2BB4273700C643D3 /* Usage.swift in Sources */, 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */, 27EFEA722BBD7B95004A3426 /* NetworkError.swift in Sources */, + 27EFEA852BBFD836004A3426 /* ChatCell.swift in Sources */, 27EFEA752BBD7BC0004A3426 /* ChatViewModel.swift in Sources */, 89A49A6C2BB4274B00C643D3 /* Message.swift in Sources */, 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */, diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index c3a1ccd3..5e6aaa89 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -9,17 +9,22 @@ import UIKit import Combine final class ChatBotViewController: UIViewController { - var chatBubbleView = ChatBubbleView() - + private lazy var chatCollectionView: ChatCollectionView = { + let configure = UICollectionLayoutListConfiguration(appearance: .plain) + let layout = UICollectionViewCompositionalLayout.list(using: configure) + let collectionView = ChatCollectionView(frame: .zero, collectionViewLayout: layout) + return collectionView + }() + private lazy var dataSource = ChatCollectionViewDataSource(collectionView: chatCollectionView) private let chatBotViewModel: ChatViewModel = .init() private var cancellable = Set() private let input: PassthroughSubject = .init() - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - view.addSubview(chatBubbleView) + setupCollectionView() + configureUI() setupConstraints() bind() } @@ -38,12 +43,23 @@ final class ChatBotViewController: UIViewController { } private extension ChatBotViewController { + func configureUI() { + view.addSubview(chatCollectionView) + } + + func setupCollectionView() { + self.chatCollectionView.dataSource = dataSource + } + func setupConstraints() { - chatBubbleView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - chatBubbleView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - chatBubbleView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - ] + chatCollectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate( + [ + chatCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + chatCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + chatCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + chatCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50) + ] ) } @@ -51,16 +67,23 @@ private extension ChatBotViewController { let output = chatBotViewModel.transform( input: input.eraseToAnyPublisher() ) - output.sink { event in + output.sink { [weak self] event in switch event { case .fetchChatResponseDidFail(let error): print(error.localizedDescription) case .fetchChatResponseDidSucceed(let response): - print(response.choices[0].message.content) + self?.applyChatResponse(response: response) case .toggleSendButton(let isEnable): print("\(isEnable)") } } .store(in: &cancellable) } + + func applyChatResponse(response: RequestModel) { + var chatCollectionViewSnapshot = ChatCollectionViewSnapshot() + chatCollectionViewSnapshot.appendSections([.messages]) + chatCollectionViewSnapshot.appendItems(response.messages) + dataSource.apply(chatCollectionViewSnapshot) + } } diff --git a/ChatBot/ChatBot/Model/Message.swift b/ChatBot/ChatBot/Model/Message.swift index 811ea13c..4d0b5b97 100644 --- a/ChatBot/ChatBot/Model/Message.swift +++ b/ChatBot/ChatBot/Model/Message.swift @@ -5,7 +5,7 @@ // Created by 이보한 on 2024/3/27. // -struct Message: Codable { +struct Message: Codable, Hashable { let role: String let content: String } diff --git a/ChatBot/ChatBot/Model/Request/RequestModel.swift b/ChatBot/ChatBot/Model/Request/RequestModel.swift index 8a362cc5..b9e5ffe6 100644 --- a/ChatBot/ChatBot/Model/Request/RequestModel.swift +++ b/ChatBot/ChatBot/Model/Request/RequestModel.swift @@ -5,7 +5,7 @@ // Created by 이보한 on 2024/3/27. // -struct RequestModel: Codable { +struct RequestModel: Codable, Hashable { var model: String = "gpt-3.5-turbo-1106" var stream: Bool = false var messages: [Message] diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index 2f45109c..18e39f5b 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -7,25 +7,45 @@ import UIKit -final class ChatBubbleView: UIView { - enum Role: String { - case system = "left" - case user = "right" - } - private var borderColor: UIColor = .clear { - didSet { - setNeedsDisplay() +final class UserBubbleView: ChatBubbleView { + private var bezierPath: UIBezierPath? + + override func draw(_ rect: CGRect) { + guard + bezierPath != nil + else { + let bezierPath = UIBezierPath() + self.bezierPath = bezierPath + setRightBubbleView(rect: rect, bezierPath: bezierPath) + bezierPath.close() + backgroundColor?.setFill() + bezierPath.fill() + return } } +} + +final class SystemBubbleView: ChatBubbleView { + private var bezierPath: UIBezierPath? - private var role: Role = .system { - didSet { - setNeedsDisplay() + override func draw(_ rect: CGRect) { + guard + bezierPath != nil + else { + let bezierPath = UIBezierPath() + self.bezierPath = bezierPath + setLeftBubbleView(rect: rect, bezierPath: bezierPath) + bezierPath.close() + backgroundColor?.setFill() + bezierPath.fill() + return } } - - private let messageView = MessageView() +} +class ChatBubbleView: UIView { + private let messageView = MessageView() + override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = .clear @@ -37,30 +57,27 @@ final class ChatBubbleView: UIView { fatalError("init(coder:) has not been implemented") } - override func draw(_ rect: CGRect) { - let bezierPath = UIBezierPath() - role == .system ? setLeftBubbleView(rect: rect, bezierPath: bezierPath) : setRightBubbleView(rect: rect, bezierPath: bezierPath) - bezierPath.close() - backgroundColor?.setFill() - borderColor.setStroke() - bezierPath.fill() - bezierPath.stroke() + func configureMessage(text: String) { + messageView.text = text + messageView.setupSize() } - - private func configureUI() { +} + +private extension ChatBubbleView { + func configureUI() { self.addSubview(messageView) messageView.translatesAutoresizingMaskIntoConstraints = false - } - private func setupConstraints() { - - NSLayoutConstraint.activate([ - self.widthAnchor.constraint(equalTo: messageView.widthAnchor, constant: 20), - self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), - messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - messageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - ]) + func setupConstraints() { + NSLayoutConstraint.activate( + [ + self.widthAnchor.constraint(equalTo: messageView.widthAnchor, constant: 20), + self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), + messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + messageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + ] + ) } func setRightBubbleView(rect: CGRect,bezierPath: UIBezierPath) { @@ -175,4 +192,3 @@ final class ChatBubbleView: UIView { ) } } - diff --git a/ChatBot/ChatBot/View/ChatCell.swift b/ChatBot/ChatBot/View/ChatCell.swift index bcc6c3f0..f916d641 100644 --- a/ChatBot/ChatBot/View/ChatCell.swift +++ b/ChatBot/ChatBot/View/ChatCell.swift @@ -5,4 +5,63 @@ // Created by 강창현 on 4/5/24. // -import Foundation +import UIKit + +final class ChatCell: UICollectionViewListCell { + static var identifier: String { + return String(describing: self) + } + + private let userBubbleView = UserBubbleView() + private let systemBubbleView = SystemBubbleView() + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + setupUserConstraints() + setupSystemConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureUser(text: String) { + userBubbleView.isHidden = false + userBubbleView.configureMessage(text: text) + } + + func configureSystem(text: String) { + systemBubbleView.isHidden = false + systemBubbleView.configureMessage(text: text) + } +} + +private extension ChatCell { + func configureUI() { + systemBubbleView.translatesAutoresizingMaskIntoConstraints = false + userBubbleView.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(userBubbleView) + self.addSubview(systemBubbleView) + } + + func setupUserConstraints() { + NSLayoutConstraint.activate( + [ + userBubbleView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), + userBubbleView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5), + userBubbleView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5) + ] + ) + } + + func setupSystemConstraints() { + NSLayoutConstraint.activate( + [ + systemBubbleView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), + systemBubbleView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5), + systemBubbleView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5) + ] + ) + } +} diff --git a/ChatBot/ChatBot/View/ChatCollectionView.swift b/ChatBot/ChatBot/View/ChatCollectionView.swift index fa371d9b..053b401b 100644 --- a/ChatBot/ChatBot/View/ChatCollectionView.swift +++ b/ChatBot/ChatBot/View/ChatCollectionView.swift @@ -5,4 +5,22 @@ // Created by 강창현 on 4/5/24. // -import Foundation +import UIKit + +final class ChatCollectionView: UICollectionView { + + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + setupChatCell() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ChatCollectionView { + func setupChatCell() { + self.register(ChatCell.self, forCellWithReuseIdentifier: ChatCell.identifier) + } +} diff --git a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift index d1b411df..a8a03a95 100644 --- a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift +++ b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift @@ -5,4 +5,26 @@ // Created by 강창현 on 4/5/24. // -import Foundation +import UIKit + +enum MessageSection { + case messages +} + +typealias ChatCollectionViewSnapshot = NSDiffableDataSourceSnapshot + +final class ChatCollectionViewDataSource: UICollectionViewDiffableDataSource { + static let cellProvider: CellProvider = { collectionView, indexPath, message in + guard + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ChatCell.identifier, for: indexPath) as? ChatCell + else { + return ChatCell() + } + message.role == "user" ? cell.configureUser(text: message.content) : cell.configureSystem(text: message.content) + return cell + } + + convenience init(collectionView: ChatCollectionView) { + self.init(collectionView: collectionView, cellProvider: Self.cellProvider) + } +} diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageView.swift index 27b7de3f..016ddce7 100644 --- a/ChatBot/ChatBot/View/MessageView.swift +++ b/ChatBot/ChatBot/View/MessageView.swift @@ -9,13 +9,10 @@ import UIKit final class MessageView: UILabel { private var width: Double = 0.0 - private var height: Double = 0.0 override init(frame: CGRect) { super.init(frame: frame) configureUI() - setupSize() - setupConstraints() } required init?(coder: NSCoder) { @@ -27,15 +24,7 @@ final class MessageView: UILabel { super.drawText(in: rect.inset(by: insets)) } - private func configureUI() { - self.text = "In the land of code, there lies a path.Where functions call themselves in wrath.It's recursion, a looping scheme.A mesmerizing, recursive dream.A function's call, within its own embrace.A cycle of enchanting grace.Like a mirror reflecting back its view.Each iteration is born anew.Through recursive calls, the task is done.A pattern spun, a code web spun.Like a never-ending dance of thought.The function's journey, never caught.In programming's realm, recursion thrives.A concept of looping that endlessly drives.A journey within, a tale untold.In the mystic, recursive code's stronghold.So embrace the recursion, don't be shy.Let your functions dance, let them fly.In programming's poetry, let them sing.The beauty of recursion, a majestic thingIn the land of code, there lies a path.Where functions call themselves in wrath.It's recursion, a looping scheme.A mesmerizing, recursive dream.A function's call, within its own embrace.A cycle of enchanting grace.Like a mirror reflecting back its view.Each iteration is born anew.Through recursive calls, the task is done.A pattern spun, a code web spun.Like a never-ending dance of thought.The function's journey, never caught.In programming's realm, recursion thrives." - self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = .clear - self.textAlignment = .left - self.numberOfLines = 0 - } - - private func setupSize() { + func setupSize() { guard let text = self.text as? NSString else { return } width = text.size( @@ -47,9 +36,21 @@ final class MessageView: UILabel { if width >= UIScreen.main.bounds.width * 0.75 { width = UIScreen.main.bounds.width * 0.75 } + setupConstraints() } +} + +private extension MessageView { + func configureUI() { + self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColor = .clear + self.textAlignment = .left + self.numberOfLines = 0 + } + + - private func setupConstraints() { + func setupConstraints() { NSLayoutConstraint.activate([ self.widthAnchor.constraint(equalToConstant: width), ]) diff --git a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift index 4fd1ae05..c5266520 100644 --- a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift +++ b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift @@ -9,8 +9,7 @@ import Foundation import Combine final class ChatViewModel { - - private var requestModel: RequestModel? + var requestModel: RequestModel? private let networkService: NetworkService private let output: PassthroughSubject = .init() private var cancellables: Set = .init() @@ -21,7 +20,7 @@ final class ChatViewModel { enum Output { case fetchChatResponseDidFail(error: NetworkError) - case fetchChatResponseDidSucceed(response: ResponseModel) + case fetchChatResponseDidSucceed(response: RequestModel) case toggleSendButton(isEnable: Bool) } @@ -33,7 +32,8 @@ final class ChatViewModel { input.sink { [weak self] event in switch event { case .sendButtonTapped(let message): - self?.fetchChatBotData(message: message) + guard let body = self?.makeBody(message: message) else { return } + self?.fetchChatBotData(body: body) } }.store(in: &cancellables) return output.eraseToAnyPublisher() @@ -41,8 +41,7 @@ final class ChatViewModel { } private extension ChatViewModel { - func fetchChatBotData(message: Message) { - let body = makeBody(message: message) + func fetchChatBotData(body: RequestModel) { self.output.send(.toggleSendButton(isEnable: false)) networkService.fetchChatBotResponse( type: .chatbot, @@ -56,7 +55,9 @@ private extension ChatViewModel { self?.output.send(.fetchChatResponseDidFail(error: error)) } } receiveValue: { [weak self] response in - self?.output.send(.fetchChatResponseDidSucceed(response: response)) + self?.requestModel?.messages.append(response.choices[0].message) + guard let requestModel = self?.requestModel else { return } + self?.output.send(.fetchChatResponseDidSucceed(response: requestModel)) } .store(in: &cancellables) } From 1b08502c5f118e4bf3fc3d594fb164e2e938ee48 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Mon, 8 Apr 2024 19:28:57 +0900 Subject: [PATCH 07/36] =?UTF-8?q?chore:=20MockData=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/Network/Mock/MockDataSample.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ChatBot/ChatBot/Model/Network/Mock/MockDataSample.swift b/ChatBot/ChatBot/Model/Network/Mock/MockDataSample.swift index 26fad268..d030d855 100644 --- a/ChatBot/ChatBot/Model/Network/Mock/MockDataSample.swift +++ b/ChatBot/ChatBot/Model/Network/Mock/MockDataSample.swift @@ -17,28 +17,28 @@ enum MockDataSample { content: "Compose a poem that explains the concept of recursion in programming." ), Message( - role: "system", - content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." + role: "assistant", + content: "$s7Combine9PublisherPAAE4sink17receiveCompletion0D5ValueAA14AnyCancellableCyAA11SubscribersO0E0Oy_7FailureQzGc_y6OutputQzctF + 304" ), Message( role: "user", - content: "Compose a poem that explains the concept of recursion in programming." + content: "$s7ChatBot0A9ViewModelC05fetchaB4Data33_C3094975EA75902AF323554274754D0BLL4bodyyAA07RequestD0V_tF + 548" ), Message( - role: "system", - content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." + role: "assistant", + content: "Y" ), Message( role: "user", - content: "Compose a poem that explains the concept of recursion in programming." + content: "ChatBot0A9ViewModelC9transform5input7Combine12AnyPublisherVyAC6OutputOs5NeverOGAHyAC5InputOALG_tFyAOcfU_ + 832" ), Message( - role: "system", - content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." + role: "assistant", + content: "Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Fatal: supplied item identifiers are not unique. Duplicate identifiers" ), Message( role: "user", - content: "Compose a poem that explains the concept of recursion in programming." + content: "17.4asdfasdfasdfasdfasdf" ), ] ) From 2f41b504222edc5de4ec9849f9c68ddc723c65b6 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Mon, 8 Apr 2024 19:29:33 +0900 Subject: [PATCH 08/36] =?UTF-8?q?chore:=20MockData=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/ViewModel/ChatViewModel.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift index 27b2bba1..4cd8be3b 100644 --- a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift +++ b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift @@ -66,15 +66,16 @@ private extension ChatViewModel { guard requestModel != nil else { - requestModel = RequestModel( - messages: [ - Message( - role: "system", - content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." - ), - message - ] - ) +// requestModel = RequestModel( +// messages: [ +// Message( +// role: "system", +// content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." +// ), +// message +// ] +// ) + requestModel = MockDataSample.requestData return requestModel } requestModel?.messages.append(message) From f48bd66cb38e7c6056e4af67b79a82b43a5256a7 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Mon, 8 Apr 2024 19:29:58 +0900 Subject: [PATCH 09/36] =?UTF-8?q?chore:=20isHidden=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatCell.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChatBot/ChatBot/View/ChatCell.swift b/ChatBot/ChatBot/View/ChatCell.swift index f916d641..909a32ec 100644 --- a/ChatBot/ChatBot/View/ChatCell.swift +++ b/ChatBot/ChatBot/View/ChatCell.swift @@ -27,12 +27,14 @@ final class ChatCell: UICollectionViewListCell { } func configureUser(text: String) { + systemBubbleView.isHidden = true userBubbleView.isHidden = false userBubbleView.configureMessage(text: text) } func configureSystem(text: String) { systemBubbleView.isHidden = false + userBubbleView.isHidden = true systemBubbleView.configureMessage(text: text) } } From ad47d2d7dcce2341588ffe419f66a888cd2ea375 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Mon, 8 Apr 2024 19:30:23 +0900 Subject: [PATCH 10/36] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatCollectionView.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ChatBot/ChatBot/View/ChatCollectionView.swift b/ChatBot/ChatBot/View/ChatCollectionView.swift index 053b401b..990beeac 100644 --- a/ChatBot/ChatBot/View/ChatCollectionView.swift +++ b/ChatBot/ChatBot/View/ChatCollectionView.swift @@ -23,4 +23,13 @@ extension ChatCollectionView { func setupChatCell() { self.register(ChatCell.self, forCellWithReuseIdentifier: ChatCell.identifier) } + + func srollToBottom() { + guard self.numberOfSections > 0 else { return } + let indexPath = IndexPath( + item: self.numberOfItems(inSection: numberOfSections - 1) - 1, + section: numberOfSections - 1 + ) + self.scrollToItem(at: indexPath, at: .bottom, animated: true) + } } From 02c23c08b0fd1abb3360b3adabbcae1591d5c066 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Mon, 8 Apr 2024 19:30:52 +0900 Subject: [PATCH 11/36] =?UTF-8?q?feat:=20UICollectionView=20=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=EC=84=A0=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=ED=9A=A8=EA=B3=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/Controller/ChatBotViewController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index 5e6aaa89..b1ab156b 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -10,9 +10,11 @@ import Combine final class ChatBotViewController: UIViewController { private lazy var chatCollectionView: ChatCollectionView = { - let configure = UICollectionLayoutListConfiguration(appearance: .plain) + var configure = UICollectionLayoutListConfiguration(appearance: .plain) + configure.showsSeparators = false let layout = UICollectionViewCompositionalLayout.list(using: configure) let collectionView = ChatCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.allowsSelection = false return collectionView }() private lazy var dataSource = ChatCollectionViewDataSource(collectionView: chatCollectionView) @@ -73,6 +75,9 @@ private extension ChatBotViewController { print(error.localizedDescription) case .fetchChatResponseDidSucceed(let response): self?.applyChatResponse(response: response) + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + self?.chatCollectionView.srollToBottom() + } case .toggleSendButton(let isEnable): print("\(isEnable)") } From 6e7b29bc5009796e9d65320036110a6be79042c2 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Mon, 8 Apr 2024 19:31:07 +0900 Subject: [PATCH 12/36] =?UTF-8?q?fix:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B7=B0=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatBubbleView.swift | 73 +++++++++++++---------- ChatBot/ChatBot/View/MessageView.swift | 48 +++++++-------- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index 18e39f5b..33cb984d 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -8,38 +8,34 @@ import UIKit final class UserBubbleView: ChatBubbleView { - private var bezierPath: UIBezierPath? + + override init(frame: CGRect) { + super.init(frame: frame) + contentMode = .redraw + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func draw(_ rect: CGRect) { - guard - bezierPath != nil - else { - let bezierPath = UIBezierPath() - self.bezierPath = bezierPath - setRightBubbleView(rect: rect, bezierPath: bezierPath) - bezierPath.close() - backgroundColor?.setFill() - bezierPath.fill() - return - } + setRightBubbleView(rect: rect) } } final class SystemBubbleView: ChatBubbleView { - private var bezierPath: UIBezierPath? + + override init(frame: CGRect) { + super.init(frame: frame) + contentMode = .redraw + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func draw(_ rect: CGRect) { - guard - bezierPath != nil - else { - let bezierPath = UIBezierPath() - self.bezierPath = bezierPath - setLeftBubbleView(rect: rect, bezierPath: bezierPath) - bezierPath.close() - backgroundColor?.setFill() - bezierPath.fill() - return - } + setLeftBubbleView(rect: rect) } } @@ -48,9 +44,11 @@ class ChatBubbleView: UIView { override init(frame: CGRect) { super.init(frame: frame) + contentMode = .redraw self.backgroundColor = .clear self.configureUI() self.setupConstraints() +// messageView.setupSize() } required init?(coder: NSCoder) { @@ -59,7 +57,6 @@ class ChatBubbleView: UIView { func configureMessage(text: String) { messageView.text = text - messageView.setupSize() } } @@ -67,25 +64,29 @@ private extension ChatBubbleView { func configureUI() { self.addSubview(messageView) messageView.translatesAutoresizingMaskIntoConstraints = false + self.translatesAutoresizingMaskIntoConstraints = false } func setupConstraints() { NSLayoutConstraint.activate( [ - self.widthAnchor.constraint(equalTo: messageView.widthAnchor, constant: 20), + + // self.widthAnchor.constraint(equalTo: messageView.widthAnchor, constant: 30), self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - messageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + // messageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + messageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), + messageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), + self.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.width * 0.75), ] ) } - func setRightBubbleView(rect: CGRect,bezierPath: UIBezierPath) { - self.backgroundColor = .systemBlue + func setRightBubbleView(rect: CGRect) { + let bezierPath = UIBezierPath() messageView.textColor = .white let bottom = rect.height let right = rect.width - bezierPath.move( to: CGPoint(x: right - 22, y: bottom) ) @@ -134,11 +135,13 @@ private extension ChatBubbleView { controlPoint1: CGPoint(x: right - 16, y: bottom), controlPoint2: CGPoint(x: right - 19, y: bottom) ) - + bezierPath.close() + UIColor(cgColor: UIColor.systemBlue.cgColor).setFill() + bezierPath.fill() } - func setLeftBubbleView(rect: CGRect, bezierPath: UIBezierPath) { - self.backgroundColor = .systemGray5 + func setLeftBubbleView(rect: CGRect) { + let bezierPath = UIBezierPath() let bottom = rect.height let right = rect.width @@ -190,5 +193,9 @@ private extension ChatBubbleView { controlPoint1: CGPoint(x: 16, y: bottom), controlPoint2: CGPoint(x: 19, y: bottom) ) + bezierPath.close() + UIColor(cgColor: UIColor.systemGray5.cgColor).setFill() + bezierPath.fill() } } + diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageView.swift index 016ddce7..85ef8984 100644 --- a/ChatBot/ChatBot/View/MessageView.swift +++ b/ChatBot/ChatBot/View/MessageView.swift @@ -8,36 +8,35 @@ import UIKit final class MessageView: UILabel { - private var width: Double = 0.0 override init(frame: CGRect) { super.init(frame: frame) configureUI() + // setupConstraints() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func drawText(in rect: CGRect) { - let insets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 0) - super.drawText(in: rect.inset(by: insets)) - } + // override func drawText(in rect: CGRect) { + // let insets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) + // super.drawText(in: rect.inset(by: insets)) + // } - func setupSize() { - guard let text = self.text as? NSString else { return } - - width = text.size( - withAttributes: [ - NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) - ] - ).width - - if width >= UIScreen.main.bounds.width * 0.75 { - width = UIScreen.main.bounds.width * 0.75 - } - setupConstraints() - } +// func setupSize() { +// guard let text = self.text as? NSString else { return } +// +// let height = text.size( +// withAttributes: +// [ +// NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) +// ] +// ).height +// NSLayoutConstraint.activate([ +// self.heightAnchor.constraint(greaterThanOrEqualToConstant: height), +// ]) +// } } private extension MessageView { @@ -49,10 +48,9 @@ private extension MessageView { } - - func setupConstraints() { - NSLayoutConstraint.activate([ - self.widthAnchor.constraint(equalToConstant: width), - ]) - } + // func setupConstraints() { + // NSLayoutConstraint.activate([ + // self.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.width * 0.75), + // ]) + // } } From 5430115894417d269d1ae1d604ab77a7ea46f5da Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Tue, 9 Apr 2024 23:09:58 +0900 Subject: [PATCH 13/36] =?UTF-8?q?feat:=20ChatInputView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatInputTextView.swift | 33 ++++++++++++++ ChatBot/ChatBot/View/ChatInputView.swift | 45 ++++++++++++++++++++ ChatBot/ChatBot/View/ChatSendButton.swift | 19 +++++++++ 3 files changed, 97 insertions(+) create mode 100644 ChatBot/ChatBot/View/ChatInputTextView.swift create mode 100644 ChatBot/ChatBot/View/ChatInputView.swift create mode 100644 ChatBot/ChatBot/View/ChatSendButton.swift diff --git a/ChatBot/ChatBot/View/ChatInputTextView.swift b/ChatBot/ChatBot/View/ChatInputTextView.swift new file mode 100644 index 00000000..ebd35936 --- /dev/null +++ b/ChatBot/ChatBot/View/ChatInputTextView.swift @@ -0,0 +1,33 @@ +// +// ChatInputTextView.swift +// ChatBot +// +// Created by 이보한 on 2024/4/8. +// + +import UIKit + +final class ChatInputTextView: UITextView, UITextViewDelegate { + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + self.font = .systemFont(ofSize: 20) + self.layer.borderWidth = 2 + self.layer.cornerRadius = 20 + self.layer.borderColor = CGColor(red: 0.1, green: 0.1, blue: 0.5, alpha: 1) + self.isScrollEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func textViewDidChange(_ textView: UITextView) { + let size = CGSize(width: self.frame.width, height: .infinity) + let estimatedSize = textView.sizeThatFits(size) + textView.constraints.forEach { constraints in + if constraints.firstAttribute == .height { + constraints.constant = estimatedSize.height + } + } + } +} diff --git a/ChatBot/ChatBot/View/ChatInputView.swift b/ChatBot/ChatBot/View/ChatInputView.swift new file mode 100644 index 00000000..4bf6e2f0 --- /dev/null +++ b/ChatBot/ChatBot/View/ChatInputView.swift @@ -0,0 +1,45 @@ +// +// ChatInputView.swift +// ChatBot +// +// Created by 이보한 on 2024/4/8. +// + +import UIKit + +final class ChatInputView: UIView { + private let chatInputTextView = ChatInputTextView() + private let chatSendButton = ChatSendButton() + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + self.addSubview(chatInputTextView) + self.addSubview(chatSendButton) + setupConstraints() + } + + private func setupConstraints() { + chatInputTextView.translatesAutoresizingMaskIntoConstraints = false + chatSendButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + chatInputTextView.leadingAnchor.constraint(equalTo: self.leadingAnchor,constant: 15), + chatInputTextView.trailingAnchor.constraint(equalTo: chatSendButton.leadingAnchor, constant: -5), + chatInputTextView.topAnchor.constraint(equalTo: self.topAnchor), + chatInputTextView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + chatSendButton.widthAnchor.constraint(equalToConstant: 45), + chatSendButton.heightAnchor.constraint(equalToConstant: 45), + chatSendButton.centerYAnchor.constraint(equalTo: chatInputTextView.centerYAnchor), + chatSendButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) + ]) + } +} diff --git a/ChatBot/ChatBot/View/ChatSendButton.swift b/ChatBot/ChatBot/View/ChatSendButton.swift new file mode 100644 index 00000000..e706e8ce --- /dev/null +++ b/ChatBot/ChatBot/View/ChatSendButton.swift @@ -0,0 +1,19 @@ +// +// ChatSendButton.swift +// ChatBot +// +// Created by 이보한 on 2024/4/8. +// + +import UIKit + +final class ChatSendButton: UIButton { + override init(frame: CGRect) { + super.init(frame: frame) + setBackgroundImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} From ef9b4f3bf951fe7b644ac63a12cf45e66250ec64 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Tue, 9 Apr 2024 23:11:04 +0900 Subject: [PATCH 14/36] =?UTF-8?q?feat:=20ChatInputView=20=EC=98=A4?= =?UTF-8?q?=ED=86=A0=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 12 ++++++++++++ .../ChatBot/Controller/ChatBotViewController.swift | 11 ++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index ec75f211..ff28ed75 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ 27EFEA832BBFD813004A3426 /* ChatCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */; }; 27EFEA852BBFD836004A3426 /* ChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA842BBFD836004A3426 /* ChatCell.swift */; }; 27EFEA872BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */; }; + 893621222BC4014D00EFB2C7 /* ChatInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621212BC4014D00EFB2C7 /* ChatInputView.swift */; }; + 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */; }; + 893621262BC40E5500EFB2C7 /* ChatSendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621252BC40E5500EFB2C7 /* ChatSendButton.swift */; }; 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A652BBE938700354947 /* ChatBubbleView.swift */; }; 89510A682BBE9BC400354947 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A672BBE9BC400354947 /* MessageView.swift */; }; 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A632BB4233C00C643D3 /* ResponseModel.swift */; }; @@ -70,6 +73,9 @@ 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionView.swift; sourceTree = ""; }; 27EFEA842BBFD836004A3426 /* ChatCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCell.swift; sourceTree = ""; }; 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewDataSource.swift; sourceTree = ""; }; + 893621212BC4014D00EFB2C7 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = ""; }; + 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputTextView.swift; sourceTree = ""; }; + 893621252BC40E5500EFB2C7 /* ChatSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSendButton.swift; sourceTree = ""; }; 89510A652BBE938700354947 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; 89510A672BBE9BC400354947 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 89A49A632BB4233C00C643D3 /* ResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseModel.swift; sourceTree = ""; }; @@ -198,6 +204,9 @@ 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */, 27EFEA842BBFD836004A3426 /* ChatCell.swift */, 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */, + 893621212BC4014D00EFB2C7 /* ChatInputView.swift */, + 893621252BC40E5500EFB2C7 /* ChatSendButton.swift */, + 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */, ); path = View; sourceTree = ""; @@ -379,6 +388,7 @@ 27EFEA6C2BBD7B32004A3426 /* URLSession+.swift in Sources */, 27EFEA792BBD7DA1004A3426 /* MockDataSample.swift in Sources */, 27EFEA7F2BBE44AA004A3426 /* NetworkURL.swift in Sources */, + 893621222BC4014D00EFB2C7 /* ChatInputView.swift in Sources */, B4B3E2C12B42D1BB00818B3C /* ChatBotViewController.swift in Sources */, 89510A682BBE9BC400354947 /* MessageView.swift in Sources */, 27EFEA7D2BBD7DFD004A3426 /* NetworkTestable.swift in Sources */, @@ -394,8 +404,10 @@ 27EFEA722BBD7B95004A3426 /* NetworkError.swift in Sources */, 27EFEA852BBFD836004A3426 /* ChatCell.swift in Sources */, 27EFEA752BBD7BC0004A3426 /* ChatViewModel.swift in Sources */, + 893621262BC40E5500EFB2C7 /* ChatSendButton.swift in Sources */, 89A49A6C2BB4274B00C643D3 /* Message.swift in Sources */, 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */, + 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */, 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */, 27EFEA6E2BBD7B53004A3426 /* NetworkService.swift in Sources */, B4B3E2BF2B42D1BB00818B3C /* SceneDelegate.swift in Sources */, diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index b1ab156b..02f1135f 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -22,6 +22,8 @@ final class ChatBotViewController: UIViewController { private var cancellable = Set() private let input: PassthroughSubject = .init() + private var chatInputView = ChatInputView() + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white @@ -47,6 +49,7 @@ final class ChatBotViewController: UIViewController { private extension ChatBotViewController { func configureUI() { view.addSubview(chatCollectionView) + view.addSubview(chatInputView) } func setupCollectionView() { @@ -54,13 +57,19 @@ private extension ChatBotViewController { } func setupConstraints() { + chatInputView.translatesAutoresizingMaskIntoConstraints = false chatCollectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate( [ chatCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), chatCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), chatCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - chatCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50) + chatCollectionView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), + + chatInputView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + chatInputView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + chatInputView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ] ) } From 2389e00cbf3c8557f1f60ec8f6e200783743f4a7 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Tue, 9 Apr 2024 23:20:48 +0900 Subject: [PATCH 15/36] =?UTF-8?q?chore:=20ChatSendButton=20->=20ChatInputS?= =?UTF-8?q?endButton=EC=9C=BC=EB=A1=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 8 ++++---- .../{ChatSendButton.swift => ChatInputSendButton.swift} | 2 +- ChatBot/ChatBot/View/ChatInputView.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename ChatBot/ChatBot/View/{ChatSendButton.swift => ChatInputSendButton.swift} (88%) diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index ff28ed75..65ff4f30 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -27,7 +27,7 @@ 27EFEA872BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */; }; 893621222BC4014D00EFB2C7 /* ChatInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621212BC4014D00EFB2C7 /* ChatInputView.swift */; }; 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */; }; - 893621262BC40E5500EFB2C7 /* ChatSendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621252BC40E5500EFB2C7 /* ChatSendButton.swift */; }; + 893621262BC40E5500EFB2C7 /* ChatInputSendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621252BC40E5500EFB2C7 /* ChatInputSendButton.swift */; }; 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A652BBE938700354947 /* ChatBubbleView.swift */; }; 89510A682BBE9BC400354947 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A672BBE9BC400354947 /* MessageView.swift */; }; 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A632BB4233C00C643D3 /* ResponseModel.swift */; }; @@ -75,7 +75,7 @@ 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewDataSource.swift; sourceTree = ""; }; 893621212BC4014D00EFB2C7 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = ""; }; 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputTextView.swift; sourceTree = ""; }; - 893621252BC40E5500EFB2C7 /* ChatSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSendButton.swift; sourceTree = ""; }; + 893621252BC40E5500EFB2C7 /* ChatInputSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputSendButton.swift; sourceTree = ""; }; 89510A652BBE938700354947 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; 89510A672BBE9BC400354947 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 89A49A632BB4233C00C643D3 /* ResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseModel.swift; sourceTree = ""; }; @@ -205,7 +205,7 @@ 27EFEA842BBFD836004A3426 /* ChatCell.swift */, 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */, 893621212BC4014D00EFB2C7 /* ChatInputView.swift */, - 893621252BC40E5500EFB2C7 /* ChatSendButton.swift */, + 893621252BC40E5500EFB2C7 /* ChatInputSendButton.swift */, 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */, ); path = View; @@ -404,7 +404,7 @@ 27EFEA722BBD7B95004A3426 /* NetworkError.swift in Sources */, 27EFEA852BBFD836004A3426 /* ChatCell.swift in Sources */, 27EFEA752BBD7BC0004A3426 /* ChatViewModel.swift in Sources */, - 893621262BC40E5500EFB2C7 /* ChatSendButton.swift in Sources */, + 893621262BC40E5500EFB2C7 /* ChatInputSendButton.swift in Sources */, 89A49A6C2BB4274B00C643D3 /* Message.swift in Sources */, 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */, 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */, diff --git a/ChatBot/ChatBot/View/ChatSendButton.swift b/ChatBot/ChatBot/View/ChatInputSendButton.swift similarity index 88% rename from ChatBot/ChatBot/View/ChatSendButton.swift rename to ChatBot/ChatBot/View/ChatInputSendButton.swift index e706e8ce..f06ba0b0 100644 --- a/ChatBot/ChatBot/View/ChatSendButton.swift +++ b/ChatBot/ChatBot/View/ChatInputSendButton.swift @@ -7,7 +7,7 @@ import UIKit -final class ChatSendButton: UIButton { +final class ChatInputSendButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setBackgroundImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) diff --git a/ChatBot/ChatBot/View/ChatInputView.swift b/ChatBot/ChatBot/View/ChatInputView.swift index 4bf6e2f0..67b39432 100644 --- a/ChatBot/ChatBot/View/ChatInputView.swift +++ b/ChatBot/ChatBot/View/ChatInputView.swift @@ -9,7 +9,7 @@ import UIKit final class ChatInputView: UIView { private let chatInputTextView = ChatInputTextView() - private let chatSendButton = ChatSendButton() + private let chatSendButton = ChatInputSendButton() override init(frame: CGRect) { super.init(frame: frame) From b353ade274933924174f451bee7005d3cc54556a Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Thu, 11 Apr 2024 10:34:09 +0900 Subject: [PATCH 16/36] =?UTF-8?q?chore:=20UITextViewDelegate=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatInputTextView.swift | 18 ++++++------------ ChatBot/ChatBot/View/ChatInputView.swift | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatInputTextView.swift b/ChatBot/ChatBot/View/ChatInputTextView.swift index ebd35936..18cf0bb8 100644 --- a/ChatBot/ChatBot/View/ChatInputTextView.swift +++ b/ChatBot/ChatBot/View/ChatInputTextView.swift @@ -7,27 +7,21 @@ import UIKit -final class ChatInputTextView: UITextView, UITextViewDelegate { +final class ChatInputTextView: UITextView { override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) - self.font = .systemFont(ofSize: 20) + self.font = .systemFont(ofSize: 17) self.layer.borderWidth = 2 - self.layer.cornerRadius = 20 + self.layer.cornerRadius = 17 self.layer.borderColor = CGColor(red: 0.1, green: 0.1, blue: 0.5, alpha: 1) self.isScrollEnabled = false + self.textContainerInset.left = 10 + self.textContainerInset.right = 10 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func textViewDidChange(_ textView: UITextView) { - let size = CGSize(width: self.frame.width, height: .infinity) - let estimatedSize = textView.sizeThatFits(size) - textView.constraints.forEach { constraints in - if constraints.firstAttribute == .height { - constraints.constant = estimatedSize.height - } - } - } + } diff --git a/ChatBot/ChatBot/View/ChatInputView.swift b/ChatBot/ChatBot/View/ChatInputView.swift index 67b39432..0be7fc47 100644 --- a/ChatBot/ChatBot/View/ChatInputView.swift +++ b/ChatBot/ChatBot/View/ChatInputView.swift @@ -7,7 +7,7 @@ import UIKit -final class ChatInputView: UIView { +final class ChatInputView: UIView, UITextViewDelegate { private let chatInputTextView = ChatInputTextView() private let chatSendButton = ChatInputSendButton() @@ -20,6 +20,16 @@ final class ChatInputView: UIView { fatalError("init(coder:) has not been implemented") } + private func textViewDidChange(_ textView: ChatInputTextView) { + let size = CGSize(width: self.frame.width, height: .infinity) + let estimatedSize = textView.sizeThatFits(size) + textView.constraints.forEach { constraints in + if constraints.firstAttribute == .height { + constraints.constant = estimatedSize.height + } + } + } + private func configureUI() { self.addSubview(chatInputTextView) self.addSubview(chatSendButton) @@ -36,8 +46,8 @@ final class ChatInputView: UIView { chatInputTextView.topAnchor.constraint(equalTo: self.topAnchor), chatInputTextView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - chatSendButton.widthAnchor.constraint(equalToConstant: 45), - chatSendButton.heightAnchor.constraint(equalToConstant: 45), + chatSendButton.widthAnchor.constraint(equalToConstant: 40), + chatSendButton.heightAnchor.constraint(equalToConstant: 40), chatSendButton.centerYAnchor.constraint(equalTo: chatInputTextView.centerYAnchor), chatSendButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) ]) From 11c4a23e46cfe67f1ad7f3d6bcbf0f9169eaf123 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Thu, 11 Apr 2024 10:35:44 +0900 Subject: [PATCH 17/36] =?UTF-8?q?feat:=20=EC=86=8C=ED=94=84=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=96=B4=20=ED=82=A4=EB=B3=B4=EB=93=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=8B=9C=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ChatBotViewController.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index 02f1135f..a4416bc3 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -9,6 +9,8 @@ import UIKit import Combine final class ChatBotViewController: UIViewController { + static let notificationCenter = NotificationCenter.default + private lazy var chatCollectionView: ChatCollectionView = { var configure = UICollectionLayoutListConfiguration(appearance: .plain) configure.showsSeparators = false @@ -27,6 +29,9 @@ final class ChatBotViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + setupCollectionView() configureUI() setupConstraints() @@ -44,6 +49,22 @@ final class ChatBotViewController: UIViewController { ) ) } + + @objc func keyboardWillShow(notification: NSNotification) { + guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { + return + } + + self.view.frame.origin.y = 0 - keyboardSize.height + } + + @objc func keyboardWillHide(notification: NSNotification) { + self.view.frame.origin.y = 0 + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.view.endEditing(true) + } } private extension ChatBotViewController { From eec20418aa5c6523d769bcd531b270b393e3f056 Mon Sep 17 00:00:00 2001 From: Paul Lee Date: Thu, 11 Apr 2024 16:26:46 +0900 Subject: [PATCH 18/36] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=ED=84=B0?= =?UTF-8?q?=EC=B9=98=20=EC=8B=9C=20=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=88=A8?= =?UTF-8?q?=EA=B9=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ChatBotViewController.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index a4416bc3..4eae4f5b 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -32,6 +32,7 @@ final class ChatBotViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + hideKeyboard() setupCollectionView() configureUI() setupConstraints() @@ -49,6 +50,17 @@ final class ChatBotViewController: UIViewController { ) ) } +} + +private extension ChatBotViewController { + func hideKeyboard() { + let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tap) + } + + @objc func dismissKeyboard() { + view.endEditing(true) + } @objc func keyboardWillShow(notification: NSNotification) { guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { @@ -62,12 +74,6 @@ final class ChatBotViewController: UIViewController { self.view.frame.origin.y = 0 } - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - self.view.endEditing(true) - } -} - -private extension ChatBotViewController { func configureUI() { view.addSubview(chatCollectionView) view.addSubview(chatInputView) From 22621e7b10e2370474a83adeefcffc153c81084b Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 16:18:38 +0900 Subject: [PATCH 19/36] =?UTF-8?q?refine:=20=EC=8B=A4=EC=A0=9C=20=EB=84=A4?= =?UTF-8?q?=ED=8A=B8=EC=9B=8C=ED=81=AC=20=ED=86=B5=EC=8B=A0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/Model/Network/NetworkService.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ChatBot/ChatBot/Model/Network/NetworkService.swift b/ChatBot/ChatBot/Model/Network/NetworkService.swift index 018cf43a..4131f33e 100644 --- a/ChatBot/ChatBot/Model/Network/NetworkService.swift +++ b/ChatBot/ChatBot/Model/Network/NetworkService.swift @@ -12,7 +12,11 @@ struct NetworkService: NetworkTestable { private let session: URLSessionProtocol - init(session: URLSessionProtocol = MockURLSession(statusCode: 200)) { +// init(session: URLSessionProtocol = MockURLSession(statusCode: 200)) { +// self.session = session +// } + + init(session: URLSessionProtocol = URLSession.shared) { self.session = session } @@ -29,3 +33,4 @@ struct NetworkService: NetworkTestable { .eraseToAnyPublisher() } } + From 005572918ac6b513c26f3e9cb5775bc10c954e81 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 16:19:21 +0900 Subject: [PATCH 20/36] =?UTF-8?q?feat:=20requestDTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/ViewModel/ChatViewModel.swift | 124 +++++++++--------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift index 4cd8be3b..efba354c 100644 --- a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift +++ b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift @@ -9,76 +9,82 @@ import Foundation import Combine final class ChatViewModel { - var requestModel: RequestModel? - private let networkService: NetworkService - private let output: PassthroughSubject = .init() - private var cancellables: Set = .init() - - enum Input { - case sendButtonTapped(message: Message) - } - - enum Output { - case fetchChatResponseDidFail(error: NetworkError) - case fetchChatResponseDidSucceed(response: RequestModel) - case toggleSendButton(isEnable: Bool) - } - - init(networkService: NetworkService = NetworkService()) { - self.networkService = networkService - } - - func transform(input: AnyPublisher) -> AnyPublisher { - input.sink { [weak self] event in - switch event { - case .sendButtonTapped(let message): - guard let body = self?.makeBody(message: message) else { return } - self?.fetchChatBotData(body: body) - } - }.store(in: &cancellables) - return output.eraseToAnyPublisher() - } + private var requestModel: RequestModel? + private let networkService: NetworkService + private let output: PassthroughSubject = .init() + private var cancellables: Set = .init() + var requestDTO: [Message] = [] + + enum Input { + case sendButtonTapped(message: Message) + } + + enum Output { + case fetchChatResponseDidFail(error: NetworkError) + case fetchChatResponseDidSucceed(response: [Message]) + case toggleSendButton(isEnable: Bool) + } + + init(networkService: NetworkService = NetworkService()) { + self.networkService = networkService + } + + func transform(input: AnyPublisher) -> AnyPublisher { + input.sink { [weak self] event in + switch event { + case .sendButtonTapped(let message): + guard let body = self?.makeBody(message: message) else { return } + self?.fetchChatBotData(body: body) + } + }.store(in: &cancellables) + return output.eraseToAnyPublisher() + } } private extension ChatViewModel { - func fetchChatBotData(body: RequestModel) { - self.output.send(.toggleSendButton(isEnable: false)) - networkService.fetchChatBotResponse( - type: .chatbot, - httpMethod: .POST(body: body) - ) - .sink { [weak self] completion in - switch completion { - case .finished: - self?.output.send(.toggleSendButton(isEnable: true)) - case .failure(let error): - self?.output.send(.fetchChatResponseDidFail(error: error)) - } - } receiveValue: { [weak self] response in - self?.requestModel?.messages.append(response.choices[0].message) - guard let requestModel = self?.requestModel else { return } - self?.output.send(.fetchChatResponseDidSucceed(response: requestModel)) - } - .store(in: &cancellables) + func fetchChatBotData(body: RequestModel) { + self.output.send(.toggleSendButton(isEnable: false)) + networkService.fetchChatBotResponse( + type: .chatbot, + httpMethod: .POST(body: body) + ) + .sink { [weak self] completion in + switch completion { + case .finished: + self?.output.send(.toggleSendButton(isEnable: false)) + case .failure(let error): + self?.output.send(.fetchChatResponseDidFail(error: error)) + } + } receiveValue: { [weak self] response in + self?.requestDTO.removeLast() + self?.requestDTO.append(response.choices[0].message) + guard let requestDTO = self?.requestDTO else { return } + self?.output.send(.fetchChatResponseDidSucceed(response: requestDTO)) + } + .store(in: &cancellables) } func makeBody(message: Message) -> RequestModel? { + let responseMessage = Message(role: "assistant", content: "") guard requestModel != nil else { -// requestModel = RequestModel( -// messages: [ -// Message( -// role: "system", -// content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." -// ), -// message -// ] -// ) - requestModel = MockDataSample.requestData + requestModel = RequestModel( + messages: [ + Message( + role: "system", + content: "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair." + ), + message + ] + ) + requestDTO.append(message) + requestDTO.append(responseMessage) return requestModel } - requestModel?.messages.append(message) + requestDTO.append(message) + requestDTO.append(responseMessage) return requestModel } } + From 193ac2ab876fc7aa12d64ef106f55767643ae776 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 16:19:39 +0900 Subject: [PATCH 21/36] =?UTF-8?q?feat:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B2=84=EB=B8=94=20=EB=B7=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ChatBotViewController.swift | 41 +++--- ChatBot/ChatBot/View/ChatBubbleView.swift | 136 ++++++++++++++++-- ChatBot/ChatBot/View/ChatCell.swift | 8 +- ChatBot/ChatBot/View/ChatCollectionView.swift | 14 +- .../View/ChatCollectionViewDataSource.swift | 13 +- .../ChatBot/View/ChatInputSendButton.swift | 3 +- ChatBot/ChatBot/View/ChatInputTextView.swift | 8 +- ChatBot/ChatBot/View/ChatInputView.swift | 52 +++++-- ChatBot/ChatBot/View/MessageView.swift | 27 ---- 9 files changed, 213 insertions(+), 89 deletions(-) diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index 4eae4f5b..21bbcb52 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -37,18 +37,15 @@ final class ChatBotViewController: UIViewController { configureUI() setupConstraints() bind() + chatInputView.setChatSendButton { [weak self] message in + self?.input.send(.sendButtonTapped(message: message)) + guard let requestDTO = self?.chatBotViewModel.requestDTO else { return } + self?.applyChatResponse(response: requestDTO) + } } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - input.send( - .sendButtonTapped( - message: Message( - role: "user", - content: "Compose a poem that explains the concept of recursion in programming." - ) - ) - ) + + deinit { + NotificationCenter.default.removeObserver(self) } } @@ -58,12 +55,16 @@ private extension ChatBotViewController { view.addGestureRecognizer(tap) } - @objc func dismissKeyboard() { + @objc + func dismissKeyboard() { view.endEditing(true) } - @objc func keyboardWillShow(notification: NSNotification) { - guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { + @objc + func keyboardWillShow(notification: NSNotification) { + guard + let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue + else { return } @@ -92,7 +93,7 @@ private extension ChatBotViewController { chatCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), chatCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), chatCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - chatCollectionView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), + chatCollectionView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor, constant: -10), chatInputView.leadingAnchor.constraint(equalTo: view.leadingAnchor), chatInputView.trailingAnchor.constraint(equalTo: view.trailingAnchor), @@ -111,20 +112,18 @@ private extension ChatBotViewController { print(error.localizedDescription) case .fetchChatResponseDidSucceed(let response): self?.applyChatResponse(response: response) - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - self?.chatCollectionView.srollToBottom() - } + self?.chatCollectionView.srollToBottom() case .toggleSendButton(let isEnable): - print("\(isEnable)") + self?.chatInputView.isEnable = isEnable } } .store(in: &cancellable) } - func applyChatResponse(response: RequestModel) { + func applyChatResponse(response: [Message]) { var chatCollectionViewSnapshot = ChatCollectionViewSnapshot() chatCollectionViewSnapshot.appendSections([.messages]) - chatCollectionViewSnapshot.appendItems(response.messages) + chatCollectionViewSnapshot.appendItems(response) dataSource.apply(chatCollectionViewSnapshot) } } diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index 33cb984d..453ede0a 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -19,11 +19,24 @@ final class UserBubbleView: ChatBubbleView { } override func draw(_ rect: CGRect) { + setupConstraints() setRightBubbleView(rect: rect) } } final class SystemBubbleView: ChatBubbleView { + var isLoading: Bool { + get { + false + } + set { + if !newValue { + removeLoadingView() + } + } + } + + private var loadingView = UIView() override init(frame: CGRect) { super.init(frame: frame) @@ -35,20 +48,71 @@ final class SystemBubbleView: ChatBubbleView { } override func draw(_ rect: CGRect) { - setLeftBubbleView(rect: rect) + guard isLoading else { + setLeftBubbleView(rect: rect) + return + } + addLoadingAnimation(to: rect) + setLoadingBubbleView() + } + + func configureMessage(text: String, isLoading: Bool) { + messageView.text = text + self.isLoading = isLoading + } + + func addLoadingAnimation(to rect: CGRect) { + let circleSize = CGSize(width: 10, height: 10) // 원의 크기 + let numberOfCircles = 3 // 원의 개수 + let circleSpacing: CGFloat = 10 // 원 사이의 간격 + let width: CGFloat = 80 + let height: CGFloat = 40 + // 로딩 중 표시를 위해 원을 그리는 뷰 생성 + loadingView = UIView(frame: CGRect(x: (width - CGFloat(numberOfCircles) * (circleSize.width + circleSpacing)) / 2, + y: height / 2 - circleSize.height / 2, + width: CGFloat(numberOfCircles) * (circleSize.width + circleSpacing), + height: circleSize.height)) + + // 원을 그리고 애니메이션을 추가 + for i in 0.. 0 else { return } - let indexPath = IndexPath( - item: self.numberOfItems(inSection: numberOfSections - 1) - 1, - section: numberOfSections - 1 - ) - self.scrollToItem(at: indexPath, at: .bottom, animated: true) + DispatchQueue.main.async { + guard self.numberOfSections > 0 else { return } + let indexPath = IndexPath( + item: self.numberOfItems(inSection: self.numberOfSections - 1) - 1, + section: self.numberOfSections - 1 + ) + self.scrollToItem(at: indexPath, at: .bottom, animated: true) + } } } diff --git a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift index a8a03a95..4677e1ac 100644 --- a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift +++ b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift @@ -16,11 +16,20 @@ typealias ChatCollectionViewSnapshot = NSDiffableDataSourceSnapshot { static let cellProvider: CellProvider = { collectionView, indexPath, message in guard - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ChatCell.identifier, for: indexPath) as? ChatCell + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: ChatCell.identifier, + for: indexPath + ) as? ChatCell else { return ChatCell() } - message.role == "user" ? cell.configureUser(text: message.content) : cell.configureSystem(text: message.content) + + guard message.role != "user" else { + cell.configureUser(text: message.content) + return cell + } + + cell.configureSystem(text: message.content, isLoading: true) return cell } diff --git a/ChatBot/ChatBot/View/ChatInputSendButton.swift b/ChatBot/ChatBot/View/ChatInputSendButton.swift index f06ba0b0..80066d40 100644 --- a/ChatBot/ChatBot/View/ChatInputSendButton.swift +++ b/ChatBot/ChatBot/View/ChatInputSendButton.swift @@ -10,7 +10,8 @@ import UIKit final class ChatInputSendButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) - setBackgroundImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) + self.isEnabled = false + self.setBackgroundImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) } required init?(coder: NSCoder) { diff --git a/ChatBot/ChatBot/View/ChatInputTextView.swift b/ChatBot/ChatBot/View/ChatInputTextView.swift index 18cf0bb8..9e095963 100644 --- a/ChatBot/ChatBot/View/ChatInputTextView.swift +++ b/ChatBot/ChatBot/View/ChatInputTextView.swift @@ -10,10 +10,10 @@ import UIKit final class ChatInputTextView: UITextView { override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) - self.font = .systemFont(ofSize: 17) - self.layer.borderWidth = 2 + self.font = .preferredFont(forTextStyle: .body) + self.layer.borderWidth = 1 self.layer.cornerRadius = 17 - self.layer.borderColor = CGColor(red: 0.1, green: 0.1, blue: 0.5, alpha: 1) + self.layer.borderColor = UIColor.systemGray2.cgColor self.isScrollEnabled = false self.textContainerInset.left = 10 self.textContainerInset.right = 10 @@ -22,6 +22,4 @@ final class ChatInputTextView: UITextView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - } diff --git a/ChatBot/ChatBot/View/ChatInputView.swift b/ChatBot/ChatBot/View/ChatInputView.swift index 0be7fc47..a1d00ef3 100644 --- a/ChatBot/ChatBot/View/ChatInputView.swift +++ b/ChatBot/ChatBot/View/ChatInputView.swift @@ -7,49 +7,73 @@ import UIKit -final class ChatInputView: UIView, UITextViewDelegate { +final class ChatInputView: UIView { private let chatInputTextView = ChatInputTextView() private let chatSendButton = ChatInputSendButton() + var isEnable: Bool { + get { + chatSendButton.isEnabled + } + set { + DispatchQueue.main.async { + self.chatSendButton.isEnabled = newValue + } + } + } override init(frame: CGRect) { super.init(frame: frame) configureUI() + self.chatInputTextView.delegate = self } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func textViewDidChange(_ textView: ChatInputTextView) { - let size = CGSize(width: self.frame.width, height: .infinity) - let estimatedSize = textView.sizeThatFits(size) - textView.constraints.forEach { constraints in - if constraints.firstAttribute == .height { - constraints.constant = estimatedSize.height - } + func setChatSendButton(completion: @escaping (_ message: Message) -> Void) { + let action = UIAction { [weak self] _ in + guard let text = self?.chatInputTextView.text else { return } + completion(Message(role: "user", content: text)) + self?.chatInputTextView.text = "" } + self.chatSendButton.addAction(action, for: .touchUpInside) } - - private func configureUI() { +} + +private extension ChatInputView { + func configureUI() { self.addSubview(chatInputTextView) self.addSubview(chatSendButton) setupConstraints() } - private func setupConstraints() { + func setupConstraints() { chatInputTextView.translatesAutoresizingMaskIntoConstraints = false chatSendButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ chatInputTextView.leadingAnchor.constraint(equalTo: self.leadingAnchor,constant: 15), chatInputTextView.trailingAnchor.constraint(equalTo: chatSendButton.leadingAnchor, constant: -5), - chatInputTextView.topAnchor.constraint(equalTo: self.topAnchor), + chatInputTextView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5), chatInputTextView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - chatSendButton.widthAnchor.constraint(equalToConstant: 40), - chatSendButton.heightAnchor.constraint(equalToConstant: 40), + chatSendButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.height / 20), + chatSendButton.heightAnchor.constraint(equalTo: chatSendButton.widthAnchor), chatSendButton.centerYAnchor.constraint(equalTo: chatInputTextView.centerYAnchor), chatSendButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) ]) } } + +extension ChatInputView: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + guard + textView.text.isEmpty + else { + self.chatSendButton.isEnabled = true + return + } + self.chatSendButton.isEnabled = false + } +} diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageView.swift index 85ef8984..a0e19170 100644 --- a/ChatBot/ChatBot/View/MessageView.swift +++ b/ChatBot/ChatBot/View/MessageView.swift @@ -12,31 +12,11 @@ final class MessageView: UILabel { override init(frame: CGRect) { super.init(frame: frame) configureUI() - // setupConstraints() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // override func drawText(in rect: CGRect) { - // let insets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) - // super.drawText(in: rect.inset(by: insets)) - // } - -// func setupSize() { -// guard let text = self.text as? NSString else { return } -// -// let height = text.size( -// withAttributes: -// [ -// NSAttributedString.Key.font : self.font ?? .preferredFont(forTextStyle: .body) -// ] -// ).height -// NSLayoutConstraint.activate([ -// self.heightAnchor.constraint(greaterThanOrEqualToConstant: height), -// ]) -// } } private extension MessageView { @@ -46,11 +26,4 @@ private extension MessageView { self.textAlignment = .left self.numberOfLines = 0 } - - - // func setupConstraints() { - // NSLayoutConstraint.activate([ - // self.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.width * 0.75), - // ]) - // } } From 513a18ded7c962ae109fc7a60c3d4b6f2a992817 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:14:03 +0900 Subject: [PATCH 22/36] =?UTF-8?q?feat:=20RequestDTO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/UIViewController+.swift | 8 +++++ .../ChatBot/Model/Request/RequestModel.swift | 9 +++++- ChatBot/ChatBot/ViewModel/ChatViewModel.swift | 30 +++++++++++++------ 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 ChatBot/ChatBot/Extensions/UIViewController+.swift diff --git a/ChatBot/ChatBot/Extensions/UIViewController+.swift b/ChatBot/ChatBot/Extensions/UIViewController+.swift new file mode 100644 index 00000000..7cc80267 --- /dev/null +++ b/ChatBot/ChatBot/Extensions/UIViewController+.swift @@ -0,0 +1,8 @@ +// +// UIViewController+.swift +// ChatBot +// +// Created by 강창현 on 4/14/24. +// + +import Foundation diff --git a/ChatBot/ChatBot/Model/Request/RequestModel.swift b/ChatBot/ChatBot/Model/Request/RequestModel.swift index b9e5ffe6..b0e8fb35 100644 --- a/ChatBot/ChatBot/Model/Request/RequestModel.swift +++ b/ChatBot/ChatBot/Model/Request/RequestModel.swift @@ -5,8 +5,15 @@ // Created by 이보한 on 2024/3/27. // -struct RequestModel: Codable, Hashable { +import Foundation + +struct RequestModel: Codable { var model: String = "gpt-3.5-turbo-1106" var stream: Bool = false var messages: [Message] } + +struct RequestDTO: Hashable { + let id: UUID + var message: Message +} diff --git a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift index efba354c..dbc3d7d1 100644 --- a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift +++ b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift @@ -13,7 +13,7 @@ final class ChatViewModel { private let networkService: NetworkService private let output: PassthroughSubject = .init() private var cancellables: Set = .init() - var requestDTO: [Message] = [] + var requestDTO: [RequestDTO] = [] enum Input { case sendButtonTapped(message: Message) @@ -21,7 +21,7 @@ final class ChatViewModel { enum Output { case fetchChatResponseDidFail(error: NetworkError) - case fetchChatResponseDidSucceed(response: [Message]) + case fetchChatResponseDidSucceed(response: [RequestDTO]) case toggleSendButton(isEnable: Bool) } @@ -51,13 +51,18 @@ private extension ChatViewModel { .sink { [weak self] completion in switch completion { case .finished: - self?.output.send(.toggleSendButton(isEnable: false)) + self?.output.send(.toggleSendButton(isEnable: true)) case .failure(let error): self?.output.send(.fetchChatResponseDidFail(error: error)) } } receiveValue: { [weak self] response in self?.requestDTO.removeLast() - self?.requestDTO.append(response.choices[0].message) + self?.requestDTO.append( + RequestDTO( + id: UUID(), + message: response.choices[0].message + ) + ) guard let requestDTO = self?.requestDTO else { return } self?.output.send(.fetchChatResponseDidSucceed(response: requestDTO)) } @@ -65,7 +70,7 @@ private extension ChatViewModel { } func makeBody(message: Message) -> RequestModel? { - let responseMessage = Message(role: "assistant", content: "") + let responseMessage = Message(role: "assistant", content: "● ● ●") guard requestModel != nil else { @@ -78,13 +83,20 @@ private extension ChatViewModel { message ] ) - requestDTO.append(message) - requestDTO.append(responseMessage) + setRequestDTO(message, responseMessage) return requestModel } - requestDTO.append(message) - requestDTO.append(responseMessage) + setRequestDTO(message, responseMessage) return requestModel } + + func setRequestDTO(_ message: Message, _ responseMessage: Message) { + requestDTO.append( + RequestDTO(id: UUID(), message: message) + ) + requestDTO.append( + RequestDTO(id: UUID(), message: responseMessage) + ) + } } From 35fbc1134512aa7b1eca162ed8e95d0312363c7b Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:15:08 +0900 Subject: [PATCH 23/36] =?UTF-8?q?refine:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B2=84=EB=B8=94=20=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatBubbleView.swift | 148 +--------------------- ChatBot/ChatBot/View/MessageView.swift | 1 - 2 files changed, 3 insertions(+), 146 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index 453ede0a..efde0bfe 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -8,105 +8,20 @@ import UIKit final class UserBubbleView: ChatBubbleView { - - override init(frame: CGRect) { - super.init(frame: frame) - contentMode = .redraw - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - override func draw(_ rect: CGRect) { - setupConstraints() setRightBubbleView(rect: rect) } } final class SystemBubbleView: ChatBubbleView { - var isLoading: Bool { - get { - false - } - set { - if !newValue { - removeLoadingView() - } - } - } - - private var loadingView = UIView() - - override init(frame: CGRect) { - super.init(frame: frame) - contentMode = .redraw - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - override func draw(_ rect: CGRect) { - guard isLoading else { - setLeftBubbleView(rect: rect) - return - } - addLoadingAnimation(to: rect) - setLoadingBubbleView() - } - - func configureMessage(text: String, isLoading: Bool) { - messageView.text = text - self.isLoading = isLoading - } - - func addLoadingAnimation(to rect: CGRect) { - let circleSize = CGSize(width: 10, height: 10) // 원의 크기 - let numberOfCircles = 3 // 원의 개수 - let circleSpacing: CGFloat = 10 // 원 사이의 간격 - let width: CGFloat = 80 - let height: CGFloat = 40 - // 로딩 중 표시를 위해 원을 그리는 뷰 생성 - loadingView = UIView(frame: CGRect(x: (width - CGFloat(numberOfCircles) * (circleSize.width + circleSpacing)) / 2, - y: height / 2 - circleSize.height / 2, - width: CGFloat(numberOfCircles) * (circleSize.width + circleSpacing), - height: circleSize.height)) - - // 원을 그리고 애니메이션을 추가 - for i in 0.. Date: Sun, 14 Apr 2024 21:15:44 +0900 Subject: [PATCH 24/36] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatInputTextView.swift | 15 +++++++++++---- ChatBot/ChatBot/View/ChatInputView.swift | 13 ------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatInputTextView.swift b/ChatBot/ChatBot/View/ChatInputTextView.swift index 9e095963..f4f021d4 100644 --- a/ChatBot/ChatBot/View/ChatInputTextView.swift +++ b/ChatBot/ChatBot/View/ChatInputTextView.swift @@ -8,8 +8,19 @@ import UIKit final class ChatInputTextView: UITextView { + override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension ChatInputTextView { + func configureUI() { self.font = .preferredFont(forTextStyle: .body) self.layer.borderWidth = 1 self.layer.cornerRadius = 17 @@ -18,8 +29,4 @@ final class ChatInputTextView: UITextView { self.textContainerInset.left = 10 self.textContainerInset.right = 10 } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } } diff --git a/ChatBot/ChatBot/View/ChatInputView.swift b/ChatBot/ChatBot/View/ChatInputView.swift index a1d00ef3..8f23d848 100644 --- a/ChatBot/ChatBot/View/ChatInputView.swift +++ b/ChatBot/ChatBot/View/ChatInputView.swift @@ -24,7 +24,6 @@ final class ChatInputView: UIView { override init(frame: CGRect) { super.init(frame: frame) configureUI() - self.chatInputTextView.delegate = self } required init?(coder: NSCoder) { @@ -65,15 +64,3 @@ private extension ChatInputView { ]) } } - -extension ChatInputView: UITextViewDelegate { - func textViewDidChange(_ textView: UITextView) { - guard - textView.text.isEmpty - else { - self.chatSendButton.isEnabled = true - return - } - self.chatSendButton.isEnabled = false - } -} From 63bb86407227751e9b2db721829fed95109d7e82 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:16:21 +0900 Subject: [PATCH 25/36] =?UTF-8?q?refactor:=20=ED=82=A4=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/ChatBotViewController.swift | 57 +++++-------------- .../Extensions/UIViewController+.swift | 55 +++++++++++++++++- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index 21bbcb52..6ddf8b30 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -9,8 +9,7 @@ import UIKit import Combine final class ChatBotViewController: UIViewController { - static let notificationCenter = NotificationCenter.default - + private lazy var chatCollectionView: ChatCollectionView = { var configure = UICollectionLayoutListConfiguration(appearance: .plain) configure.showsSeparators = false @@ -23,25 +22,16 @@ final class ChatBotViewController: UIViewController { private let chatBotViewModel: ChatViewModel = .init() private var cancellable = Set() private let input: PassthroughSubject = .init() - - private var chatInputView = ChatInputView() + private let chatInputView = ChatInputView() override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white - NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - - hideKeyboard() + setupKeyboardNotification() setupCollectionView() configureUI() setupConstraints() bind() - chatInputView.setChatSendButton { [weak self] message in - self?.input.send(.sendButtonTapped(message: message)) - guard let requestDTO = self?.chatBotViewModel.requestDTO else { return } - self?.applyChatResponse(response: requestDTO) - } + setupChatInputView() } deinit { @@ -50,32 +40,8 @@ final class ChatBotViewController: UIViewController { } private extension ChatBotViewController { - func hideKeyboard() { - let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) - view.addGestureRecognizer(tap) - } - - @objc - func dismissKeyboard() { - view.endEditing(true) - } - - @objc - func keyboardWillShow(notification: NSNotification) { - guard - let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue - else { - return - } - - self.view.frame.origin.y = 0 - keyboardSize.height - } - - @objc func keyboardWillHide(notification: NSNotification) { - self.view.frame.origin.y = 0 - } - func configureUI() { + view.backgroundColor = .white view.addSubview(chatCollectionView) view.addSubview(chatInputView) } @@ -94,7 +60,6 @@ private extension ChatBotViewController { chatCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), chatCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), chatCollectionView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor, constant: -10), - chatInputView.leadingAnchor.constraint(equalTo: view.leadingAnchor), chatInputView.trailingAnchor.constraint(equalTo: view.trailingAnchor), chatInputView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) @@ -102,6 +67,14 @@ private extension ChatBotViewController { ) } + func setupChatInputView() { + chatInputView.setChatSendButton { [weak self] message in + self?.input.send(.sendButtonTapped(message: message)) + guard let requestDTO = self?.chatBotViewModel.requestDTO else { return } + self?.applyChatResponse(response: requestDTO) + } + } + func bind() { let output = chatBotViewModel.transform( input: input.eraseToAnyPublisher() @@ -112,7 +85,6 @@ private extension ChatBotViewController { print(error.localizedDescription) case .fetchChatResponseDidSucceed(let response): self?.applyChatResponse(response: response) - self?.chatCollectionView.srollToBottom() case .toggleSendButton(let isEnable): self?.chatInputView.isEnable = isEnable } @@ -120,10 +92,11 @@ private extension ChatBotViewController { .store(in: &cancellable) } - func applyChatResponse(response: [Message]) { + func applyChatResponse(response: [RequestDTO]) { var chatCollectionViewSnapshot = ChatCollectionViewSnapshot() chatCollectionViewSnapshot.appendSections([.messages]) chatCollectionViewSnapshot.appendItems(response) dataSource.apply(chatCollectionViewSnapshot) + chatCollectionView.srollToBottom() } } diff --git a/ChatBot/ChatBot/Extensions/UIViewController+.swift b/ChatBot/ChatBot/Extensions/UIViewController+.swift index 7cc80267..dd59faca 100644 --- a/ChatBot/ChatBot/Extensions/UIViewController+.swift +++ b/ChatBot/ChatBot/Extensions/UIViewController+.swift @@ -5,4 +5,57 @@ // Created by 강창현 on 4/14/24. // -import Foundation +import UIKit + +extension UIViewController { + func setupKeyboardNotification() { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + addKeyboardGesture() + } + + private func addKeyboardGesture() { + let tapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(dismissKeyboard) + ) + view.addGestureRecognizer(tapGestureRecognizer) + } + + @objc + private func dismissKeyboard() { + view.endEditing(true) + } + + @objc + private func keyboardWillShow(notification: NSNotification) { + guard + let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue + else { + return + } + + self.view.frame.size.height -= keyboardSize.height + } + + @objc + private func keyboardWillHide(notification: NSNotification) { + guard + let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue + else { + return + } + + self.view.frame.size.height += keyboardSize.height + } +} From 94d547c9e872a3e03ce025208f18a40ecd691168 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:16:41 +0900 Subject: [PATCH 26/36] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20De?= =?UTF-8?q?legate=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatInputSendButton.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/ChatBot/ChatBot/View/ChatInputSendButton.swift b/ChatBot/ChatBot/View/ChatInputSendButton.swift index 80066d40..d3ce44cd 100644 --- a/ChatBot/ChatBot/View/ChatInputSendButton.swift +++ b/ChatBot/ChatBot/View/ChatInputSendButton.swift @@ -10,7 +10,6 @@ import UIKit final class ChatInputSendButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) - self.isEnabled = false self.setBackgroundImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) } From 2362a16e11a3ecaae2b9201ddfafc69ed98d8b40 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:16:54 +0900 Subject: [PATCH 27/36] =?UTF-8?q?refine:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B2=84=EB=B8=94=20=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatCell.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatCell.swift b/ChatBot/ChatBot/View/ChatCell.swift index e7f7e4e9..967f8285 100644 --- a/ChatBot/ChatBot/View/ChatCell.swift +++ b/ChatBot/ChatBot/View/ChatCell.swift @@ -32,7 +32,7 @@ final class ChatCell: UICollectionViewListCell { userBubbleView.configureMessage(text: text) } - func configureSystem(text: String, isLoading: Bool) { + func configureSystem(text: String) { systemBubbleView.isHidden = false userBubbleView.isHidden = true systemBubbleView.configureMessage(text: text) @@ -54,6 +54,8 @@ private extension ChatCell { userBubbleView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5), userBubbleView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5), userBubbleView.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.width * 0.75), + userBubbleView.widthAnchor.constraint(greaterThanOrEqualToConstant: UIScreen.main.bounds.width * 0.1), + userBubbleView.heightAnchor.constraint(greaterThanOrEqualToConstant: UIScreen.main.bounds.width * 0.1), ] ) } @@ -64,7 +66,8 @@ private extension ChatCell { systemBubbleView.widthAnchor.constraint(lessThanOrEqualToConstant: UIScreen.main.bounds.width * 0.75), systemBubbleView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), systemBubbleView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5), - systemBubbleView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5) + systemBubbleView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5), + systemBubbleView.widthAnchor.constraint(greaterThanOrEqualToConstant: UIScreen.main.bounds.width * 0.1), ] ) } From 5c4729102bdff7c20c3806da26708b23904266d0 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:17:05 +0900 Subject: [PATCH 28/36] =?UTF-8?q?refactor:=20=ED=82=A4=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 4 ++++ .../ChatBot/View/ChatCollectionViewDataSource.swift | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index 65ff4f30..b28ee3dd 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2770189E2BCBF4A20040A6A9 /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */; }; 27EFEA4F2BBC174D004A3426 /* ChatBotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA4E2BBC174D004A3426 /* ChatBotTests.swift */; }; 27EFEA562BBD3EDB004A3426 /* ChatBotNetworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA552BBD3EDB004A3426 /* ChatBotNetworkTests.swift */; }; 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA602BBD6D75004A3426 /* Bundle+.swift */; }; @@ -53,6 +54,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; 27EFEA4C2BBC174D004A3426 /* ChatBotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatBotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 27EFEA4E2BBC174D004A3426 /* ChatBotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBotTests.swift; sourceTree = ""; }; 27EFEA552BBD3EDB004A3426 /* ChatBotNetworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBotNetworkTests.swift; sourceTree = ""; }; @@ -151,6 +153,7 @@ children = ( 27EFEA602BBD6D75004A3426 /* Bundle+.swift */, 27EFEA6B2BBD7B32004A3426 /* URLSession+.swift */, + 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */, ); path = Extensions; sourceTree = ""; @@ -407,6 +410,7 @@ 893621262BC40E5500EFB2C7 /* ChatInputSendButton.swift in Sources */, 89A49A6C2BB4274B00C643D3 /* Message.swift in Sources */, 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */, + 2770189E2BCBF4A20040A6A9 /* UIViewController+.swift in Sources */, 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */, 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */, 27EFEA6E2BBD7B53004A3426 /* NetworkService.swift in Sources */, diff --git a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift index 4677e1ac..8e3f1adb 100644 --- a/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift +++ b/ChatBot/ChatBot/View/ChatCollectionViewDataSource.swift @@ -11,10 +11,10 @@ enum MessageSection { case messages } -typealias ChatCollectionViewSnapshot = NSDiffableDataSourceSnapshot +typealias ChatCollectionViewSnapshot = NSDiffableDataSourceSnapshot -final class ChatCollectionViewDataSource: UICollectionViewDiffableDataSource { - static let cellProvider: CellProvider = { collectionView, indexPath, message in +final class ChatCollectionViewDataSource: UICollectionViewDiffableDataSource { + static let cellProvider: CellProvider = { collectionView, indexPath, model in guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: ChatCell.identifier, @@ -23,13 +23,13 @@ final class ChatCollectionViewDataSource: UICollectionViewDiffableDataSource Date: Sun, 14 Apr 2024 21:19:57 +0900 Subject: [PATCH 29/36] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/Model/Network/NetworkService.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ChatBot/ChatBot/Model/Network/NetworkService.swift b/ChatBot/ChatBot/Model/Network/NetworkService.swift index 4131f33e..b2f8c15a 100644 --- a/ChatBot/ChatBot/Model/Network/NetworkService.swift +++ b/ChatBot/ChatBot/Model/Network/NetworkService.swift @@ -12,10 +12,6 @@ struct NetworkService: NetworkTestable { private let session: URLSessionProtocol -// init(session: URLSessionProtocol = MockURLSession(statusCode: 200)) { -// self.session = session -// } - init(session: URLSessionProtocol = URLSession.shared) { self.session = session } From 7fd3fc420a0871bf50ad118ed30a13c5f916055c Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 14 Apr 2024 21:26:14 +0900 Subject: [PATCH 30/36] =?UTF-8?q?fix:=20Request=20Body=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/ViewModel/ChatViewModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift index dbc3d7d1..10e763f8 100644 --- a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift +++ b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift @@ -83,9 +83,11 @@ private extension ChatViewModel { message ] ) + requestModel?.messages.append(message) setRequestDTO(message, responseMessage) return requestModel } + requestModel?.messages.append(message) setRequestDTO(message, responseMessage) return requestModel } From 914b98c6067154956fbd8e70ab02fcc896bb3256 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 21 Apr 2024 16:26:54 +0900 Subject: [PATCH 31/36] =?UTF-8?q?refactor:=20ChattingView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=ED=9B=84=20ViewController=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 4 + .../Controller/ChatBotViewController.swift | 66 +++------------- ChatBot/ChatBot/View/ChatCollectionView.swift | 2 +- ChatBot/ChatBot/View/ChattingView.swift | 79 +++++++++++++++++++ 4 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 ChatBot/ChatBot/View/ChattingView.swift diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index b28ee3dd..9b505002 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 2770189E2BCBF4A20040A6A9 /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */; }; + 27E09B7B2BD4F1A000C37C48 /* ChattingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E09B7A2BD4F1A000C37C48 /* ChattingView.swift */; }; 27EFEA4F2BBC174D004A3426 /* ChatBotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA4E2BBC174D004A3426 /* ChatBotTests.swift */; }; 27EFEA562BBD3EDB004A3426 /* ChatBotNetworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA552BBD3EDB004A3426 /* ChatBotNetworkTests.swift */; }; 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA602BBD6D75004A3426 /* Bundle+.swift */; }; @@ -55,6 +56,7 @@ /* Begin PBXFileReference section */ 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; + 27E09B7A2BD4F1A000C37C48 /* ChattingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChattingView.swift; sourceTree = ""; }; 27EFEA4C2BBC174D004A3426 /* ChatBotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatBotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 27EFEA4E2BBC174D004A3426 /* ChatBotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBotTests.swift; sourceTree = ""; }; 27EFEA552BBD3EDB004A3426 /* ChatBotNetworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBotNetworkTests.swift; sourceTree = ""; }; @@ -210,6 +212,7 @@ 893621212BC4014D00EFB2C7 /* ChatInputView.swift */, 893621252BC40E5500EFB2C7 /* ChatInputSendButton.swift */, 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */, + 27E09B7A2BD4F1A000C37C48 /* ChattingView.swift */, ); path = View; sourceTree = ""; @@ -412,6 +415,7 @@ 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */, 2770189E2BCBF4A20040A6A9 /* UIViewController+.swift in Sources */, 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */, + 27E09B7B2BD4F1A000C37C48 /* ChattingView.swift in Sources */, 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */, 27EFEA6E2BBD7B53004A3426 /* NetworkService.swift in Sources */, B4B3E2BF2B42D1BB00818B3C /* SceneDelegate.swift in Sources */, diff --git a/ChatBot/ChatBot/Controller/ChatBotViewController.swift b/ChatBot/ChatBot/Controller/ChatBotViewController.swift index 6ddf8b30..ca9cc77f 100644 --- a/ChatBot/ChatBot/Controller/ChatBotViewController.swift +++ b/ChatBot/ChatBot/Controller/ChatBotViewController.swift @@ -9,27 +9,16 @@ import UIKit import Combine final class ChatBotViewController: UIViewController { - - private lazy var chatCollectionView: ChatCollectionView = { - var configure = UICollectionLayoutListConfiguration(appearance: .plain) - configure.showsSeparators = false - let layout = UICollectionViewCompositionalLayout.list(using: configure) - let collectionView = ChatCollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.allowsSelection = false - return collectionView - }() - private lazy var dataSource = ChatCollectionViewDataSource(collectionView: chatCollectionView) + private let chatBotViewModel: ChatViewModel = .init() - private var cancellable = Set() + private let chattingView: ChattingView = .init() + private var cancellable: Set = .init() private let input: PassthroughSubject = .init() - private let chatInputView = ChatInputView() - override func viewDidLoad() { - super.viewDidLoad() + override func loadView() { + super.loadView() + self.view = chattingView setupKeyboardNotification() - setupCollectionView() - configureUI() - setupConstraints() bind() setupChatInputView() } @@ -40,38 +29,11 @@ final class ChatBotViewController: UIViewController { } private extension ChatBotViewController { - func configureUI() { - view.backgroundColor = .white - view.addSubview(chatCollectionView) - view.addSubview(chatInputView) - } - - func setupCollectionView() { - self.chatCollectionView.dataSource = dataSource - } - - func setupConstraints() { - chatInputView.translatesAutoresizingMaskIntoConstraints = false - chatCollectionView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate( - [ - chatCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - chatCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - chatCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - chatCollectionView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor, constant: -10), - chatInputView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - chatInputView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - chatInputView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ] - ) - } - func setupChatInputView() { - chatInputView.setChatSendButton { [weak self] message in + chattingView.setChatSendButton { [weak self] message in self?.input.send(.sendButtonTapped(message: message)) guard let requestDTO = self?.chatBotViewModel.requestDTO else { return } - self?.applyChatResponse(response: requestDTO) + self?.chattingView.applyChatResponse(response: requestDTO) } } @@ -84,19 +46,11 @@ private extension ChatBotViewController { case .fetchChatResponseDidFail(let error): print(error.localizedDescription) case .fetchChatResponseDidSucceed(let response): - self?.applyChatResponse(response: response) + self?.chattingView.applyChatResponse(response: response) case .toggleSendButton(let isEnable): - self?.chatInputView.isEnable = isEnable + self?.chattingView.isSendButtonEnable(isEnable) } } .store(in: &cancellable) } - - func applyChatResponse(response: [RequestDTO]) { - var chatCollectionViewSnapshot = ChatCollectionViewSnapshot() - chatCollectionViewSnapshot.appendSections([.messages]) - chatCollectionViewSnapshot.appendItems(response) - dataSource.apply(chatCollectionViewSnapshot) - chatCollectionView.srollToBottom() - } } diff --git a/ChatBot/ChatBot/View/ChatCollectionView.swift b/ChatBot/ChatBot/View/ChatCollectionView.swift index 5cdac307..bf5652e1 100644 --- a/ChatBot/ChatBot/View/ChatCollectionView.swift +++ b/ChatBot/ChatBot/View/ChatCollectionView.swift @@ -24,7 +24,7 @@ extension ChatCollectionView { self.register(ChatCell.self, forCellWithReuseIdentifier: ChatCell.identifier) } - func srollToBottom() { + func scrollToBottom() { DispatchQueue.main.async { guard self.numberOfSections > 0 else { return } let indexPath = IndexPath( diff --git a/ChatBot/ChatBot/View/ChattingView.swift b/ChatBot/ChatBot/View/ChattingView.swift new file mode 100644 index 00000000..2ddd7ae1 --- /dev/null +++ b/ChatBot/ChatBot/View/ChattingView.swift @@ -0,0 +1,79 @@ +// +// ChattingView.swift +// ChatBot +// +// Created by 강창현 on 4/21/24. +// + +import UIKit + +final class ChattingView: UIView { + private lazy var chatCollectionView: ChatCollectionView = { + var configure = UICollectionLayoutListConfiguration(appearance: .plain) + configure.showsSeparators = false + let layout = UICollectionViewCompositionalLayout.list(using: configure) + let collectionView = ChatCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.allowsSelection = false + return collectionView + }() + + private lazy var dataSource = ChatCollectionViewDataSource(collectionView: chatCollectionView) + + private let chatInputView = ChatInputView() + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + setupCollectionView() + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setChatSendButton(completion: @escaping (_ message: Message) -> Void) { + chatInputView.setChatSendButton(completion: completion) + } + + func isSendButtonEnable(_ isEnable: Bool) { + chatInputView.isEnable = isEnable + } + + func applyChatResponse(response: [RequestDTO]) { + var chatCollectionViewSnapshot = ChatCollectionViewSnapshot() + chatCollectionViewSnapshot.appendSections([.messages]) + chatCollectionViewSnapshot.appendItems(response) + dataSource.apply(chatCollectionViewSnapshot) + chatCollectionView.scrollToBottom() + } +} + +private extension ChattingView { + func configureUI() { + self.backgroundColor = .white + self.addSubview(chatCollectionView) + self.addSubview(chatInputView) + } + + func setupCollectionView() { + self.chatCollectionView.dataSource = dataSource + } + + func setupConstraints() { + chatInputView.translatesAutoresizingMaskIntoConstraints = false + chatCollectionView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate( + [ + chatCollectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + chatCollectionView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), + chatCollectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + chatCollectionView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor, constant: -10), + chatInputView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + chatInputView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + chatInputView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor) + ] + ) + } +} From f5b8308f7fd798760d086574f7a6807c9633a75e Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 21 Apr 2024 18:14:45 +0900 Subject: [PATCH 32/36] =?UTF-8?q?refine:=20ChatBubbleMakable=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20MessageView=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatBubbleMakable.swift | 8 ++++++++ .../View/{MessageView.swift => MessageLabel.swift} | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 ChatBot/ChatBot/View/ChatBubbleMakable.swift rename ChatBot/ChatBot/View/{MessageView.swift => MessageLabel.swift} (85%) diff --git a/ChatBot/ChatBot/View/ChatBubbleMakable.swift b/ChatBot/ChatBot/View/ChatBubbleMakable.swift new file mode 100644 index 00000000..2b50ae3a --- /dev/null +++ b/ChatBot/ChatBot/View/ChatBubbleMakable.swift @@ -0,0 +1,8 @@ +// +// ChatBubbleMakable.swift +// ChatBot +// +// Created by 강창현 on 4/21/24. +// + +import Foundation diff --git a/ChatBot/ChatBot/View/MessageView.swift b/ChatBot/ChatBot/View/MessageLabel.swift similarity index 85% rename from ChatBot/ChatBot/View/MessageView.swift rename to ChatBot/ChatBot/View/MessageLabel.swift index 5b8c9a18..a7136b05 100644 --- a/ChatBot/ChatBot/View/MessageView.swift +++ b/ChatBot/ChatBot/View/MessageLabel.swift @@ -7,7 +7,7 @@ import UIKit -final class MessageView: UILabel { +final class MessageLabel: UILabel { override init(frame: CGRect) { super.init(frame: frame) @@ -19,7 +19,7 @@ final class MessageView: UILabel { } } -private extension MessageView { +private extension MessageLabel { func configureUI() { self.backgroundColor = .clear self.textAlignment = .left From 999547616bf321067847460030ff38d769acedbe Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 21 Apr 2024 18:15:04 +0900 Subject: [PATCH 33/36] =?UTF-8?q?chore:=20extension=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatCollectionView.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatCollectionView.swift b/ChatBot/ChatBot/View/ChatCollectionView.swift index bf5652e1..6e77fe5a 100644 --- a/ChatBot/ChatBot/View/ChatCollectionView.swift +++ b/ChatBot/ChatBot/View/ChatCollectionView.swift @@ -17,12 +17,6 @@ final class ChatCollectionView: UICollectionView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } -} - -extension ChatCollectionView { - func setupChatCell() { - self.register(ChatCell.self, forCellWithReuseIdentifier: ChatCell.identifier) - } func scrollToBottom() { DispatchQueue.main.async { @@ -35,3 +29,9 @@ extension ChatCollectionView { } } } + +private extension ChatCollectionView { + func setupChatCell() { + self.register(ChatCell.self, forCellWithReuseIdentifier: ChatCell.identifier) + } +} From 0fa815612497ad060268047c14759082c05b83fa Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 21 Apr 2024 18:15:22 +0900 Subject: [PATCH 34/36] =?UTF-8?q?refactor:=20ChatBubbleMakable=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=86=A0=EC=BD=9C=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot.xcodeproj/project.pbxproj | 20 ++- ChatBot/ChatBot/View/ChatBubbleMakable.swift | 149 ++++++++++++++++- ChatBot/ChatBot/View/ChatBubbleView.swift | 164 +++---------------- 3 files changed, 180 insertions(+), 153 deletions(-) diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index 9b505002..1a10aa10 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 2770189E2BCBF4A20040A6A9 /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */; }; 27E09B7B2BD4F1A000C37C48 /* ChattingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E09B7A2BD4F1A000C37C48 /* ChattingView.swift */; }; + 27E09B7D2BD50DD700C37C48 /* ChatBubbleMakable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E09B7C2BD50DD700C37C48 /* ChatBubbleMakable.swift */; }; 27EFEA4F2BBC174D004A3426 /* ChatBotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA4E2BBC174D004A3426 /* ChatBotTests.swift */; }; 27EFEA562BBD3EDB004A3426 /* ChatBotNetworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA552BBD3EDB004A3426 /* ChatBotNetworkTests.swift */; }; 27EFEA612BBD6D75004A3426 /* Bundle+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EFEA602BBD6D75004A3426 /* Bundle+.swift */; }; @@ -31,7 +32,7 @@ 893621242BC4017F00EFB2C7 /* ChatInputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */; }; 893621262BC40E5500EFB2C7 /* ChatInputSendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893621252BC40E5500EFB2C7 /* ChatInputSendButton.swift */; }; 89510A662BBE938700354947 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A652BBE938700354947 /* ChatBubbleView.swift */; }; - 89510A682BBE9BC400354947 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A672BBE9BC400354947 /* MessageView.swift */; }; + 89510A682BBE9BC400354947 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89510A672BBE9BC400354947 /* MessageLabel.swift */; }; 89A49A642BB4233C00C643D3 /* ResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A632BB4233C00C643D3 /* ResponseModel.swift */; }; 89A49A662BB426ED00C643D3 /* RequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A652BB426ED00C643D3 /* RequestModel.swift */; }; 89A49A682BB4272600C643D3 /* Choice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A49A672BB4272600C643D3 /* Choice.swift */; }; @@ -57,6 +58,7 @@ /* Begin PBXFileReference section */ 2770189D2BCBF4A20040A6A9 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; 27E09B7A2BD4F1A000C37C48 /* ChattingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChattingView.swift; sourceTree = ""; }; + 27E09B7C2BD50DD700C37C48 /* ChatBubbleMakable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleMakable.swift; sourceTree = ""; }; 27EFEA4C2BBC174D004A3426 /* ChatBotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatBotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 27EFEA4E2BBC174D004A3426 /* ChatBotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBotTests.swift; sourceTree = ""; }; 27EFEA552BBD3EDB004A3426 /* ChatBotNetworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBotNetworkTests.swift; sourceTree = ""; }; @@ -81,7 +83,7 @@ 893621232BC4017F00EFB2C7 /* ChatInputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputTextView.swift; sourceTree = ""; }; 893621252BC40E5500EFB2C7 /* ChatInputSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputSendButton.swift; sourceTree = ""; }; 89510A652BBE938700354947 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; - 89510A672BBE9BC400354947 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 89510A672BBE9BC400354947 /* MessageLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; 89A49A632BB4233C00C643D3 /* ResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseModel.swift; sourceTree = ""; }; 89A49A652BB426ED00C643D3 /* RequestModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestModel.swift; sourceTree = ""; }; 89A49A672BB4272600C643D3 /* Choice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Choice.swift; sourceTree = ""; }; @@ -205,7 +207,8 @@ isa = PBXGroup; children = ( 89510A652BBE938700354947 /* ChatBubbleView.swift */, - 89510A672BBE9BC400354947 /* MessageView.swift */, + 27E09B7C2BD50DD700C37C48 /* ChatBubbleMakable.swift */, + 89510A672BBE9BC400354947 /* MessageLabel.swift */, 27EFEA822BBFD813004A3426 /* ChatCollectionView.swift */, 27EFEA842BBFD836004A3426 /* ChatCell.swift */, 27EFEA862BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift */, @@ -394,9 +397,10 @@ 27EFEA6C2BBD7B32004A3426 /* URLSession+.swift in Sources */, 27EFEA792BBD7DA1004A3426 /* MockDataSample.swift in Sources */, 27EFEA7F2BBE44AA004A3426 /* NetworkURL.swift in Sources */, + 27E09B7D2BD50DD700C37C48 /* ChatBubbleMakable.swift in Sources */, 893621222BC4014D00EFB2C7 /* ChatInputView.swift in Sources */, B4B3E2C12B42D1BB00818B3C /* ChatBotViewController.swift in Sources */, - 89510A682BBE9BC400354947 /* MessageView.swift in Sources */, + 89510A682BBE9BC400354947 /* MessageLabel.swift in Sources */, 27EFEA7D2BBD7DFD004A3426 /* NetworkTestable.swift in Sources */, 27EFEA872BBFDD0F004A3426 /* ChatCollectionViewDataSource.swift in Sources */, 27EFEA832BBFD813004A3426 /* ChatCollectionView.swift in Sources */, @@ -608,7 +612,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = K9NKZR257N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ChatBot/Supportings/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -621,7 +625,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.tacocat.ChatBot; + PRODUCT_BUNDLE_IDENTIFIER = com.tacocat.ChatBot.test; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -636,7 +640,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = K9NKZR257N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ChatBot/Supportings/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -649,7 +653,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.tacocat.ChatBot; + PRODUCT_BUNDLE_IDENTIFIER = com.tacocat.ChatBot.test; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/ChatBot/ChatBot/View/ChatBubbleMakable.swift b/ChatBot/ChatBot/View/ChatBubbleMakable.swift index 2b50ae3a..5ea1911a 100644 --- a/ChatBot/ChatBot/View/ChatBubbleMakable.swift +++ b/ChatBot/ChatBot/View/ChatBubbleMakable.swift @@ -5,4 +5,151 @@ // Created by 강창현 on 4/21/24. // -import Foundation +import UIKit + +protocol ChatBubbleMakable where Self: UIView { + var messageView: MessageLabel { get } + func configureUI() + func setupConstraints() + func setRightBubbleView(rect: CGRect) + func setLeftBubbleView(rect: CGRect) + func configureMessage(text: String) +} + +extension ChatBubbleMakable { + func configureUI() { + self.addSubview(messageView) + messageView.translatesAutoresizingMaskIntoConstraints = false + self.translatesAutoresizingMaskIntoConstraints = false + } + + func setupConstraints() { + NSLayoutConstraint.activate( + [ + self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), + messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + messageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), + messageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), + ] + ) + } + + func configureMessage(text: String) { + messageView.text = text + } + + func setRightBubbleView(rect: CGRect) { + let path = UIBezierPath() + messageView.textColor = .white + let bottom = rect.height + let right = rect.width + path.move( + to: CGPoint(x: right - 22, y: bottom) + ) + path.addLine( + to: CGPoint(x: 17, y: bottom) + ) + path.addCurve( + to: CGPoint(x: 0, y: bottom - 18), + controlPoint1: CGPoint(x: 7.61, y: bottom), + controlPoint2: CGPoint(x: 0, y: bottom - 7.61) + ) + path.addLine( + to: CGPoint(x: 0, y: 17) + ) + path.addCurve( + to: CGPoint(x: 17, y: 0), + controlPoint1: CGPoint(x: 0, y: 7.61), + controlPoint2: CGPoint(x: 7.61, y: 0) + ) + path.addLine( + to: CGPoint(x: right - 21, y: 0) + ) + path.addCurve( + to: CGPoint(x: right - 4, y: 17), + controlPoint1: CGPoint(x: right - 11.61, y: 0), + controlPoint2: CGPoint(x: right - 4, y: 7.61) + ) + path.addLine( + to: CGPoint(x: right - 4, y: bottom - 11) + ) + path.addCurve( + to: CGPoint(x: right, y: bottom), + controlPoint1: CGPoint(x: right - 4, y: bottom - 1), + controlPoint2: CGPoint(x: right, y: bottom) + ) + path.addLine( + to: CGPoint(x: right + 0.05, y: bottom - 0.01) + ) + path.addCurve( + to: CGPoint(x: right - 11.04, y: bottom - 4.04), + controlPoint1: CGPoint(x: right - 4.07, y: bottom + 0.43), + controlPoint2: CGPoint(x: right - 8.16, y: bottom - 1.06) + ) + path.addCurve( + to: CGPoint(x: right - 22, y: bottom), + controlPoint1: CGPoint(x: right - 16, y: bottom), + controlPoint2: CGPoint(x: right - 19, y: bottom) + ) + path.close() + UIColor(cgColor: UIColor.systemBlue.cgColor).setFill() + path.fill() + } + + func setLeftBubbleView(rect: CGRect) { + let path = UIBezierPath() + let bottom = rect.height + let right = rect.width + path.move( + to: CGPoint(x: 22, y: bottom) + ) + path.addLine( + to: CGPoint(x: right - 17, y: bottom) + ) + path.addCurve( + to: CGPoint(x: right, y: bottom - 18), + controlPoint1: CGPoint(x: right - 7.61, y: bottom), + controlPoint2: CGPoint(x: right, y: bottom - 7.61) + ) + path.addLine( + to: CGPoint(x: right, y: 17) + ) + path.addCurve( + to: CGPoint(x: right - 17, y: 0), + controlPoint1: CGPoint(x: right, y: 7.61), + controlPoint2: CGPoint(x: right - 7.61, y: 0) + ) + path.addLine( + to: CGPoint(x: 21, y: 0) + ) + path.addCurve( + to: CGPoint(x: 4, y: 17), + controlPoint1: CGPoint(x: 11.61, y: 0), + controlPoint2: CGPoint(x: 4, y: 7.61) + ) + path.addLine( + to: CGPoint(x: 4, y: bottom - 11) + ) + path.addCurve( + to: CGPoint(x: 0, y: bottom), + controlPoint1: CGPoint(x: 4, y: bottom - 1), + controlPoint2: CGPoint(x: 0, y: bottom) + ) + path.addLine( + to: CGPoint(x: -0.05, y: bottom - 0.01) + ) + path.addCurve( + to: CGPoint(x: 11.04, y: bottom - 4.04), + controlPoint1: CGPoint(x: 4.07, y: bottom + 0.43), + controlPoint2: CGPoint(x: 8.16, y: bottom - 1.06) + ) + path.addCurve( + to: CGPoint(x: 22, y: bottom), + controlPoint1: CGPoint(x: 16, y: bottom), + controlPoint2: CGPoint(x: 19, y: bottom) + ) + path.close() + UIColor(cgColor: UIColor.systemGray5.cgColor).setFill() + path.fill() + } +} diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index efde0bfe..11c4cc85 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -7,169 +7,45 @@ import UIKit -final class UserBubbleView: ChatBubbleView { - override func draw(_ rect: CGRect) { - setRightBubbleView(rect: rect) - } -} - -final class SystemBubbleView: ChatBubbleView { - override func draw(_ rect: CGRect) { - setLeftBubbleView(rect: rect) - } -} - -class ChatBubbleView: UIView { - private let messageView = MessageView() +final class UserBubbleView: UIView { + var messageView: MessageLabel = .init() override init(frame: CGRect) { super.init(frame: frame) contentMode = .redraw self.backgroundColor = .clear - self.configureUI() + configureUI() setupConstraints() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - func configureMessage(text: String) { - messageView.text = text + override func draw(_ rect: CGRect) { + setRightBubbleView(rect: rect) } } -private extension ChatBubbleView { - func configureUI() { - self.addSubview(messageView) - messageView.translatesAutoresizingMaskIntoConstraints = false - self.translatesAutoresizingMaskIntoConstraints = false - } +extension UserBubbleView: ChatBubbleMakable { } + +final class SystemBubbleView: UIView { + var messageView: MessageLabel = .init() - func setupConstraints() { - NSLayoutConstraint.activate( - [ - self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), - messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - messageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), - messageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), - ] - ) + override init(frame: CGRect) { + super.init(frame: frame) + contentMode = .redraw + self.backgroundColor = .clear + self.configureUI() + setupConstraints() } - func setRightBubbleView(rect: CGRect) { - let bezierPath = UIBezierPath() - messageView.textColor = .white - let bottom = rect.height - let right = rect.width - bezierPath.move( - to: CGPoint(x: right - 22, y: bottom) - ) - bezierPath.addLine( - to: CGPoint(x: 17, y: bottom) - ) - bezierPath.addCurve( - to: CGPoint(x: 0, y: bottom - 18), - controlPoint1: CGPoint(x: 7.61, y: bottom), - controlPoint2: CGPoint(x: 0, y: bottom - 7.61) - ) - bezierPath.addLine( - to: CGPoint(x: 0, y: 17) - ) - bezierPath.addCurve( - to: CGPoint(x: 17, y: 0), - controlPoint1: CGPoint(x: 0, y: 7.61), - controlPoint2: CGPoint(x: 7.61, y: 0) - ) - bezierPath.addLine( - to: CGPoint(x: right - 21, y: 0) - ) - bezierPath.addCurve( - to: CGPoint(x: right - 4, y: 17), - controlPoint1: CGPoint(x: right - 11.61, y: 0), - controlPoint2: CGPoint(x: right - 4, y: 7.61) - ) - bezierPath.addLine( - to: CGPoint(x: right - 4, y: bottom - 11) - ) - bezierPath.addCurve( - to: CGPoint(x: right, y: bottom), - controlPoint1: CGPoint(x: right - 4, y: bottom - 1), - controlPoint2: CGPoint(x: right, y: bottom) - ) - bezierPath.addLine( - to: CGPoint(x: right + 0.05, y: bottom - 0.01) - ) - bezierPath.addCurve( - to: CGPoint(x: right - 11.04, y: bottom - 4.04), - controlPoint1: CGPoint(x: right - 4.07, y: bottom + 0.43), - controlPoint2: CGPoint(x: right - 8.16, y: bottom - 1.06) - ) - bezierPath.addCurve( - to: CGPoint(x: right - 22, y: bottom), - controlPoint1: CGPoint(x: right - 16, y: bottom), - controlPoint2: CGPoint(x: right - 19, y: bottom) - ) - bezierPath.close() - UIColor(cgColor: UIColor.systemBlue.cgColor).setFill() - bezierPath.fill() + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - func setLeftBubbleView(rect: CGRect) { - let bezierPath = UIBezierPath() - let bottom = rect.height - let right = rect.width - bezierPath.move( - to: CGPoint(x: 22, y: bottom) - ) - bezierPath.addLine( - to: CGPoint(x: right - 17, y: bottom) - ) - bezierPath.addCurve( - to: CGPoint(x: right, y: bottom - 18), - controlPoint1: CGPoint(x: right - 7.61, y: bottom), - controlPoint2: CGPoint(x: right, y: bottom - 7.61) - ) - bezierPath.addLine( - to: CGPoint(x: right, y: 17) - ) - bezierPath.addCurve( - to: CGPoint(x: right - 17, y: 0), - controlPoint1: CGPoint(x: right, y: 7.61), - controlPoint2: CGPoint(x: right - 7.61, y: 0) - ) - bezierPath.addLine( - to: CGPoint(x: 21, y: 0) - ) - bezierPath.addCurve( - to: CGPoint(x: 4, y: 17), - controlPoint1: CGPoint(x: 11.61, y: 0), - controlPoint2: CGPoint(x: 4, y: 7.61) - ) - bezierPath.addLine( - to: CGPoint(x: 4, y: bottom - 11) - ) - bezierPath.addCurve( - to: CGPoint(x: 0, y: bottom), - controlPoint1: CGPoint(x: 4, y: bottom - 1), - controlPoint2: CGPoint(x: 0, y: bottom) - ) - bezierPath.addLine( - to: CGPoint(x: -0.05, y: bottom - 0.01) - ) - bezierPath.addCurve( - to: CGPoint(x: 11.04, y: bottom - 4.04), - controlPoint1: CGPoint(x: 4.07, y: bottom + 0.43), - controlPoint2: CGPoint(x: 8.16, y: bottom - 1.06) - ) - bezierPath.addCurve( - to: CGPoint(x: 22, y: bottom), - controlPoint1: CGPoint(x: 16, y: bottom), - controlPoint2: CGPoint(x: 19, y: bottom) - ) - bezierPath.close() - UIColor(cgColor: UIColor.systemGray5.cgColor).setFill() - bezierPath.fill() + override func draw(_ rect: CGRect) { + setLeftBubbleView(rect: rect) } } +extension SystemBubbleView: ChatBubbleMakable { } From fe71b9df1b1175cfbfbbc963136baf3638837282 Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 21 Apr 2024 20:30:51 +0900 Subject: [PATCH 35/36] =?UTF-8?q?chore:=20MessageView=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatBubbleMakable.swift | 18 +++++++++--------- ChatBot/ChatBot/View/ChatBubbleView.swift | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatBubbleMakable.swift b/ChatBot/ChatBot/View/ChatBubbleMakable.swift index 5ea1911a..b2f1aa9c 100644 --- a/ChatBot/ChatBot/View/ChatBubbleMakable.swift +++ b/ChatBot/ChatBot/View/ChatBubbleMakable.swift @@ -8,7 +8,7 @@ import UIKit protocol ChatBubbleMakable where Self: UIView { - var messageView: MessageLabel { get } + var messageLabel: MessageLabel { get } func configureUI() func setupConstraints() func setRightBubbleView(rect: CGRect) @@ -18,29 +18,29 @@ protocol ChatBubbleMakable where Self: UIView { extension ChatBubbleMakable { func configureUI() { - self.addSubview(messageView) - messageView.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(messageLabel) + messageLabel.translatesAutoresizingMaskIntoConstraints = false self.translatesAutoresizingMaskIntoConstraints = false } func setupConstraints() { NSLayoutConstraint.activate( [ - self.heightAnchor.constraint(equalTo: messageView.heightAnchor, constant: 15), - messageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - messageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), - messageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), + self.heightAnchor.constraint(equalTo: messageLabel.heightAnchor, constant: 15), + messageLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), + messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), ] ) } func configureMessage(text: String) { - messageView.text = text + messageLabel.text = text } func setRightBubbleView(rect: CGRect) { let path = UIBezierPath() - messageView.textColor = .white + messageLabel.textColor = .white let bottom = rect.height let right = rect.width path.move( diff --git a/ChatBot/ChatBot/View/ChatBubbleView.swift b/ChatBot/ChatBot/View/ChatBubbleView.swift index 11c4cc85..1bfa756b 100644 --- a/ChatBot/ChatBot/View/ChatBubbleView.swift +++ b/ChatBot/ChatBot/View/ChatBubbleView.swift @@ -8,7 +8,7 @@ import UIKit final class UserBubbleView: UIView { - var messageView: MessageLabel = .init() + var messageLabel: MessageLabel = .init() override init(frame: CGRect) { super.init(frame: frame) @@ -29,7 +29,7 @@ final class UserBubbleView: UIView { extension UserBubbleView: ChatBubbleMakable { } final class SystemBubbleView: UIView { - var messageView: MessageLabel = .init() + var messageLabel: MessageLabel = .init() override init(frame: CGRect) { super.init(frame: frame) From fe7de10d3590b900b38dfe4ec017dd200883e7cc Mon Sep 17 00:00:00 2001 From: Changhyun Kang Date: Sun, 21 Apr 2024 20:31:18 +0900 Subject: [PATCH 36/36] =?UTF-8?q?fix:=20prepareReuse=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=B7=B0=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChatBot/ChatBot/View/ChatCell.swift | 8 +++++++- ChatBot/ChatBot/View/ChattingView.swift | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/ChatBot/ChatBot/View/ChatCell.swift b/ChatBot/ChatBot/View/ChatCell.swift index 967f8285..18297ea0 100644 --- a/ChatBot/ChatBot/View/ChatCell.swift +++ b/ChatBot/ChatBot/View/ChatCell.swift @@ -12,7 +12,7 @@ final class ChatCell: UICollectionViewListCell { return String(describing: self) } - private let userBubbleView = UserBubbleView() + private var userBubbleView = UserBubbleView() private var systemBubbleView = SystemBubbleView() override init(frame: CGRect) { @@ -26,6 +26,12 @@ final class ChatCell: UICollectionViewListCell { fatalError("init(coder:) has not been implemented") } + override func prepareForReuse() { + super.prepareForReuse() + configureUser(text: "") + configureSystem(text: "") + } + func configureUser(text: String) { systemBubbleView.isHidden = true userBubbleView.isHidden = false diff --git a/ChatBot/ChatBot/View/ChattingView.swift b/ChatBot/ChatBot/View/ChattingView.swift index 2ddd7ae1..88da39bc 100644 --- a/ChatBot/ChatBot/View/ChattingView.swift +++ b/ChatBot/ChatBot/View/ChattingView.swift @@ -16,30 +16,30 @@ final class ChattingView: UIView { collectionView.allowsSelection = false return collectionView }() - + private lazy var dataSource = ChatCollectionViewDataSource(collectionView: chatCollectionView) - + private let chatInputView = ChatInputView() - + override init(frame: CGRect) { super.init(frame: frame) configureUI() setupCollectionView() setupConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func setChatSendButton(completion: @escaping (_ message: Message) -> Void) { chatInputView.setChatSendButton(completion: completion) } - + func isSendButtonEnable(_ isEnable: Bool) { chatInputView.isEnable = isEnable } - + func applyChatResponse(response: [RequestDTO]) { var chatCollectionViewSnapshot = ChatCollectionViewSnapshot() chatCollectionViewSnapshot.appendSections([.messages]) @@ -55,15 +55,15 @@ private extension ChattingView { self.addSubview(chatCollectionView) self.addSubview(chatInputView) } - + func setupCollectionView() { self.chatCollectionView.dataSource = dataSource } - + func setupConstraints() { chatInputView.translatesAutoresizingMaskIntoConstraints = false chatCollectionView.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate( [ chatCollectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor),