Skip to content

Commit

Permalink
[GWL-162] Trinet 소켓 Providable 추가 + 테스트 코드 추가 (#177)
Browse files Browse the repository at this point in the history
* add: WebSocketTaskProtocol 추가

- WebSocketTask의 모든 사용을 방지하고, 오로지 send, receive만 처리할 수 있도록 추상화했습니다.

* add: URLSessionWebSocketProtocol 추가

- URLSession에서 사용하는 WebSocketTask 메서드를 하나의 프로토콜로 추상화했습니다.
- Mocking하기 위함입니다.

* feat: TNSocketProvider 구현

* feat: WebSocketFrame 추가 및 send에 래핑

* add: SocketURL xcconfig 코드 추가

* add: Add comments

* feat: 테스트 샘플 코드 추가

* delete: 쓰지 않는 import문 삭제

* test: MockWebSocketSession 테스트 코드 추가

* chore: WebSocketFrame event 값 수정

- workout_session으로 변경 (서버 api 변경)
  • Loading branch information
WhiteHyun authored Nov 30, 2023
1 parent bb8eae7 commit 640e26a
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import UIKit

class AViewController: UITabBarController {
class AViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import UIKit

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private var coordinating: AppCoordinating?

func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let navigationController = UINavigationController()
window = UIWindow(windowScene: windowScene)
window?.rootViewController = navigationController
let coordinator = RecordFeatureCoordinator(navigationController: navigationController)
let coordinator = AppCoordinator(navigationController: navigationController)
coordinating = coordinator
coordinator.start()
window?.makeKeyAndVisible()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,182 @@
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Log
import Trinet
import UIKit

class CViewController: UITabBarController {
// MARK: - ParticipantsCodable

struct ParticipantsCodable: Codable {
let id: UUID
let nickname: String
let message: String

init(message: String) {
id = .init()
self.message = message
nickname = "MyNickname"
}
}

// MARK: - CViewController

final class CViewController: UIViewController {
private let returnSubject = PassthroughSubject<String, Never>()
private let repository = TestRepository(session: URLSession.shared)
private var subscriptions = Set<AnyCancellable>()

private let textField = UITextField()

private let textView: UITextView = .init()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red

textView.isUserInteractionEnabled = false
view.addSubview(textField)
view.addSubview(textView)
textField.placeholder = "입력해주세요."
textField.borderStyle = .roundedRect
textField.delegate = self

textView.textColor = .white
textView.backgroundColor = .black
textField.translatesAutoresizingMaskIntoConstraints = false
textView.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
let safeArea = view.safeAreaLayoutGuide
let recognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
view.addGestureRecognizer(recognizer)
NSLayoutConstraint.activate(
[
textField.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 20),
textField.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor),
textField.widthAnchor.constraint(equalToConstant: 240),
textField.heightAnchor.constraint(equalToConstant: 40),

textView.topAnchor.constraint(equalTo: safeArea.centerYAnchor),
textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
textView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
]
)
bind()
}

func bind() {
returnSubject
.flatMap(repository.sendMyHealth(healthData:))
.sink { _ in
Log.make().error("Completion!!!!!!")
} receiveValue: { _ in
Log.make().debug("Yay! Send successfully!")
}
.store(in: &subscriptions)

repository.fetchParticipantsRealTime()
.receive(on: RunLoop.main)
.sink { _ in
Log.make().error("Socket Receive Completion!!!!")
} receiveValue: { [weak self] dataString in
self?.textView.text = [self!.textView.text + dataString].joined(separator: "\n")
Log.make().debug("\(dataString)")
}
.store(in: &subscriptions)
}

@objc
private func tapped() {
textField.resignFirstResponder()
}
}

// MARK: UITextFieldDelegate

extension CViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
returnSubject.send(textField.text!)

textField.text = ""
return true
}
}

// MARK: - TestRepositoryRepresentable

protocol TestRepositoryRepresentable {
func fetchParticipantsRealTime() -> AnyPublisher<String, Error>
func sendMyHealth(healthData: String) -> AnyPublisher<Bool, Error>
}

// MARK: - TestRepository

struct TestRepository {
private let provider: TNSocketProvider<TestEndPoint>

private var task: Task<Void, Error>?

private let subject: PassthroughSubject<String, Error> = .init()

init(session: URLSessionWebSocketProtocol) {
provider = .init(session: session, endPoint: .init())
task = receiveParticipantsData()
}

private func receiveParticipantsData() -> Task<Void, Error> {
return Task {
Log.make(with: .network).debug("receive Ready")
while let data = try await provider.receive() {
switch data {
case let .string(string):
Log.make(with: .network).debug("received \(string)")
subject.send(string)
default:
fatalError("절대 여기 와서는 안 됨")
}
}
Log.make().fault("You can't enter this line")
}
}
}

// MARK: TestRepositoryRepresentable

extension TestRepository: TestRepositoryRepresentable {
func fetchParticipantsRealTime() -> AnyPublisher<String, Error> {
subject.eraseToAnyPublisher()
}

func sendMyHealth(healthData: String) -> AnyPublisher<Bool, Error> {
Future { promise in
Task {
do {
try await provider.send(model: ParticipantsCodable(message: healthData))
promise(.success(true))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
}

// MARK: - TestEndPoint

struct TestEndPoint: TNEndPoint {
let baseURL: String = Bundle.main.infoDictionary?["SocketURL"] as? String ?? ""

let path: String = ""

let method: TNMethod = .get

let query: Encodable? = nil

let body: Encodable? = nil

let headers: TNHeaders = [
.authorization(bearer: ""),
]
}
44 changes: 44 additions & 0 deletions iOS/Projects/Core/Network/Sources/MockWebSocketSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// MockWebSocketSession.swift
// Trinet
//
// Created by 홍승현 on 11/29/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

// MARK: - MockWebSocketTask

public final class MockWebSocketTask: WebSocketTaskProtocol {
private var sentMessage: URLSessionWebSocketTask.Message?
private var receiveContinuation: CheckedContinuation<URLSessionWebSocketTask.Message, Never>?

public func send(_ message: URLSessionWebSocketTask.Message) async throws {
sentMessage = message
receiveContinuation?.resume(returning: message)
}

public func receive() async throws -> URLSessionWebSocketTask.Message {
if let receivedMessage = sentMessage {
sentMessage = nil
return receivedMessage
}

return await withCheckedContinuation { continuation in
receiveContinuation = continuation
}
}

public func resume() {}
}

// MARK: - MockWebSocketSession

public struct MockWebSocketSession: URLSessionWebSocketProtocol {
var webSocketTask: MockWebSocketTask = .init()

public func webSocketTask(with _: URLRequest) -> WebSocketTaskProtocol {
return webSocketTask
}
}
51 changes: 51 additions & 0 deletions iOS/Projects/Core/Network/Sources/TNSocketProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// TNSocketProvider.swift
// Trinet
//
// Created by 홍승현 on 11/29/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

// MARK: - TNSocketProvidable

public protocol TNSocketProvidable {
func send<Model: Codable>(model: Model) async throws
func receive() async throws -> URLSessionWebSocketTask.Message?
}

// MARK: - TNSocketProvider

public struct TNSocketProvider<EndPoint: TNEndPoint>: TNSocketProvidable {
private let session: URLSessionWebSocketProtocol
private var task: WebSocketTaskProtocol?
private let jsonEncoder: JSONEncoder = .init()

public init(session: URLSessionWebSocketProtocol = URLSession.shared, endPoint: EndPoint) {
self.session = session
task = try? session.webSocketTask(with: endPoint.request())
task?.resume()
}

public func send(model: some Codable) async throws {
let frame = WebSocketFrame(data: model)
try await task?.send(.data(jsonEncoder.encode(frame)))
}

public func receive() async throws -> URLSessionWebSocketTask.Message? {
return try await task?.receive()
}
}

// MARK: - WebSocketFrame

struct WebSocketFrame<T: Codable>: Codable {
let event: String
let data: T

init(event: String = "workout_session", data: T) {
self.event = event
self.data = data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// URLSessionWebSocketProtocol.swift
// Trinet
//
// Created by 홍승현 on 11/29/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

// MARK: - URLSessionWebSocketProtocol

public protocol URLSessionWebSocketProtocol {
func webSocketTask(with request: URLRequest) -> WebSocketTaskProtocol
}

// MARK: - URLSession + URLSessionWebSocketProtocol

extension URLSession: URLSessionWebSocketProtocol {
public func webSocketTask(with request: URLRequest) -> WebSocketTaskProtocol {
let socketTask: URLSessionWebSocketTask = webSocketTask(with: request)
return socketTask
}
}
23 changes: 23 additions & 0 deletions iOS/Projects/Core/Network/Sources/WebSocketTaskProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// WebSocketTaskProtocol.swift
// Trinet
//
// Created by 홍승현 on 11/29/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

// MARK: - WebSocketTaskProtocol

public protocol WebSocketTaskProtocol {
func send(_ message: URLSessionWebSocketTask.Message) async throws

func receive() async throws -> URLSessionWebSocketTask.Message

func resume()
}

// MARK: - URLSessionWebSocketTask + WebSocketTaskProtocol

extension URLSessionWebSocketTask: WebSocketTaskProtocol {}
Loading

0 comments on commit 640e26a

Please sign in to comment.