Watch Connectivity로 폰-워치 통신 구현 OE - 1
기존의 앱은 storyboard를 사용해서 만들었지만 워치까지 이용하기 위해서는 swiftUI로의 이사가 필수였다.
구현할 것은 다음과 같다.
1. watch connectivity를 이용해서 아이폰에서 워치로 메시지 전송
2. 받은 문자열을 워치에서 점자로 변환
3. 변환한 점자를 워치의 크라운을 사용해서 앞,뒤 글씨로 이동하며 읽기 구현
4. 점자의 요철에 따라 점마다 터치시 진동
5. 워치 진동 구현
6. coreML swiftUI로 구현
7. 파이어베이스 연결
8. 추후 백엔드와 연결
1. Watch connectivity 구현
https://kka3seb.tistory.com/682
iOS App에서 Apple watch로 message를 전송하는 방법
iOS App에서 Apple watch app으로 message를 전송하는 방법입니다. 참고로 제가 사용한 기기는 Apple Watch Series 7와 iPhone 11이며, SwiftUI를 사용했습니다. 1. 프로젝트 생성 Create a new Xcode project로 새로운 프로
kka3seb.tistory.com
Xcode 14 이후로 watch Extention이 사라져서 알아보느라 고생을 좀 했는데,
좋은 기본 예제가 있어서 코드의 분석 정도만 주석으로 추가했다.
먼저 iPhone쪽의 ViewModel코드이다.
//
// ViewModelPhone.swift
// CounterConnect
//
// Created by 서정덕 on 2023/04/06.
//
import Foundation
import WatchConnectivity
// ViewModelPhone 클래스: WatchConnectivity를 사용하여 iPhone과 Apple Watch 간 통신을 관리하는 뷰 모델
class ViewModelPhone : NSObject, WCSessionDelegate {
// WCSession 인스턴스를 저장할 변수
var session: WCSession
// 초기화 메서드: 외부에서 WCSession을 전달하거나 기본값을 사용하여 ViewModelPhone 인스턴스를 생성.
init(session: WCSession = .default){
self.session = session
super.init() // 상위 클래스인 NSObject의 초기화 메서드 호출
self.session.delegate = self // WCSessionDelegate를 self로 설정
session.activate() // WCSession을 활성화
}
// WCSessionDelegate 메서드: 세션 활성화가 완료되면 호출
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
// WCSessionDelegate 메서드: 세션이 비활성화된 경우 호출
func sessionDidBecomeInactive(_ session: WCSession) {
}
// WCSessionDelegate 메서드: 세션이 비활성화된 후 다시 활성화되기 전에 호출
func sessionDidDeactivate(_ session: WCSession) {
}
}
WCSessionDelegate 프로토콜은 WatchConnectivity 프레임워크의 일부로, iPhone과 Apple Watch 간의 통신에 대한 상태 및 이벤트 변경을 처리하기 위한 메서드를 정의한다.
WCSessionDelegate를 self로 설정하는 것은 WCSession 인스턴스의 이벤트를 처리할 객체를 ViewModelPhone 인스턴스 자체로 지정하겠다는 것을 말한다. 이렇게 함으로써 WCSession 객체는 이벤트가 발생할 때마다 ViewModelPhone 객체의 적절한 메서드를 호출하게 되어, 앱에서 필요한 이벤트 처리를 수행할 수 있다.
다음은 watch쪽의 ViewModel 코드이다.
//
// ViewModelWatch.swift
// CounterConnect Watch App
//
// Created by 서정덕 on 2023/04/06.
//
import Foundation
import WatchConnectivity
// ViewModelWatch 클래스: WatchConnectivity를 사용하여 Apple Watch와 iPhone 간 통신을 관리하는 뷰 모델
class ViewModelWatch : NSObject, WCSessionDelegate, ObservableObject {
// WCSession 인스턴스를 저장할 변수
var session: WCSession
// messageText 프로퍼티: iPhone으로부터 받은 메시지를 저장하는 문자열 변수, 초기값은 빈 문자열
@Published var messageText = ""
// 초기화 메서드: 외부에서 WCSession을 전달하거나 기본값을 사용하여 ViewModelWatch 인스턴스를 생성
init(session: WCSession = .default){
self.session = session
super.init()
self.session.delegate = self // WCSessionDelegate를 self로 설정
session.activate() // WCSession을 활성화
}
// WCSessionDelegate 메서드: 세션 활성화가 완료되면 호출
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
// WCSessionDelegate 메서드: iPhone으로부터 메시지를 받으면 호출
// 메시지에 포함된 "message"라는 key를 가진 문자열을 messageText 프로퍼티에 할당
// 메시지에 "message" key가 없으면 빈 문자열을 messageText에 할당
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
DispatchQueue.main.async {
self.messageText = message["message"] as? String ?? ""
}
}
}
마지막의 session(_ session: WCSession, didReceiveMessage message: [String: Any]) 정도만 보면 된다.
DispatchQueue.main.async 블록은 작업을 비동기적으로 처리하기 위해 사용되었다. 원래 Swift에서는 기본적으로 단일 스레드에서 동기적으로 코드가 실행된다. 하지만, DispatchQueue.main.async를 사용하면 메인 스레드에서 UI 관련 작업을 비동기적으로 처리하면서도 안전하게 실행할 수 있다.
// phone ContentView.swift
// 뷰모델 인스턴스 생성, 초기화
var model = ViewModelPhone()
//...
// "Send Message" 버튼: 클릭 시 입력한 메시지를 Apple Watch로 전송
Button(action: {
self.model.session.sendMessage(["message": self.messageText], replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}) {
Text("Send Message")
}
// watch ContentView.swift
@ObservedObject var model = ViewModelWatch()
//...
Text("값: \(str)")
// 변화를 감지할 변수이름에 $를 붙여 감시, 파라미터는 변화한 값
.onReceive(self.model.$messageText) { message in
self.str = message
}
워치쪽 CV에서 뷰모델이 @ObservedObject로 선언되었는데, 이는 객체가 SwiftUI View에 의해 관찰될 때 변경 사항을 자동으로 알리는 데 사용하기 위함이다.
@State는 로컬 상태를 관리하고 UI를 업데이트하기 위한 속성인데 반해, @ObservedObject는 외부 객체의 변경 사항을 감지하고 해당 변경 사항을 반영하기 위한 속성이라는 차이가 있다.
2. 받은 문자열을 워치에서 점자로 변환
/// 일반 String 받아서 점자 Int arr로 반환. 입력: "Hello", 출력: [[0,1,1,0,0,0],[0,0,0,1,1,0]]
func convert(str string: String) -> [[Int]]{
/// 소문자로 저장된 일반 String
let str = string.lowercased()
/// 반환할 배열, ["100011", "010010"]의 형식을 갖고있음
var returnValue: [[Int]] = []
/// [글자: 점자] 딕셔너리
let eng2Braille: [Character: Character] = [
"a": "⠁", "b": "⠃", "c": "⠉", "d": "⠙",
// ...
"8": "⠦", "9": "⠔"
]
/// [점자: 이진수] 딕셔너리
let braille2IntArr: [Character: [Int]] = [
"⠀": [0,0,0,0,0,0], "⠁": [0,0,0,0,0,1],
// ...
"⠾": [1,1,1,1,1,0], "⠿": [1,1,1,1,1,1]
]
// 입력받은 문자열의 각 글자를 순회하면서 점자로 변환하고,
// 점자를 이진 숫자 배열로 변환하여 반환할 배열에 추가
for i in 0..<str.count {
// 입력받은 문자열에서 i번째 글자를 가져옴
let char: Character = str.getChar(at: i)
// i번째 글자에 해당하는 점자 문자를 딕셔너리에서 찾음 braille: "⠗"
if let braille: Character = eng2Braille[char] {
print(braille)
// 점자 문자에 해당하는 이진 숫자 배열을 반환할 배열에 추가
returnValue.append(braille2IntArr[braille]!)
}
}
// 변환된 이진 숫자 2DArr을 반환
return returnValue
}
한글 점역도 고려하여 일반 문자를 바로 이진배열로 치환하지 않고 중간에 점자 글씨로 바꾸는 기능을 추가했다.
3. 변환한 점자를 워치의 크라운을 사용해서 앞,뒤 글씨로 이동하며 읽기 구현
기능 구현을 위해 digitalCrownRotation을 사용했다.
https://developer.apple.com/documentation/swiftui/view/digitalcrownrotation(_:onchange:onidle:)
digitalCrownRotation(_:onChange:onIdle:) | Apple Developer Documentation
Tracks Digital Crown rotations by updating the specified binding.
developer.apple.com
@State private var value: Double = 0
var body: some View {
VStack {
Text("Value: \(value)")
}
.digitalCrownRotation($value, onChange: { event in
// Digital Crown 회전 이벤트 처리
}, onIdle: {
// Digital Crown 회전이 멈춘 후 처리
})
}
binding: Digital Crown의 회전으로 인해 업데이트되는 값에 대한 바인딩
onChange: Digital Crown의 회전 이벤트가 발생할 때 호출되는 클로저. DigitalCrownEvent 타입의 매개변수를 받으며, 회전 이벤트에 대한 세부 정보를 제공
onIdle: Digital Crown의 회전이 멈추고 일정 시간 이상 아무런 회전 이벤트가 발생하지 않았을 때 호출되는 클로저