1

Here is my JSON Structure (Ignore the username to email part of it, that is for a test)

{
  "Username" : {
    "Username" : {
      "email" : "test@gmail.com"
    }
  },
  "uid_0" : {
    "Song List" : [ "The National Anthem" ]
  }
}

I am making a very simplistic app using Swift and Xcode. Basically, the user signs into their account after they register. From there the user is able to choose their favorite songs from a band's discography into an empty array. The user is then able to click a button that segues them to a table view controller that contains the songs they added. The way the user adds songs is through a view controller that contains labels that have the song names and buttons next to each song name. When the user clicks the button the label is added to the array.

I want to be able to save the array to firebase so that each user will have their own favorite songs. I have done a decent amount of research and can not find anything to steer me in the right direction as this is incredibly simple. The labels have no keys and are literally just labels being added to an array.

I have the firebase storage and database pods installed as well and I am able to upload images to fire base and have the user save that image to their specific account but how to do the same thing for an array? Here is my code for the array so you can get an idea of how it works.

import UIKit
import Firebase
import FirebaseStorage
import FirebaseAuth
import LocalAuthentication
import FirebaseDatabase

//var refSongs: DatabaseReference! {
//  return Database.database().reference()

//}

var list = [String]()
class SongsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var ref: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.ref = Database.database().reference()
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list.count
    }

    @IBAction func saveSongs(_ sender: Any) {
        //upload array to firebase storage
        //let defaults = UserDefaults.standard.set(list, forKey: "KeySave")
        //addSongs()

        // create add list reference in fire database
        // let addListRef = refSongs.child("Song List").childByAutoId()

        // let addList1 = list

        // addListRef.setValue(addList1)

        let ref = self.ref.child("uid_0").child("Song List")
        // let songArray = [list] *** Removed this
        ref.setValue(list) //changed to list
        retriveList()
    }

    func retriveList() {
        let ref = self.ref.child("uid_0").child("Song List")
        ref.observeSingleEvent(of: .value, with: {snapshot in
            var mySongArray = [String]()
            for child in snapshot.children {
                let snap = child as! DataSnapshot
                let song = snap.value as! String //!! Program crashes here!!
                mySongArray.append(song)
            }
            print(mySongArray)
        })
    }

    // add labels to array
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "cell")
        cell.textLabel?.text = list[indexPath.row]
        return (cell)
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == UITableViewCellEditingStyle.delete
        {
            list.remove(at: indexPath.row)
            myTableView.reloadData()
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        myTableView.reloadData()
    }

    @IBOutlet weak var myTableView: UITableView!
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • Please don't use Arrays in Firebase. They are not queryable or editable (add/remove) and there are usually much better options for storing your data. See the answer to [this question](https://stackoverflow.com/questions/43179477/firebase-changing-layout-of-child-data-information-in-android/43191862#43191862) before you go down that path. [This one](https://stackoverflow.com/questions/45785134/create-firebase-database-rule-on-key-name/45804389#45804389) as well. – Jay Dec 06 '17 at 18:05
  • @jay thank you for the input. The problem is that I hard coded the adding songs part of this app. I am not sure what direction to go in without the array. All I need is for the array to be saved to a user so that when that user signs in, they have their own favorite songs list. –  Dec 06 '17 at 19:27
  • Right. What if they want to change the order of the songs? You will have to read the songs, make the change, delete the node in firebase, re-write the songs. Suppose the user has 1000 songs and they want to query for a song - that can't be done with a firebase array. Suppose they want to insert a song at index #3 in the playlist. Can't do that either! Working with an array in code is best practice - just don't do it in Firebase. Let me put an answer together with some options. – Jay Dec 06 '17 at 21:40
  • @jay if you do not mind, I edited my original post with my updated code that adds the array to firebase. I am trying to now retrieve that data and send it back to the app so that when I close the app and reopen the data is still in the array. Any input is much appreciated. I have spent a few hours trying to retrieve the data but have had no success. Thank you! –  Dec 07 '17 at 03:13
  • You can't close the app and then re-open the app with a populated array. When an app is closed, all data is purged (deallocated). The array will be empty until it's populated from somewhere the data was saved to (Firebase, local disk etc). You can however, read the data from Firebase on app start which is what I think you are wanting to do. – Jay Dec 07 '17 at 15:07
  • Also - it seems you are writing the songs out and then reading them back in? There's two issues 1) While this will work most of the time, you should not read data in until you are certain it's been written. In this case the code to read the songs may actually execute before the data has been totally written. You should use a closure with your setValue. 2) Don't do #1 as there's no reason to. Add a .childAdded observer to the song list child node and whenever you write data to it, that listener will be called. Within that closure, add that child data to your array and then update your UI. – Jay Dec 07 '17 at 15:15
  • @jay Appreciate the response and the help! But sadly this will not retrieve the data because I am using an array and I cannot cast the value of an array to a string. When using the code you submitted I receive the following error, Could not cast value of type '__NSArrayM' (0x10fd0e888) to 'NSString' (0x10ee82d68). I also am having a lot of trouble figuring out how to update the array to only the user signing in. For example, user 1 has their own array with song x and user 2 has their own array with song y. Thank you buddy, I really appreciate it! –  Dec 08 '17 at 00:08
  • At no point are we casting and array to a string, and my answer reads and writes an array per your question. You will need to use the code in my answer to write the array to firebase exactly like I have it in the answer, and also to read that array. If that data structure doesn't match, the code won't work. For example, if you put the uid_0 node as a child of another node it wont work. You *can* do that but you would need to adjust the references in both code examples. Don't worry about doing it for multiple users yet. Get it working for one and that can then be expanded on. – Jay Dec 08 '17 at 00:21
  • @jay thanks for the input. The only difference in my code is: first when uploading the array to firebase i do, let ref= refSongs.child("uid_0").child("Song List") let songArray = [list (this is the array)] ref.setValue(songArray) –  Dec 08 '17 at 00:45
  • @jay For some reason when I apply self.refSongs I get an error that says there is no member refSongs in my class. I appreciate the quick response by the way! My retrival code is exactly the same apart from variable names obviously. I just create a function called retriveSongs and implement your code. Then I call that function in my saveSongs function. The saveSongs function is a button that uploads the songs to firebase. I am currently not by my code but I will upload once I get the chance! –  Dec 08 '17 at 00:46
  • @jay also, my structure is like this, AppName - uid_0 - 0 - 0: song name, 1: song name2, etc. –  Dec 08 '17 at 01:02
  • There is no *refSongs* in my answer and that's what the issue is. You should follow my code exactly as what you are doing won't work. The only change is that my main firebase node is accesses with *self.ref* and your will be something different. Get that part of the code reading and writing and then you can worry about the rest. Please include Firebase structures as text within the question (Firebase Console->Export JSON) – Jay Dec 08 '17 at 01:32
  • @jay when I try and do let ref = ref.child("uid_0).child("song list") I get the following error, "Variable used within its own initial value". I cannot do let ref = self.ref because I get the error "Value of type 'SongsViewController' has no member 'ref'". I have uploaded my updated code the my original post. –  Dec 08 '17 at 15:33
  • *let ref = ref.* doesn't make any sense, which is why you are getting an error. *ref* didn't exist before that statement so it has no value. *self.ref* on the other hand does. Also, when referencing a class var, use *self.* to make it clear; self.ref, self.list etc. The first item in your array is unnecessary and there are no songs. You write data and then immediately retrieve it (see above comment). That's not necessary and may fail. – Jay Dec 08 '17 at 15:58
  • @jay yes I understand that. I am unsure of how to access the node without using self. When I use self I receive and error saying Value of type 'SongsViewController' has no member 'ref'. Thanks for continuing to respond, I am at a loss right now on how to solve the issue. –  Dec 08 '17 at 16:32
  • Well, what it's telling you is correct; you've defined it outside the class so therefore it's not a class var. I just updated my answer (see the bottom) as to how to do it correctly. – Jay Dec 08 '17 at 16:59
  • @Jay Oh my god... Can't believe I did not realize that due to my messy code. I fixed up my code to read exactly like the code you submitted. Am still receiving an error saying "Could not cast value of type '__NSArrayM' (0x108d61888) to 'NSString' (0x107ed5d68)." I have updated my code above to show my current code. –  Dec 08 '17 at 17:21
  • @jay alright to fix the error all I did was remove the line "let songArray = [list] as that was causing my error. I already created the array so there is no need to recreate it. The program does not crash anymore but it also does not update the array still. I have updated my code above. My guess is that it may not be actually accessing the child nodes? I have no idea and my research online has not helped. –  Dec 08 '17 at 18:04
  • @jay also put my Structure above –  Dec 08 '17 at 18:34
  • Well, list is not a class var and it's empty. Why don't you try copy and pasting the *exact* setValue code from my answer? All three lines. Then, maybe in another section of code that is activated by a button press, copy and paste my observe code into that. Run the app. verify that when the array is written, it looks like the array in my answer. Then press the test button which will read that same array and write it to the console. Get the basics down before going any further. – Jay Dec 08 '17 at 18:36
  • 1
    @Jay thank you, you advice gave me a way better understanding of this. I was able to load your array into firebase, as well as retrieve it (displayed in console when I clicked the button). I can close the app and click the reload button (Will implement the reloadData later) and the array appears in my table view controller. Now I need to figure out how to implement this with my empty array that the user populates. –  Dec 08 '17 at 19:09
  • 1
    Move the *list* array inside the class definition. Add a .childAdded observer to that users playlist in Firebase. When the user adds a song, write it to that node. Adding the song to Firebase will then fire the observer and within that observer closure, a snapshot will be passed in containing the song data that was just added. Add the song to the array tableView dataSource, and reload your tableview. That's about it. If my answer helped, please accept it so it can help others! – Jay Dec 08 '17 at 19:13
  • @Jay Thanks, I will attempt to implement that. Basically my program has a bunch of classes to correspond to each album by the band. In these classes I have buttons that add the label (the song name) to the empty array List. I honestly have no idea how I should implement this. –  Dec 08 '17 at 19:19
  • @jay is there anyway you can provide me with an example on how to impmement this? I am very confused and dont know to do this. In addition to adding my songs to the array, should I also add the songs to firebase? If that is the case how would I be able to do that? Thank you! If I move the list array into the class then how do I add the songs to it? When moving the list to the class I am unable to append the list from my album classes. –  Dec 08 '17 at 19:53
  • This conversation is getting a bit lengthy and I think you may have moved on to a different question. Conceptually, Firebase is used as the permanent storage of the data and loaded into memory via an array which is used as a tableView datasource. I would advise going through the Firebase getting started guide as there are a number of examples of reading/writing data and working with observers. There is also a wealth of information here on stackoverflow that answers your specific question. Generally, accepted answers are the ones that were the most helpful so don't forget to accept mine. – Jay Dec 08 '17 at 20:03
  • @jay yes,you are right, thank you for understanding. I have watched a lot of videos have read through many threads and have not really been able to tackle the problem :/. I appreciate the help though. I may ask a new question later today if I still cant figure it out. –  Dec 08 '17 at 20:13
  • Good! Keep going with it. Please take a look at [Accepting an Answer](https://stackoverflow.com/help/someone-answers) – Jay Dec 08 '17 at 20:18

2 Answers2

9

Writing an array to Firebase is incredibly easy and Firebase does most of the work for you.

Suppose the user selects three songs and now you want to store them in the users playlist

let ref = self.ref.child("uid_0").child("playlist")
let songArray = ["Us and Them", "Get Back", "Children of the Sun"]
ref.setValue(songArray)

will result in

uid_0
  playlist
    0: "Us and Them"
    1: "Get Back"
    2: "Children of the Sun"

Now the user selects another song to add to the playlist and you want to save it. Oh wait.. snap. Firebase treats the array as one object so you have to read the node, delete that node and re-write the node with the new data.

Now suppose the user wants to query for the title of song #2. Can't do that either! Firebase Can't query inside an array.

Ok, so you have 100 users and want to add a feature to show the users whose favorite song is Children of the Sun. Can't do that either.

What if the user wants to have multiple playists?

Basically Arrays Are Evil and limited and there are usually much better options for storing data.

That being said, arrays can be useful in some cases - two options are shown here.

Here's an example

song_library
   song_0
     title: "Us and Them"
     artist: "Pink Floyd"
     play_count: 100
     total_likes: 167
   song_1
     title: "Get Back"
     artist: "Beatles"
     play_count: 50
     total_likes: 87
   song_2
     title: "Children of the Sun"
     artist: "Billy Thorpe"
     play_count: 98
     total_likes: 1050
   song_3
     title: "21st Century Man"
     artist: "Billy Thorpe"
     play_count: 36
     total_likes: 688
   song_4
     title: "East of Edens Gate"
     artist: "Billy Thorpe"
     play_count: 45
     total_likes: 927

uid_0
   playlist_0
     song_1: 2
     song_2: 0
     song_4: 1
     //or use an array here
     //0: song_2
     //1: song_4
     //2: song_0
   playlist_1
     song_3: 0
     song_4: 1

With this structure, you can re-use songs in playlists, the sequence can be easily modified, songs and be removed or added and data duplication is significantly reduced as the playlists just keep references to the actual song data.

EDIT:

To read that node back into an array, here's one option

let ref = self.ref.child("uid_0").child("playlist")
ref.observeSingleEvent(of: .value, with: { snapshot in
    var mySongArray = [String]()
    for child in snapshot.children {
        let snap = child as! DataSnapshot
        let song = snap.value as! String
        mySongArray.append(song)
    }
    print(mySongArray)
})

Note there are other options but this guarantees the order.

Edit 2

This is to help the OP define their class vars correctly.

class ViewController: UIViewController {

    var ref: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad() 
        self.ref = Database.database().reference()
Jay
  • 34,438
  • 18
  • 52
  • 81
  • appreciate the input immensely. Thank you for taking your time to respond! I figured out how to upload the array, and now am having trouble retrieving the data and sending it back to the app. I will change the structure once I figure out how to retrieve the data but for now I just want to get the app to work. My current issue is that when I close the app, none of the data is saved in the array which is obviously because I am not retrieving it from firebase. I have tried a multitude of things but have had no success. Here is what I have atm –  Dec 07 '17 at 03:04
  • @Justin I got ya! Updated my answer to also read the data back into an array. If this answer helped, be sure to accept it so it can help others. – Jay Dec 07 '17 at 15:05
6

Firebase isn't that bad with arrays. I assume this functionality is newer than this thread, but I'm posting here for future people like myself.

Here's the section in the Firebase documentation that says how to make changes to an array. It's not perfect, just another tool in the shed.

Quoted from their docs:

let washingtonRef = db.collection("cities").document("DC")

// Atomically add a new region to the "regions" array field.
washingtonRef.updateData([
    "regions": FieldValue.arrayUnion(["greater_virginia"])
])

// Atomically remove a region from the "regions" array field.
washingtonRef.updateData([
    "regions": FieldValue.arrayRemove(["east_coast"])
])
Sean
  • 340
  • 3
  • 11