This time I’m going to do a small series of blog posts about sharing a content to Capacitor based app. This is done by adding the app to an Android Sharesheet or iOS Activity View.
If you are building a communicator, social, file transfer, data transfer, task management or similar type of app then this is probably a must have feature for you.
In the first post I will show how to add a share extension to an iOS app and retrieve a shared content inside Ionic app. In the next posts I will show how to prepopulate and extend the share activity modals as well as adding it on Android.
tldr; As usual you can jump directly to this repository and grab all the prepared examples. This post will be under branch post-1
Prepare an Ionic Capacitor project with iOS platform enabled. Versions used when preparing this post:
node -v
v16.14.2
ionic -v
6.20.8
Add a shared extension target
For starters we need to add a new target. Go to File -> New -> Target and search for extension
Xcode will ask about extension name, provision profile and where to embed the contet. You can set whatever name you feel right, I will just use „Share”. After that Xcode will need to reload the project since there’s a new Target.
This is sufficient for iOS to know to list our app inside the Share Sheet.
However if we try to share something to the app it will open a default text modal
Then if we send it to the app, it simply won’t be handled since there’s no code yet to do so.
Supported formats
It is rather clear that most apps don’t support _everything_. When adding a Share extension, Xcode will set the plist configuration to allow any content to be shared to the app. This is set through a key NSExtensionActivationRule. Default value is set as string to TRUEPREDICATE.
Note: this is in the share extension plist, not the plist of your app.
<dict>
<key>NSExtensionActivationRule</key>
<string>TRUEPREDICATE</string>
</dict>
The data can be limited based on image, movies, attachments. or normal files. It may restrict or not text content, urls or web pages. Here’s an example allowing app to receive maximum one image and text content:
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true />
</dict>
For full list of supported configurations check Apple documentation.
Handle input data
We know how to make app visible in the share sheet and how to restrict the data types. Now we need to handle the data itself. There are numerous things that could be used from background processing, sending over http to saving data in the storage – and that’s what we would do to keep the post extremely simple.
We will add a special json containing information about shares. Each share will be defined as a text and an image encoded as base64 (not very efficient, but simple). Capacitor plugin will allow read the file and import any data shared to the app and clear the data.
First we need to create App Group to share the container between Share & App Targets. This needs to be done per each target. Go to Signing & Capabilities -> Add capability -> App Groups. When capability is added, add a new container. Repeat for Share, but this time don’t add a new container just enable the one already created in the App.
Now it’s time to create a class responsible for storing shared items. This class will be shared between both targets App and Share.
// ShareDataManager.swift
import Foundation
import UIKit
import Social
import UniformTypeIdentifiers
class ShareData: Encodable, Decodable{
var items: [ShareDataItem]
init() {
items = []
}
init(val: [ShareDataItem]){
self.items = val
}
}
class ShareDataItem: Encodable, Decodable {
var text: String
var image: String
init(text: String, image: String) {
self.text = text
self.image = image
}
}
class ShareDataManager {
let file = "share.json"
fileprivate func sharedContainerURL() -> URL? {
let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "YOUR_APP_GROUP_KEY")
return groupURL
}
func clear() {
let data = ShareData()
do {
if let dir = sharedContainerURL() {
let fileURL = dir.appendingPathComponent(file)
let encoder = JSONEncoder()
let json = try encoder.encode(data)
try json.write(to: fileURL)
}
} catch {
print("error:\(error)")
}
}
func read() -> ShareData {
if let dir = sharedContainerURL() {
let filename = dir.appendingPathComponent(file)
do {
let data = try Data(contentsOf: filename)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(ShareData.self, from: data)
return jsonData
} catch {
print("error:\(error)")
return ShareData()
}
}
return ShareData()
}
func write(image: String,
text: String) {
do {
if let dir = self.sharedContainerURL() {
let jsonURL = dir.appendingPathComponent(file)
let encoder = JSONEncoder()
let data = self.read()
let item = ShareDataItem(text: text, image: image)
data.items.append(item)
let json = try encoder.encode(data)
try json.write(to: jsonURL)
}
}
catch {
print("error:\(error)")
}
}
}
ShareDataManager class will be responsible for keeping up record of SharedData in a json file saved in shared App Group. There’s nothing fancy here. Simple deserialization and serialization. read method returns ShareData, write adds a new item to ShareData and saves a file, clear removes all saved records in ShareData.
Now let’s take a look at a class ShareViewController inside Share target. That’s where the real magic happens:
// ShareViewController.swift
import UIKit
import Social
import UniformTypeIdentifiers
class ShareViewController: SLComposeServiceViewController {
override func isContentValid() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didSelectPost() {
let attachments = (self.extensionContext?.inputItems.first as? NSExtensionItem)?.attachments ?? []
let imageType = UTType.jpeg.identifier as String
for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(imageType) {
provider.loadItem(forTypeIdentifier: imageType,
options: nil) { (data, error) in
guard error == nil else { return }
if let imageURL = data as? NSURL {
let sdm = ShareDataManager()
let uiImage = UIImage(contentsOfFile: imageURL.path!)!
let filmeManager = FileManager()
let fileExists = filmeManager.fileExists(atPath: imageURL.path!)
let data = uiImage.jpegData(compressionQuality: 1.0)?.base64EncodedString() ?? ""
sdm.write(image: "data:image/jpeg;base64,\(data)", text: self.contentText)
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
} else {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
fatalError("Impossible to read image")
}
}
} else if provider.hasItemConformingToTypeIdentifier(imageType) {
provider.loadItem(forTypeIdentifier: imageType,
options: nil) { (data, error) in
}
}else {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
}
if attachments.count == 0 {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
}
override func configurationItems() -> [Any]! {
return []
}
}
Let’s see what’s happening here.
- First we check if there are any attachments send over
- If so we iterate over attachments and check if the attachment is of supported type (to keep example short, code has only jpg support)
- If the attachment is of image type we load it and then create UIImage
- Out of the UIImage we get the image into base64 string
- We pass the text of share (self.contentText) and image base 64 data to ShareDataManager which saves it
- We need to call extensionContext.completeRequest after all, so that iOS will know everything is OK
At this point we are mostly focused on the didSelectPost method. The other methods could be used to configure the UI or add some extra options. This will be covered in next blog post.
This code is very limited to an example case when we accept only an image in jpeg format. If you want to extend this to more formats you need to properly implement it by checking hasItemConformingToTypeIdentifier per available types.
Note: this is quite an inefficient implementation, but it’s serves the purpose of showing quickly how to use ShareViewController
Take it as an exercise and try to add a provider yourself or try to save content to file and reuse it.
Read shared data
So far we have implemented saving the shared data into the App Group’s storage. Now we need to access this data in the Ionic app. To do so, let’s add a plugin that will have two methods:
- Read – get all the available share data
- Clear – clear all data
If you haven’t created any Capacitor Plugins yet, you can take a learn it from my previous post.
First we create a plugin in Xcode. Object-C header:
// ShareExtensionDataPlugin.m
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(ShareExtensionDataPlugin, "ShareExtensionDataPlugin",
CAP_PLUGIN_METHOD(read, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(clear, CAPPluginReturnNone);
)
Swift plugin code
// ShareExtensionDataPlugin.swift
import Capacitor
import UniformTypeIdentifiers
@objc(ShareExtensionDataPlugin)
public class ShareExtensionDataPlugin: CAPPlugin {
@objc func clear(_ call: CAPPluginCall) {
let sdm = ShareDataManager()
sdm.clear()
call.resolve()
}
@objc func read(_ call: CAPPluginCall) {
let sdm = ShareDataManager()
let data = sdm.read()
var result:[Any] = []
for item in data.items {
let itemDict = [
"text": item.text,
"image": item.image
]
result.append(itemDict)
}
call.resolve(["items": result])
}
}
Nothing really fancy about the plugin. We just pass the action over to ShareDataManager. Let’s add a typescript service:
//share-extension-data-plugin.service.ts
import { Injectable } from '@angular/core';
import { Capacitor, registerPlugin } from '@capacitor/core';
const _pluginName: string = 'ShareExtensionDataPlugin';
export class ShareData {
items: ShareDataItem[] = [];
}
export class ShareDataItem {
text: string = '';
image: string = '';
}
export interface ShareExtensionDataPlugin {
clear(): Promise<void>;
read(): Promise<ShareData>;
}
const ShareExtensionDataPlugin =
registerPlugin<ShareExtensionDataPlugin>(_pluginName);
@Injectable({
providedIn: 'root',
})
export class ShareExtensionDataPluginService {
async clear(): Promise<void> {
if (Capacitor.isPluginAvailable(_pluginName)) {
await ShareExtensionDataPlugin.clear();
}
}
async read(): Promise<ShareDataItem[] | null> {
if (Capacitor.isPluginAvailable(_pluginName)) {
return (await ShareExtensionDataPlugin.read())?.items ?? null;
}
return null;
}
}
Easy! Now let’s add read, clear buttons to UI and some simple output:
<!--home.page.html-->
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Blank
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Capacitor share sheet extensions example # 1</ion-title>
</ion-toolbar>
</ion-header>
<div id="container">
<div>
<ion-buttons>
<ion-button primary (click)="read()">
Read
</ion-button>
<ion-button secondary (click)="clear()">
Clear
</ion-button>
</ion-buttons>
</div>
<div>
<h3>Result</h3>
<div *ngFor="let item of data">
<p>{{item.text}}</p>
<img [src]="item.image" />
</div>
</div>
</div>
</ion-content>
//home.page.ts
import {
ShareExtensionDataPluginService,
ShareDataItem,
} from './../services/share-extension-data-plugin.service';
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
data: ShareDataItem[] = [];
constructor(private shareService: ShareExtensionDataPluginService) {}
async read() {
let data = await this.shareService.read();
this.data = data ?? [];
}
async clear() {
await this.shareService.clear();
this.data = [];
}
}
Well, that’s about it! Now, give it a try.
Summary
Creating a shared extension for an iOS App seems pretty easy. iOS SDK lets developers easily limit the available to digest data types and add it to Share Sheet. Example here was quite a simple one. Only one image. In your app you may allow multiple images, videos or URLs which could lead to download file and so on.
Share sheet also allows developers to show more customizable UI. I will write about this in next blog post.
Capacitor based apps works great with Share Sheet functionality. Much more work you’d found around Cordova app since that could often unlink your additional Targets. Capacitor’s one of biggest advantages is keeping the project native which allows adding a lot of great native functionality.
Komentarze
3 odpowiedzi na „Sharing content to your Capacitor app”
[…] my previous post I showed how to create a Share Extension in Ionic Capacitor app. Now it’s time to make this […]
[…] This post is post #3 of series around sharing content. You can find previous post and read how to share a post to iOS and how to customize share view in […]
[…] you want to feed it with data created in Ionic you can look at this post and check how to use App […]