Implementing Rich Push Notifications (Images, Buttons) in Mobile Apps
Standard notification — title and text. Rich Push adds an image, action buttons, extended text, sometimes video or GIF. On iOS this requires Notification Service Extension. On Android — natively through NotificationCompat.BigPictureStyle. Let's cover both paths.
iOS: Notification Service Extension
Without NSE, images in push on iOS don't work. NSE — a separate target in Xcode that intercepts a notification before display, downloads media and attaches it as UNNotificationAttachment.
// NotificationService.swift
class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
let content = request.content.mutableCopy() as! UNMutableNotificationContent
guard let urlString = content.userInfo["image_url"] as? String,
let url = URL(string: urlString) else {
contentHandler(content)
return
}
downloadImage(from: url) { localURL in
if let localURL,
let attachment = try? UNNotificationAttachment(identifier: "image",
url: localURL) {
content.attachments = [attachment]
}
contentHandler(content)
}
}
private func downloadImage(from url: URL, completion: @escaping (URL?) -> Void) {
URLSession.shared.downloadTask(with: url) { tempURL, _, _ in
completion(tempURL)
}.resume()
}
}
NSE has execution time limit — about 30 seconds. If media hasn't downloaded in that time, iOS will show the notification without an image. So optimizing media size matters: recommended image size — up to 1 MB, JPEG or PNG format.
Action buttons on iOS. Notification categories are registered at app startup:
let likeAction = UNNotificationAction(identifier: "LIKE", title: "❤️ Like", options: [])
let replyAction = UNNotificationAction(identifier: "REPLY", title: "Reply", options: [.foreground])
let category = UNNotificationCategory(identifier: "POST_CATEGORY",
actions: [likeAction, replyAction],
intentIdentifiers: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
In the notification payload we specify category: "POST_CATEGORY" — iOS will show the buttons. Handling the tap:
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
switch response.actionIdentifier {
case "LIKE":
likePost(id: response.notification.request.content.userInfo["post_id"] as? String)
case "REPLY":
openReplyScreen(for: response.notification.request.content.userInfo)
default:
break
}
completionHandler()
}
Android: BigPictureStyle and Action Buttons
On Android, custom UI natively:
val bitmap = BitmapFactory.decodeStream(
URL(imageUrl).openConnection().getInputStream()
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.setSummaryText(body)
)
.addAction(
R.drawable.ic_like,
"Like",
getLikePendingIntent(postId)
)
.addAction(
R.drawable.ic_reply,
"Reply",
getReplyPendingIntent(postId)
)
.build()
Bitmap loading can't be done on main thread. Either use Glide with Glide.with(context).asBitmap().load(url).submit().get() in a separate thread, or WorkManager task when receiving data message.
FCM Data Message (not Notification Message) — the right approach for Rich Push on Android when the app is in foreground. Notification Message is processed by Android itself, without your code, and there's no standard way to attach an image there. Data Message always goes to onMessageReceived of your FirebaseMessagingService.
Media Formats
| Platform | Image | Video | GIF |
|---|---|---|---|
| iOS NSE | JPEG, PNG, GIF (static preview), HEIC | MP4 (up to 50 MB) | Not natively |
| Android | JPEG, PNG | Not natively | Not natively |
GIF in iOS notification — only through .gif file in NSE, iOS will show the first frame as preview. Full animation works only in Notification Content Extension (second target, for custom UI on long press).
Common Mistakes
OneSignal and similar automatically download images in NSE through their SDK code. But if you have a custom NSE or forgot to add OneSignalNotificationServiceExtension to the list of targets App Groups — images won't appear, and there won't be errors in the logs.
On Android: if the app is killed and FCM arrives with both notification + data field — Android will show a system notification (without image) and onMessageReceived won't be called. Use only data payload for full control.
Timeframe
Setting up NSE for iOS, implementing action buttons for iOS and Android, custom NotificationCompat.Builder with BigPictureStyle — 4–7 working days.







