Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions mirroringBooth/StoreTests/CameraPreviewStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ struct CameraPreviewStoreTests {
let store = CameraPreviewStore(
browser: Browser(),
manager: manager,
deviceName: deviceName
deviceName: deviceName,
watchConnectionManager: nil
)
return (store, manager)
}
Expand Down Expand Up @@ -154,9 +155,9 @@ struct CameraPreviewStoreTests {
@Test func 미러링_연결이_끊기면_상태가_반영됨() {
let (store, _) = makeSUT()

store.send(.isMirroringDisconnected)
store.send(.isPrimaryDeviceDisconnected)

#expect(store.state.isMirroringDisconnected == true)
#expect(store.state.isPrimaryDeviceDisconnected == true)
}

// MARK: - reduce 테스트 (Browser 관련 Intent 대체)
Expand Down
2 changes: 1 addition & 1 deletion mirroringBooth/WatchStoreTests/WatchStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct WatchStoreTests {
@Test func 대기_화면이_나타나면_대기_상태가_켜짐() {
let store = makeSUT()

store.send(.setIsWaiting(true))
store.send(.isWaiting(true))

#expect(store.state.isWaiting == true)
}
Expand Down
2 changes: 1 addition & 1 deletion mirroringBooth/mirroringBooth/App/RootStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension RootStore {
for await stream in advertiser.rootStream {
switch stream {
case .onHeartbeatTimeout:
await MainActor.run {
await MainActor.run { [weak self] in
self?.send(.showTimeoutAlert(true))
}
}
Expand Down
4 changes: 2 additions & 2 deletions mirroringBooth/mirroringBooth/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ struct RootView: View {
case .advertising:
AdvertisingView()

case .connectionList(let list, let browser):
ConnectionCheckView(list, browser: browser)
case .connectionList(let list, let browser, let watchConnectionManager):
ConnectionCheckView(list, browser: browser, watchConnectionManager: watchConnectionManager)

case .completion:
StreamingCompletionView()
Expand Down
2 changes: 1 addition & 1 deletion mirroringBooth/mirroringBooth/Core/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class Router {
enum CameraRoute: Hashable {
case browsing
case advertising
case connectionList(ConnectionList, Browser)
case connectionList(ConnectionList, Browser, WatchConnectionManager)
case completion
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ final class Browser: NSObject, BrowserCommandDelegate {
/// 타이머 모드 선택 명령 수신 콜백
var onSelectedTimerModeCommand: (() -> Void)?

/// 타이머 모드가 선택되었는지 여부 (워치 disconnect 무시용)
var isTimerModeSelected: Bool = false

// MARK: - Stream Manager

let streamManager = BrowserStreamManager()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ struct BrowsingView: View {
ConnectionList(
cameraName: store.browser.myDeviceName,
mirroringName: mirroringDevice.id,
remoteName: store.state.remoteDevice?.id ?? nil
remoteName: store.state.remoteDevice?.id
),
store.browser
store.browser,
store.watchConnectionManager
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ protocol BrowserCommandDelegate: AnyObject {
var onSelectedTimerModeCommand: (() -> Void)? { get }
var mirroringHeartBeater: HeartBeater { get }
var remoteHeartBeater: HeartBeater? { get }
var isTimerModeSelected: Bool { get set }
}

/// 명령 수신 및 실행을 담당하는 매니저
Expand Down Expand Up @@ -42,11 +43,14 @@ final class BrowserCommandManager {
self?.delegate?.sendRemoteCommand(.navigateToRemoteComplete)
}
case .setRemoteMode:
delegate?.isTimerModeSelected = false
DispatchQueue.main.async { [weak self] in
self?.delegate?.onRemoteModeCommand?()
self?.delegate?.sendRemoteCommand(.navigateToRemoteCapture)
}
case .selectedTimerMode:
// 워치 disconnect 무시를 위해 동기적으로 먼저 플래그 설정
delegate?.isTimerModeSelected = true
DispatchQueue.main.async { [weak self] in
self?.delegate?.onSelectedTimerModeCommand?()
self?.delegate?.sendRemoteCommand(.navigateToHome)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final class WatchConnectionManager: NSObject {
var onReachableChanged: ((Bool) -> Void)?
var onReceiveCaptureRequest: (() -> Void)?
var onReceiveConnectionAck: (() -> Void)?
var onWatchDisconnected: (() -> Void)?

override init() {
if WCSession.isSupported() {
Expand Down Expand Up @@ -239,6 +240,11 @@ extension WatchConnectionManager: WCSessionDelegate {
self.prepareWatchToCapture()
}
}
} else if actionValue == ActionValue.disconnect.rawValue {
self.logger.info("워치에서 연결 해제 요청 수신됨.")
Task { @MainActor in
self.onWatchDisconnected?()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ final class ConnectionCheckStore: StoreProtocol {
private(set) var state: State = .init()

let browser: Browser
let watchConnectionManager: WatchConnectionManager
let cameraDevice: String
let mirroringDevice: String

private var heartbeatTask: Task<Void, Never>?

init(
_ list: ConnectionList,
_ browser: Browser
_ browser: Browser,
_ watchConnectionManager: WatchConnectionManager
) {
self.cameraDevice = list.cameraName
self.mirroringDevice = list.mirroringName
self.browser = browser
self.watchConnectionManager = watchConnectionManager

Task { @MainActor in
reduce(.setRemoteDevice(list.remoteName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ struct ConnectionCheckView: View {
@Environment(Router.self) var router: Router
@State private var store: ConnectionCheckStore

init(_ list: ConnectionList, browser: Browser) {
self.store = .init(list, browser)
init(_ list: ConnectionList, browser: Browser, watchConnectionManager: WatchConnectionManager) {
self.store = .init(list, browser, watchConnectionManager)
}

var body: some View {
Expand Down Expand Up @@ -86,6 +86,7 @@ struct ConnectionCheckView: View {
CameraPreview(
store.browser,
mirroringName: store.mirroringDevice,
watchConnectionManager: store.watchConnectionManager,
onDismissByCaptureCompletion: {
store.send(.navigateToCompletion(true))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ struct CameraPreview: View {
init(
_ browser: Browser,
mirroringName: String,
watchConnectionManager: WatchConnectionManager?,
onDismissByCaptureCompletion: (() -> Void)? = nil
) {
self.store = .init(browser: browser, manager: CameraManager(), deviceName: mirroringName)
self.store = .init(
browser: browser,
manager: CameraManager(),
deviceName: mirroringName,
watchConnectionManager: watchConnectionManager
)
self.onDismissByCaptureCompletion = onDismissByCaptureCompletion
}

Expand Down Expand Up @@ -93,7 +99,7 @@ struct CameraPreview: View {
}
.homeAlert(
isPresented: Binding(
get: { store.state.isMirroringDisconnected },
get: { store.state.isPrimaryDeviceDisconnected },
set: { _ in }
),
message: "기기 연결이 끊겼습니다.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ final class CameraPreviewStore: StoreProtocol {
var animationFlag: Bool = false
var isTransferring: Bool = false
var isCaptureCompleted: Bool = false
var isMirroringDisconnected: Bool = false
var isPrimaryDeviceDisconnected: Bool = false
}

enum Intent {
case entry(withAngle: Int)
case exit
case updateAngle(rawValue: Int)
case isMirroringDisconnected
case isPrimaryDeviceDisconnected
case browserEvent(CameraStreamEvents)
}

Expand All @@ -41,12 +41,13 @@ final class CameraPreviewStore: StoreProtocol {
case setIsTransferring(Bool)
case captureCompleted
case resetCaptureCompleted
case isMirroringDisconnected
case setIsPrimaryDeviceDisconnected
}

private(set) var state: State
private let browser: Browser
private let cameraManager: CameraManageable
private let watchConnectionManager: WatchConnectionManager?
private var cancellables = Set<AnyCancellable>()
private var heartbeatTask: Task<Void, Never>?

Expand All @@ -58,12 +59,15 @@ final class CameraPreviewStore: StoreProtocol {
browser: Browser,
manager: CameraManageable,
deviceName: String,
watchConnectionManager: WatchConnectionManager?
) {
self.browser = browser
self.cameraManager = manager
self.watchConnectionManager = watchConnectionManager
self.state = State(deviceName: deviceName)

setupHeartbeatListener()
setupWatchConnectionListener()
}

deinit {
Expand All @@ -89,8 +93,8 @@ final class CameraPreviewStore: StoreProtocol {
case .updateAngle(let rawValue):
return [.updateAngle(rawValue)]

case .isMirroringDisconnected:
return [.isMirroringDisconnected]
case .isPrimaryDeviceDisconnected:
return [.setIsPrimaryDeviceDisconnected]

case .browserEvent(let event):
return handleBrowserEvent(event)
Expand Down Expand Up @@ -122,8 +126,8 @@ final class CameraPreviewStore: StoreProtocol {
case .resetCaptureCompleted:
state.isCaptureCompleted = false

case .isMirroringDisconnected:
state.isMirroringDisconnected = true
case .setIsPrimaryDeviceDisconnected:
state.isPrimaryDeviceDisconnected = true
}
self.state = state
}
Expand Down Expand Up @@ -187,16 +191,32 @@ extension CameraPreviewStore {
await MainActor.run {
switch event {
case .heartbeatTimeout:
self.send(.isMirroringDisconnected)
self.browser.disconnect(useType: .remote)
self.handleDisconnection()
case .remoteHeartbeatTimeout:
self.browser.sendCommand(.switchSelectModeView)
self.handleDisconnection()
}
}
}
}
}

private func setupWatchConnectionListener() {
watchConnectionManager?.onWatchDisconnected = { [weak self] in
guard let self else { return }
Task { @MainActor in
// 타이머 모드가 선택되었으면 워치 disconnect 무시
guard !self.browser.isTimerModeSelected else { return }
self.handleDisconnection()
}
}
}

@MainActor
private func handleDisconnection() {
send(.isPrimaryDeviceDisconnected)
browser.disconnect()
}
Comment on lines +215 to +218
Copy link
Collaborator

Choose a reason for hiding this comment

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

꼭 분리할 필요가 있었을까? 라는 생각을 했지만
메서드 이름이 handleDisconnection이니 오히려 직관적으로 다가와서 좋은 것 같네요 👀


private func handleBrowserEvent(_ event: CameraStreamEvents) -> [Result] {
switch event {
case .sendPhoto:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ final class WatchConnectionManager: NSObject {
)
}

/// 사용자가 X 버튼을 눌러 연결을 해제했음을 iPhone에 알립니다.
func sendDisconnectRequest() {
self.sendMessage(
action: .disconnect,
rejectedActionString: "연결 해제 요청을 보낼 수 없습니다."
)
}

private nonisolated func handleAppStateUpdate(_ applicationContext: [String: Any]) {
let appStateRawValue = applicationContext[MessageKey.appState.rawValue] as? String
let appStateValue = appStateRawValue.flatMap { AppStateValue(rawValue: $0) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ final class WatchConnectionStore: StoreProtocol {
self.connectionManager.start()

case .disconnect:
self.connectionManager.sendDisconnectRequest()
self.connectionManager.stop()
return [.setConnectionState(.notConnected)]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ struct RemoteCaptureView: View {
}
}
}
.navigationBarBackButtonHidden()
}
}