본문 바로가기
Swift

GeometryReader, Gesture, AVAudioPlayer로 진동 구현 OE - 2

by JDeoks 2023. 5. 1.

이진 배열([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 적용에 도전할 예정이다.