capacitor-plugins icon indicating copy to clipboard operation
capacitor-plugins copied to clipboard

feat: [Storage Plugin] Support for iOS suites

Open ThomasKientz opened this issue 5 years ago • 11 comments

Feature Request

Describe the Feature Request

While developing extensions in iOS, you may find a need to share application or user data between the host app and the extension.

To do so, we need to pass a "suite name" at the initialization of the userDefaults

init?(suiteName suitename: String?)

Platform Support Requested

  • [ ] Android
  • [x] iOS
  • [ ] Electron
  • [ ] Web

Describe Preferred Solution

Possibility to initialize the store with a suite name.

Describe Alternatives

An old cordova plugin is doing the job : https://github.com/apla/me.apla.cordova.app-preferences

Related Code

Additional Context

May concerns Android as well.

ThomasKientz avatar Dec 15 '19 12:12 ThomasKientz

Issues tagged with feature request are closed but tracked for reactions to gauge interest.

Or if you are really interested and have the knowledge to implement this, you can send a PR.

jcesarmobile avatar Jan 13 '20 15:01 jcesarmobile

I just started looking into created an iOS widget to show some data on the home screen from within my ioinic capacitor app. I'm going to need a way of defining a suiteName so that when I store some data in the app, the widget extension can read the data and display it. Is there any plan to work on this now that iOS 14 beta is out?

ryanflores79 avatar Jul 10 '20 17:07 ryanflores79

Let's go fellas! iOS 14 is here. No time to delay.

jgirdner avatar Jul 10 '20 22:07 jgirdner

Pretty please implement this! :)

hocococaspar avatar Jan 27 '21 14:01 hocococaspar

Support for this is needed for many app extension scenarios, for example share extension, today extension and many of the other extensions.

We've ran into this when trying to create a share extension, and it seems like others have too (just one example from your forums): https://forum.ionicframework.com/t/capacitor-plugin-for-share-extension-from-ios-android/187970

sandstrom avatar May 18 '21 11:05 sandstrom

@sandstrom @ThomasKientz There is a workaround for this. @capacitor/storage is using UserDefaults.standard for storing data which is not able to share with app extension. What you need to do is migrating UserDefaults to App Groups. You can find the example code here This migration will be run when iOS app is loaded in AppDelegate.swift. I'd recommend put it in func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool You don't needs to migrate all the keys/values though, just pick the important keys to share with your App Extension.

hieuhoangtrung avatar Jul 11 '21 06:07 hieuhoangtrung

I just posted about this yesterday in the forums before finding this post today. Does the Capacitor Storage plugin still only support reading from UserDefaults.standard? I noticed the Storage plugin has a "group" property in the options object, but it seems that doesn't refer to an App Group...

taylorhlabs avatar Aug 10 '21 16:08 taylorhlabs

@taylorhlabs it does still only support reading from UserDefaults.standard

Make sure to upvote the issue so the ionic team can track it

ThomasKientz avatar Aug 10 '21 17:08 ThomasKientz

@hieuhoangtrung Thanks for your suggestion. In your scenario how did you access (on the Ionic/js/capacitor side) the data stored in the group's UserDefaults? The Storage plugin does not seem to allow for this, not sure how else it might be done?

viking2917 avatar Jun 03 '22 19:06 viking2917

so, I'm a poor iOS Developer (an Ionic hacker who learned Swift last night), but I implemented some changes to the Storage plugin that would allow for the passing of a 'suite' keyword, and then the Storage plugin would respect/retrieve from the NSUserDefaults of that suite.

I needed this because I want to share data between by Share Extension (heh heh) and my main app, as others here mentioned.

If someone competent wanted to review my changes beforehand (@jcesarmobile ??), I'd be willing to do a PR, but I'm not confident enough in my knowledge to just go for it. (Also I have not tried Android although I think the extra parameter, if passed, would just be ignored).

Changes were these: (complete altered files attached).

In definitions.d.ts, add the suite parameter

export interface ConfigureOptions {
    /**
     * Set the storage group.
     *
     * Storage groups are used to organize key/value pairs.
     *
     * Using the value 'NativeStorage' provides backwards-compatibility with
     * [`cordova-plugin-nativestorage`](https://www.npmjs.com/package/cordova-plugin-nativestorage).
     * WARNING: The `clear()` method can delete unintended values when using the
     * 'NativeStorage' group.
     *
     * @default CapacitorStorage
     * @since 1.0.0
     */
    group?: string;
    suite?: string;
}

in the Storage.swift file, add the suite parameter, hang on to it during initialization,

public struct StorageConfiguration {
    public enum Group {
        case named(String), cordovaNativeStorage
    }

    let group: Group
    // add optional suite string for use with NSUserDefaults
    let suite: String;

    public init(for suite: String = "", for group: Group = .named("CapacitorStorage")) {
        self.group = group
        self.suite = suite
    }
}

then alter the creation of the defaults variable to include the suite as necessary:


public class Storage {
    private let configuration: StorageConfiguration

    private var defaults: UserDefaults {
      if(configuration.suite.isEmpty) {
        return UserDefaults.standard
      } else {
        // if the suite configuration parameter is passed, use that suite for the UserDefaults DB.
       return UserDefaults(suiteName: configuration.suite) ?? UserDefaults.standard
      }
    }

in StoragePlugin.swift, grab the suite parameter if it is passed and give it to the constructor

public class StoragePlugin: CAPPlugin {
    private var storage = Storage(with: StorageConfiguration())

    @objc func configure(_ call: CAPPluginCall) {
        let group = call.getString("group")
        let suite = call.getString("suite")
        let configuration: StorageConfiguration

        if let suite = suite, group == nil {
          // if only suite is passed, just use that.
          configuration = StorageConfiguration(for: suite);
        } else {
          if let group = group, let suite = suite {
              if group == "NativeStorage" {
                  configuration = StorageConfiguration(for: suite, for: .cordovaNativeStorage)
              } else {
                  configuration = StorageConfiguration(for: suite, for: .named(group))
              }
          } else {
              configuration = StorageConfiguration()
          }
        }
        storage = Storage(with: configuration)
        call.resolve()
    }

In my share extension, to set the parameter:

...
UserDefaults(suiteName: <your-app-group-id>)?.set(value, forKey: <your desired storage key>)
...

then, in my (Ionic) app, I do this, and it works:

const setOptions = async () => {
   await Storage.configure({ suite: '<your-app-group-id>' });
};
setOptions();

const checkValue = async () => {
   const { value } = await Storage.get({ key: '<your desired storage key>' });
   console.log('storage:', value);
};
checkValue();

The only gotcha I have run across is that when you set the value, you must prepend 'CapacitorStorage.' to the key name because the Storage plugin will prepend that prefix before asking for it. E.g. if you want to store a key called 'foo', you must actually store 'CapacitorStorage.foo' .

Also I don't know that the suite name always has to be a shared App Group ID, but in particular if you want to share data between a Share Extension and an App, I believe that is required.

StorageTweaks.zip

viking2917 avatar Jun 04 '22 00:06 viking2917

@jcesarmobile ping

ThomasKientz avatar Sep 01 '22 18:09 ThomasKientz