Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GWL-162] Trinet 소켓 Providable 추가 + 테스트 코드 추가 #177

Merged
merged 10 commits into from
Nov 30, 2023
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK!

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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

받자마자 실행되는 녀석이군요...? 혹시 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provider를 사용하는 레포지토리에서 이 Task를 어느 시점에 시작(resume)시켜야할지를 모르겠더라고요. 결국 소켓 통신하는 시점은 생성되는 시점일 것이 분명할 것이고, resume을 해주는 타이밍을 다른 객체가 관리하는 것도 애매하다고 판단해서 바로 실행시켜두었습니다.

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 = "events", 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
Loading