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-184] Socket과 HealthKit 연결, CombineCocoa와 Log 수정 #197

Merged
merged 36 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7d26eaa
add: 소켓으로 통신할 WorkoutRealTimeModel 추가
WhiteHyun Nov 30, 2023
bbed509
add: WorkoutHealth 모델 프로퍼티 수정 (API와 맞춤)
WhiteHyun Nov 30, 2023
cdf36d3
add: Add WorkoutSocketRepositoryRepresentable
WhiteHyun Nov 30, 2023
8170560
add: Add WorkoutSocketRepository
WhiteHyun Nov 30, 2023
33fb443
feat: WorkoutSocketRepository가 자신의 Representable를 준수
WhiteHyun Nov 30, 2023
d9f99eb
add: WorkoutSessionUseCaseDependency 추가
WhiteHyun Nov 30, 2023
4af1c8d
rename: Rename WorkoutHealth to WorkoutDataForm
WhiteHyun Nov 30, 2023
16ea56c
delete: WorkoutCoordinator 제거
WhiteHyun Nov 30, 2023
a2023b8
add: WorkoutHealthForm 추가
WhiteHyun Nov 30, 2023
a724c59
delete: 임시 state case 제거
WhiteHyun Nov 30, 2023
910a824
refactor: 의존성을 외부에서 주입받도록 수정
WhiteHyun Nov 30, 2023
63781d3
feat: HealthKit으로부터 받은 데이터를 모델링하는 작업 구현
WhiteHyun Nov 30, 2023
6d1f91a
rename: WorkoutSessionUseCase가 갖는 repository명 수정
WhiteHyun Nov 30, 2023
8552575
add: Add socketRepository in WorkoutSessionUseCase
WhiteHyun Nov 30, 2023
5aa802d
chore: sendMyWorkout(with:) Output타입을 Void로 수정
WhiteHyun Nov 30, 2023
60aefbe
feat: socket으로 데이터를 전달하는 Flow 추가
WhiteHyun Nov 30, 2023
e00b6b7
feat: usecase에서 참여자 운동 정보 실시간 수신 연동
WhiteHyun Nov 30, 2023
6cb9052
chore: 운동 정보 데이터를 Double에서 Int로 전부 변경
WhiteHyun Nov 30, 2023
f853c95
feat: WorkoutSessionViewModel과 WorkoutSessionUseCase를 연결
WhiteHyun Nov 30, 2023
bdeb369
feat: Workout Session timer 설정
WhiteHyun Nov 30, 2023
9944b0c
add: SessionPeerType 추가
WhiteHyun Nov 30, 2023
d2e7f70
feat: 사용자 정보를 받아다가 CollectionViewCell에 업데이트
WhiteHyun Nov 30, 2023
ac1cb5a
chore: Cell의 초기 상태를 설정해주는 함수를 한 번만 호출되도록 설정
WhiteHyun Nov 30, 2023
179abc8
chore: fatalError 제거
WhiteHyun Nov 30, 2023
85ce464
refactor: WorkoutSession Scene마다 Dependency 설정
WhiteHyun Nov 30, 2023
68851fe
fix: workoutSession을 보여줄 때 임시 dependency 설정
WhiteHyun Dec 1, 2023
db13f3e
fix: 음수 시간대가 나오는 현상 수정
WhiteHyun Dec 1, 2023
fc626c3
add: Logger category 추가
WhiteHyun Dec 1, 2023
13e6dd5
add: withUnretained Operator 추가
WhiteHyun Dec 1, 2023
a05b53f
fix: continuation bug 수정
WhiteHyun Dec 1, 2023
2d52549
fix: continuation fatal error 버그 수정
WhiteHyun Dec 1, 2023
916f8d3
chore: 0으로 나누는 문제 수정
WhiteHyun Dec 1, 2023
e220865
delete: 쓰지 않는 변수 삭제
WhiteHyun Dec 1, 2023
ab87da2
chore: health data publisher의 Failure를 Never로 설정
WhiteHyun Dec 2, 2023
4a65ada
delete: Remove RecordFeatureTests
WhiteHyun Dec 2, 2023
5922ac2
Merge branch 'develop' into feature/iOS/GWL-184
WhiteHyun Dec 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions iOS/Projects/Core/Network/Sources/MockWebSocketSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public final class MockWebSocketTask: WebSocketTaskProtocol {
private var sentMessage: URLSessionWebSocketTask.Message?
private var receiveContinuation: CheckedContinuation<URLSessionWebSocketTask.Message, Never>?

public init() {}

public func send(_ message: URLSessionWebSocketTask.Message) async throws {
sentMessage = message
receiveContinuation?.resume(returning: message)
Expand All @@ -38,6 +40,8 @@ public final class MockWebSocketTask: WebSocketTaskProtocol {
public struct MockWebSocketSession: URLSessionWebSocketProtocol {
var webSocketTask: MockWebSocketTask = .init()

public init() {}

public func webSocketTask(with _: URLRequest) -> WebSocketTaskProtocol {
return webSocketTask
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import XCTest

final class OnboardingFeatureTests: XCTestCase {
func testAlwaysPassed() {
XCTAssertTrue(true)
}
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ public final class HealthRepository {
/// 사용자에게 HealthKit 사용 인가를 요청합니다.
private func requestAuthorization() {
if HKHealthStore.isHealthDataAvailable() == false {
Log.make().error("Not available HealthKit.")
Log.make(with: .healthKit).error("Not available HealthKit.")
return
}

Log.make().notice("Requesting HealthKit authorization...")
Log.make(with: .healthKit).notice("Requesting HealthKit authorization...")

healthStore.requestAuthorization(toShare: nil, read: healthDataTypeValues) { _, error in
if let error {
Log.make().error("Received an HealthKit error type: \(error)")
Log.make(with: .healthKit).error("Received an HealthKit error type: \(error)")
}
}
}
Expand All @@ -70,7 +70,13 @@ public final class HealthRepository {
private func query(startDate: Date, identifier: HKQuantityTypeIdentifier, anchor: HKQueryAnchor?) async throws -> ([HKSample]?, HKQueryAnchor?) {
return try await withCheckedThrowingContinuation { continuation in

let handler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = { _, samples, _, newAnchor, error in
let query = HKAnchoredObjectQuery(
type: HKQuantityType(identifier),
predicate: HKQuery.predicateForSamples(withStart: startDate, end: nil),
anchor: anchor,
limit: HKObjectQueryNoLimit
) { _, samples, _, newAnchor, error in
Log.make(with: .healthKit).notice("\(samples ?? []), \(newAnchor)")
if let error {
continuation.resume(throwing: error)
} else {
Expand All @@ -79,20 +85,20 @@ public final class HealthRepository {
guard let samples,
samples.isEmpty == false
else {
Log.make().notice("\(identifier.rawValue) Samples are empty.")
Log.make(with: .healthKit).notice("\(identifier.rawValue) Samples are empty.")
return
}
}

let query = HKAnchoredObjectQuery(
type: HKQuantityType(identifier),
predicate: HKQuery.predicateForSamples(withStart: startDate, end: nil),
anchor: anchor,
limit: HKObjectQueryNoLimit,
resultsHandler: handler
)

query.updateHandler = handler
query.updateHandler = { _, samples, _, newAnchor, _ in
Log.make(with: .healthKit).notice("\(samples ?? []), \(newAnchor)")
guard let samples,
samples.isEmpty == false
else {
Log.make(with: .healthKit).notice("\(identifier.rawValue) Samples are empty.")
return
}
}

switch identifier {
case .activeEnergyBurned:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public struct WorkoutRecordRepository: WorkoutRecordRepositoryRepresentable {
/// - Parameter locationData: 사용자의 위치 정보
/// - Parameter healthData: 사용자의 건강 정보
/// - Returns: 기록 고유 Identifier
func record(usingLocation locationData: [LocationDTO], andHealthData healthData: WorkoutHealth) -> AnyPublisher<Int, Error> {
func record(usingLocation locationData: [LocationDTO], andHealthData healthData: WorkoutDataForm) -> AnyPublisher<Int, Error> {
return Deferred {
Future<Data, Error> { promise in
Task {
Expand Down Expand Up @@ -65,7 +65,7 @@ extension WorkoutRecordRepository {

var headers: TNHeaders = .init(headers: [])

init(locationList _: [LocationDTO], health _: WorkoutHealth) {
init(locationList _: [LocationDTO], health _: WorkoutDataForm) {
// TODO: 요청 모델 설정 필요
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// WorkoutSocketRepository.swift
// RecordFeature
//
// Created by 홍승현 on 11/30/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation
import Log
import Trinet

// MARK: - WorkoutSocketRepositoryDependency

protocol WorkoutSocketRepositoryDependency {
var roomID: String { get }
}

// MARK: - WorkoutSocketRepository

struct WorkoutSocketRepository {
private let provider: TNSocketProvider<WorkoutSocketEndPoint>

private let jsonDecoder: JSONDecoder = .init()
private var task: Task<Void, Error>?

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

init(session: URLSessionWebSocketProtocol, dependency: WorkoutSocketRepositoryDependency) {
provider = .init(
session: session,
endPoint: .init(headers: [.init(key: "roomId", value: dependency.roomID)])
)
task = receiveParticipantsData()
}

private func stringToWorkoutRealTimeModel(rawString: String) throws -> WorkoutRealTimeModel {
guard let jsonData = rawString.data(using: .utf8) else {
throw WorkoutSocketRepositoryError.invalidStringForConversion
}
return try jsonDecoder.decode(WorkoutRealTimeModel.self, from: jsonData)
}

private func receiveParticipantsData() -> Task<Void, Error> {
return Task {
Log.make(with: .network).debug("receive Ready")
while true {
do {
switch try await provider.receive() {
case let .string(string):
Log.make(with: .network).debug("received \(string)")
try subject.send(stringToWorkoutRealTimeModel(rawString: string))
default:
Log.make().error("You can't enter this line")
}
} catch {
subject.send(completion: .failure(error))
}
}
Log.make().fault("You can't enter this line")
}
}
}

// MARK: WorkoutSocketRepositoryRepresentable

extension WorkoutSocketRepository: WorkoutSocketRepositoryRepresentable {
func fetchParticipantsRealTime() -> AnyPublisher<WorkoutRealTimeModel, Error> {
subject.eraseToAnyPublisher()
}

func sendMyWorkout(with model: WorkoutRealTimeModel) -> AnyPublisher<Void, Error> {
Future { promise in
Task {
do {
try await provider.send(model: model)
promise(.success(()))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
}

// MARK: WorkoutSocketRepository.WorkoutSocketRepositoryError

extension WorkoutSocketRepository {
enum WorkoutSocketRepositoryError: LocalizedError {
case invalidStringForConversion

var errorDescription: String? {
return "문자열을 Data로 변환할 수 없습니다. 문자열이 비어있는지 확인하세요."
}
}
}

// MARK: - WorkoutSocketEndPoint

private struct WorkoutSocketEndPoint: 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

init(headers: TNHeaders) {
self.headers = headers
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// SessionPeerType.swift
// RecordFeature
//
// Created by 홍승현 on 11/30/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

/// 운동 세션 중 사용자의 UI 정보를 업데이트 하기 위해 사용합니다.
struct SessionPeerType: Identifiable {
/// 사용자 닉네임
let nickname: String

/// 사용자의 Identifier
let id: String

/// 사용자의 프로필 이미지 주소
let profileImageURL: URL
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// WorkoutDataForm.swift
// RecordFeature
//
// Created by 홍승현 on 11/25/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

/// 건강 데이터를 body로 전달하기 위한 요청(request) 모델입니다. 운동 세션이 종료될 때 이 모델을 사용합니다.
public struct WorkoutDataForm: Encodable {
/// 운동 누적 시간
let workoutTime: Int

/// 총 운동한 거리
let distance: Int?

/// 소모한 칼로리
let calorie: Int?

/// 평균 심박수
let averageHeartRate: Int?

/// 운동 중에 기록한 최소 심박수
let minimumHeartRate: Int?

/// 운동 중에 기록한 최대 심박수
let maximumHeartRate: Int?

enum CodingKeys: String, CodingKey {
case workoutTime
case distance
case calorie
case averageHeartRate = "avgHeartRate"
case minimumHeartRate = "minHeartRate"
case maximumHeartRate = "maxHeartRate"
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
//
// WorkoutHealth.swift
// WorkoutHealthForm.swift
// RecordFeature
//
// Created by 홍승현 on 11/25/23.
// Created by 홍승현 on 11/30/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

// TODO: 서버 API와 맞추어야합니다.

/// 건강 데이터를 body로 전달하기 위한 요청(request) 모델입니다. 운동 세션이 종료될 때 이 모델을 사용합니다.
public struct WorkoutHealth: Encodable {
/// 자신이 기록한 건강 데이터를 저장할 때 사용합니다.
public struct WorkoutHealthForm {
/// 총 운동한 거리
let distance: Double?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// WorkoutRealTimeModel.swift
// RecordFeature
//
// Created by 홍승현 on 11/30/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Foundation

// MARK: - WorkoutRealTimeModel

/// 운동 데이터를 송수신하는 모델입니다. 소켓 통신 시 사용합니다.
struct WorkoutRealTimeModel: Codable, Identifiable {
/// User Identifier
let id: String

/// room Identifier
let roomID: String

/// 사용자 닉네임
let nickname: String

/// 사용자 건강 데이터
let health: WorkoutHealthRealTimeModel

enum CodingKeys: String, CodingKey {
case id
case nickname
case health
case roomID = "roomId"
}
}

// MARK: - WorkoutHealthRealTimeModel

struct WorkoutHealthRealTimeModel: Codable {
/// 총 운동한 거리
let distance: Double?

/// 소모한 총 칼로리
let calories: Double?

/// 현재 심박수
let heartRate: Double?
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ import Combine
import Foundation

protocol WorkoutRecordRepositoryRepresentable {
func record(usingLocation locationData: [LocationDTO], andHealthData healthData: WorkoutHealth) -> AnyPublisher<Int, Error>
func record(usingLocation locationData: [LocationDTO], andHealthData healthData: WorkoutDataForm) -> AnyPublisher<Int, Error>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// WorkoutSocketRepositoryRepresentable.swift
// RecordFeature
//
// Created by 홍승현 on 11/30/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation

protocol WorkoutSocketRepositoryRepresentable {
/// 참여자의 실시간 운동 정보를 가져옵니다.
func fetchParticipantsRealTime() -> AnyPublisher<WorkoutRealTimeModel, Error>

/// 나의 운동 정보를 전달합니다.
func sendMyWorkout(with model: WorkoutRealTimeModel) -> AnyPublisher<Void, Error>
}
Loading