SwiftUI : Sound Effects _ AVKit
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
싱글톤 패턴이란?
특정 용도로 객체를 하나만 생성하여, 공용으로 사용하고 싶을 때 사용하는 디자인 유형
한 단계 나아가서 우리는 이 SoundEffect
를 다른 View에서도 사용하고 싶은 경우가 많이 생길 수 도 있습니다. 그런데 위 코드처럼 특정 View
안에서만 변수 soundSetting
을 넣어주면 이 뷰에서만 사용할 수밖에 없겠죠. 그러므로 우리는 이 SoundSetting
이 앱 전체에 걸쳐 있도록 해줘야 합니다. 그러므로 뷰에서 변수를 만들기보다는 class
내 에서 싱글톤을 만들어 주는 것이 가장 좋습니다.
class SoundSetting: ObservableObject {
//1. soundSetting의 단일 인스턴스를 만듬
/// singleton ? :
/*싱글 톤은 한 번만 생성 된 다음 사용해야하는 모든 곳에서 공유해야하는 객체입니다 */
static let instance = SoundSetting()
}
자 우리는 instance
를 생성했고 SoundSetting
에서 몇 가지 기능을 추가하여 실제로 소리를 재생하려고 합니다.
AVKit import
첫 번째로 해야할것은 AVkit
을 import
해주는 것입니다. AV에서 A는 audio를 의미하고 V는 Video를 의미합니다. 이 키트안에는 앱에서 사운드와 비디오를 재생하는데 사용할 수 있는 아주 편리한 구성요소를 가지고 있습니다.
음악 재생을 위한 변수를 옵셔널 타입으로 추가해줍니다.
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
이 없기 때문에 공백으로 남겨줬고, 이제 생성한 url
을 player
에 넣어줍니다.
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.url
의 withExtension
을 사용할 것입니다.
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 추가
거의 다 왔습니다. View
의 Body
부분에 일단 버튼을 두 개 만들어줄게요.
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
문의 forResource
도 click
이 아닌 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 👇🏻