diff --git a/Emotilt/Emotilt.xcodeproj/project.pbxproj b/Emotilt/Emotilt.xcodeproj/project.pbxproj index 9d989e9..2de88b6 100644 --- a/Emotilt/Emotilt.xcodeproj/project.pbxproj +++ b/Emotilt/Emotilt.xcodeproj/project.pbxproj @@ -374,6 +374,8 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Emotilt/Info.plist; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Emotilt에서 로컬 네트워크를 이용하고자 합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"Emotilt wants to connect!\""; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; @@ -408,6 +410,8 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Emotilt/Info.plist; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Emotilt에서 로컬 네트워크를 이용하고자 합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"Emotilt wants to connect!\""; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; diff --git a/Emotilt/Emotilt/Info.plist b/Emotilt/Emotilt/Info.plist index e89e01e..ceac6a1 100644 --- a/Emotilt/Emotilt/Info.plist +++ b/Emotilt/Emotilt/Info.plist @@ -2,16 +2,14 @@ - SERVICE_NAME - $(SERVICE_NAME) - SERVICE_IDENTIFIER - $(SERVICE_IDENTIFIER) - NSLocalNetworkUsageDescription - Emotilt에서 로컬 네트워크를 이용하고자 합니다. NSBonjourServices $(BONJOUR_SERVICES) + SERVICE_IDENTIFIER + $(SERVICE_IDENTIFIER) + SERVICE_NAME + $(SERVICE_NAME) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Emotilt/Emotilt/Network/MPCSessionManager.swift b/Emotilt/Emotilt/Network/MPCSessionManager.swift index 73d72c3..6756c1a 100644 --- a/Emotilt/Emotilt/Network/MPCSessionManager.swift +++ b/Emotilt/Emotilt/Network/MPCSessionManager.swift @@ -17,7 +17,9 @@ class MPCSessionManager: NSObject { @Published var received: (MCPeerID, Data)? @Published var connectionState: (MCPeerID, MCSessionState)? + @Published var didSendMessage: Bool? + override init() { self.session = .init(peer: localPeerID, securityIdentity: nil, @@ -74,18 +76,22 @@ class MPCSessionManager: NSObject { func sendMessage(_ message: Message, to peerID: MCPeerID) { if !session.connectedPeers.contains(peerID) { print("unconnected peer") + didSendMessage = false return } guard let data = try? JSONEncoder().encode(message) else { // fail to encode Message into data + didSendMessage = false return } do { try session.send(data, toPeers: [peerID], with: .reliable) + didSendMessage = true } catch let error { print(error) + didSendMessage = false } } } diff --git a/Emotilt/Emotilt/Network/PeerSessionManager.swift b/Emotilt/Emotilt/Network/PeerSessionManager.swift index ccc515e..0a3bcef 100644 --- a/Emotilt/Emotilt/Network/PeerSessionManager.swift +++ b/Emotilt/Emotilt/Network/PeerSessionManager.swift @@ -21,6 +21,9 @@ class PeerSessionManager: NSObject { /// 현재 내 로컬 디바이스와 가장 가까이 있는 기기의 discoveryToken @Published var nearestPeerToken: NIDiscoveryToken? + + /// 메세지 발신 성공 여부 + @Published var didSendMessage: Bool? private var bag = Set() @@ -51,21 +54,27 @@ class PeerSessionManager: NSObject { self?.receivedMessage = message } }.store(in: &bag) + + mpcSessionManager.$didSendMessage.receive(on: RunLoop.main).sink { [weak self] success in + self?.didSendMessage = success + }.store(in: &bag) } func sendMessageToNearestPeer(_ message: Message) { guard let token = nearestPeerToken else { + didSendMessage = false print("no nearest peer's token") return } - print("trying to send to \(token)") guard let peerID = peerList.first(where: { $0.token == token })?.id else { + didSendMessage = false print("no matching peer in peerList") return } mpcSessionManager.sendMessage(message, to: peerID) + print("Message success!") } /// 새로운 Peer를 추가하고 연결받을 준비를 합니다. @@ -111,7 +120,6 @@ class PeerSessionManager: NSObject { print("no matching peer") return } - peer.token = token let config = NINearbyPeerConfiguration(peerToken: token) peer.session.run(config) @@ -125,12 +133,20 @@ extension PeerSessionManager: NISessionDelegate { nearestPeerToken = getNearestPeer(from: nearbyObjects) } - // MARK: Get nearest device - /// Sort nearbyObjects by direction and return the nearest object's discoveryToken private func getNearestPeer(from nearbyObjects: [NINearbyObject]) -> NIDiscoveryToken { - let directions = nearbyObjects.sorted { $0.distance ?? .zero < $1.distance ?? .zero } - return directions[0].discoveryToken + let directionFiltered = nearbyObjects.filter { $0.direction != nil } + if !directionFiltered.isEmpty { + let directionSorted = directionFiltered.sorted { + norm_one($0.direction!) < norm_one($1.direction!) + } + return directionSorted.first!.discoveryToken + } else { + let distanceSorted = nearbyObjects.sorted { + $0.distance ?? .zero < $1.distance ?? .zero + } + return distanceSorted.first!.discoveryToken + } } } diff --git a/Emotilt/Emotilt/ViewModels/HomeViewModel.swift b/Emotilt/Emotilt/ViewModels/HomeViewModel.swift index a02f738..1f788e6 100644 --- a/Emotilt/Emotilt/ViewModels/HomeViewModel.swift +++ b/Emotilt/Emotilt/ViewModels/HomeViewModel.swift @@ -1,4 +1,4 @@ -// +///Users/grace/Library/Mobile Documents/com~apple~CloudDocs/Desktop/EmoTilt_Xcode13.4/EmoTilt_Xcode13.4/Config.xcconfig // HomeViewModel.swift // Emotilt // @@ -6,6 +6,16 @@ // import Foundation +import CoreMotion +import UIKit + +enum viewState { + case sendingSuccess //메세지 보내기 성공 직후 + 5초 + case sendingFailure //메세지 보내기 실패 직후 + 5초 + case motionDetectFailure //장전 이후 모션 감지 실패 + case sendingTimer //장전 단계 + case none //디폴트 화면 +} class HomeViewModel: BaseViewModel, ObservableObject { /// 연결된 peer 목록 @@ -14,18 +24,28 @@ class HomeViewModel: BaseViewModel, ObservableObject { /// 수신한 메시지 @Published var receivedMessage: Message? + /// 메세지 발신 성공 여부 + @Published var didSendMessage: Bool? + var didReceiveMessage: Bool { receivedMessage != nil } + @Published var currentState: viewState = .none + override init(peerSessionManager: PeerSessionManager) { super.init(peerSessionManager: peerSessionManager) - peerSessionManager.$peerList.assign(to: &$peerList) peerSessionManager.$receivedMessage.assign(to: &$receivedMessage) + peerSessionManager.$didSendMessage.assign(to: &$didSendMessage) } - func sendMessage(_ message: Message) { + func sendMessage(emoji: String, content: String){ + let message = Message(emoji: emoji, content: content) peerSessionManager.sendMessageToNearestPeer(message) } } + + + + diff --git a/Emotilt/Emotilt/Views/HomeView.swift b/Emotilt/Emotilt/Views/HomeView.swift index fd7fb0a..d8187e1 100644 --- a/Emotilt/Emotilt/Views/HomeView.swift +++ b/Emotilt/Emotilt/Views/HomeView.swift @@ -7,20 +7,50 @@ import SwiftUI import Combine +import CoreMotion struct HomeView: View { @ObservedObject var viewModel: HomeViewModel - @State private var isEmojiSheetOpen: Bool = false - @State private var emoji: String = "" - @State private var content: String = "" @FocusState private var isTextFieldFocused: Bool + //MARK: - 메세지 전송 관련 값 + let motionManager = CMMotionManager() + @State private var sendTimer: Timer? + @State private var currentState: viewState = .none + @State private var isSending = false + @State private var isAccelerating = false + @State private var accelerationRate = 0.0 + @State private var counter = 0 + @State private var isReadyForSending = false + @State var isDetected = false + + @State var emoji : String = "" + @State var content : String = "" + var body: some View { VStack(alignment: .center) { Spacer() + //MARK: - viewState label + switch currentState { + case .sendingTimer: + Text("\(counter)") + .font(.system(size: 40, weight: .bold)) + case .sendingSuccess: + Text("메세지 전송 성공!") + .font(.system(size: 20, weight: .bold)) + case .sendingFailure: + Text("메세지 전송 실패 ㅠ") + .font(.system(size: 20, weight: .bold)) + case .motionDetectFailure: + Text("메시지를 보내려면 흔들어주세요!") + .font(.system(size: 20, weight: .bold)) + case .none: + EmptyView() + } + ZStack { Button { isEmojiSheetOpen = true @@ -37,45 +67,49 @@ struct HomeView: View { } } + ZStack { -// DirectionIndicatorView() + //DirectionIndicatorView() } }.frame(width: 280, height: 280) Group { - TextField("", text: $content, axis: .vertical) - .placeholder(when: content.isEmpty && !isTextFieldFocused) { + + if #available(iOS 16.0, *) { + TextField("", text: $content, axis: .vertical) + .placeholder(when: content.isEmpty && !isTextFieldFocused){ Text("20자 이내") .foregroundColor(.gray) .opacity(0.8) - } - .focused($isTextFieldFocused) - .font(.system(size: 24, weight: .bold)) - .lineLimit(2) - .frame(height: 64) - .multilineTextAlignment(.center) - .onReceive(Just(content)) { _ in - if content.count > 20 { - content = String(content.prefix(20)) } - } - .submitLabel(.done) - .onChange(of: content) { text in - if text.last == "\n" { - content = String(text.dropLast()) - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil) + .focused($isTextFieldFocused) + .font(.system(size: 24, weight: .bold)) + .lineLimit(2) + .frame(height: 64) + .multilineTextAlignment(.center) + .onReceive(Just($content)) { _ in + if content.count > 20 { + content = String(content.prefix(20)) + } } - } - + .submitLabel(.done) + .onChange(of: content) { text in + if text.last == "\n" { + content = String(text.dropLast()) + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil) + } + } + } else { + // Fallback on earlier versions + } } Spacer() Spacer() RoundedButton(label: "Send") { - viewModel.sendMessage(.init(emoji: "🤔", content: "Nyam")) + detectAcceleration() } - } .padding(.horizontal, 36) .padding(.vertical, 24) @@ -88,8 +122,73 @@ struct HomeView: View { } } +//MARK: - message sending +extension HomeView { + func detectAcceleration() { + isDetected = false + isSending = true + currentState = .sendingTimer + + motionManager.startAccelerometerUpdates(to: OperationQueue.current!) { [self] motion, error in + withAnimation { + isAccelerating = true + accelerationRate = (motion?.acceleration.x)! + 1 + if (motion?.acceleration.x)! > 0.35 { + isDetected = true + motionManager.stopAccelerometerUpdates() + UINotificationFeedbackGenerator().notificationOccurred(.success) + sendMessage() + } + } + } + + startTimer() + } + + ///5초 카운트다운을 시작함 + func startTimer() { + if counter == 0 { counter = 5 } + sendTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { tempTimer in + withAnimation { + if self.counter > 0 { self.counter -= 1 } + if self.counter == 0 && !self.isDetected { + self.currentState = .motionDetectFailure + self.stopTimer() + } + } + } + } + + func stopTimer() { + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { // 3초 딜레이 + withAnimation { + self.currentState = .none + self.isSending = false + self.sendTimer?.invalidate() + self.sendTimer = nil + self.emoji = "" + self.content = "" + self.currentState = .none + self.motionManager.stopAccelerometerUpdates() + self.isAccelerating = false + } + } + } + + func sendMessage() { + viewModel.sendMessage(emoji: emoji, content: content) + if let didSendMessage = viewModel.didSendMessage { + withAnimation { + currentState = didSendMessage ? .sendingSuccess : .sendingFailure + } + stopTimer() + } + } +} + struct ContentView_Previews: PreviewProvider { static var previews: some View { HomeView(viewModel: .init(peerSessionManager: .debug)) } } +