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

send message #5

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions Emotilt/Emotilt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Emotilt/Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Emotilt에서 로컬 네트워크를 이용하고자 합니다.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
Expand Down Expand Up @@ -408,6 +409,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Emotilt/Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Emotilt에서 로컬 네트워크를 이용하고자 합니다.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
Expand Down
10 changes: 4 additions & 6 deletions Emotilt/Emotilt/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SERVICE_NAME</key>
<string>$(SERVICE_NAME)</string>
<key>SERVICE_IDENTIFIER</key>
<string>$(SERVICE_IDENTIFIER)</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Emotilt에서 로컬 네트워크를 이용하고자 합니다.</string>
<key>NSBonjourServices</key>
<array>
<string>$(BONJOUR_SERVICES)</string>
</array>
<key>SERVICE_IDENTIFIER</key>
<string>$(SERVICE_IDENTIFIER)</string>
<key>SERVICE_NAME</key>
<string>$(SERVICE_NAME)</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
Expand Down
6 changes: 5 additions & 1 deletion Emotilt/Emotilt/Network/MPCSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,25 @@ class MPCSessionManager: NSObject {
}

/// Send message to a specific peer
func sendMessage(_ message: Message, to peerID: MCPeerID) {
func sendMessage(_ message: Message, to peerID: MCPeerID, completion: (Bool) -> ()) {
if !session.connectedPeers.contains(peerID) {
print("unconnected peer")
completion(false)
return
}

guard let data = try? JSONEncoder().encode(message) else {
// fail to encode Message into data
completion(false)
return
}

do {
try session.send(data, toPeers: [peerID], with: .reliable)
completion(true)
} catch let error {
print(error)
completion(false)
}
}
}
Expand Down
23 changes: 18 additions & 5 deletions Emotilt/Emotilt/Network/PeerSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,24 @@ class PeerSessionManager: NSObject {
}.store(in: &bag)
}

func sendMessageToNearestPeer(_ message: Message) {
//TODO: nearestPeerToken should be direction first, then distance
func sendMessageToNearestPeer(_ message: Message, completion: (Bool) -> ()) {
guard let token = nearestPeerToken else {
completion(false)
print("no nearest peer's token")
return
}
print("trying to send to \(token)")

//print("trying to send to \(token)")
guard let peerID = peerList.first(where: { $0.token == token })?.id else {
completion(false)
print("no matching peer in peerList")
return
}

mpcSessionManager.sendMessage(message, to: peerID)
mpcSessionManager.sendMessage(message, to: peerID) { success in
completion(success)
}
}

/// 새로운 Peer를 추가하고 연결받을 준비를 합니다.
Expand Down Expand Up @@ -129,8 +134,16 @@ extension PeerSessionManager: NISessionDelegate {

/// 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(by: {
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[0].discoveryToken
}
Copy link
Owner

Choose a reason for hiding this comment

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

사소하지만..Swift 스타일로 수정해주시면 좋을 것 같아요

}
}

Expand Down
83 changes: 81 additions & 2 deletions Emotilt/Emotilt/ViewModels/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 목록
Expand All @@ -18,14 +28,83 @@ class HomeViewModel: BaseViewModel, ObservableObject {
receivedMessage != nil
}

//MARK: - 뷰 관련 값
@Published var emoji : String = ""
@Published var content : String = ""
Copy link
Owner

Choose a reason for hiding this comment

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

여기 emojicontent를 뷰모델로 뺀 이유가 있나요? 뷰모델에서 뷰로 publish할 필요가 있을지 궁금하네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

메세지를 보내는 로직이 뷰쪽에 있으면 mvvm 구조랑 맞지 앚는 것 같아서 뷰모델으로 빼놓았습니다. 뷰에서는 뷰모델의 viewstate 값에 따라서 중간 레이블 값만 (타이머, 전송 성공, 전송 실패, 모션 감지 실패 등) 바꿀 수 있도록 했고, 뷰에서 모션 감지나 보내는 로직이 들어가 있으면 복잡할 것 같아서요. 어떻게 생각하시나요?

Copy link
Owner

Choose a reason for hiding this comment

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

네 메시지를 보내는 로직이 뷰모델이 있는건 맞는데, emojicontent 자체는 뷰모델의 지시에 따라 바뀌어야하는 값이 아니므로 뷰가 독립적으로 들고 있다가 뷰모델쪽으로 그때그때 넘겨주는 방식이 맞아보여요


//MARK: - 메세지 전송 관련 값
let motionManager = CMMotionManager()
@Published var sendTimer: Timer?
@Published var currentState: viewState = .none
@Published var isSending: Bool = false
@Published var isAccelerating: Bool = false
@Published var accelerationRate: Double = 0.0
@Published var counter: Int = 0
@Published var isReadyForSending: Bool = false
var isDetected: Bool = false
Copy link
Owner

Choose a reason for hiding this comment

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

이 변수들 모두 view 내부에서만 사용되고 뷰모델을 통한 세션과의 연결은 필요하지 않으므로 특별한 이유가 없다면 뷰에 있는 것이 맞아보이는데 혹시 어떻게 생각하시나요


override init(peerSessionManager: PeerSessionManager) {
super.init(peerSessionManager: peerSessionManager)

peerSessionManager.$peerList.assign(to: &$peerList)
peerSessionManager.$receivedMessage.assign(to: &$receivedMessage)
}
}

//MARK: - message send logic
extension HomeViewModel {

///장전을 시작하고 모션을 감지함
func detectAcceleration(){
isDetected = false
isSending = true
currentState = .sendingTimer

motionManager
.startAccelerometerUpdates(to: OperationQueue.current!, withHandler: { [self] (motion, error) in
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
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){
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(_ message: Message) {
peerSessionManager.sendMessageToNearestPeer(message)
func sendMessage(){
let message = Message(emoji: emoji, content: content)
peerSessionManager.sendMessageToNearestPeer(message){ success in
self.currentState = (success) ? .sendingSuccess : .sendingFailure
}
stopTimer()
}
}
127 changes: 75 additions & 52 deletions Emotilt/Emotilt/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,85 +11,108 @@ import Combine
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

var body: some View {
VStack(alignment: .center) {
Spacer()

//MARK: - viewState label
switch (viewModel.currentState){
case .sendingTimer:
Text("\(viewModel.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:
Text("")
}

ZStack {
Button {
isEmojiSheetOpen = true
} label: {
ZStack {
if emoji.isEmpty {
RoundedRectangle(cornerRadius: 32)
.fill(.tertiary.opacity(0.4))
.frame(width: 168, height: 168)
if viewModel.emoji.isEmpty {
RoundedRectangle(cornerRadius: 32)
.fill(.tertiary.opacity(0.4))
.frame(width: 168, height: 168)
}

Text(viewModel.emoji)
.font(.system(size: 168))
}

Text(emoji)
.font(.system(size: 168))
}
}


ZStack {
//DirectionIndicatorView()
}
}.frame(width: 280, height: 280)

ZStack {
// DirectionIndicatorView()
}
}.frame(width: 280, height: 280)

Group {
TextField("", text: $content, axis: .vertical)
.placeholder(when: content.isEmpty && !isTextFieldFocused) {
Group {
if #available(iOS 16.0, *) {
TextField("", text: $viewModel
.content, axis: .vertical)
.placeholder(when: viewModel.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($viewModel.content)) { _ in
if viewModel.content.count > 20 {
viewModel.content = String(viewModel.content.prefix(20))
}
}
.submitLabel(.done)
.onChange(of: viewModel.content) { text in
if text.last == "\n" {
viewModel.content = String(text.dropLast())
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

이 부분은 입력값의 마지막이 개행 문자인지 검사하는 것보다는 .onSubmit 등을 이용하는 것이 어떨까요? 그리고 @FocusState를 이용 중이므로 UIApplication ~ 한 줄을 위해 UIKit을 import하는 것보다는 SwiftUI스럽게 isTextFieldFocused = false로 수정하면 어떨지 제안 드립니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이 부분은 지호님께서 수정하신 부분인것 같은데 깃에서 제가 추가한걸로 인식한 것 같네요!

} else {
// Fallback on earlier versions
}

}

Spacer()
Spacer()

RoundedButton(label: "Send") {
//viewModel.detectAcceleration()
viewModel.sendMessage()
}
}

Spacer()
Spacer()

RoundedButton(label: "Send") {
viewModel.sendMessage(.init(emoji: "🤔", content: "Nyam"))
}

}
.padding(.horizontal, 36)
.padding(.vertical, 24)
.onTapGesture(perform: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
})
.sheet(isPresented: $isEmojiSheetOpen) {
EmojiSheetView(selected: $emoji)
.padding(.horizontal, 36)
.padding(.vertical, 24)
.onTapGesture(perform: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
})
.sheet(isPresented: $isEmojiSheetOpen) {
EmojiSheetView(selected: $viewModel.emoji)
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView(viewModel: .init(peerSessionManager: .debug))
}
}