Help me understand sharing data using App Groups

JonasRembrattJonasRembratt SEUniversity ✭✭✭

I must have misunderstood something very basic somewhere but I'm unable to see it right now.

As I understand iOS App Groups they can be used for two (or more) apps to share some data, such as files or user preferences.

Now, to make this work this needs to be set up:
1. An App group must be registered and given an identity with the team's developer portal (example: group.com.mycompany.myappgroup)
2. An App Id must be set up in the dev portal that allows app groups and that references the set up app group id (example: com.mycompany.myappgroup)
3. A provisioning profile must be registered that references the new App Id
4. Each app needs to be signed with a Bundle Id that corresponds to the App Id (example: com.mycompany.myappgroup)

Now, what I don't understand is the last item. If apps needs to have bundle id's that matches the same App Id then they would no longer be uniquely identifiable. What am I missing?

Best Answer

Answers

  • TedRogersTedRogers USMember ✭✭✭✭

    Your understanding of the last item is incorrect. "com.mycompany.myappgroup" is not an app id, it is the app group id. All apps that you want to share with need to specify that app group in their entitlements and in their provisioning profile.

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭

    @TedRogers said:
    Your understanding of the last item is incorrect. "com.mycompany.myappgroup" is not an app id, it is the app group id. All apps that you want to share with need to specify that app group in their entitlements and in their provisioning profile.

    I was under the impression that app group id's needed to be prefixed with "group.". Is that also wrong?

  • TedRogersTedRogers USMember ✭✭✭✭

    Sorry, I just copy/pasted the wrong text. All the apps need to reference "group.com.mycompany.myappgroup" in their entitlements and provisioning profile.

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭
    edited November 2017

    Ok, that is also how I understand it.
    This is what I did, in detail (I am only spiking this, to understand btw) ...

    1. Created App group in dev portal: group.com.mycompany.testappgroups
    2. Created two App id's in dev portal, both referencing the above app group: The app ids are com.mycompany.appgroups1 and com.mycompany.appgroups2 respectively.
    3. Created two new provisioning profiles to reference those two new app ids. Downloaded and installed them.
    4. Created two iOS Xamarin Forms apps with bundle ids com.mycompany.appgroups1 and com.mycompany.appgroups2 respectively.
    5. Made sure each iOS app signs its bundle using the correct provisioning profile.
    6. Enabled app groups and included the (group.com.mycompany.testappgroups) app group in each iOS app's Entitlements.plist file.
    7. Included the Entitlements.plist file in each iOS app's bundle signing options.

    Wrote this XAML start page:

    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:local="clr-namespace:Shared"
                 x:Class="Shared.MainPage">
        <ContentPage.BindingContext>
            <local:SharedPageVM />
        </ContentPage.BindingContext>
        <ContentPage.Content>
            <StackLayout Orientation="Vertical">
                <Entry Placeholder="key ..." Text="{Binding Key}" VerticalOptions="CenterAndExpand" />
                <Entry Placeholder="value ..." Text="{Binding Value}" VerticalOptions="CenterAndExpand" />
                <Button Text="Get" Command="{Binding GetCommand}" />
                <Button Text="Set" Command="{Binding SetCommand}" />
            </StackLayout>
        </ContentPage.Content>
    </ContentPage>
    

    ... and this view model for it:

    public class SharedPageVM : INotifyPropertyChanged
    {
        string _key;
        string _value;
    
        public string Key
        {
            get => _key;
            set { _key = value; onPropertyChanged(nameof(Key)); }
        }
    
        public string Value
        {
            get => _value;
            set { _value = value; onPropertyChanged(nameof(Value)); }
        }
    
        public ICommand GetCommand { get; }
        public ICommand SetCommand { get; }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        void onPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        public SharedPageVM()
        {
            GetCommand = new Command(() =>
            {
                if (string.IsNullOrWhiteSpace(Key))
                    return;
    
                var prefs = DependencyService.Get<ISharedUserDefaults>();
                var value = prefs.Get(Key);
                if (value != null)
                    Value = value;
            });
            SetCommand = new Command(() =>
            {
                if (string.IsNullOrWhiteSpace(Key) || string.IsNullOrWhiteSpace(Value))
                    return;
    
                var prefs = DependencyService.Get<ISharedUserDefaults>();
                prefs.Set(Key, Value);
            });
        }
    }
    

    Then, in each of the iOS apps (called "Shared A" and "Shared B") I implemented the ISharedUserDefaults interface and registered with the DependencyService like so ...

    // same implementation in both iOS apps (just replace the "A" with a "B" for the "Shared B" iOS app) ...
    [assembly: Xamarin.Forms.Dependency(typeof(SharedA.iOS.SharedUserDefaults))]
    namespace SharedA.iOS
    {
        class SharedUserDefaults : ISharedUserDefaults
        {
            public string Get(string key)
            {
                var userPrefs = getDafaults();
                return userPrefs.StringForKey(key);
            }
    
            public void Set(string key, string value)
            {
                var userPrefs = getDafaults();
                userPrefs.SetString(value, key);
            }
    
            NSUserDefaults getDafaults()
                => new NSUserDefaults("group.com.mycompany.testappgroups", NSUserDefaultsType.SuiteName);
        }
    }
    

    These are my repro steps:
    1. Run "Shared A" app
    2. Type "MyKey" into the "Key" entry
    3. Type "Hello World" into the "Value" entry
    4. Tap "Set"
    5. Ensure value was written (remove "Value" content and tap "Get" button). I now get the "Hello World" value back.
    6. Close "Shared A" app
    7. Run "Shared B" app
    8. Type "MyKey" into the "Key" entry
    9. Tap "Get" button
    == Result
    I now get a (null) value back from NSUserDefaults. I was expecting the (shared) preference value: "Hello World".

  • TedRogersTedRogers USMember ✭✭✭✭
    edited November 2017

    App groups don't currently work in iOS11 simulator in Xamarin. Is that possibly your problem. Try a 10 sim.

    Also, above you didn't mention setting the entitlements for the apps.

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭
    edited November 2017

    You are correct. I missed mentioning adding the entitlement and also to include the Entitlements.plist file in each iOS app's Bundle signing options. I have done that however and I'll edit the post for improved clarity.

    Currently I am not even able to run iOS 11+ on any of the iPhone simulators (thath's a different issue I haven't been able to resolve) so I am running 10.0 for the time being.

    Was my steps correct or have I misunderstood how this is supposed to work?

  • TedRogersTedRogers USMember ✭✭✭✭

    Looks good to me. Can you try getting a directory location to see if that returns a non-null url? Here is some code:

    string suiteName = "group." + NSBundle.MainBundle.BundleIdentifier;
    var groupUrl = NSFileManager.DefaultManager.GetContainerUrl(suiteName);
    
  • JonasRembrattJonasRembratt SEUniversity ✭✭✭

    I'll try that and be right back ...

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭
    edited November 2017

    groupUrl gets a (null) value back.

    However, the group id is not "group." + the bundle id. As each app needs its own unique bunlde id I added a "1" and "2" to each bundle id respectively.

    If I instead go with NSFileManager.DefaultManager.GetContainerUrl("group.com.mycompany.testappgroups"); (the id I'm using to fetch the NSUserDefaults in my code) I get this value back:

    "file:///Users/(me)/Library/Developer/CoreSimulator/Devices/EDCD949D-D8C2-4BF4-87F6-883F273955F9/data/Containers/Shared/AppGroup/1E5F21EF-E303-45F5-9823-4DC45F97EEFB/"

  • TedRogersTedRogers USMember ✭✭✭✭

    Yes, that is just what I use for my group id but I have used it in another app with a different bundle id.

    Ok, so something is messed up with entitlements or provisioning profile. Usually, it is hard to get the app to run if the entitlements and provisioning profile are not correct so I am a bit puzzled as to what is going on.

    Have you looked at the simulator or device logs? Also, dive into your app file and check the embedded provisioning profile and entitlements and make sure they look correct. You can find it in your "bin" directory.

  • TedRogersTedRogers USMember ✭✭✭✭

    BTW...I think simulator setup doesn't normally reference the entitlements file...all coming back to me now.

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭

    The problem was I forgot to Synchronize the prefs. Thank you! (silly mistake though)

  • TedRogersTedRogers USMember ✭✭✭✭

    Glad you sorted it out but curious as to why GetContainerUrl() was returning null.

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭
    edited November 2017

    For clarity, this is the code I used to check that out:

        var suiteName = "group." + NSBundle.MainBundle.BundleIdentifier;
        var groupUrl = NSFileManager.DefaultManager.GetContainerUrl(suiteName); // null
        groupUrl = NSFileManager.DefaultManager.GetContainerUrl("group.com.mycompany.testappgroups"); // (value)
        groupUrl = NSFileManager.DefaultManager.GetContainerUrl(NSBundle.MainBundle.BundleIdentifier); // null
    

    I would expect line 2 to not return anything, seeing I am asking four a suite name but am using a different suite (sans the "1" or "2" in the bundle id)
    I would also expect line 4 to return a value however (I assume those are the non-shared, app specific user preferences?).

    Would you agree?

  • TedRogersTedRogers USMember ✭✭✭✭

    I would only expect line 3 to return a value. Since your bundle identifier is not an app group, it should return null.

  • JonasRembrattJonasRembratt SEUniversity ✭✭✭

    Ok, then I have misunderstood how the NSFileManager works in this case. I will read up and see what I find. Thanks for your assistance. It's greatly appreciated!

    Cheers.

    /Jonas

  • kenneth.leekenneth.lee USMember ✭✭✭
    edited July 2018
    var appGroupContainer = NSFileManager.DefaultManager.GetContainerUrl("group.com.company.app.ios"); //null
    appGroupContainerPath = appGroupContainer.Path;
    

    I had that code for my app and I'm able to get a value for appGroupContainer when building on an actual iOS device, however doing so in and iOS simulator gives me a null. I have checked that both my app and extension using the group have it referenced in their Entitlements.plist

    I wonder if you guys have any idea what I might be doing wrong @TedRogers @JonasRembratt

    EDIT: I just saw Ted's comment on another post saying that it doesn't work for iOS 11 simulators and i downloaded a iOS10 simulator to try and it worked!

  • TedRogersTedRogers USMember ✭✭✭✭

    @kenneth.lee It is working now in iOS 11 simulators or at least last time I checked. They fixed that a couple months ago. There must be a discrepancy between your provisioning profiles and your entitlements. You have to specifically select the entitlements.plist for the simulator build. It sounds like you did that but something to double check.

    Here is my entitlements.plist in case it might help:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>com.apple.developer.icloud-container-identifiers</key>
        <array>
            <string>iCloud.$(CFBundleIdentifier)</string>
        </array>
        <key>com.apple.developer.icloud-services</key>
        <array>
            <string>CloudDocuments</string>
        </array>
        <key>com.apple.developer.ubiquity-container-identifiers</key>
        <array>
            <string>iCloud.$(CFBundleIdentifier)</string>
        </array>
        <key>com.apple.developer.ubiquity-kvstore-identifier</key>
        <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
        <key>com.apple.security.application-groups</key>
        <array>
            <string>group.com.mycompany.myapp</string>
        </array>
        <key>keychain-access-groups</key>
        <array>
            <string>$(AppIdentifierPrefix)com.mycompany.myapp</string>
        </array>
    </dict>
    </plist>
    
  • kenneth.leekenneth.lee USMember ✭✭✭
    edited July 2018

    Thanks, @TedRogers

    My Entitlements.plist contains pretty much the same key as yours except it doesn't have the keys associated with the iCloud properties and mine has the key for associated domains. By any chance do you know which updates fixed this issue or where I can find those patch notes?

  • TedRogersTedRogers USMember ✭✭✭✭

    @kenneth.lee I don't remember when it was fixed but it was at least a couple months ago so if you are on the latest stable you should be good. The other settings in the entitlements don't matter for this.

    I am attaching my bundle signing page as that is what you need setup correctly for it to work on a simulator.

    Also, have you looked at the simulator device logs to see if something helpful is in there?
    Ted

  • kenneth.leekenneth.lee USMember ✭✭✭

    @TedRogers

    I wasn't able to find anything out of the ordinary in the simulator system logs. I did tho see a fair few warnings about "One of the two will be used. Which one is undefined." but that seems to be something related to Cocoapods.

  • Cdn_EuroCdn_Euro Member ✭✭✭

    @JonasRembratt Did it work in the end? I am trying to share code between the iOS project and an Extension (Notification Service Extension), namely I am trying to share the user preferences.

  • TedRogersTedRogers USMember ✭✭✭✭
    edited September 2018

    @Cdn_Euro Preferences can easily be shared by setting up your app group and then accessing NSUserDefaults like this:

    string suiteName = "group." + NSBundle.MainBundle.BundleIdentifier;
    userDefaults = new NSUserDefaults(suiteName, NSUserDefaultsType.SuiteName);
    
  • Cdn_EuroCdn_Euro Member ✭✭✭

    @TedRogers Thanks that worked, I managed to save to app group user default prefs in the main project and retrieve that information in the extension. Feels great!

Sign In or Register to comment.