이진 배열([0, 0, 0, 1, 1, 1])을 받으면 1인 부분에 손가락이 닿으면 진동하는 기능을 구현할 것이다. 시뮬레이터에서는 진동이 기능하지 않기 때문에 진동소리 오디오파일를 재생하는 방식으로 구현할 것이다.
아래는 사용자가 드래그하는 동안 사각형 영역 내부에 있는지 여부에 따라 배경색을 변경하는 간단한 예제이다.
//
// ContentView.swift
// GestureRecMac
//
// Created by 서정덕 on 2023/04/28.
//
import SwiftUI
struct ContentView: View {
@State private var backgroundColor = Color.white
@State private var isDragging = false
@State private var dragLocation: CGPoint = .zero
private let rectangleSize: CGSize = CGSize(width: 200, height: 200)
private func isInsideRectangle(_ location: CGPoint, geometry: GeometryProxy) -> Bool {
let rect = CGRect(origin: CGPoint(x: (geometry.size.width - rectangleSize.width) / 2,
y: (geometry.size.height - rectangleSize.height) / 2),
size: rectangleSize)
return rect.contains(location)
}
var body: some View {
GeometryReader { geometry in
ZStack {
backgroundColor.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(Color.blue)
.frame(width: rectangleSize.width, height: rectangleSize.height)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
isDragging = true
dragLocation = value.location
if isInsideRectangle(dragLocation, geometry: geometry) {
backgroundColor = Color.red
} else {
backgroundColor = Color.white
}
}
.onEnded { _ in
isDragging = false
}
)
}
}
}
뷰 안에서 제스쳐를 받아오기 위해 .gesture를 사용하고, 제스처로 받은 위치가 뷰의 안에 있는지 확인하기 위해 GeometryReader를 사용한다.
onChanged: 드래그 중일 때 호출되며, isDragging 값을 true로 설정하고, dragLocation 값을 업데이트한다. dragLocation이 사각형 내부에 있는지 확인하고, 배경색을 변경한다.
onEnded: 드래그가 끝날 때 호출되며, isDragging 값을 false로 설정한다.
https://github.com/JDeoks/SwiftUI/tree/main/Watch/GestureRecMac
GitHub - JDeoks/SwiftUI
Contribute to JDeoks/SwiftUI development by creating an account on GitHub.
github.com
ForEach(0..<3) { row in
ForEach(0..<2) { col in
let index = row + (col * 3)
GeometryReader { geo in
let width = geo.size.width / 2
let height = geo.size.height / 3
Text("\(index + 1)")
.font(.largeTitle)
.opacity(brl2DArr[crownIdx][5 - index] == 1 ? 1 : 0.4)
.frame(width: width, height: height)
.position(x: geo.frame(in: .local).midX, y: geo.frame(in: .local).midY)
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ value in
/// 터치 좌표
let loc: CGPoint = value.location
if isInside(loc, geo: geo) && brl2DArr[crownIdx][5 - index] == 1{
print(index + 1)
}
})
)
}
.aspectRatio(1, contentMode: .fit)
}
}
워치의 CV에 적용한 모습이다.
문제가 하나 발생했는데 GeometryReader가 여러 개 생기게 되어 하나의 dot을 누른채로 드래그해서 이동하게 되면 인식이 되지 않는 문제였다.
따라서 GeometryReader를 제일 밖으로 빼고, 터치 이벤트를 받는 뷰도 더 상위뷰로 바꾸었다.
//var body: some View {} 내부
GeometryReader { geo in
VStack{
// Text("\(str)\(crownIdx): \(String(str[crownIdx]))")
LazyVGrid(columns: [
GridItem(.flexible()), GridItem(.flexible())
], spacing: 0) {
ForEach(0..<3) { row in
ForEach(0..<2) { col in
let idx = row + (col * 3)
let width = geo.size.width / 2
let height = geo.size.height / 3
Text("\(idx + 1)")
.font(.largeTitle)
.opacity(brl2DArr[crownIdx][5 - idx] == 1 ? 1 : 0.4)
.frame(width: width, height: height)
}
}
}
}
// 뷰에 제스처 감지
.gesture(DragGesture(minimumDistance: 0)
// 터치 좌표값이 변화했을 때 변화한 값을 파라미터로 받는 클로저
.onChanged({ value in
/// 터치 좌표 저장 변수
let loc: CGPoint = value.location
/// 터치 좌표를 통해 누른 셀의 인덱스 가져와서 저장
let touchedIdx = getIdx(loc, geo: geo)
// 누른 인덱스에 해당하는 점자이진 배열값이 1이고, 손을 떼기 전 마지막 터치한 인덱스가 같지 않을 때 진동
if brl2DArr[crownIdx][5 - touchedIdx] == 1 && lastTouch != touchedIdx {
print("\(touchedIdx + 1) / 6")
// 진동 구현 부분
}
// lastTouch 업데이트
lastTouch = touchedIdx
})
//터치가 끝났을 때 lastTouch초기화 해서 같은 블록을 연속으로 클릭해도 진동하게 함
.onEnded { _ in
lastTouch = -1
}
)
}
// ...
/// 터치좌표값을 받아서 0~5 인덱스 반환
func getIdx(_ location: CGPoint, geo: GeometryProxy) -> Int {
let cellWidth = geo.size.width / 2
let cellHeight = geo.size.height / 3
let col = Int(location.x / cellWidth)
let row = Int(location.y / cellHeight)
return row + col * 3
}
DragGesture의 value.location은 뷰의 좌표계를 기준으로 하며, 뷰의 좌측 상단이 (0, 0)이다.화면상의 좌표와는 다르다.
이제 오디오 파일을 재생 할 차례이다.
해당 블로그를 참조했다.
https://seons-dev.tistory.com/entry/SwiftUI-Sound-Effects#%EC%A0%84%EC%B2%B4_%EC%BD%94%EB%93%9C
SwiftUI : Sound Effects _ AVKit
sound effect 에 대해 알아보도록 합시다. Sound Effect 이번에는 앱에 아무 간단한 음향 효과를 추가하는 방법에 대해 알아보려고 합니다. 이 기능은 앱을 만들어면서 아주 유용하게 사용되므로 꼭 알
seons-dev.tistory.com
//
// SoundSetting.swift
// OE_SwiftUI Watch App
//
// Created by 서정덕 on 2023/04/29.
//
import SwiftUI
import AVFoundation
class SoundSetting: ObservableObject {
// 전역에서 하나의 객체를 사용하는 싱글톤 패턴
static let instance = SoundSetting()
var player: AVAudioPlayer?
func playSound(){
// Bundle 클래스를 사용하여 파일 경로 참조, 이를 url 변수에 할당. 실패시 else
guard let url = Bundle.main.url(forResource: "vibration", withExtension: "m4a") else {
print("사운드 재생 오류")
return
}
// AVAudioPlayer 객체를 생성, contentsOf 메서드를 사용하여 url에 있는 파일 로드. 실패시 else
do {
player = try AVAudioPlayer(contentsOf: url)
player?.play()
} catch _ {
print("사운드 재생 오류")
}
}
}
오디오파일을 재생할 SoundSetting클래스 파일을 만들었다.
static으로 선언하여 전역에서 하나의 객체를 사용하여, 여러 곳에서 공유되는 싱글톤 패턴을 적용했다.
// 워치 CV
// 누른 인덱스에 해당하는 점자이진 배열값이 1이고, 손을 떼기 전 마지막 터치한 인덱스가 같지 않을 때 진동
if brl2DArr[crownIdx][5 - touchedIdx] == 1 && lastTouch != touchedIdx {
print("\(touchedIdx + 1) / 6")
// 진동 구현 부분
SoundSetting.instance.playSound()// 추가된 사운드 재생 명령
}
전체 코드
https://github.com/JDeoks/OPEN-EYES
GitHub - JDeoks/OPEN-EYES: ocr, object detection을 통한 사진 -> 점자 번역 어플리케이션
ocr, object detection을 통한 사진 -> 점자 번역 어플리케이션. Contribute to JDeoks/OPEN-EYES development by creating an account on GitHub.
github.com
이제 워치에서 구현할 것은 전부 다 구현했다.
다음에는 coreML 적용에 도전할 예정이다.
'Swift' 카테고리의 다른 글
타입 메서드 (Type method) (0) | 2023.05.01 |
---|---|
SwiftUI에서 UIImagePickerController 사용 (0) | 2023.05.01 |
Watch Connectivity로 폰-워치 통신 구현 OE - 1 (0) | 2023.05.01 |
swiftUI로 게임 만들기(떨어지는 동전 잡기) (0) | 2023.05.01 |
Swift 흐름제어 구문 (0) | 2023.05.01 |