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.