Skip to content

Commit

Permalink
[Merge/#273] 채팅 로그 뷰 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
nolanMinsung authored Nov 17, 2024
2 parents 8a18a68 + 388c678 commit c78f31c
Show file tree
Hide file tree
Showing 23 changed files with 1,240 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
86 changes: 85 additions & 1 deletion Offroad-iOS/Offroad-iOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// ChatLogNavigationAnimator.swift
// Offroad-iOS
//
// Created by 김민성 on 11/13/24.
//

import UIKit

class ChatLogPushAnimator: NSObject, UIViewControllerAnimatedTransitioning {

func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
return 0.5
}

func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from) else {
transitionContext.completeTransition(false)
return
}

let containerView = transitionContext.containerView
containerView.addSubview(toView)
toView.frame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)

UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, animations: {
toView.frame = fromView.frame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}

}

class ChatLogPopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
return 0.5
}

func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from) else {
transitionContext.completeTransition(false)
return
}

let containerView = transitionContext.containerView
let finalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)
containerView.insertSubview(toView, belowSubview: fromView)

if transitionContext.isInteractive {
UIView.animate(withDuration: 0.5, delay: 0, options: .curveLinear, animations: {
fromView.frame = finalFrame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
} else {
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, animations: {
fromView.frame = finalFrame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// ORBNavigationController.swift
// Offroad-iOS
//
// Created by 김민성 on 11/13/24.
//

import UIKit

class ORBNavigationController: UINavigationController {

var customPopTransition: UIPercentDrivenInteractiveTransition?
lazy var screenEdgePanGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))

override func viewDidLoad() {
super.viewDidLoad()

setupGestures()
setupDelegates()
}

}


extension ORBNavigationController {

//MARK: - @objc Func

@objc func handlePanGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
let translation = gesture.translation(in: view)
let progress = min(max(translation.x / view.bounds.width, 0), 1)

switch gesture.state {
case .began:
customPopTransition = UIPercentDrivenInteractiveTransition()
popViewController(animated: true)
case .changed:
customPopTransition?.update(progress)
case .ended, .cancelled:
let shouldFinish = progress > 0.5 || gesture.velocity(in: view).x > 0
if shouldFinish {
customPopTransition?.finish()
} else {
customPopTransition?.cancel()
}
customPopTransition = nil
default:
break
}
}

//MARK: - Private Func

private func setupGestures() {
screenEdgePanGesture.edges = .left
view.addGestureRecognizer(screenEdgePanGesture)
}

private func setupDelegates() {
delegate = self
interactivePopGestureRecognizer?.delegate = self
}

//MARK: - Func

func pushChatLogViewController(characterName: String) {
guard let snapshot = topViewController?.view.snapshotView(afterScreenUpdates: true) else { return }
let chatLogViewController = CharacterChatLogViewController(background: snapshot, characterName: characterName)
pushViewController(chatLogViewController, animated: true)
}

}

//MARK: - UINavigationControllerDelegate

extension ORBNavigationController: UINavigationControllerDelegate {

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
guard let tabBarController = tabBarController as? OffroadTabBarController else { return }
guard !tabBarController.isTabBarShown else { return }
tabBarController.disableTabBarInteraction()
}

func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {

if topViewController is CharacterChatLogViewController {
screenEdgePanGesture.isEnabled = true
interactivePopGestureRecognizer?.isEnabled = false
} else {
screenEdgePanGesture.isEnabled = false
interactivePopGestureRecognizer?.isEnabled = true
}

guard let tabBarController = tabBarController as? OffroadTabBarController else { return }
tabBarController.enableTabBarInteraction()
}

func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController
) -> (any UIViewControllerAnimatedTransitioning)? {
if operation == .push && toVC is CharacterChatLogViewController {
return ChatLogPushAnimator()
} else if operation == .pop && fromVC is CharacterChatLogViewController {
return ChatLogPopAnimator()
}
return nil
}

func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: any UIViewControllerAnimatedTransitioning) -> (any UIViewControllerInteractiveTransitioning)? {
return customPopTransition
}

}

//MARK: - UIGestureRecognizerDelegate

extension ORBNavigationController: UIGestureRecognizerDelegate {

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == interactivePopGestureRecognizer
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ final class CharacterChatService: BaseService, CharacterChatServiceProtocol {
case .failure(let error):
print(error.localizedDescription)
switch error {
case .underlying(let erorr, let response):
case .underlying(let error, let response):
print(error.localizedDescription)
if response == nil {
completion(.networkFail)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

struct CharacterChatGetResponseDTO: Codable {
var message: String
var chatDataList: [ChatData]
var data: [ChatData]
}


Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,38 @@ struct ChatData: Codable {
var content: String
var createdAt: String
}

struct ChatDataModel {
var role: String
var content: String
var createdDate: Date?
var formattedTimeString: String {
guard let createdDate else { return "" }
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ko_KR")
dateFormatter.dateFormat = "a hh:mm"
return dateFormatter.string(from: createdDate)
}
var formattedDateString: String {
guard let createdDate else { return "" }
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ko_KR")
dateFormatter.dateFormat = "M월 d일 EEEE"
return dateFormatter.string(from: createdDate)
}

init(data: ChatData) {
let formatter = ISO8601DateFormatter()

self.role = data.role
self.content = data.content
formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]
formatter.timeZone = TimeZone(identifier: "Asia/Seoul")
if let date = formatter.date(from: data.createdAt) {
self.createdDate = date
} else {
self.createdDate = nil
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ extension ORBCharacterChatViewController {
//MARK: - @objc Func

@objc private func keyboardWillShow(_ notification: Notification) {
guard rootView.userChatInputView.isFirstResponder else { return }
rootView.layoutIfNeeded()
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRect = keyboardFrame.cgRectValue
Expand Down
Loading

0 comments on commit c78f31c

Please sign in to comment.