SWIFT: How to load local images remote HTML - ios

Currently I'm developing an app for Android and iOS. It is a simple webView which is calling a remote URL.
This works perfectly fine - but now I have a problem figuring out how to intercept the loading of images.
I'm trying to achieve the following:
* Load remote URL
* Intercept load and check for images
* If the image exists within the app (in a certain folder) load the local image, otherwise load the remote image from the server
On Android it is pretty easy:
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
try {
if( url.endsWith("png") ) {
return new WebResourceResponse("image/png", "ISO-8859-1", ctx.getAssets().open(url.substring(basepath.length())));
}
if( url.endsWith("jpg") ) {
return new WebResourceResponse("image/jpg", "ISO-8859-1", ctx.getAssets().open(url.substring(basepath.length())));
}
} catch (IOException e) {
}
return super.shouldInterceptRequest(view, url);
}
On iOS - especially SWIFT I haven't found a solution to it yet. So far this is what I have for my webView:
#IBOutlet var webView: UIWebView!
var urlpath = "http://stackoverflow.com"
func loadAddressURL(){
let requesturl = NSURL(string: urlpath!)
let request = NSURLRequest(URL: requesturl)
webView.loadRequest(request) }
override func viewDidLoad() {
super.viewDidLoad()
loadAddressURL() }
Could anyone point me into the correct direction on how to achieve the above mentioned result?
Many thanks in advance.

You can do that using NSURLProtocol, here's a quick example:
Subclass NSURLProtocol
class Interceptor: NSURLProtocol {
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return request
}
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
// returns true for the requests we want to intercept (*.png)
return request.URL.pathExtension == "png"
}
override func startLoading() {
// a request for a png file starts loading
// custom response
let response = NSURLResponse(URL: request.URL, MIMEType: "image/png", expectedContentLength: -1, textEncodingName: nil)
if let client = self.client {
client.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
// reply with data from a local file
client.URLProtocol(self, didLoadData: NSData(contentsOfFile: "local file path")!)
client.URLProtocolDidFinishLoading(self)
}
}
override func stopLoading() {
}
}
Somewhere in your code, register the Interceptor class:
NSURLProtocol.registerClass(Interceptor)
There's also a nice article on NSHipster if you want to read more about NSURLProtocol.

Related

Issue when setting cookies of WKWebView by copying cookies from HTTPCookieStorage to WKWebsiteDataStore

I want to show a website in a WKWebView in my iOS app for which I need to set specific http cookies for the user's session. The cookies are already stored in the shared HttpCookieStorage of the app.
At the moment, I try to copy them from there to the WKWebsiteDataStore of the web view before loading the website with the following code:
class ViewerViewController: UIViewController {
// MARK: - Properties
var webView: WKWebView!
// MARK: - Life Cycle
override func loadView() {
webView = WKWebView()
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
addCookiesToWebView {
print("Load website")
if let url = Bundle.main.url(forResource: "MyWebsite", withExtension: "html"), let html = try? String(contentsOf: url) {
self.webView.loadHTMLString(html, baseURL: url)
}
}
}
// MARK: - Functions
func addCookiesToWebView(completionHandler: #escaping (() -> Void)) {
let sharedCookies = HTTPCookieStorage.shared.cookies!
let webViewCookieStore = webView.configuration.websiteDataStore.httpCookieStore
var count = 0
sharedCookies.forEach { cookie in
DispatchQueue.main.async {
webViewCookieStore.setCookie(cookie) {
count += 1
print("Added cookie \(cookie.name)")
if count == sharedCookies.count {
completionHandler()
}
}
}
}
}
}
Now, what is incomprehensible for me, is that this sometimes works and the user is authenticated in his user session, but sometimes it doesn't.
It seems like there might be a race condition that leads to the web view loading the website before all the cookies have been copied? When I researched this problem I noticed that other people have problems with cookies in WKWebView as well, but I didn't find any solution so far that actually solves my issue.
Is there a bug in my code or is there a better approach for copying multiple cookies from HttpCookieStorage to a WKWebView?

iOS PDFKit textfield form input

I am currently developing a iOS app which can download PDF(Have form on it) from server to iPad, then user can fill in the form on the iPad.
The problem is we need to support chinese on the field input.
Here is the field.
When I use "Quick" input method to type any character, it repeat 3 times.
When I type one more time, it repeat again.
Do anyone have issues with typing Chinese in textfield using PDFKit on iOS?
Update, Add code work
For the server-side, just a endpoint can download pdf, the pdf already added textfield on it.
On client-side, pdf is downloaded in viewDidLoad() and set into pdfView as following code.
override func viewDidLoad() {
super.viewDidLoad()
if let code = self.code {
let url = URL(string : API.pdf + "/" + code)
let loading = self.showLoading()
var request = URLRequest(url: url!)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request, completionHandler: { (responseData: Data?, response: URLResponse?, error: Error?) in
DispatchQueue.main.async {
self.pdfView.document = PDFDocument(data : responseData!)
self.pdfView.scaleFactor = 1
self.pdfView.backgroundColor = UIColor.lightGray
self.pdfView.autoScales = true
loading.dismiss(animated: true, completion: nil)
}
})
task.resume()
}
}
In story board, it is just a UIView and set custom class as PDFView
Updated 2
After that, i do a simple testing.
I create a simple testing controller and load pdf with just a simple textfield annotation. Result is same.
import UIKit
import PDFKit
class PDFTestViewController : ViewController {
#IBOutlet weak var pdfView: PDFView!
override func viewDidLoad() {
super.viewDidLoad()
if let url = Bundle.main.url(forResource: "4547315264964", withExtension: "pdf"),
let doc = PDFDocument(url: url){
self.pdfView.document = doc
self.pdfView.scaleFactor = 1
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

WebView can't handle response when using a custom NSURLProtocol

I'm using a custom NSURLProtocol in order to be able to save the username and password from a form that is loaded in a webView and automatically log the user in.
My issue is that I can get the data, but I can't complete the action to login the user as the webpage uses some JS to listen to the response and complete the form action.
Is there a way to do it?
Here's my current CustomURLProtocol class:
class CustomURLProtocol: NSURLProtocol {
var connection: NSURLConnection!
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
if NSURLProtocol.propertyForKey("customProtocolKey", inRequest: request) != nil {
return false
}
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return request
}
override func startLoading() {
let newRequest = request.mutableCopy() as! NSMutableURLRequest
NSURLProtocol.setProperty(true, forKey: "customProtocolKey", inRequest: newRequest)
// Look for user credentials
if request.URL?.description == "https://www.website.com/Account/Login" {
if let data = request.HTTPBody {
let dataString: String = NSString(data: data, encoding: NSASCIIStringEncoding) as! String
// Code for reading user credentials goes here
}
}
connection = NSURLConnection(request: newRequest, delegate: self)
}
override func stopLoading() {
if connection != nil {
connection.cancel()
}
connection = nil
}
func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
client!.URLProtocol(self, didLoadData: data)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
client!.URLProtocolDidFinishLoading(self)
}
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
client!.URLProtocol(self, didFailWithError: error)
}
}
Is there something I'm missing or is wrong in my logic?
Thanks in advance
So if I understand correctly, you're intercepting the call just to preserve the credentials so you can log the user in automatically later? If you only care about the login request you could move the url check to canInitWithRequest, but that's a side issue.
Using a custom NSURLProtocol correctly shouldn't affect the response, so the web page should behave normally. I notice that you're using NSURLConnection, which is deprecated, so you might want to look at using NSURLSession but again that's a side issue.
Have you stepped through the code to see where it's failing? Are your connection delegate methods being called? Is "webViewDidFinishLoad" being called on the web view?
Or are you not talking about the initial form submission, but subsequent ones when you're trying to log in the user with the stored credentials?

How to add HTTP headers in request globally for iOS in swift

func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {
var request = NSMutableURLRequest(URL: navigationAction.request.URL)
request.setValue("value", forHTTPHeaderField: "key")
decisionHandler(.Allow)
}
In the above code I want to add a header to the request.
I have tried to do navigationAction.request.setValue("IOS", forKey: "DEVICE_APP") but it doesn't work.
please help me in any way.
AFAIK sadly you cannot do this with WKWebView.
It most certainly does not work in webView:decidePolicyForNavigationAction:decisionHandler: because the navigationAction.request is read-only and a non-mutable NSURLRequest instance that you cannot change.
If I understand correctly, WKWebView runs sandboxed in a separate content and network process and, at least on iOS, there is no way to intercept or change it's network requests.
You can do this if you step back to UIWebView.
There are many different ways to do that, I found that the easiest solution was to subclass WKWebView and override the loadRequest method. Something like this:
class CustomWebView: WKWebView {
override func load(_ request: URLRequest) -> WKNavigation? {
guard let mutableRequest: NSMutableURLRequest = request as? NSMutableURLRequest else {
return super.load(request)
}
mutableRequest.setValue("custom value", forHTTPHeaderField: "custom field")
return super.load(mutableRequest as URLRequest)
}
}
Then simply use the CustomWebView class as if it was a WKWebView.
EDIT NOTE: This will only work on the first request as pointed out by #Stefan Arentz.
NOTE: Some fields cannot be overridden and will not be changed. I haven't done a thorough testing but I know that the User-Agent field cannot be overridden unless you do a specific hack (check here for an answer to that)
I have modified Au Ris answer to use NavigationAction instead of NavigationResponse, as jonny suggested. Also, this fixes situations where the same url is called subsequently and you don't have to keep track of the current url anymore. This only works for GET requests but can surely be adapted for other request types if neccessary.
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView?
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect.zero)
webView!.navigationDelegate = self
view.addSubview(webView!)
// [...] set constraints and stuff
// Load first request with initial url
loadWebPage(url: "https://my.url")
}
func loadWebPage(url: URL) {
var customRequest = URLRequest(url: url)
customRequest.setValue("true", forHTTPHeaderField: "x-custom-header")
webView!.load(customRequest)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping
(WKNavigationActionPolicy) -> Void) {
if navigationAction.request.httpMethod != "GET" || navigationAction.request.value(forHTTPHeaderField: "x-custom-header") != nil {
// not a GET or already a custom request - continue
decisionHandler(.allow)
return
}
decisionHandler(.cancel)
loadWebPage(url: navigationAction.request.url!)
}
}
With some limitations, but you can do it. Intercept the response in the delegate function webView:decidePolicyFornavigationResponse:decisionHandler:, if the url changes cancel it by passing decisionHandler(.cancel) and reload the webview with newURLRequest which sets the custom headers and the intercepted url. In this way each time a url changes (e.g. users tap on links) you cancel that request and create a new one with custom headers.
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView?
var loadUrl = URL(string: "https://www.google.com/")!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect.zero)
webView!.navigationDelegate = self
view.addSubview(webView!)
webView!.translatesAutoresizingMaskIntoConstraints = false
webView!.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
webView!.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
webView!.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
webView!.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
// Load first request with initial url
loadWebPage(url: loadUrl)
}
func loadWebPage(url: URL) {
var customRequest = URLRequest(url: url)
customRequest.setValue("some value", forHTTPHeaderField: "custom header key")
webView!.load(customRequest)
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
guard let url = (navigationResponse.response as! HTTPURLResponse).url else {
decisionHandler(.cancel)
return
}
// If url changes, cancel current request which has no custom headers appended and load a new request with that url with custom headers
if url != loadUrl {
loadUrl = url
decisionHandler(.cancel)
loadWebPage(url: url)
} else {
decisionHandler(.allow)
}
}
}
Here's how you do it:
The strategy is to have your WKNavigationDelegate cancel the request, modify a mutable copy of it and re-initiate it. An if-else is used to allow the request to proceed if it already has the desired header; otherwise you will end up in an endless load / decidePolicy loop.
Not sure what's up, but weird things happen if you set the header on every request, so for best results only set the header on requests to the domain(s) you care about.
The example here sets a header field for requests to header.domain.com, and allows all other requests without the header:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL * actionURL = navigationAction.request.URL;
if ([actionURL.host isEqualToString:#"header.domain.com"]) {
NSString * headerField = #"x-header-field";
NSString * headerValue = #"value";
if ([[navigationAction.request valueForHTTPHeaderField:headerField] isEqualToString:headerValue]) {
decisionHandler(WKNavigationActionPolicyAllow);
} else {
NSMutableURLRequest * newRequest = [navigationAction.request mutableCopy];
[newRequest setValue:headerValue forHTTPHeaderField:headerField];
decisionHandler(WKNavigationActionPolicyCancel);
[webView loadRequest:newRequest];
}
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
To add custom headers to AJAX requests, I use a combination of two three hacks. The first provides a synchronous communication channel between my native Swift code and javascript. The second overrides the XMLHttpRequest send() method. The third injects the override into the web page that is loaded into my WKWebView.
So, the combination works like this:
instead of request.setValue("value", forHTTPHeaderField: "key"):
in the ViewController:
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt headerName: String, defaultText _: String?, initiatedByFrame _: WKFrameInfo, completionHandler: #escaping (String?) -> Void) {
if headerName == "key" {
completionHandler("value")
} else {
completionHandler(nil)
}
}}
in viewDidLoad:
let script =
"XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;"
"XMLHttpRequest.prototype.send = function (body) {"
"let value = window.prompt('key');"
"this.setRequestHeader('key', value);"
"this.realSend(body)"
"};"
webView.configuration.userContentController.addUserScript(WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))
and this is the test HTML file:
<html>
<head>
<script>
function loadAjax() {
const xmlhttp = new XMLHttpRequest()
xmlhttp.onload = function() {
document.getElementById("load").innerHTML = this.responseText
}
xmlhttp.open("GET", "/ajax")
xmlhttp.send()
}
</script>
</head>
<body>
<button onClick="loadAjax()">Change Content</button> <br />
<pre id="load">load…</pre>
</body>
</html>
Call to /ajax brings a generic echo, including all request headers. This way I know that the task is fullfilled.
private var urlrequestCurrent: URLRequest?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
//print("WEB decidePolicyFor navigationAction: \(navigationAction)")
if let currentrequest = self.urlrequestCurrent {
//print("currentrequest: \(currentrequest), navigationAction.request: \(navigationAction.request)")
if currentrequest == navigationAction.request {
self.urlrequestCurrent = nil
decisionHandler(.allow)
return
}
}
decisionHandler(.cancel)
var customRequest = navigationAction.request
customRequest.setValue("myvaluefffs", forHTTPHeaderField: "mykey")
self.urlrequestCurrent = customRequest
webView.load(customRequest)
}
my solution is copy request and add headers then load it again
if navigationAction.request.value(forHTTPHeaderField: "key") == nil {
decisionHandler(.cancel)
var req:URLRequest = navigationAction.request;
req.addValue("value", forHTTPHeaderField: "key");
webView.load(req);
} else {
decisionHandler(.allow)
}
The above mentioned solutions seems to work on iOS 14 but on iOS < 14, the POST request Body is always null causing server-side rejects of the request. It turned out that this is a known bug in WKWebView and in WebKit causing navigationLink.Request.Body to be always nil !! very frustrating and stupid bug from Apple forcing UIWebView migration to non-stable WKWebView !
Anyway, the solution is that you should (before canceling the request), grab POST body by running a javascript function and then assign the result back to navigationAction.Request (if navigationAction.Request.Body is null) and then cancel the action and request it again with the updated navigationAction.Request :
Solution is in Xamarin but native iOS is very close.
[Foundation.Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
public async void DecidePolicy(WebKit.WKWebView webView, WebKit.WKNavigationAction navigationAction, Action<WebKit.WKNavigationActionPolicy> decisionHandler)
{
try
{
var url = navigationAction.Request.Url;
// only apply to requests being made to your domain
if (url.Host.ToLower().Contains("XXXXX"))
{
if (navigationAction.Request.Headers.ContainsKey((NSString)"Accept-Language"))
{
var languageHeaderValue = (NSString)navigationAction.Request.Headers[(NSString)"Accept-Language"];
if (languageHeaderValue == Globalization.ActiveLocaleId)
{
decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
return;
}
else
{
decisionHandler(WKNavigationActionPolicy.Cancel);
var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);
// Temp fix for navigationAction.Request.Body always null on iOS < 14
// causing form not to submit correctly
updatedRequest = await FixNullPostBody(updatedRequest);
WebView.LoadRequest(updatedRequest);
}
}
else
{
decisionHandler(WKNavigationActionPolicy.Cancel);
var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);
// Temp fix for navigationAction.Request.Body always null on iOS < 14
// causing form not to submit correctly
updatedRequest = await FixNullPostBody(updatedRequest);
WebView.LoadRequest(updatedRequest);
}
}
else
{
decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
}
}
catch (Exception ex)
{
Logger.LogException(ex);
decisionHandler?.Invoke(WKNavigationActionPolicy.Allow);
}
}
}
private async Task<NSMutableUrlRequest> FixNullPostBody(NSMutableUrlRequest urlRequest)
{
try
{
// if on iOS 14 and higher, don't do this
//if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
//return urlRequest;
// only resume on POST http methods
if (urlRequest.HttpMethod.ToLowerSafe() != "post")
return urlRequest;
// if post body is already there, exit
if(urlRequest.Body != null)
return urlRequest;
if (WebView == null)
return urlRequest;
// get body post by running javascript
var body = await WebView.EvaluateJavaScriptAsync("$('form').serialize()");//.ConfigureAwait(true);
if (body != null)
{
//urlRequest.Body = urlRequest.Body; // always null on iOS < 14
var bodyString = body.ToString();
if (!bodyString.IsNullOrEmpty())
urlRequest.Body = NSData.FromString(bodyString);
}
}
//This method will throw a NSErrorException if the JavaScript is not evaluated successfully.
catch (NSErrorException ex)
{
DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
}
catch (Exception ex)
{
DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
}
return urlRequest;
}
private NSMutableUrlRequest SetHeaders(NSMutableUrlRequest urlRequest)
{
try
{
if (this.UsePOST)
{
urlRequest.HttpMethod = "POST";
urlRequest.Body = postParameters.Encode(NSStringEncoding.UTF8, false);
}
var keys = new object[] { "Accept-Language" };
var objects = new object[] { Globalization.ActiveLocaleId };
var dictionnary = NSDictionary.FromObjectsAndKeys(objects, keys);
if (urlRequest.Headers == null)
{
urlRequest.Headers = dictionnary;
}
else
{
NSMutableDictionary httpHeadersCopy = new NSMutableDictionary(urlRequest.Headers);
httpHeadersCopy.Remove((NSString)"Accept-Language");
httpHeadersCopy.Add((NSString)"Accept-Language", (NSString)Globalization.ActiveLocaleId);
urlRequest.Headers = null;
urlRequest.Headers = (NSDictionary)httpHeadersCopy;
}
}
catch (Exception ex)
{
Logger.LogException(ex);
}
return urlRequest;
}

Share Extension to open containing app

I want to create an Android Style share feature for my app.
I created a share extension which gets called when you select pictures inside the stock photo app and press share.
Now I want those pictures to be sent to the main app and get handled over there.
My question is now:
Can iOS open my app after a button is pressed on the share extension window?
How do I get the picture files inside my main app?
Swift 4+ (tested on iOS 13)
#objc should be added to the declaration of openURL, that is,
#objc func openURL(_ url: URL) -> Bool {
// Code below.
}
Without it one would see this compiler error:
Argument of '#selector' refers to instance method 'openURL' that is not exposed to Objective-C
Working solution in Swift 3.1 (tested in iOS10):
You need to create your own URL Scheme, then add this function to your ViewController and call it with openURL("myScheme://myIdentifier")
// Function must be named exactly like this so a selector can be found by the compiler!
// Anyway - it's another selector in another instance that would be "performed" instead.
func openURL(_ url: URL) -> Bool {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
return application.perform(#selector(openURL(_:)), with: url) != nil
}
responder = responder?.next
}
return false
}
Edit: Notes for clarification:
openURL is a method of UIApplication - since your ShareExtension is not derived from UIApplication I added my own openURL with the same definition as the one from UIApplication to keep the compiler happy (so that #selector(openURL(_:) can be found).
Then I go through the responders until I find one that is really derived from UIApplication and call openURL on that.
More stripped-down-example-code which copies files in a ShareExtension to a local directory, serializing filenames and calling openURL on another app:
//
// ShareViewController.swift
//
import UIKit
import Social
import MobileCoreServices
class ShareViewController: UIViewController {
var docPath = ""
override func viewDidLoad() {
super.viewDidLoad()
let containerURL = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.com.my-domain")!
docPath = "\(containerURL.path)/share"
// Create directory if not exists
do {
try FileManager.default.createDirectory(atPath: docPath, withIntermediateDirectories: true, attributes: nil)
} catch let error as NSError {
print("Could not create the directory \(error)")
} catch {
fatalError()
}
// removing previous stored files
let files = try! FileManager.default.contentsOfDirectory(atPath: docPath)
for file in files {
try? FileManager.default.removeItem(at: URL(fileURLWithPath: "\(docPath)/\(file)"))
}
}
override func viewDidAppear(_ animated: Bool) {
let alertView = UIAlertController(title: "Export", message: " ", preferredStyle: .alert)
self.present(alertView, animated: true, completion: {
let group = DispatchGroup()
NSLog("inputItems: \(self.extensionContext!.inputItems.count)")
for item: Any in self.extensionContext!.inputItems {
let inputItem = item as! NSExtensionItem
for provider: Any in inputItem.attachments! {
let itemProvider = provider as! NSItemProvider
group.enter()
itemProvider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { data, error in
if error == nil {
// Note: "data" may be another type (e.g. Data or UIImage). Casting to URL may fail. Better use switch-statement for other types.
// "screenshot-tool" from iOS11 will give you an UIImage here
let url = data as! URL
let path = "\(self.docPath)/\(url.pathComponents.last ?? "")"
print(">>> sharepath: \(String(describing: url.path))")
try? FileManager.default.copyItem(at: url, to: URL(fileURLWithPath: path))
} else {
NSLog("\(error)")
}
group.leave()
}
}
}
group.notify(queue: DispatchQueue.main) {
NSLog("done")
let files = try! FileManager.default.contentsOfDirectory(atPath: self.docPath)
NSLog("directory: \(files)")
// Serialize filenames, call openURL:
do {
let jsonData : Data = try JSONSerialization.data(
withJSONObject: [
"action" : "incoming-files"
],
options: JSONSerialization.WritingOptions.init(rawValue: 0))
let jsonString = (NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let result = self.openURL(URL(string: "myapp://com.myapp.share?\(jsonString!)")!)
} catch {
alertView.message = "Error: \(error.localizedDescription)"
}
self.dismiss(animated: false) {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
}
})
}
// Function must be named exactly like this so a selector can be found by the compiler!
// Anyway - it's another selector in another instance that would be "performed" instead.
#objc func openURL(_ url: URL) -> Bool {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
return application.perform(#selector(openURL(_:)), with: url) != nil
}
responder = responder?.next
}
return false
}
}
Technically you can't open containing app from share extension, but you can schedule local notification, and that's what I end up doing. Just before I call super.didSelectPost, I schedule local notification with some text, and if user wants to open containing app, they can, and if not - they can continue with their workflow. I even think its a better approach than automatically opening containing app and disrupting what they are doing.
Currently there's no way to do this. A share extension cannot open the containing app.
The intended approach for share extensions is that they handle all of the necessary work themselves. Extensions can share code with their containing apps by using custom frameworks, so in most cases that's no problem.
If you want to make data available to your app, you can set up an app group so that you have a shared directory. The extension can write data there, and the app can read it. That won't happen until the next time the user launches the app, though.
I opened the host app from shared extension with a trick.
Using a webview with clear background color.
below is the code
NSString *customURL = #"MY_HOST_URL_SCHEME_APP://";
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)];
webView.backgroundColor = [UIColor clearColor];
webView.tintColor = [UIColor clearColor];
[webView setOpaque:NO];
[self.view addSubview:webView];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:customURL]];
[webView loadRequest:urlRequest];
[self didSelectCancel];
Implement custom url schema in host app and call openURL(url:) method
like openURL(url:NSURL(string:"schema_name://"))
extension SLComposeServiceViewController {
func openURL(url: NSURL) -> Bool {
do {
let application = try self.sharedApplication()
return application.performSelector("openURL:", withObject: url) != nil
}
catch {
return false
}
}
func sharedApplication() throws -> UIApplication {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
return application
}
responder = responder?.nextResponder()
}
throw NSError(domain: "UIInputViewController+sharedApplication.swift", code: 1, userInfo: nil)
}
}
Xamarin.iOS version of #coyer answer:
using System;
using Foundation;
using UIKit;
using MobileCoreServices;
using CoreFoundation;
using System.Linq;
using Newtonsoft.Json;
using System.Collections.Generic;
using ObjCRuntime;
using System.Runtime.InteropServices;
namespace Your.ShareExtension
{
public partial class ShareViewController : UIViewController
{
public ShareViewController(IntPtr handle) : base(handle)
{
}
string docPath = "";
public override void ViewDidLoad()
{
base.ViewDidLoad();
try
{
var containerURL = new NSFileManager().GetContainerUrl("group.com.qsiga.startbss");
docPath = $"{containerURL.Path}/share";
// Create directory if not exists
try
{
NSFileManager.DefaultManager.CreateDirectory(docPath, true, null);
}
catch (Exception e)
{ }
// removing previous stored files
NSError contentError;
var files = NSFileManager.DefaultManager.GetDirectoryContent(docPath, out contentError);
foreach (var file in files)
{
try
{
NSError err;
NSFileManager.DefaultManager.Remove($"{docPath}/{file}", out err);
}
catch (Exception e)
{ }
}
}
catch (Exception e)
{
Console.WriteLine("ShareViewController exception: " + e);
}
}
public override void ViewDidAppear(bool animated)
{
var alertView = UIAlertController.Create("Export", " ", UIAlertControllerStyle.Alert);
PresentViewController(alertView, true, () =>
{
var group = new DispatchGroup();
foreach (var item in ExtensionContext.InputItems)
{
var inputItem = item as NSExtensionItem;
foreach (var provider in inputItem.Attachments)
{
var itemProvider = provider as NSItemProvider;
group.Enter();
itemProvider.LoadItem(UTType.Data.ToString(), null, (data, error) =>
{
if (error == null)
{
// Note: "data" may be another type (e.g. Data or UIImage). Casting to URL may fail. Better use switch-statement for other types.
// "screenshot-tool" from iOS11 will give you an UIImage here
var url = data as NSUrl;
var path = $"{docPath}/{(url.PathComponents.LastOrDefault() ?? "")}";
NSError err;
NSFileManager.DefaultManager.Copy(url, NSUrl.CreateFileUrl(path, null), out err);
}
group.Leave();
});
}
}
group.Notify(DispatchQueue.MainQueue, () =>
{
try
{
var jsonData = JsonConvert.SerializeObject(new Dictionary<string, string>() { { "action", "incoming-files" } });
var jsonString = NSString.FromData(jsonData, NSStringEncoding.UTF8).CreateStringByAddingPercentEncoding(NSUrlUtilities_NSCharacterSet.UrlQueryAllowedCharacterSet);
var result = openURL(new NSUrl($"startbss://share?{jsonString}"));
}
catch (Exception e)
{
alertView.Message = $"Error: {e.Message}";
}
DismissViewController(false, () =>
{
ExtensionContext?.CompleteRequest(new NSExtensionItem[] { }, null);
});
});
});
}
public bool openURL(NSUrl url)
{
UIResponder responder = this;
while (responder != null)
{
var application = responder as UIApplication;
if (application != null)
return CallSelector(application, url);
responder = responder?.NextResponder;
}
return false;
}
[DllImport(Constants.ObjectiveCLibrary, EntryPoint = "objc_msgSend")]
static extern bool _callSelector(
IntPtr target,
IntPtr selector,
IntPtr url,
IntPtr options,
IntPtr completionHandler
);
private bool CallSelector(UIApplication application, NSUrl url)
{
Selector selector = new Selector("openURL:options:completionHandler:");
return _callSelector(
application.Handle,
selector.Handle,
url.Handle,
IntPtr.Zero,
IntPtr.Zero
);
}
}
}
I'm able to get this working by accessing the shared UIApplication instance via key-value coding and calling openURL on that:
let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as! UIApplication
let selector = NSSelectorFromString("openURL:")
let url = URL(string: "jptest://")!
application.perform(selector, with: url)
I was having this problem, and in iOS 11+ none of the previous answers work. I ended up adding a completion handler to my JavaScript code, and from there setting window.location="myapp://". It's a bit hacky but it doesn't look to bad and the user can follow along.
Not only there is no way (and won't be) to do this:
there is no NEED to handle this in the app.
The extension is supposed to handle this with the very
same codebase as the main app. You should create a framework
with extension safe API shared between the app and the extesnion targets.
This is the top topic here:
https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1
Extra rationale: in the extension you'd have to work with a much
smaller memory allowance meaning: if you use the images
of decent size as in the main app you will likely crash and burn.
In extension you'd have to work with jpeg or reasonable small size
and even then make sure size is small enough otherwise you'd be booted out trying to unpack the image from disk into memory
(see size limitation above)
EDIT: This solution works for today extension (Widget).
An extension can open the hosting app:
- (IBAction)launchHostingApp:(id)sender
{
NSURL *pjURL = [NSURL URLWithString:#"hostingapp://home"];
[self.extensionContext openURL:pjURL completionHandler:nil];
}
And like Apple says in Handling Commons Scenarios :
An extension doesn’t directly tell its containing app to open; instead, it uses the openURL:completionHandler: method of NSExtensionContext to tell the system to open its containing app. When an extension uses this method to open a URL, the system validates the request before fulfilling it.

Resources