SWIFTUI/Others

SwiftUI : Sound Effects _ AVKit

서근 2021. 6. 27. 17:30
반응형

sound effect 에 대해 알아보도록 합시다.

 

Sound Effect 

이번에는 앱에 아무 간단한 음향 효과를 추가하는 방법에 대해 알아보려고 합니다. 이 기능은 앱을 만들어면서 아주 유용하게 사용되므로 꼭 알아두시는 것이 좋습니다. 그럼 바로 시작하죠!

 

우선 새로운 프로젝트를 만들고 SoundSetting이라는 class를 하나 생성해주겠습니다. 그리고 ContentView에 이 클래스를 변수를 사용하여 가져와볼게요.

import SwiftUI

class SoundSetting: ObservableObject {
    

}

struct SoundEffectView: View {
    
    var soundSetting = SoundSetting()
    
    var body: some View {
        Text("Hello, World!")
    }
}

Singleton Pattern

TIP
 
 

싱글톤 패턴이란?
특정 용도로 객체를 하나만 생성하여, 공용으로 사용하고 싶을 때 사용하는 디자인 유형

한 단계 나아가서 우리는 이 SoundEffect를 다른 View에서도 사용하고 싶은 경우가 많이 생길 수 도 있습니다. 그런데 위 코드처럼 특정 View 안에서만 변수 soundSetting을 넣어주면 이 뷰에서만 사용할 수밖에 없겠죠. 그러므로 우리는 이 SoundSetting이 앱 전체에 걸쳐 있도록 해줘야 합니다. 그러므로 뷰에서 변수를 만들기보다는 class내 에서 싱글톤을 만들어 주는 것이 가장 좋습니다.

class SoundSetting: ObservableObject {
    
    //1. soundSetting의 단일 인스턴스를 만듬
    /// singleton ? :
    /*싱글 톤은 한 번만 생성 된 다음 사용해야하는 모든 곳에서 공유해야하는 객체입니다 */
    static let instance = SoundSetting()
}

자 우리는 instance를 생성했고 SoundSetting에서 몇 가지 기능을 추가하여 실제로 소리를 재생하려고 합니다. 

AVKit import

첫 번째로 해야할것은 AVkitimport해주는 것입니다. AV에서 Aaudio를 의미하고 VVideo를 의미합니다. 이 키트안에는 앱에서 사운드와 비디오를 재생하는데 사용할 수 있는 아주 편리한 구성요소를 가지고 있습니다. 

 

음악 재생을 위한 변수를 옵셔널 타입으로 추가해줍니다.

import SwiftUI
import AVKit

class SoundSetting: ObservableObject {
    
    //1. soundSetting의 단일 인스턴스를 만듬
    /// singleton ? :
    /*싱글 톤은 한 번만 생성 된 다음 사용해야하는 모든 곳에서 공유해야하는 객체입니다 */
    static let instance = SoundSetting()
    
    var player: AVAudioPlayer?
}

그 밑에 함수를 추가해주겠습니다. 함수 안에 첫번째로 새 플레이어를 초기화해줘야 합니다. player = AVAudioPlayer를 입력 후 괄호를 열어 자동 완성된 리스트를 살펴보죠

 

일단 주목해야 할 두 가지가 있는데 우리는 오디오 파일이 실제로 저장되어있는 url이 필요하고, 오류가 생기면 처리하는 throws가 있습니다. 우리가 사용할 것은 contentsOf: 입니다.

Player에는 URL을 넣어줘야 하기 때문에 guard let을 사용하여 url을 만들어줬습니다. 하지만 아직 우리는 sound url이 없기 때문에 공백으로 남겨줬고, 이제 생성한 urlplayer에 넣어줍니다.

    func playSound() {
        
        guard let url = URL(string: "") else { return }
        
        player = AVAudioPlayer(contentsOf: url)
    }

넣어주고 나니 오류가 생겼죠? 오류를 한번 살펴볼게요.

" 호출이 발생할 수 있지만 'try'로 표시되지 않고 오류가 처리되지 않습니다. "
player = try AVAudioPlayer(contentsOf: url)

이제 여기에서 발생하는 모든 오류를 catch해야 합니다.

    func playSound() {
        
        guard let url = URL(string: "") else { return }
        
        do {
            player = try AVAudioPlayer(contentsOf: url)
        } catch let error {
            print("재생하는데 오류가 발생했습니다. \(error.localizedDescription)")
        }
    }

오류를 프린트해줬고 이제 Player 해줘야겠네요.

        do {
            player = try AVAudioPlayer(contentsOf: url)
            player?.play()
        } catch let error {
             print("재생하는데 오류가 발생했습니다. \(error.localizedDescription)")
        }

실제로 player옵셔널이지만 성공하면 옵셔널이 아니게 됩니다. 자, 이러면 Plyer가 실행되고 만약 오류가 있으면 무슨 오류인지 콘솔에 나타나게 됩니다.

Add Sound

이제 사운드를 찾고 url을 넣어줄게요. 일단 아래 링크를 통해 원하는 사운드를 찾아줍니다.

 

 

저는 이곳의 click과 knock 사운드를 다운로드했습니다.

mp3 파일을 바탕화면에 저장했으면 다시 Xcode로 돌아와서 새로운 그룹 하나를 생성하고 이름은 Sounds라고 해주겠습니다. 그리고 이 폴더에 파일을 넣어줍니다.

위 체크박스가 체크되어있는지 반드시 확인해줍니다. 이것이 바로 번들인 것인데 코드의 url은 필요가 없겠죠? 공백으로 뒀던 url은 지워주고 다음과 같이 수정합니다.

우리는 Bundle.main.urlwithExtension을 사용할 것입니다.

        guard let url = Bundle.main.url(forResource: String?, withExtension: String?) else { return }

forResource에는 mp3파일 명을 넣어주고, withExtension에는 확장자 명을 넣어주면 됩니다.

guard let url = Bundle.main.url(forResource: "Click", withExtension: ".mp3") else { return }

Button 추가

거의 다 왔습니다. ViewBody부분에 일단 버튼을 두 개 만들어줄게요.

struct SoundEffectView: View {
    
    var body: some View {
        VStack(spacing: 20) {
            Button {
                //some action 1
            } label: {
                imageViews(imageName: "cursorarrow.rays", iconColor: .yellow)
            }
            
            Button {
                //some action 2
            } label: {
                imageViews(imageName: "person.fill.questionmark", iconColor: .pink)
            }
        }
    }
}

//MARK: IMAGE VIEWS
struct imageViews: View {
    var imageName: String
    var iconColor: Color
    var body: some View {
        Image(systemName: imageName)
            .resizable()
            .scaledToFit()
            .frame(width: 50, height: 50)
            .foregroundColor(iconColor)
    }
}

싱글톤 사용

버튼은 됐고! 이제 액션에 로직을 넣어주려고 하는데 앞에 말했듯이 우리는 SoundSetting클래스에 싱글톤을 사용하여 instance를 생성했고 이것을 그냥 사용하기만 하면 됩니다.

            Button {
                SoundSetting.instance.playSound()
            } label: {
                imageViews(imageName: "cursorarrow.rays", iconColor: .yellow)
            }

시뮬레이터를 켜서 정상적으로 작동하는지 확인해보면 정상적으로 기능이 사용됩니다. 

 

하지만 우리는 클래스에 click사운드 단 하나만 넣어줬었죠? enum을 사용하여 개별적으로 각각 사용할 수 있도록 해줄게요.

//SoundSetting Class

    static let instance = SoundSetting()
    
    var player: AVAudioPlayer?
    
    enum SoundOption: String {
        case Click
        case Knock
    }

이제 이 SoundOption을 함수에 전달해줍니다.

 func playSound(sound: SoundOption) {

다음으로 guard let문의 forResourceclick이 아닌 sound.rawValue로 수정합니다.

 guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".mp3") else { return }

이제 Body에서 이 enum을 사용할 수 있습니다.

struct SoundEffectView: View {
    
    var body: some View {
        VStack(spacing: 20) {
            Button {
                SoundSetting.instance.playSound(sound: .Click)
            } label: {
                imageViews(imageName: "cursorarrow.rays", iconColor: .yellow)
            }
            
            Button {
                SoundSetting.instance.playSound(sound: .Knock)
            } label: {
                imageViews(imageName: "person.fill.questionmark", iconColor: .pink)
            }
        }
    }
}

타란! 아주 쉽죠? 😁

정리

우리는 SoundSetting을 만들었고, 전체 앱에서 사용 가능한 싱글톤 클래스를 만들었습니다. 다른 View에서 이것을 사용하려면 단지 SoundSetting.instance만 사용하면 되는 것이죠!!

 

그리고 SoundSetting에 모든 옵션이 포함된 enum도 만들었죠. 함수를 만들어서 음악이 재생될 수 있게도 했고... 또 url이 아닌 번들을 가져와서 직접 다운로드한 mp3유형의 파일을 앱에서 실행될 수 있게 했습니다! 이게 끝입니다. 앞으로 이것을 활용해서 더 다양한 앱을 만들 수 있을 것 같네요!!

 

전체 코드

<hide/>

import SwiftUI
import AVKit

class SoundSetting: ObservableObject {
    
    /// singleton ? :
    /*싱글 톤은 한 번만 생성 된 다음 사용해야하는 모든 곳에서 공유해야하는 객체입니다 */
    static let instance = SoundSetting()
    
    var player: AVAudioPlayer?
    
    enum SoundOption: String {
        case Click
        case Knock
    }
    
    func playSound(sound: SoundOption) {
        
        guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".mp3") else { return }
        
        do {
            player = try AVAudioPlayer(contentsOf: url)
            player?.play()
        } catch let error {
            print("재생하는데 오류가 발생했습니다. \(error.localizedDescription)")
        }
    }
}

struct SoundEffectView: View {
    
    var body: some View {
        VStack(spacing: 20) {
            Button {
                SoundSetting.instance.playSound(sound: .Click)
            } label: {
                imageViews(imageName: "cursorarrow.rays", iconColor: .yellow)
            }
            
            Button {
                SoundSetting.instance.playSound(sound: .Knock)
            } label: {
                imageViews(imageName: "person.fill.questionmark", iconColor: .pink)
            }
        }
    }
}

//MARK: IMAGE VIEWS
struct imageViews: View {
    var imageName: String
    var iconColor: Color
    var body: some View {
        Image(systemName: imageName)
            .resizable()
            .scaledToFit()
            .frame(width: 50, height: 50)
            .foregroundColor(iconColor)
    }
}

struct SoundEffectView_Previews: PreviewProvider {
    static var previews: some View {
        SoundEffectView()
    }
}

또 다른 예시

<hide/>

import SwiftUI
import AVKit

class SoundManager: ObservableObject {
    
    static let instance = SoundManager()
    
    var player: AVAudioPlayer?
    
    enum soundOption: String {
        case Click
        case Knock
    }
    
    func playSound(sounds: soundOption) {
        guard let url = Bundle.main.url(forResource: sounds.rawValue, withExtension: ".mp3") else { return }
        
        do {
            player = try AVAudioPlayer(contentsOf: url)
            player?.play()
        } catch let error {
            print("재생하는데 오류가 생겼습니다. 오류코드 \(error.localizedDescription)")
        }
    }
    
}

struct sounds: View {
    @State var isLoading: Bool = false
    var body: some View {
        NavigationView {
            VStack {
                VStack(alignment: .leading, spacing: 30) {
                    profileViews(name: "서근개발노트", description: "안녕하세요")
                    profileViews(name: "하나", description: "스며들어 할지라도 낙원을 그들의 타오르고 역사를 보라.")
                    profileViews(name: "둘", description: "이것은 곧 인간이 공자는 길을 그들의 하는 때까지 없는 것이다. 싸인 주며, 이상 얼마나 사람은 부패뿐이다.")
                    profileViews(name: "셋", description: "있는 이상이 아니한 열락의 광야에서 하였으며, 물방아 안고, 있는가? 인도하겠다는 이상이 청춘의 몸이 안고, 이것이야말로 있으며, 봄바람이다.")
                }
                .padding()
                .navigationBarTitle("서근 개발 블로그")
                .redacted(reason: isLoading ? [] : .placeholder)
                Spacer()
                
                Button {
                    startNetworkCall()
                    SoundManager.instance.playSound(sounds: .Knock)
                } label: {
                    Text("숨김 / 펼치기")
                }
                
            }
        }
        
    }
    func startNetworkCall() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            isLoading.toggle()
        }
    }
}

struct profileViews: View {
    var name: String
    var description: String
    var body: some View {
        HStack(spacing: 10) {
            Circle()
                .frame(width: 50, height: 50)
            VStack(alignment: .leading, spacing: 3) {
                Text(name)
                    .font(.headline)
                Text(description)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .frame(height: 70)
        
    }
}

struct sounds_Previews: PreviewProvider {
    static var previews: some View {
        sounds()
    }
}

 

 

읽어주셔서 감사합니다🤟

 

본 게시글의 전체 코드 GitHub 👇🏻

 

Seogun95/SwiftUI_SoundEffects

SwiftUI_SoundPlayer. Contribute to Seogun95/SwiftUI_SoundEffects development by creating an account on GitHub.

github.com