Save/Restore state in iOS

Hi!! I’m brand new to JUCE and iOS app development and would like some help on the following issue:

I am building an iOS app where, very basically, users can create projects to be accessed later. I have some underlying data that each project contains (string project name, int project index, Array arr1, Array arr2) and I want the app to retain the project data upon closing and re opening the app. I have read up on AudioProcessorValueTreeState and XML etc etc but found none of it to be helpful since I am not using a JUCE plugin and the data to be saved/restored is not associated with a plugin, it’s just data as a part of the app… any ideas? Again, I’m brand new to this stuff but I’m learning as I go.

You can use ValueTree’s on their own. Or you can manually convert your data structures to XML for saving/restoring. Or you can define your own persistent file format and convert your data structures to this for saving/restoring.

Here is a good video for understanding ValueTree’s: https://www.youtube.com/watch?v=3IaMjH5lBEY

Thank you so much for your prompt reply. I will look at the video you sent right away.

@cpr I have a Project class that deals with individual attributes of each project but I also have a ProjectSelector class that manages all the projects a user has created. Both have data that needs to be restored during app state restoration, where should I put my ValueTree implementation?

Do you mean what code should create/load/save the ValueTree? It seems likely the data for Project and ProjectSelector would be stored in different files, in which case you would want separate ValueTrees for each, and that each class would own their ValueTree.

@cpr Hey! So I was able to consolidate all the project data within the ProjectSelector file, so I went ahead and tried using a ValueTree implementation for save/restore. I am essentially writing the ValueTree to a file when saving, and loading the ValueTree from the file (if it exists) on launch of the app after terminating. Still running into issues, however, but check my next post for my code (it wasn’t letting me post two pictures in one post).

Upon creating a ProjectSelectorCells object (refer to next post with the ss of the constructor), which is only done once when the app is first launched, File saveDirectory is initialized and I call a method to setup my ValueTree.


So here’s the setValueTreeConfiguration method implementation. If a file already exists, I prompt the app to retrieve the ValueTree from the file and restore all the data. If a file doesn’t exist, I assume this is the first launch of the app and thus I create a ValueTree and write to the file to save it.

Any ideas where I could be going wrong? The implementation doesn’t seem to be working right now. One of my guesses is that I save data when the app is first launched and it doesn’t save again, so the restore takes the data from whence the app was first launched, aka the default data, and loads that. Maybe I need to be making the call for setValueTreeConfiguration somewhere else? Or perhaps the way I’m dealing with the file I’m writing to is incorrect? I apologize for the long post but any help is much appreciated!

Here’s the other ss to be used in reference to the post above.

ProjectSelectorCells constructor:

Yes, you are correct in discerning that you will need to write the ValueTree after any of it’s data has been updated, just as you would have to do for any data you write to a file. Depending on the user experience you want, you can choose to do that when the user exits the app, or whenever the data changes. For the later you could choose to do it manually, by inserting calls to your save function where ever changes are made to the ValueTree, or setting up a timer to periodically save, or implement a more intelligent method that simply listens to the valuetree for any changes, and then does a save. I have a class that I use which does the last option I listed, that I could share with you.

Also, I am saving in XML format, instead of binary, if for no other reason to be able to look at the file when debugging issues around saving/restoring.

Okay okay, this is starting to make some sense now. Yes please, would you mind sharing your class that listens to the valuetree for changes? I will try to write to an XML file instead of binary as well.

I need to put this on github, but for now I’ll just post it here:

ValueTreeFile.h

#ifndef _VALUE_TREE_FILE_H_
#define _VALUE_TREE_FILE_H_

#include "../JuceLibraryCode/JuceHeader.h"

// ValueTreeFile connects a ValueTree and a file, with auto-save functionality
//
// Auto-save is triggered when every anything in the ValueTree changes. The auto-save takes place
// on an independent thread, so as not to interrupt the message thread.
//
// To reduce the number of saves when there are many changes in a short period of time, it is a deferred
// save, waiting 'saveDelayTime' before doing the actual save. If second change happens
// the time is restarted. To keep a long series of rapid changes from keeping the save from
// happening, a save will be done if the total time since the first change exceeds maxSaveDelayTime.

class ValueTreeFile : private ValueTree::Listener,
                      private Timer,
                      private Thread
{
public:
    ValueTreeFile ();
    ValueTreeFile (ValueTree vt, File f, bool enableAutoSave);
    ~ValueTreeFile ();

    void init (ValueTree vt, File f, bool enableAutoSave);
    void save ();
    void load ();
    void requestAutoSave ();
    void setAutoSaveTimes (uint32 sdt, uint32 msdt);
    void enableAutoSave (bool isEnabled);

private:
    ValueTree vtData;
    File file;
    uint32 saveDelayTime    { 1000 };
    uint32 maxSaveDelayTime { 5000 };
    uint32 mostRecentSaveRequestedTime { 0 };
    uint32 initialSaveRequestedTime    { 0 };
    bool autoSaveEnabled { false };
    CriticalSection xmlDataCS;
    std::unique_ptr<XmlElement> xml;

    void save (XmlElement* xmlToWrite);
    void run () override;
    void valueTreePropertyChanged (ValueTree& treeWhosePropertyHasChanged, const Identifier& property) override;
    void valueTreeChildAdded (ValueTree& parentTree, ValueTree& childWhichHasBeenAdded) override;
    void valueTreeChildRemoved (ValueTree& parentTree, ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved) override;
    void valueTreeChildOrderChanged (ValueTree& parentTreeWhoseChildrenHaveMoved, int oldIndex, int newIndex) override;
    void valueTreeParentChanged (ValueTree& treeWhoseParentHasChanged) override;
    void timerCallback () override;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueTreeFile)
};

#endif // _VALUE_TREE_FILE_H_

ValueTreeFile.cpp

#include "ValueTreeFile.h"

ValueTreeFile::ValueTreeFile ()
    : Thread ("ValueTreeFile")
{
}

ValueTreeFile::ValueTreeFile (ValueTree vt, File f, bool enableAutoSave)
    : ValueTreeFile ()
{
    init (vt, f, enableAutoSave);
}

ValueTreeFile::~ValueTreeFile ()
{
    stopThread (5000);
    vtData.removeListener (this);
}

void ValueTreeFile::init (ValueTree vt, File f, bool enableAutoSave)
{
    vtData = vt;
    file = f;
    autoSaveEnabled = enableAutoSave;
    vtData.addListener (this);
    load ();
}

void ValueTreeFile::enableAutoSave (bool isEnabled)
{
    const auto wasEnabled = autoSaveEnabled;
    autoSaveEnabled = isEnabled;

    // if toggling from disabled to enabled, then request a delayed save
    if (! wasEnabled && isEnabled)
        requestAutoSave ();
}

void ValueTreeFile::save ()
{
    auto xmlToWrite = vtData.createXml ();
    save (xmlToWrite.get ());
}

void ValueTreeFile::save (XmlElement* xmlToWrite)
{
    xmlToWrite->writeTo (file, {});
}

void ValueTreeFile::load ()
{
    if (file.exists ())
    {
        XmlDocument xmlDoc (file);
        auto xmlToRead = xmlDoc.getDocumentElement ();
        vtData.copyPropertiesAndChildrenFrom (ValueTree::fromXml (*xmlToRead), nullptr);
    }
}

void ValueTreeFile::setAutoSaveTimes (uint32 sdt, uint32 msdt)
{
    saveDelayTime = sdt;
    maxSaveDelayTime = jmax (sdt, msdt);
}

void ValueTreeFile::requestAutoSave ()
{
    if (autoSaveEnabled)
    {
        if (initialSaveRequestedTime == 0)
        {
            startTimer (saveDelayTime / 2);
            initialSaveRequestedTime = Time::getMillisecondCounter ();
        }
        mostRecentSaveRequestedTime = Time::getMillisecondCounter ();
    }
}

void ValueTreeFile::timerCallback ()
{
    const uint32 curTime = Time::getMillisecondCounter ();
    if (curTime - mostRecentSaveRequestedTime >= saveDelayTime ||
        curTime - initialSaveRequestedTime >= maxSaveDelayTime)
    {
        ScopedLock xmlLock (xmlDataCS);
        if (xml.get () == nullptr)
        {
            stopTimer ();
            xml = vtData.createXml ();
            startThread ();
            initialSaveRequestedTime = 0;
        }
    }
}

void ValueTreeFile::run ()
{
    if (! threadShouldExit ())
    {
        std::unique_ptr<XmlElement> xmlToWrite;
        {
            ScopedLock xmlLock (xmlDataCS);
            xmlToWrite.reset (xml.release ());
        }
        if (xmlToWrite.get () != nullptr)
            save (xmlToWrite.get ());
    }
}

void ValueTreeFile::valueTreePropertyChanged (ValueTree& , const Identifier&)
{
    requestAutoSave ();
}
void ValueTreeFile::valueTreeChildAdded (ValueTree& , ValueTree&)
{
    requestAutoSave ();
}
void ValueTreeFile::valueTreeChildRemoved (ValueTree& , ValueTree& , int)
{
    requestAutoSave ();
}
void ValueTreeFile::valueTreeChildOrderChanged (ValueTree& , int , int)
{
    requestAutoSave ();
}
void ValueTreeFile::valueTreeParentChanged (ValueTree&)
{
    requestAutoSave ();
}