Since both the new app extensions of iOS 8 and Swift are both fairly new, I created a sample app that demonstrates how a simple Today extension can be made for iOS 8 in Swift. This widget will show the latest blog posts of the Xebia Blog within the today view of the notification center.
- Swift Note 1 0 – Widget For Notifications Centered
- Swift Note 1 0 – Widget For Notifications Centerpieces
- Swift Note 1 0 – Widget For Notifications Centerpiece
- Swift Note 1 0 – Widget For Notifications Centers
- Swift Note 1 0 – Widget For Notifications Center
The source code of the app is available on GitHub. The app is not available on the App Store because it’s an example app, though it might be quite useful for anyone following this Blog.
In this tutorial, I’ll go through the following steps:
The Notification Center framework helps you create and manage app extensions that implement Today widgets. The framework provides an API you can use to specify whether a Today widget has content to display, and to customize aspects of its appearance and behavior. From the widget, users can create new notes, start a list, and access recent notes. I have way too many Notification Center widgets, but I’m glad to see OneNote finally join the party.
New Xcode project
Even though an extension for iOS 8 is a separate binary than an app, it’s not possible to create an extension without an app. That makes it unfortunately impossible to create stand alone widgets, which this sample would be since it’s only purpose is to show the latest posts in the Today view. So we create a new project in Xcode and implement a very simple view. The only thing the app will do for now is tell the user to swipe down from the top of the screen to add the widget.
Time to add our extension target. From the File menu we choose New > Target… and from the Application Extension choose Today Extension.
We’ll name our target XebiaBlogRSSWidget and of course use Swift as Language.
The created target will have the following files:
- TodayViewController.swift
- MainInterface.storyboard
- Info.plist
Since we’ll be using a storyboard approach, we’re fine with this setup. If however we wanted to create the view of the widget programatically we would delete the storyboard and replace the NSExtensionMainStoryboard key in the Info.plist with NSExtensionPrincipalClass and TodayViewController as it’s value. Since (at the time of this writing) Xcode cannot find Swift classes as extension principal classes, we also would have to add the following line to our TodayViewController:
[objc]
@objc (TodayViewController)
[/objc]
@objc (TodayViewController)
[/objc]
Update: Make sure to set the “Embedded Content Contains Swift Code” build setting of the main app target to YES. Otherwise your widget written in Swift will crash.
Add dependencies with cocoapods
The widget will get the latest blog posts from the RSS feed of the blog: https://xebia.com/blog/feed/. That means we need something that can read and parse this feed. A search on RSS at Cocoapods gives us the BlockRSSParser as first result. Seems to do exactly what we want, so we don’t need to look any further and create our Podfile with the following contents:
[ruby]
platform :ios, '8.0'
platform :ios, '8.0'
target 'XebiaBlog' do
end
target 'XebiaBlogRSSWidget' do
pod ‘BlockRSSParser’, ‘~> 2.1’
end
[/ruby]
pod ‘BlockRSSParser’, ‘~> 2.1’
end
[/ruby]
It’s important to only add the dependency to the XebiaBlogRSSWidget target since Xcode will build two binaries, one for the app itself and a separate one for the widget. If we would add the dependency to all targets it would be included in both binaries and thus increasing the total download size for our app. Always only add the necessary dependencies to both your app target and widget target(s).
Note: Cocoapods or Xcode might give you problems when you have a target without any Pod dependencies. In that case you may add a dependency to your main target and run pod install, after which you might be able to delete it again.
The BlockRSSParser is written in objective-c, which means we need to add an objective-c bridging header in order to use it from Swift. We add the file XebiaBlogRSSWidget-Bridging-Header.h to our target and add the import.
[objc]
#import 'RSSParser.h'
[/objc]
#import 'RSSParser.h'
[/objc]
We also have to tell the Swift compiler about it in our build settings:
Load RSS feed
Finally time to do some coding. The generated TodayViewController has a function called widgetPerformUpdateWithCompletionHandler. This function gets called every once in awhile to ask for new data. It also gets called right after viewDidLoad when the widget is displayed. The function has a completion handler as parameter, which we need to call when we’re done loading data. A completion handler is used instead of a return function so we can load our feed asynchronously.
In objective-c we would write the following code to load our feed:
[objc]
[RSSParser parseRSSFeedForRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@'https://xebia.com/blog/feed/']] success:^(NSArray *feedItems) {
// success
} failure:^(NSError *error) {
// failure
}];
[/objc]
[objc]
[RSSParser parseRSSFeedForRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@'https://xebia.com/blog/feed/']] success:^(NSArray *feedItems) {
// success
} failure:^(NSError *error) {
// failure
}];
[/objc]
In Swift this looks slightly different. Here the the complete implementation of widgetPerformUpdateWithCompletionHandler:
[objc]
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
let url = NSURL(string: 'https://xebia.com/blog/feed/')
let req = NSURLRequest(URL: url)
[objc]
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
let url = NSURL(string: 'https://xebia.com/blog/feed/')
let req = NSURLRequest(URL: url)
RSSParser.parseRSSFeedForRequest(req,
success: { feedItems in
self.items = feedItems as? [RSSItem]
completionHandler(.NewData)
},
failure: { error in
println(error)
completionHandler(.Failed)
})
}
[/objc]
success: { feedItems in
self.items = feedItems as? [RSSItem]
completionHandler(.NewData)
},
failure: { error in
println(error)
completionHandler(.Failed)
})
}
[/objc]
We assign the result to a new optional variable of type RSSItem array:
[objc]
var items : [RSSItem]?
[/objc]
[objc]
var items : [RSSItem]?
[/objc]
The completion handler gets called with either NCUpdateResult.NewData if the call was successful or NCUpdateResult.Failed when the call failed. A third option is NCUpdateResult.NoData which is used to indicate that there is no new data. We’ll get to that later in this post when we cache our data.
Show items in a table view
Now that we have fetched our items from the RSS feed, we can display them in a table view. We replace our normal View Controller with a Table View Controller in our Storyboard and change the superclass of TodayViewController and add three labels to the prototype cell. No different than in iOS 7 so I won’t go into too much detail here (the complete project is on GitHub).
We also create a new Swift class for our custom Table View Cell subclass and create outlets for our 3 labels.
[objc]
import UIKit
[objc]
import UIKit
class RSSItemTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var authorLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var authorLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
}
[/objc]
[/objc]
Now we can implement our Table View Data Source functions.
[objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return items.count
}
return 0
}
[/objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return items.count
}
return 0
}
[/objc]
Since items is an optional, we use Optional Binding to check that it’s not nil and then assign it to a temporary non optional variable: let items. It’s fine to give the temporary variable the same name as the class variable.
Swift Note 1 0 – Widget For Notifications Centered
[objc]
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier('RSSItem', forIndexPath: indexPath) as RSSItemTableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier('RSSItem', forIndexPath: indexPath) as RSSItemTableViewCell
if let item = items?[indexPath.row] {
cell.titleLabel.text = item.title
cell.authorLabel.text = item.author
cell.dateLabel.text = dateFormatter.stringFromDate(item.pubDate)
}
cell.titleLabel.text = item.title
cell.authorLabel.text = item.author
cell.dateLabel.text = dateFormatter.stringFromDate(item.pubDate)
}
return cell
}
[/objc]
}
[/objc]
In our storyboard we’ve set the type of the prototype cell to our custom class RSSItemTableViewCell and used RSSItem as identifier so here we can dequeue a cell as a RSSItemTableViewCell without being afraid it would be nil. We then use Optional Binding to get the item at our row index. We could also use forced unwrapping since we know for sure that items is not nil here:
[objc]
let item = items![indexPath.row]
[/objc]
[objc]
let item = items![indexPath.row]
[/objc]
But the optional binding makes our code saver and prevents any future crash in case our code would change.
We also need to create the date formatter that we use above to format the publication dates in the cells:
[objc]
let dateFormatter : NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateStyle = .ShortStyle
return formatter
}()
[/objc]
[objc]
let dateFormatter : NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateStyle = .ShortStyle
return formatter
}()
[/objc]
Here we use a closure to create the date formatter and to initialise it with our preferred date style. The return value of the closure will then be assigned to the property.
Preferred content size
To make sure we that we can actually see the table view we need to set the preferred content size of the widget. We’ll add a new function to our class that does this.
[objc]
func updatePreferredContentSize() {
preferredContentSize = CGSizeMake(CGFloat(0), CGFloat(tableView(tableView, numberOfRowsInSection: 0)) * CGFloat(tableView.rowHeight) + tableView.sectionFooterHeight)
}
[/objc]
func updatePreferredContentSize() {
preferredContentSize = CGSizeMake(CGFloat(0), CGFloat(tableView(tableView, numberOfRowsInSection: 0)) * CGFloat(tableView.rowHeight) + tableView.sectionFooterHeight)
}
[/objc]
Since the widgets all have a fixed width, we can simply specify 0 for the width. The height is calculated by multiplying the number of rows with the height of the rows. Since this will set the preferred height greater than the maximum allowed height of a Today widget it will automatically shrink. We also add the sectionFooterHeight to our calculation, which is 0 for now but we’ll add a footer later on.
When the preferred content size of a widget changes it will animate the resizing of the widget. To have the table view nicely animating along this transition, we add the following function to our class which gets called automatically:
[objc]
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition({ context in
self.tableView.frame = CGRectMake(0, 0, size.width, size.height)
}, completion: nil)
}
[/objc]
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition({ context in
self.tableView.frame = CGRectMake(0, 0, size.width, size.height)
}, completion: nil)
}
[/objc]
Here we simply set the size of the table view to the size of the widget, which is the first parameter.
Of course we still need to call our update method as well as reloadData on our tableView. So we add these two calls to our success closure when we load the items from the feed
[objc]
success: { feedItems in
self.items = feedItems as? [RSSItem]
success: { feedItems in
self.items = feedItems as? [RSSItem]
self.tableView.reloadData()
self.updatePreferredContentSize()
self.updatePreferredContentSize()
completionHandler(.NewData)
},
[/objc]
},
[/objc]
Let’s run our widget:
It works, but we can make it look better. Table views by default have a white background color and black text color and that’s no different within a Today widget. We’d like to match the style with the standard iOS Today widget so we give the table view a clear background and make the text of the labels white. Unfortunately that does make our labels practically invisible since the storyboard editor in Xcode will still show a white background for views that have a clear background color.
If we run again, we get a much better looking result:
Open post in Safari
To open a Blog post in Safari when tapping on an item we need to implement the tableView:didSelectRowAtIndexPath: function. In a normal app we would then use the openURL: method of UIApplication. But that’s not available within a Today extension. Instead we need to use the openURL:completionHandler: method of NSExtensionContext. We can retrieve this context through the extensionContext property of our View Controller.
[objc]
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let item = items?[indexPath.row] {
if let context = extensionContext {
context.openURL(item.link, completionHandler: nil)
}
}
}
[/objc]
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let item = items?[indexPath.row] {
if let context = extensionContext {
context.openURL(item.link, completionHandler: nil)
}
}
}
[/objc]
More and Less buttons
Right now our widget takes up a bit too much space within the notification center. So let’s change this by showing only 3 items by default and 6 items maximum. Toggling between the default and expanded state can be done with a button that we’ll add to the footer of the table view. When the user closes and opens the notification center, we want to show it in the same state as it was before so we need to remember the expand state. We can use the NSUserDefaults for this. Using a computed property to read and write from the user defaults is a nice way to write this:
[objc]
let userDefaults = NSUserDefaults.standardUserDefaults()
let userDefaults = NSUserDefaults.standardUserDefaults()
var expanded : Bool {
get {
return userDefaults.boolForKey('expanded')
}
set (newExpanded) {
userDefaults.setBool(newExpanded, forKey: 'expanded')
userDefaults.synchronize()
}
}
[/objc]
get {
return userDefaults.boolForKey('expanded')
}
set (newExpanded) {
userDefaults.setBool(newExpanded, forKey: 'expanded')
userDefaults.synchronize()
}
}
[/objc]
This allows us to use it just like any other property without noticing it gets stored in the user defaults. We’ll also add variables for our button and number of default and maximum rows:
[objc]
let expandButton = UIButton()
let expandButton = UIButton()
let defaultNumRows = 3
let maxNumberOfRows = 6
[/objc]
let maxNumberOfRows = 6
[/objc]
Based on the current value of the expanded property we’ll determine the number of rows that our table view should have. Of course it should never display more than the actual items we have so we also take that into account and change our function into the following:
[objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return min(items.count, expanded ? maxNumberOfRows : defaultNumRows)
}
return 0
}
[/objc]
[objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return min(items.count, expanded ? maxNumberOfRows : defaultNumRows)
}
return 0
}
[/objc]
Then the code to make our button work:
updateExpandButtonTitle()
expandButton.addTarget(self, action: 'toggleExpand', forControlEvents: .TouchUpInside)
tableView.sectionFooterHeight = 44
}
expandButton.addTarget(self, action: 'toggleExpand', forControlEvents: .TouchUpInside)
tableView.sectionFooterHeight = 44
}
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return expandButton
}
[/objc]
return expandButton
}
[/objc]
Depending on the current value of our expanded property we wil show either “Show less” or “Show more” as button title.
[objc]
func updateExpandButtonTitle() {
expandButton.setTitle(expanded ? 'Show less' : 'Show more', forState: .Normal)
}
[/objc]
func updateExpandButtonTitle() {
expandButton.setTitle(expanded ? 'Show less' : 'Show more', forState: .Normal)
}
[/objc]
When we tap the button, we’ll invert the expanded property, update the button title and preferred content size and reload the table view data.
[objc]
func toggleExpand() {
expanded = !expanded
updateExpandButtonTitle()
updatePreferredContentSize()
tableView.reloadData()
}
[/objc]
func toggleExpand() {
expanded = !expanded
updateExpandButtonTitle()
updatePreferredContentSize()
tableView.reloadData()
}
[/objc]
And as a result we can now toggle the number of rows we want to see.
Caching
At this moment, each time we open the widget, we first get an empty list and then once the feed is loaded, the items are displayed. To improve this, we can cache the retrieved items and display those once the widget is opened before we load the items from the feed. The TMCache library makes this possible with little effort. We can add it to our Pods file and bridging header the same way we did for the BlockRSSParser library.
Also here, a computed property works nice for caching the items and hide the actual implementation:
[objc]
var cachedItems : [RSSItem]? {
get {
return TMCache.sharedCache().objectForKey('feed') as? [RSSItem]
}
set (newItems) {
TMCache.sharedCache().setObject(newItems, forKey: 'feed')
}
}
[/objc]
[objc]
var cachedItems : [RSSItem]? {
get {
return TMCache.sharedCache().objectForKey('feed') as? [RSSItem]
}
set (newItems) {
TMCache.sharedCache().setObject(newItems, forKey: 'feed')
}
}
[/objc]
Since the RSSItem class of the BlockRSSParser library conforms to the NSCoding protocol, we can use them directly with TMCache. When we retrieve the items from the cache for the first time, we’ll get nil since the cache is empty. Therefore cachedItems needs to be an optional as well as the downcast and therefore we need to use the as? operator.
Swift Note 1 0 – Widget For Notifications Centerpieces
We can now update the cache once the items are loaded simply by assigning a value to the property. So in our success closure we add the following:
[objc]
self.cachedItems = self.items
[/objc]
[objc]
self.cachedItems = self.items
[/objc]
And then to load the cached items, we add two more lines to the end of viewDidLoad:
[objc]
items = cachedItems
updatePreferredContentSize()
[/objc]
[objc]
items = cachedItems
updatePreferredContentSize()
[/objc]
And we’re done. Now each time the widget is opened it will first display the cached items.
There is one last thing we can do to improve our widget. As mentioned earlier, the completionHandler of widgetPerformUpdateWithCompletionHandler can also be called with NCUpdateResult.NoData. Now that we have the items that we loaded previously we can compare newly loaded items with the old and use NoData in case they haven’t changed. Here is our final implementation of the success closure:
[objc]
success: { feedItems in
if self.items nil || self.items! != feedItems {
self.items = feedItems as? [RSSItem]
self.tableView.reloadData()
self.updatePreferredContentSize()
self.cachedItems = self.items
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
},
[/objc]
success: { feedItems in
if self.items nil || self.items! != feedItems {
self.items = feedItems as? [RSSItem]
self.tableView.reloadData()
self.updatePreferredContentSize()
self.cachedItems = self.items
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
},
[/objc]
And since it’s Swift, we can simply use the != operator to see if the arrays have unequal content.
Source code on GitHub
As mentioned in the beginning of this post, the source code of the project is available on GitHub with some minor changes that are not essential to this blog post. Of course pull requests are always welcome. Also let me know in the comments below if you’d wish to see this widget released on the App Store.
Do you want to know more about this subject?
Look at our consultancy services, training offers and careers below or contact us at [email protected]
Look at our consultancy services, training offers and careers below or contact us at [email protected]
Services
Training offers
A cross platform plugin for displaying local notifications.
Table of contents #
- ⚠ Caveats and limitations
- ⚙️ Android Setup
- ⚙️ iOS setup
- ❓ Usage
- Initialisation
? Supported platforms #
- Android 4.1+. Uses the NotificationCompat APIs so it can be run older Android devices
- iOS 8.0+. On iOS versions older than 10, the plugin will use the UILocalNotification APIs. The UserNotification APIs (aka the User Notifications Framework) is used on iOS 10 or newer.
- macOS 10.11+. On macOS versions older than 10.14, the plugin will use the NSUserNotification APIs. The UserNotification APIs (aka the User Notifications Framework) is used on macOS 10.14 or newer.
✨ Features #
- Mockable (plugin and API methods aren't static)
- Display basic notifications
- Scheduling when notifications should appear
- Periodically show a notification (interval based)
- Schedule a notification to be shown daily at a specified time
- Schedule a notification to be shown weekly on a specified day and time
- Retrieve a list of pending notification requests that have been scheduled to be shown in the future
- Cancelling/removing notification by id or all of them
- Specify a custom notification sound
- Ability to handle when a user has tapped on a notification, when the app is the foreground, background or terminated
- Determine if an app was launched due to tapping on a notification
- [Android] Configuring the importance level
- [Android] Configuring the priority
- [Android] Customising the vibration pattern for notifications
- [Android] Configure the default icon for all notifications
- [Android] Configure the icon for each notification (overrides the default when specified)
- [Android] Configure the large icon for each notification. The icon can be a drawable or a file on the device
- [Android] Formatting notification content via (HTML markup)
- [Android] Support for the following notification styles
- Big picture
- Big text
- Inbox
- Messaging
- Media
- While media playback control using a
MediaSession.Token
is not supported, with this style you let Android treat thelargeIcon
bitmap as album artwork
- While media playback control using a
- [Android] Group notifications
- [Android] Show progress notifications
- [Android] Configure notification visibility on the lockscreen
- [Android] Ability to create and delete notification channels
- [Android] Retrieve the list of active notifications
- [Android] Full-screen intent notifications
- [iOS (all supported versions) & macOS 10.14+] Request notification permissions and customise the permissions being requested around displaying notifications
- [iOS 10 or newer and macOS 10.14 or newer] Display notifications with attachments
⚠ Caveats and limitations #
The cross-platform facing API exposed by the
FlutterLocalNotificationsPlugin
class doesn't expose platform-specific methods as its goal is to provide an abstraction for all platforms. As such, platform-specific configuration is passed in as data. There are platform-specific implementations of the plugin that can be obtained by calling the resolvePlatformSpecificImplementation
. An example of using this is provided in the section on requesting permissions on iOS. In spite of this, there may still be gaps that don't cover your use case and don't make sense to add as they don't fit with the plugin's architecture or goals. Developers can fork or maintain their own code for showing notifications in these situations.Compatibility with firebase_messaging
Previously, there were issues that prevented this plugin working properly with the
firebase_messaging
plugin. This meant that callbacks from each plugin might not be invoked. This has been resolved since version 6.0.13 of the firebase_messaging
plugin so please make sure you are using more recent versions of the firebase_messaging
plugin and follow the steps covered in firebase_messaging
's readme file located hereScheduled Android notifications
Some Android OEMs have their own customised Android OS that can prevent applications from running in the background. Consequently, scheduled notifications may not work when the application is in the background on certain devices (e.g. by Xiaomi, Huawei). If you experience problems like this then this would be the reason why. As it's a restriction imposed by the OS, this is not something that can be resolved by the plugin. Some devices may have setting that lets users control which applications run in the background. The steps for these can be vary and but is still up to the users of your application to do given it's a setting on the phone itself.
It has been reported that Samsung's implementation of Android has imposed a maximum of 500 alarms that can be scheduled via the Alarm Manager API and exceptions can occur when going over the limit.
iOS pending notifications limit
There is a limit imposed by iOS where it will only keep 64 notifications that will fire the soonest.
Scheduled notifications and daylight savings
The notification APIs used on iOS versions older than 10 (aka the
UILocalNotification
APIs) have limited supported for time zones.Updating application badge
This plugin doesn't provide APIs for directly setting the badge count for your application. If you need this for your application, there are other plugins available, such as the
flutter_app_badger
plugin.Custom notification sounds
iOS and macOS restrictions apply (e.g. supported file formats).
macOS differences
Due to limitations currently within the macOS Flutter engine,
getNotificationAppLaunchDetails
will return null on macOS versions older than 10.14. These limitations will mean that conflicts may occur when using this plugin with other notification plugins (e.g. for push notifications).The
schedule
, showDailyAtTime
and showWeeklyAtDayAndTime
methods that were implemented before macOS support was added and have been marked as deprecated aren't implemented on macOS.? Screenshots #
Platform | Screenshot |
---|---|
Android | |
iOS | |
macOS |
? Acknowledgements #
- Javier Lecuona for submitting the PR that added the ability to have notifications shown daily
- Jeff Scaturro for submitting the PR to fix the iOS issue around showing daily and weekly notifications and migrating the plugin to AndroidX
- Ian Cavanaugh for helping create a sample to reproduce the problem reported in issue #88
- Zhang Jing for adding 'ticker' support for Android notifications
- .and everyone else for their contributions. They are greatly appreciated
⚙️ Android Setup #
Swift Note 1 0 – Widget For Notifications Centerpiece
Before proceeding, please make sure you are using the latest version of the plugin. The reason for this is that since version 3.0.1+4, the amount of setup needed has been reduced. Previously, applications needed changes done to the
AndroidManifest.xml
file and there was a bit more setup needed for release builds. If for some reason, your application still needs to use an older version of the plugin then make use of the release tags to refer back to older versions of readme.Custom notification icons and sounds
Notification icons should be added as a drawable resource. The example project/code shows how to set default icon for all notifications and how to specify one for each notification. It is possible to use launcher icon/mipmap and this by default is
@mipmap/ic_launcher
in the Android manifest and can be passed AndroidInitializationSettings
constructor. However, the offical Android guidance is that you should use drawable resources. Custom notification sounds should be added as a raw resource and the sample illustrates how to play a notification with a custom sound. Refer to the following links around Android resources and notification icons.https://not-free.mystrikingly.com/blog/apple-excel-alternative. When specifying the large icon bitmap or big picture bitmap (associated with the big picture style), bitmaps can be either a drawable resource or file on the device. This is specified via a single property (e.g. the
largeIcon
property associated with the AndroidNotificationDetails
class) where a value that is an instance of the DrawableResourceAndroidBitmap
means the bitmap should be loaded from an drawable resource. If this is an instance of the FilePathAndroidBitmap
, this indicates it should be loaded from a file referred to by a given file path.⚠️ For Android 8.0+, sounds and vibrations are associated with notification channels and can only be configured when they are first created. Showing/scheduling a notification will create a channel with the specified id if it doesn't exist already. If another notification specifies the same channel id but tries to specify another sound or vibration pattern then nothing occurs.
Full-screen intent notifications
If your application needs the ability to schedule full-screen intent notifications, add the following attributes to the activity you're opening. For a Flutter application that is typically only ony activity extends from
FlutterActivity
. Emu casino free spins 2019. These attributes ensure the screen turns on and shows when the device is locked.For reference, the example app's
AndroidManifest.xml
file can be found here.Note that when a full-screen intent notification actually occurs (as opposed to a heads-up notification that the system may decide should occur), the plugin will act as though the user has tapped on a notification so handle those the same way (e.g.
onSelectNotification
callback) to display the appropriate page for your application.Release build configuration
Before creating the release build of your app (which is the default setting when building an APK or app bundle) you will need to customise your ProGuard configuration file as per this link. Rules specific to the GSON dependency being used by the plugin will need to be added. These rules can be found here. The example app has a consolidated Proguard rules (
proguard-rules.pro
) file that combines these together for reference here.⚠️ Ensure that you have configured the resources that should be kept so that resources like your notification icons aren't discarded by the R8 compiler by following the instructions here. If you fail to do this, notifications might be broken. In the worst case they will never show, instead silently failing when the system looks for a resource that has been removed. If they do still show, you might not see the icon you specified. The configuration used by the example app can be found here where it is specifying that all drawable resources should be kept, as well as the file used to play a custom notification sound (sound file is located here).
⚙️ iOS setup #
General setup
Add the following lines to the
didFinishLaunchingWithOptions
method in the AppDelegate.m/AppDelegate.swift file of your iOS projectObjective-C:
Swift:
Handling notifications whilst the app is in the foreground
By design, iOS applications do not display notifications while the app is in the foreground unless configured to do so.
For iOS 10+, use the presentation options to control the behaviour for when a notification is triggered while the app is in the foreground. The default settings of the plugin will configure these such that a notification will be displayed when the app is in the foreground.
For older versions of iOS, you need to handle the callback as part of specifying the method that should be fired to the
onDidReceiveLocalNotification
argument when creating an instance IOSInitializationSettings
object that is passed to the function for initializing the plugin.Here is an example:
❓ Usage #
Before going on to copy-paste the code snippets in this section, double-check you have configured your application correctly.If you encounter any issues please refer to the API docs and the sample code in the
example
directory before opening a request on Github.Example app #
The
example
directory has a sample application that demonstrates the features of this plugin.API reference #
Checkout the lovely API documentation generated by pub.
Initialisation #
The first step is to create a new instance of the plugin class and then initialise it with the settings to use for each platform
![Center Center](https://photos5.appleinsider.com/gallery/32794-56521-000-lead-Notification-Center-xl.jpg)
Initialisation can be done is in the
main
function of your application or can be done within the first page shown in your app. Developers can refer to the example app that has code for the initialising within the main
function. The code above has been simplified for explaining the concepts. Here we have specified the default icon to use for notifications on Android (refer to the Android setup section) and designated the function (selectNotification
) that should fire when a notification has been tapped on via the onSelectNotification
callback. Specifying this callback is entirely optional but here it will trigger navigation to another page and display the payload associated with the notification.In the real world, this payload could represent the id of the item you want to display the details of. Once the initialisation is complete, then you can manage the displaying of notifications.
The
IOSInitializationSettings
and MacOSInitializationSettings
provides default settings on how the notification be presented when it is triggered and the application is in the foreground. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled.On iOS and macOS, initialisation may show a prompt to requires users to give the application permission to display notifications (note: permissions don't need to be requested on Android). Depending on when this happens, this may not be the ideal user experience for your application. If so, please refer to the next section on how to work around this.
Note: from version 4.0 of the plugin, calling
initialize
will not trigger the onSelectNotification
callback when the application was started by tapping on a notification to trigger. Use the getNotificationAppLaunchDetails
method that is available in the plugin if you need to handle a notification triggering the launch for an app e.g. change the home route of the app for deep-linking.[iOS (all supported versions) and macOS 10.14+] Requesting notification permissions #
The constructor for the
IOSInitializationSettings
and MacOSInitializationSettings
classes has three named parameters (requestSoundPermission
, requestBadgePermission
and requestAlertPermission
) that controls which permissions are being requested. If you want to request permissions at a later point in your application on iOS, set all of the above to false when initialising the plugin.Then call the
requestPermissions
method with desired permissions at the appropriate point in your applicationFor iOS:
For macOS:
Play online video slots for free. Here the call to
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
returns the iOS implementation of the plugin that contains APIs specific to iOS if the application is running on iOS. Similarly, the macOS implementation is returned by calling flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()
. The ?.
operator is used as the result will be null when run on other platforms. Developers may alternatively choose to guard this call by checking the platform their application is running on.Displaying a notification #
In this block of code, the details specific to the Android platform is specified. This includes the channel details that is required for Android 8.0+. Whilst not shown, it's possible to specify details for iOS and macOS as well using the optional
iOS
and macOS
named parameters if needed. The payload has been specified ('item x'), that will passed back through your application when the user has tapped on a notification. Note that for Android devices that notifications will only in appear in the tray and won't appear as a toast aka heads-up notification unless things like the priority/importance has been set appropriately. Refer to the Android docs (https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Heads-up) for additional information. Note that the 'ticker' text is passed here though it is optional and specific to Android. This allows for text to be shown in the status bar on older versions of Android when the notification is shown.Scheduling a notification #
Starting in version 2.0 of the plugin, scheduling notifications now requires developers to specify a date and time relative to a specific time zone. This is to solve issues with daylight savings that existed in the
schedule
method that is now deprecated. A new zonedSchedule
method is provided that expects an instance TZDateTime
class provided by the timezone
package. As the flutter_local_notifications
plugin already depends on the timezone
package, it's not necessary for developers to add the timezone
package as a direct dependency. In other words, the timezone
package will be a transitive dependency after you add the flutter_local_notifications
plugin as a dependency in your application.Usage of the
timezone
package requires initialisation that is covered in the package's readme. For convenience the following are code snippets used by the example app.Import the
timezone
packageInitialise the time zone database
Once the time zone database has been initialised, developers may optionally want to set a default local location/time zone
The
timezone
package doesn't provide a way to obtain the current time zone on the device so developers will need to use platform channels (which is what the example app does) or use other packages that may be able to provide the information (e.g. flutter_native_timezone
).Assuming the local location has been set, the
zonedScheduled
method can then be called in a manner similar to the following codeOn Android, the
androidAllowWhileIdle
is used to determine if the notification should be delivered at the specified time even when the device in a low-power idle mode.The
uiLocalNotificationDateInterpretation
is required as on iOS versions older than 10 as time zone support is limited. This means it's not possible schedule a notification for another time zone and have iOS adjust the time the notification will appear when daylight savings happens. With this parameter, it is used to determine if the scheduled date should be interpreted as absolute time or wall clock time.There is an optional
matchDateTimeComponents
parameter that can be used to schedule a notification to appear on a daily or weekly basis by telling the plugin to match on the time or a combination of day of the week and time respectively.Swift Note 1 0 – Widget For Notifications Centers
If you are trying to update your code so it doesn't use the deprecated methods for showing daily or weekly notifications that occur on a specific day of the week then you'll need to perform calculations that would determine the next instance of a date that meets the conditions for your application. See the example application that shows one of the ways that can be done e.g. how schedule a weekly notification to occur on Monday 10:00AM.
Periodically show a notification with a specified interval #
Retrieving pending notification requests #
[Android only] Retrieving active notifications #
Grouping notifications #
iOS
For iOS, you can specify
threadIdentifier
in IOSNotificationDetails
. Notifications with the same threadIdentifier
will get grouped together automatically.Android
This is a 'translation' of the sample available at https://developer.android.com/training/notify-user/group.html
Cancelling/deleting a notification #
Cancelling/deleting all notifications #
Getting details on if the app was launched via a notification created by this plugin #
[iOS only] Periodic notifications showing up after reinstallation #
Swift Note 1 0 – Widget For Notifications Center
If you have set notifications to be shown periodically on older iOS versions (< 10) and the application was uninstalled without cancelling all alarms, then the next time it's installed you may see the 'old' notifications being fired. If this is not the desired behaviour then you can add code similar to the following to the
didFinishLaunchingWithOptions
method of your AppDelegate
class.Objective-C:
Swift:
? Testing #
As the plugin class is not static, it is possible to mock and verify its behaviour when writing tests as part of your application. Check the source code for a sample test suite that has been kindly implemented (test/flutter_local_notifications_test.dart) that demonstrates how this can be done.
If you decide to use the plugin class directly as part of your tests, the methods will be mostly no-op and methods that return data will return default values.
Part of this is because the plugin detects if you're running on a supported plugin to determine which platform implementation of the plugin should be used. If it's neither Android or iOS, then it defaults to the aforementioned behaviour to reduce friction when writing tests. If this not desired then consider using mocks.
If a platform-specific implementation of the plugin is required for your tests, a named constructor is available that allows you to specify the platform required e.g. a
FakePlatform
.