Sharing UserDefaults between main app and Widget in iOS 14 - ios

I am writing a widget for iOS, seems UserDefaults is not accessible in the widget, I added app group as following, still it's nil:
let prefs = UserDefaults(suiteName:"group.myco.myapp")
Still when I try to read something from prefs which I set in the main app, it's nil here.

You'll need to add the AppGroup capability to the widget target. Select the project, then select the widget target, go to the Signing & Capabilities tab, and click the + Capability button. Then choose AppGroup and configure it with your group.

As #Ilya Muradymov commented blow
You need to set data for corresponding group UserDefault first if you want to get the data between widget and container app:
//object-C
[[NSUserDefaults.alloc initWithSuiteName:#"group.com.your.groupname"] setObject:mObject forKey:#"xxxxxxxx"];
//swift
UserDefaults(suiteName:"group.com.your.groupname").set(mObject, forKey: "xxxxxxxx")

Related

ios 14 widget: How to reload timeline after UserDefaults changed

I have a ios 14 widget that refresh every 5 minutes
let timeline = Timeline(entries: entries, policy: .atEnd)
The entries depends on the configuration on my MainApp.
I use UserDefaults to share data between MainApp and Widget.
#AppStorage("FollowingCatalog", store: UserDefaults(suiteName: "group.vn.f19.com"))
var catalogItemsData: Data = Data()
I've successfully mirrored the widget content base on UserDefaults data. BUT my problem is the my widget refresh the UI only after .atEnd policy, every 5 minutes
That cause a bad UX
How can I refresh widget content immediately right after my configuration in UserDefaults was changed?
Thanks for your supports.
To clarify the whole workflow, I add some note here:
You can call WidgetCenter not only from widget but from your main app.
WidgetCenter.shared.reloadAllTimelines()
WidgetCenter.shared.reloadTimelines(ofKind: "com.myApp.myWidget........")
So the flow is:
User change something on MainApp, eg: I rearrange the stock items in the following list
MainApp save the data to UserDefaults, eg: save the list item order
MainApp trigger reload widget by:
WidgetCenter.shared.reloadTimelines(ofKind: "mone24h_widget")
You can get the kind by looking into your widget entry file:
#main
struct mone24h_widget: Widget {
let kind: String = "mone24h_widget"
Widget will reload the timeline, I will get the shared data from UserDefaults here. Done the work. Eg: Re-render my stocks list base on the arrangement passed from MainApp
In Keeping a Widget Up to Date by Apple, to inform your widgets to update its timeline and its content, you call:
WidgetCenter.shared.reloadAllTimelines()
This reloads all widgets that your app has. If you want to reload a specific widget (in the case your app has multiple different widget types), use WidgetCenter.shared.reloadTimelines(ofKind: "com.myApp.myWidget") instead.
BTW, if you're using react-native, I've already created a module to support this issue:
react-native-widget-bridge

iOS Notification Content Extension - How to pass data to app?

I wrote a custom Notification Content Extension for my Push Notifications like this:
The thing is, whenever the user is on a certain item in the carousel, I want the GO TO APP button to send a String to the app when it's opening, and from there, handle that string to move the user to the correct ViewController.
I already have the handling part inside the app, I just need to know how to pass that String from the Notification Content Extension to the container app.
Thanks! :)
Enable app groups in capabilities and use suite userDefaults to write the key and read it in the app
NSUserDefaults*defaults= [[NSUserDefaults alloc] initWithSuiteName:#"group.com.company.appName"];
// Write in extension
[defaults setObject:#"anyThing" forKey:#"sharedContent"];
// Read in app
[defaults objectForKey:#"sharedContent"];
If your app is configured for Universal Links or you have defined a Custom URL Scheme for your app, you can also open your app's URL (e.g. with data in query parameters) by calling
extensionContext?.open(url)
in your NotificationViewController.
iOS 13, Swift 5.
Based on the answer by Sh_Khan, here is some Swift Syntax. Obviously I have added App Group as a capability to the target of the main app + the target of the extension, naming the group as "group.ch.Blah" for this example.
Setting your app group, saving a string in our case, needed to set the type as Any cause strings not a type that is available in groups.
let localK = getPrivateKey64() as Any
let defaults = UserDefaults.init(suiteName: "group.ch.Blah")
defaults?.set(localK, forKey: "privateK")
Setting your app group, and reading the string back, needed to recast it back to string.
let defaults = UserDefaults.init(suiteName: "group.ch.Blah")
let localK = defaults?.object(forKey: "privateK") as? String
Worked perfectly with a notification service extension.

Determine if the widget is enabled

Is there any way to determine if my Today Widget is already added to Notification Centre by user? I need to know so I can change some Labels in host app accordingly.
There is no API for that, but you could have your today widget write something to the shared container that you can read from your app to determine if it's been displayed. The main problems with that are that it won't happen until the widget has been displayed at least once, and you can't relly tell if they've installed and then removed it.
func widgetHasRun() {
if let sharedContainer = NSUserDefaults(suiteName: "group.com.my.app") {
sharedContainer.setBool(true, forKey: "today widget installed")
sharedContainer.synchronize()
}
}
We use this technique to determine whether we should prompt new users to install our widget.

Export audiofiles via “open in:” from Voice Memos App

I have the exact same issue as "Paul" posted here: Can not export audiofiles via "open in:" from Voice Memos App - no answers have yet been posted on this topic.
Essentially what I'm trying to do is simple:
After having recorded a Voice Memo on iOS, I select "Open With" and from the popup that is shown I want to be able to select my app.
I've tried everything I can think of and experimented with LSItemContentTypes without success.
Unfortunately I don't have enough reputation to comment on the existing post above, and I'm getting quite desperate for a solution to this. Any help is hugely appreciated, even just to know whether it's doable or not.
Thanks!
After some experimentation and much guidance from this blog post ( http://www.theappguruz.com/blog/share-extension-in-ios-8 ), it appears that it is possible to do this using a combination of app extensions (specifically an Action Extension) and app groups. I'll describe the first part which will enable you to get your recording from Voice Memos to your app extension. The second part -- getting the recording from the app extension to the containing app (your "main" app) -- can be done using app groups; please consult the blog post above for how to do this.
Create a new target within your project for the app extension, by selecting File > New > Target... from Xcode's menu. In the dialog box that prompts you to "Choose a template for your new target:" choose the "Action Extension" and click "Next".
CAUTION: Do not choose the "Share Extension" as is done in the blog post example above. That approach is more appropriate for sharing with another user or posting to a website.
Fill in the "Product Name:" for your Action Extension, e.g., MyActionExtension. Also, for "Action Type:" I selected "Presents User Interface" because this is the way Dropbox appears to do it. Selecting this option adds a view controller (ActionViewController) and storyboard (Maininterface.storyboard) to your app extension. The view controller is a good place to provide feedback to the user and to give the user an opportunity to rename the audio file before exporting it to your app.
Click "Finish." You will be prompted to "Activate “MyActionExtension” scheme?". Click "Activate" and this new scheme will be made active. Building it will build both the action extension and the containing app.
Click the disclosure triangle for the "MyActionExtension" folder in the Project Navigator (Cmd-0) to reveal the newly-created storyboard, ActionViewController source file(s), and Info.plist. You will need to customize these files for your needs. But for now ...
Build and run the scheme you just created. You will be prompted to "Choose an app to run:". Select "Voice Memos" from the list and click "Run". (You will probably need a physical device for this; I don't think the simulator has Voice Memos on it.) This will build and deploy your action extension (and its containing app) to your device. and then proceed to launch "Voice Memos" on your device. If you now make a recording with "Voice Memos" and then attempt to share it, you should see your action extension (with a blank icon) in the bottom row. If you don't see it there, tap on the "More" button in that row and set the switch for your action extension to "On". Tapping on your action extension will just bring up an empty view with a "Done" button. The template code looks for an image file, and finding none does nothing. We'll fix this in the next step.
Edit ActionViewController.swift to make the following changes:
6a. Add import statements for AVFoundation and AVKit near the top of the file:
// the next two imports are only necessary because (for our sample code)
// we have chosen to present and play the audio in our app extension.
// if all we are going to be doing is handing the audio file off to the
// containing app (the usual scenario), we won't need these two frameworks
// in our app extension.
import AVFoundation
import AVKit
6b. Replace the entirety of override func viewDidLoad() {...} with the following:
override func viewDidLoad() {
super.viewDidLoad()
// Get the item[s] we're handling from the extension context.
// For example, look for an image and place it into an image view.
// Replace this with something appropriate for the type[s] your extension supports.
print("self.extensionContext!.inputItems = (self.extensionContext!.inputItems)")
var audioFound :Bool = false
for inputItem: AnyObject in self.extensionContext!.inputItems {
let extensionItem = inputItem as! NSExtensionItem
for attachment: AnyObject in extensionItem.attachments! {
print("attachment = \(attachment)")
let itemProvider = attachment as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMPEG4Audio as String)
//|| itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMP3 as String)
// the audio format(s) we expect to receive and that we can handle
{
itemProvider.loadItemForTypeIdentifier(kUTTypeMPEG4Audio as String,
options: nil, completionHandler: { (audioURL, error) in
NSOperationQueue.mainQueue().addOperationWithBlock {
if let audioURL = audioURL as? NSURL {
// in our sample code we just present and play the audio in our app extension
let theAVPlayer :AVPlayer = AVPlayer(URL: audioURL)
let theAVPlayerViewController :AVPlayerViewController = AVPlayerViewController()
theAVPlayerViewController.player = theAVPlayer
self.presentViewController(theAVPlayerViewController, animated: true) {
theAVPlayerViewController.player!.play()
}
}
}
})
audioFound = true
break
}
}
if (audioFound) {
break // we only handle one audio recording at a time, so stop looking for more
}
}
}
6c. Build and run as in the previous step. This time, tapping on your action extension will bring up the same view controller as before but now overlaid with the AVPlayerViewController instance containing and playing your audio recording. Also, the two print() statements I've inserted in the code should give output that looks something like the following:
self.extensionContext!.inputItems = [<NSExtensionItem: 0x127d54790> - userInfo: {
NSExtensionItemAttachmentsKey = (
"<NSItemProvider: 0x127d533c0> {types = (\n \"public.file-url\",\n \"com.apple.m4a-audio\"\n)}"
);
}]
attachment = <NSItemProvider: 0x127d533c0> {types = (
"public.file-url",
"com.apple.m4a-audio"
)}
Make the following changes to the action extension's Info.plist file:
7a. The Bundle display name defaults to whatever name you gave your action extension (MyActionExtension in this example). You might wish to change this to Save to MyApp. (By way of comparison, Dropbox uses Save to Dropbox.)
7b. Insert a line for the key CFBundleIconFile and set it to Type String (2nd column), and set its value to MyActionIcon or some such. You will then need to provide the corresponding 5 icon files. In our example, these would be: MyActionIcon.png, MyActionIcon#2x.png, MyActionIcon#3x.png, MyActionIcon~ipad.png, and MyActionIcon#2x~ipad.png. (These icons should be 60x60 points for iphone and 76x76 points for ipad. Only the alpha channel is used to determine which pixels are gray, the RGB channels are ignored.) Add these icon files to your app extension's bundle, NOT the containing app's bundle.
7c. At some point you will need to set the value for the key NSExtension > NSExtensionAttributes > NSExtensionActivationRule to something other than TRUEPREDICATE. If you want your action extension to only be activated for audio files, and not for video files, pdf files, etc., this is where you would specify such a predicate.
The above takes care of getting the audio recording from Voice Memos to your app extension. Below is an outline of how to get the audio recording from the app extension to the containing app. (I'll flesh it out later, time permitting.) This blog post ( http://www.theappguruz.com/blog/ios8-app-groups ) might also be useful.
Set up your app to use App Groups. Open the Project Navigator (Cmd-0) and click on the first line to show your project and targets. Select the target for your app, click on the "Capabilities" tab, look for the App Groups capability, and set its switch to "On". Once the various entitlements have been added, click on the "+" sign to add your App Group, giving it a name like group.com.mycompany.myapp.sharedcontainer. (It must begin with group. and should probably use some form of reverse-DNS naming.)
Repeat the above for your app extension's target, giving it the same name as above (group.com.mycompany.myapp.sharedcontainer).
Now you can write the url of the audio recording to the app group's shared container from the app extension side. In ActionViewController.swift, replace the code fragment that instantiates and presents the AVPlayerViewController with the following:
let sharedContainerDefaults = NSUserDefaults.init(suiteName:
"group.com.mycompany.myapp.sharedcontainer") // must match the name chosen above
sharedContainerDefaults?.setURL(audioURL, forKey: "SharedAudioURLKey")
sharedContainerDefaults?.synchronize()
Similarly, you can read the url of the audio recording from the containing app's side using something like this:
let sharedContainerDefaults = NSUserDefaults.init(suiteName:
"group.com.mycompany.myapp.sharedcontainer") // must match the name chosen above
let audioURL :NSURL? = sharedContainerDefaults?.URLForKey("SharedAudioURLKey")
From here, you can copy the audio file into your app's sandbox, e.g., your app's Documents directory or your app's NSTemporaryDiretory(). Read this blog post ( http://www.atomicbird.com/blog/sharing-with-app-extensions ) for ideas on how to do this in a coordinated fashion using NSFileCoordinator.
References:
Creating an App Extension
Sharing Data with Your Containing App

Unable to access Realm database in app group folder

I am unable to access data stored in my realm database after moving it into an app group container in order to use with a watch kit extension. I tried following this Guide, I can successfully print the path to the default Realm after moving it into the app group file, but If I try to print out the amount of objects in the realm it returns 0, which causes my Table to be empty. I have app groups enabled in both my ios, and watch targets with the correct app group name. Heres where I set the default realm folder inside the app delegate.
let directory: NSURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.io.github.brady131313.Deck-of-Death")!
let realmPath = directory.path!.stringByAppendingPathComponent("db.realm")
Realm.defaultPath = realmPath
Verify that the directory returned by NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.io.github.brady131313.Deck-of-Death") points to a path containing Containers/Shared/AppGroup. It will happily return a path within your normal container if you've made a typo in the App Group ID or don't have the necessary entitlements / provisioning profile set up. You should see the same path returned within both your extension and your parent application.
You may want to review Realm's sample WatchKit app for a working example of using Realm with App Groups. You can find it in the Xcode project at https://github.com/realm/realm-cocoa/tree/master/examples/ios/objc.
The problem was that I had set the default realm path after let realm = Realm(), so it was defaulting to the old path instead of the new one.
You set realmPath as directory.path but it isn`t. Change the realmPath difinition.

Resources