I'm wondering how to open a linked pdf file with the turbolinks-ios framework in iOS.
Currently, I'm experiencing the issue that when a turbolinks page links to a pdf or other file, then the link will open in safari rather than the embedded view.
Background
The turbolinks-5 library together with the turbolinks-ios framework provide a way to connect a web application to the native navigation controllers of the corresponding mobile app.
The screenshot is taken from the turbolinks README.
Desired behavior
When clicking a link that refers to a pdf, a seaparate view controller should be pushed to the current navigation controller, such that the user can read the pdf and easily navigate back to the document index.
Observed behavior
The linked pdf is opened in safari rather than within the app. Unfortunately, safari asks for authentication, again. Furthermore, the user has to leave the application.
Intercept the click of the pdf link
For a link to a pdf file, the didProposeVisitToURL mechanism is not triggered for the session delegate. Thus, one can't decide from there how to handle the linked pdf.
Instead, one could intercept clicking the link by becoming turbolinks' web view's navigation delegate as shown in the README:
extension NavigationController: SessionDelegate {
// ...
func sessionDidLoadWebView(session: Session) {
session.webView.navigationDelegate = self
}
}
extension NavigationController: WKNavigationDelegate {
func webView(webView: WKWebView,
decidePolicyForNavigationAction navigationAction: WKNavigationAction,
decisionHandler: (WKNavigationActionPolicy) -> ()) {
// This method is called whenever the webView within the
// visitableView attempts a navigation action. By default, the
// navigation has to be cancelled, since when clicking a
// turbolinks link, the content is shown in a **new**
// visitableView.
//
// But there are exceptions: When clicking on a PDF, which
// is not handled by turbolinks, we have to handle showing
// the pdf manually.
//
// We can't just allow the navigation since this would not
// create a new visitable controller, i.e. there would be
// no back button to the documents index. Therefore, we have
// to create a new view controller manually.
let url = navigationAction.request.URL!
if url.pathExtension == "pdf" {
presentPdfViewController(url)
}
decisionHandler(WKNavigationActionPolicy.Cancel)
}
}
Present the pdf view controller
Similarly to presenting the visitable view as shown in the turbolinks-ios demo application, present the pdf view controller:
extension NavigationController {
func presentPdfViewController(url: NSURL) {
let pdfViewController = PdfViewController(URL: url)
pushViewController(pdfViewController, animated: true)
}
}
Or, if you'd like to show other file types as well, call it fileViewController rather than pdfViewController.
PdfViewController
The new view controller inherits from turbolinks' VisitableViewController to make use of the initialization by url.
class PdfViewController: FileViewController {
}
class FileViewController: Turbolinks.VisitableViewController {
lazy var fileView: WKWebView = {
return WKWebView(frame: CGRectZero)
}()
lazy var filename: String? = {
return self.visitableURL?.pathComponents?.last
}()
override func viewDidLoad() {
view.addSubview(fileView)
fileView.bindFrameToSuperviewBounds() // https://stackoverflow.com/a/32824659/2066546
self.title = filename // https://stackoverflow.com/a/39022302/2066546
fileView.loadRequest(NSURLRequest(URL: visitableURL))
}
}
To get the web view to the correct size, I used bindFrameToSuperviewBounds as shown in this stackoverflow answer, but I'm sure there are other methods.
Optional: Sharing cookies
If loading the pdf needs authentication, it's convenient to share the cookies with the turbolinks-ios webview as described in the README.
For example, create a webViewConfiguration which can be passed to the pdfViewController:
extension NavigationController {
let webViewProcessPool = WKProcessPool()
lazy var webViewConfiguration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.processPool = self.webViewProcessPool
// ...
return configuration
}()
lazy var session: Session = {
let session = Session(webViewConfiguration: self.webViewConfiguration)
session.delegate = self
return session
}()
}
The same webViewConfiguration needs to be passed to the session (shown above) as well as to the new pdf view controller.
extension NavigationController {
func presentPdfViewController(url: NSURL) {
let pdfViewController = PdfViewController(URL: url)
pdfViewController.webViewConfiguration = self.webViewConfiguration
pushViewController(pdfViewController, animated: true)
}
}
class FileViewController: Turbolinks.VisitableViewController {
var webViewConfiguration: WKWebViewConfiguration
lazy var fileView: WKWebView = {
return WKWebView(frame: CGRectZero, configuration: self.webViewConfiguration)
}()
// ...
}
Demo
Related
I am trying to support Apple's Markup of PDFs via UIDocumentInteractionController for files in my Documents folder on iPad. I want the documents edited in-place, so my app can load them again after the user is finished. I have set the Info.plist options for this, and the in-place editing does seem to work. Changes are saved to the same file.
When I bring up the UIDocumentInteractionController popover for the PDF, I am able to choose "Markup", which then shows the PDF ready for editing. I can edit it too. The problem is when I click "Done": I get a menu appear with the options "Save File To..." and "Delete PDF". No option just to close the editor or save.
The frustrating thing is, I can see via Finder that the file is actually edited in-place in the simulator, and is already saved when this menu appears. I just want the editor to disappear and not confuse the user. Ie I want "Done" to be "Done".
Perhaps related, and also annoying, is that while the markup editor is visible, there is an extra directory added to Documents called (A Document Being Saved By <<My App Name>>), and that folder is completely empty the whole time. Removing the folder during editing does not change anything.
Anyone have an idea if I am doing something wrong, or if there is a way to have the Done button simply dismiss?
In case others have this issue, I believe it is a bug in UIDocumentInteractionController in how it sets up the QLPreviewController it uses internally. If I proxy the delegate of the QLPreviewController, and return .updateContents from previewController(_:editingModeFor:), it works as expected.
Here is my solution. The objective is simple enough, but actually capturing the private QLPreviewController was not easy, and I ended up using a polling timer. There may be a better way, but I couldn't find it.
import QuickLook
class ViewController: UIViewController, UIDocumentInteractionControllerDelegate {
/// This wraps the original delegaet of the QLPreviewController,
/// so we can return .updateContents from previewController(_:editingModeFor:)
let delegateProxy = QLPreviewDelegateProxy()
var documentInteractionController = UIDocumentInteractionController()
/// A timer we use to update the QL controller
/// Ideally, we would use callbacks or delegate methds, but couldn't
/// find a satisfactory set to do the job. Instead we poll (like an animal)
var quicklookControllerPollingTimer: Timer?
/// Use this to track the preview controller created by UIDocumentInteractionController
var quicklookController: QLPreviewController?
/// File URL of the PDF we are editing
var editURL: URL!
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
quicklookControllerPollingTimer?.invalidate()
}
#IBAction func showPopover(_ sender: Any?) {
documentInteractionController.url = editURL
documentInteractionController.delegate = self
documentInteractionController.presentOptionsMenu(from: button.bounds, in: button, animated: true)
quicklookControllerPollingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [unowned self] timer in
guard quicklookController == nil else {
if quicklookController?.view.window == nil {
quicklookController = nil
}
return
}
if let ql = presentedViewController?.presentedViewController as? QLPreviewController, ql.view.window != nil {
self.quicklookController = ql
// Extra delay gives UI time to update
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
guard let ql = self.quicklookController else { return }
delegateProxy.originalDelegate = ql.delegate
ql.delegate = delegateProxy
ql.reloadData()
}
}
}
}
}
class QLPreviewDelegateProxy: NSObject, QLPreviewControllerDelegate {
weak var originalDelegate: QLPreviewControllerDelegate?
/// All this work is just to return .updateContents here. Doing this makes it all work properly.
/// Must be a bug in UIDocumentInteractionController
func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
.updateContents
}
}
Hello I want to create an application with several webview that once they have been loaded once, I don't want them to be recreated anymore I want them to keep their instance.
I create in my application A navigation view which includes a list of 10 sites. When I click on a item of the list I want to display the corresponding webview with NavigationLink. It works. But the problem is that if I click on another item of the list and I will return to the previous one it returns to the home page, the webview loads again. I want the webview to be created only once and always stay alive in as long as the app is alive. I know that swiftui always refresh views is the problem, how can I prevent my webview from being refreshed by swiftUI?
In uitkit it's simple I create an array of wkwebview at launch of the app, I load all my webview url, in a singleton class. And depending on the item selected from my tableview I display the corresponding wkwebview. And even if I change the item all my webview are alive even if we don't see them.
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
let web = WKWebView()
web.load(request)
return web
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
Here is simplified demo of possible approach. The idea is to cache created WKWebView objects by some identifiers and just reuse them in created (or recreated) thin-wrapper SwiftUI view representable.
Tested & works with Xcode 11.2 / iOS 13.2
struct WebView: UIViewRepresentable {
private static var cache: [Int: WKWebView] = [:]
// the only allowed entry point to create WebView, so have control either
// to create new instance of WKWebView or provide already loaded
static func view(with id: Int, request: URLRequest) -> WebView {
var web = cache[id] // it is UI thread so safe to access static
if web == nil {
web = WKWebView()
cache[id] = web
}
return WebView(with: web!, request: request)
}
private init(with web: WKWebView, request: URLRequest) {
self.web = web
self.request = request
}
private let web: WKWebView
private let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
if web.url == nil { // just created, so perform initial loading
web.load(request)
}
return web
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
// Just simple test view
struct TestOnceLoadedWebView: View {
private let urls: [String] = [
"http://www.apple.com",
"http://www.google.com",
"http://www.amazon.com"
]
var body: some View {
NavigationView {
List(0 ..< urls.count) { i in
NavigationLink("Link \(i)", destination:
WebView.view(with: i, request:
URLRequest(url: URL(string: self.urls[i])!)))
}
}
}
}
Scenario:
Creating a social network app type application, when user type the web address or valid url in the text area my app should identify the valid url,then I have to do the following tasks,
1) If it is a valid web url then automatically show the preview of the web contents in the preview section.
2) Share button should not be enabled until the preview is fully loaded, if not there are some chances that URL alone may be posted instead of the web contents.
What I have tried?
Created a Text view and a preview WebUI view, I used following code to identify the valid URL, if valid url then I am showing the web view and loading the web page.
Code what I tried:
class CommentsViewController: UIViewController {
#IBOutlet weak var comments: UITextField!
#IBOutlet weak var preview: UIWebView!
#IBOutlet weak var btnShare: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
preview.isHidden = true
btnShare.isEnabled = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func valueModified(_ sender: Any) {
//Load web preview only after Check for Valid Web URL
if verifyUrl(urlString: comments.text) == true {
preview.isHidden = false
preview.loadRequest(URLRequest(url: URL(string: comments.text!)!))
btnShare.isEnabled = true
}
}
//Check for Valid Web URL
func verifyUrl (urlString: String?) -> Bool {
//Check for nil
if let urlString = urlString {
// create NSURL instance
if let url = NSURL(string: urlString) {
// check if your application can open the NSURL instance
return UIApplication.shared.canOpenURL(url as URL)
}
}
return false
}
}
What I need exactly?
I do not want to load the web page, I have to load the preview of the page.
Share button should be hidden until the preview is fully loaded.
Screen shot of my code output:
Screen shot of what I am expecting:
I need open url programmatically when user click to cell, but not need segue to Safari browser. It is need for statistic on site.
This is need to imitation like post request
I do:
UIApplication.shared.openURL(URL(string: url)!)
But this open Safari browser.
If I'm not mistaking, you want to open the URL in the application it self (without navigating to external -Safari- browser). Well, if that's the case, you should use UIWebView:
You can use the UIWebView class to embed web content in your app. To
do so, create a UIWebView object, attach it to a window, and send it a
request to load web content. You can also use this class to move back
and forward in the history of webpages, and you can even set some web
content properties programmatically.
WebViewController:
I suggest to add a UIWebView into a ViewController (called WebViewController) and present the ViewController when needed; ViewController on storyboard should looks like:
DON'T forget to assign a Storyboard ID for it.
And the ViewController:
class WebViewController: UIViewController {
#IBOutlet weak private var webVIew: UIWebView!
var urlString: String?
override func viewDidLoad() {
if let unwrappedUrlString = urlString {
let urlRequest = URLRequest(url: URL(string: unwrappedUrlString)!)
webVIew.loadRequest(urlRequest)
}
}
#IBAction private func donePressed(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
Usage:
Consider that you want present it when the user taps a button in another ViewController:
class ViewController: UIViewController {
#IBAction private func donePressed(_ sender: Any) {
let stoyrboard = UIStoryboard(name: "Main", bundle: nil)
// withIdentifier: the used storyboard ID:
let webViewController = stoyrboard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
webViewController.urlString = "https://www.google.com/"
}
}
Or specifically for your case (selecting a cell):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// withIdentifier: the used storyboard ID:
let webViewController = stoyrboard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
webViewController.urlString = "URL GOES HERE..."
}
Hope this helped.
if your question really means that you have some data on an external website that you need to access, so that you can extract a piece of information from it, then you probably don't need to display the rendered webpage at all.
You can extract the html content of a page like this
var text = ""
let url = URL(string: "https://www.bbc.co.uk")
do
{
text = try String(contentsOf: url!, encoding: String.Encoding.utf8)
}
catch
{
print("error \(error.localizedDescription)")
}
if !text.isEmpty
{
parseThisPageSomehow(text)
}
How you parse the text depends on what you need to get out of it, but this approach will give you the data you need.
If you use http rather than https, you will run into the well-documented transport security issue, which will force you to either set up a temporary override for http, or start using https
Transport security has blocked a cleartext HTTP
When you open url that contain PDF file safari ask you if you want to open it on safari or in iBook.
I want to do the same thing ,
in my project i had a collection view contains videos and photos,
i want the user to chose if he want to open the file on the app or to open it with other media player.
For loading into your own app it depends on which class you're using to display content on the exact code you'd use but for opening in another app you'd normally use a share button. Here is example code that will work if you wire up the #IBAction and #IBOutlet to the same bar button in your UI (and place a file at the fileURL that you specify):
import UIKit
class ViewController: UIViewController {
// UIDocumentInteractionController instance is a class property
var docController:UIDocumentInteractionController!
#IBOutlet weak var shareButton: UIBarButtonItem!
// called when bar button item is pressed
#IBAction func shareDoc(sender: AnyObject) {
// present UIDocumentInteractionController
if let barButton = sender as? UIBarButtonItem {
docController.presentOptionsMenuFromBarButtonItem(barButton, animated: true)
}
else {
print("Wrong button type, check that it is a UIBarButton")
}
}
override func viewDidLoad() {
super.viewDidLoad()
// retrieve URL to file in main bundle
if let fileURL = NSBundle.mainBundle().URLForResource("MyImage", withExtension: "jpg") {
// Instantiate the interaction controller
self.docController = UIDocumentInteractionController(URL: fileURL)
}
else {
shareButton.enabled = false
print("File missing! Button has been disabled")
}
}
}
Notes
A UIDocumentInteractionController is used to enable the sharing of documents between your app and other apps installed on a user's device. It is simple to set up as long as you remember three rules:
Always make the UIDocumentInteractionController instance a class
(type) property. If you only retain a reference to the controller
for the life of the method that is triggered by the button press
your app will crash.
Configure the UIDocumentInteractionController
before the button calling the method is pressed so that there is not
a wait in which the app is waiting for the popover to appear. This is important because while
the presentation of the controller happens asynchronously, the instantiation does not. And you may find that there is a noticeable delay to open the popover if
you throw all the code for instantiation and presentation inside a
single method called on the press of a button. (When testing you might see a delay anyway because the share button is likely going to be pressed almost straightaway but in real world use there should be more time for the controller to prepare itself and so the possibility of lag is less likely.)
The third rule is that you must test this on a real device not in the simulator.
More can be found in my blogpost on the subject.
Edit: Using a UIActivityViewController
Code for using UIActivityViewController instead of UIDocumentInteractionController
import UIKit
class ViewController: UIViewController {
// UIDocumentInteractionController instance is a class property
var activityController: UIActivityViewController!
#IBOutlet weak var shareButton: UIBarButtonItem!
// called when bar button item is pressed
#IBAction func shareStuff(sender: AnyObject) {
if let barButton = sender as? UIBarButtonItem {
self.presentViewController(activityController, animated: true, completion: nil)
let presCon = activityController.popoverPresentationController
presCon?.barButtonItem = barButton
}
else {
print("not a bar button!")
}
}
override func viewDidLoad() {
super.viewDidLoad()
// retrieve URL to file in main bundle
if let img = UIImage(named:"MyImage.jpg") {
// Instantiate the interaction controller
activityController = UIActivityViewController(activityItems: [img], applicationActivities: nil)
}
else {
shareButton.enabled = false
print("file missing!")
}
}
}
You can also add custom activities to the UIActivityViewController and here is code for adding an "Open In..." button to a UIActivityViewController so that you can switch to a UIDocumentInteractionController from a UIActivityViewController.
I did the same code for saving a PDF file from a URL (whether it's a local URL in your device storage, or it's a URL from somewhere on the internet)
Here is the Code for Swift 3 :
#IBOutlet weak var pdfWebView: UIWebView!
#IBOutlet weak var shareBtnItem: UIBarButtonItem!
var pdfURL : URL!
var docController : UIDocumentInteractionController!
then in viewDidLoad()
// retrieve URL to file in main bundle`
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("YOUR_FILE_NAME.pdf")
//Instantiate the interaction controller`
self.docController = UIDocumentInteractionController(url: fileURL)`
and in your barButtonItem tapped method (which I have called openIn(sender)):
#IBAction func openIn(_ sender: UIBarButtonItem)
{
// present UIDocumentInteractionController`
docController.presentOptionsMenu(from: sender, animated: true)
}
FYI: You need a webView in your storyboard if you wish to show the pdf file as well
Hope this helps.