For our music project, we were assigned to create a music-related game. As I was already following an online course on IOS development, I thought this as a perfect opportunity for me to practice what I had learned. The game I decided to create was a game that tests one's ability to recognize different keys in notes, chords, and scales.
I started by making a menu page which consists of three buttons, a title and an icon at the button right indicating how many coins the user has. The three buttons navigate to the three different tests: single notes, chords, and scales.
Then, I needed the sound files of the notes and found this website. At that point, I could not find an efficient way to play the sounds until I came across this StackOverflow post. Not only does it contain highly reusable functions, it also can play multiple sound files at once. I can configure the time span for the sounds as well.
Here is what I ended up implementing:
import Foundation import AVFoundation import Combine var audioPlayer: AVAudioPlayer? func playSound(sound: String){ if let path = Bundle.main.path(forResource: sound, ofType: "wav"){ do{ audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) audioPlayer?.play() }catch{ print("cound not") } } } class GSAudio: NSObject, AVAudioPlayerDelegate { static let sharedInstance = GSAudio() private override init() { } var players: [URL: AVAudioPlayer] = [:] var duplicatePlayers: [AVAudioPlayer] = [] func playSound(soundFileName: String) { guard let bundle = Bundle.main.path(forResource: soundFileName, ofType: "wav") else { return } let soundFileNameURL = URL(fileURLWithPath: bundle) if let player = players[soundFileNameURL] { //player for sound has been found if !player.isPlaying { //player is not in use, so use that one player.prepareToPlay() player.play() } else { // player is in use, create a new, duplicate, player and use that instead do { let duplicatePlayer = try AVAudioPlayer(contentsOf: soundFileNameURL) duplicatePlayer.delegate = self //assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing duplicatePlayers.append(duplicatePlayer) //add duplicate to array so it doesn't get removed from memory before finishing duplicatePlayer.prepareToPlay() duplicatePlayer.play() } catch let error { print(error.localizedDescription) } } } else { //player has not been found, create a new player with the URL if possible do { let player = try AVAudioPlayer(contentsOf: soundFileNameURL) players[soundFileNameURL] = player player.prepareToPlay() player.play() } catch let error { print(error.localizedDescription) } } } func playSounds(soundFileNames: [String]) { for soundFileName in soundFileNames { playSound(soundFileName: soundFileName) } } func playSounds(soundFileNames: String...) { for soundFileName in soundFileNames { playSound(soundFileName: soundFileName) } } func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds for (index, soundFileName) in soundFileNames.enumerated() { let delay = withDelay * Double(index) let _ = Timer.scheduledTimer(timeInterval: delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName": soundFileName], repeats: false) } } @objc func playSoundNotification(_ notification: NSNotification) { if let soundFileName = notification.userInfo?["fileName"] as? String { playSound(soundFileName: soundFileName) } } func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { if let index = duplicatePlayers.firstIndex(of: player) { duplicatePlayers.remove(at: index) } } }
The codes for each of the pages weren't that challenging as I decided to use a picker slider for the user to select the choices. Then, for each mode, I created a page to show whether the user got the correct key. I simply checked the boolean value of a Binding variable "answer" and navigated to the "correct" or the "wrong" page. Later on, to further users' playing experience, I decided to implement a renowned psychological device - the coin. Depending on how well the user performs in the game, a different amount of coins is given.
At first, I was honestly quite confused with the view protocol. Binding variable? State variable? As I had been used to IOS development in the traditional Swift-Storyboard way, all these extra features of Swift UI sounded superfluous to me. After doing some research, I soon found out that the binding variable and state variables are extremely powerful and minimalize a lot of redundant codes.
To implement the coin feature, I had to access the internal storage of the user's device. Otherwise, the coin variable would not remain the same the next time user opens the app.
@State private var coin = UserDefaults.standard.integer(forKey: "coin")
For users to have good use of the coins, I created a feature. In chords mode, the user has a choice to play the chords separately by submitting a fee of 50 coins. Similarly, in scale mode, the user has the option to pay 50 coins to play the scale slower. I designed a pop up for the payment confirmation.
I wrote a ton of codes for the frontend. Even though that may be inevitable, I still feel like there is a lot of redundancy. For instance, this is the code for one of the modes:
struct chord: View { @Binding var status: Int @Binding var current: Array@Binding var Index: Int @Binding var answer: Bool @Binding var coin: Int @State private let pickerValue: Array = ["a", "b", "c", "d", "e", "f", "g"] @State private var oneS = true @State private var one = "" @State private var twoS = true @State private var two = "" @State private var threeS = true @State private var three = "" @State private var isAlert = false var body: some View { VStack{ Picker(selection: $Index, label: Text("")) { ForEach(0 ..< pickerValue.count) { Text(self.pickerValue[$0]) } }.labelsHidden().padding() .overlay( RoundedRectangle(cornerRadius: 16) .stroke(Color.orange, lineWidth: 2) ) HStack { Button(action:{ self.oneS.toggle() self.one = self.pickerValue[self.Index] }){ if oneS{ Text("") .font(.system(size: 30)) .foregroundColor(Color.black) .padding(8) }else{ Text("\(one)") .font(.system(size: 30)) .foregroundColor(Color.black) .padding(8) } }.overlay( RoundedRectangle(cornerRadius: 20) .stroke(Color.orange, lineWidth: 3) .frame(width: 100, height: 90, alignment: Alignment.center) ).padding(40) Button(action:{ self.twoS.toggle() self.two = self.pickerValue[self.Index] }){ if twoS{ Text("") .font(.system(size: 30)) .foregroundColor(Color.black) .padding(8) }else{ Text("\(two)") .font(.system(size: 30)) .foregroundColor(Color.black) .padding(8) } }.overlay( RoundedRectangle(cornerRadius: 20) .stroke(Color.orange, lineWidth: 3) .frame(width: 100, height: 90, alignment: Alignment.center) ).padding(40) Button(action:{ self.threeS.toggle() self.three = self.pickerValue[self.Index] }){ if threeS{ Text("") .font(.system(size: 30)) .foregroundColor(Color.black) .padding(8) }else{ Text("\(three)") .font(.system(size: 30)) .foregroundColor(Color.black) .padding(8) } }.overlay( RoundedRectangle(cornerRadius: 20) .stroke(Color.orange, lineWidth: 3) .frame(width: 100, height: 90, alignment: Alignment.center) ).padding(40) } Button(action: { GSAudio.sharedInstance.playSounds(soundFileNames: self.current) }){ HStack { Image("icons8-music-record-60") Text("Play Chord") .font(.system(size: 20)) }.frame(minWidth: 0, maxWidth: .infinity) .padding() .foregroundColor(.white) .background(LinearGradient(gradient: Gradient(colors: [Color.orange, Color.yellow]), startPoint: .leading, endPoint: .trailing)) .cornerRadius(40) .padding(.horizontal, 20) .padding(3).scaleEffect(x: 1.1, y: 1.1, anchor: UnitPoint.center) } Button(action: { self.isAlert = true }){ HStack { Image("icons8-music-record-60") Text("Saperate - 10 Coins") .font(.system(size: 20)) }.frame(minWidth: 0, maxWidth: .infinity) .padding() .foregroundColor(.white) .background(LinearGradient(gradient: Gradient(colors: [Color.orange, Color.yellow]), startPoint: .leading, endPoint: .trailing)) .cornerRadius(40) .padding(.horizontal, 20) .padding(3).scaleEffect(x: 1.1, y: 1.1, anchor: UnitPoint.center) }.alert(isPresented: $isAlert) { () -> Alert in Alert(title: Text("Purchase"), message: Text("Are you sure you want to purchase with 10 coins?"), primaryButton: .default(Text("Yes"), action: { if self.coin > 10{ let sortedArray = self.current.sorted(by: <) GSAudio.sharedInstance.playSounds(soundFileNames: sortedArray, withDelay: 0.5) self.coin -= 10 UserDefaults.standard.set(self.coin, forKey: "coin") } }), secondaryButton: .default(Text("No"))) } Button(action:{ let ans = [self.one, self.two, self.three] self.status = 5 if ans.containsSameElements(as: self.current){ self.answer = true self.coin += 25 UserDefaults.standard.set(self.coin, forKey: "coin") } else{ self.answer = false } print(self.current) self.one = "" self.two = "" self.three = "" }){ Text("submit") .frame(minWidth: 0, maxWidth: .infinity) .padding() .foregroundColor(Color.orange) .overlay( RoundedRectangle(cornerRadius: 40) .stroke(Color.orange, lineWidth: 3) ) .padding(.horizontal, 20) .padding(6).scaleEffect(x: 1.1, y: 1.1, anchor: UnitPoint.center) } HStack (spacing: 100){ Button(action: { self.one = "" self.two = "" self.three = "" }) { Text("Retry") .foregroundColor(Color.red) .font(.system(size: 25)) } Button(action: { self.status = 3 }){ Text("Menu") .foregroundColor(Color.red) .font(.system(size: 25)) } }.padding() HStack { Image("icons8-coin-48") Text("\(self.coin)") .font(.title) }.offset(x: 120, y: -10) }.edgesIgnoringSafeArea(.all).padding(30).offset(y: 30) } } struct chord_Previews: PreviewProvider { static var previews: some View { ContentView() } } extension Array where Element: Comparable { func containsSameElements(as other: [Element]) -> Bool { return self.count == other.count && self.sorted() == other.sorted() }
There is a lot of reusable elements, and I could have structured the codes a bit better.
The code can be found on github
After doing this project, I felt more comfortable manipulating the basic elements in SwiftUI, including @State and @Binding variables, and how to initialize them in a view controller. Spending a couple of days working on this game, I realized how sophisticated IOS development is - protocols, the usage of struct and class, optional, and more. Using swift to develop an IOS software just seems cumbersome. Maybe in the near future, I can study react native or other javascript libraries to develop IOS related software.