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
}
}
Related
I trying implement Rewarded Ad - Rewarded Ads New APIs (Beta). Video is load and isReady property is changing to true in a couple of seconds.
I have a button on which user press and Rewarded Video appear
This is function which is fire when user press on button
func presentRewardAd(from viewController: UIViewController) {
if rewardedAd.isReady {
rewardedAd.present(fromRootViewController: viewController, delegate: self)
}
}
The problem is
I want to hide button until video isReady == true, and when it's ready show button. So i want to get notify when rewardedAd.isReady is changing.
What i try so far:
class CustomRewardAd: GADRewardedAd {
private var observation: NSKeyValueObservation?
override init(adUnitID: String) {
super.init(adUnitID: adUnitID)
observation = observe(\.isReady, options: [.old, .new]) { object, change in
print("isReady changed from: \(change.oldValue!), updated to: \(change.newValue!)")
}
}
}
Also i tried this Using Key-Value Observing in Swift but same result.
But changeHandler never gets called. Am i doing something wrong?
Thanks!
I found solution, not ideal but it's works! Maybe this is can help someone in future.
When new rewarded request finishes, isReady property is set to true or false depends what response is.
private func createAndLoadRewardedAd() -> GADRewardedAd {
let rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
rewardedAd.load(GADRequest()) { [weak self] error in
guard let self = self else { return }
self.videoIsReady?(rewardedAd.isReady) // already set
}
return rewardedAd
}
You are welcome!
I'm trying to add an intro sequence to my code so that if it's the first time the app is opened, the user can enter some basic information (which I can then store in UserDefaults).
The way that I was thinking of doing this is by having a variable called isFirstTime which is initially set to true. Every time the app is opened, it'll check if there is a value for isFirstTime in UserDefaults. If it isn't there, it'll trigger the View Controller that has my intro sequence to appear. Once the intro sequence is finished, isFirstTime will be set to false and then stored in UserDefaults.
Is this a correct implementation, or is there a more efficient way?
EDIT: If anyone is interested, this is the code I used to implement my intro sequence. I first assign a boolean variable outside of my View Controller that keeps track of whether it's the first time opening the app or not.
var isFirstTime = true
Then, in my ViewDidAppear (it does not work in the ViewDidLoad method), I added this code which checks whether or not I already have a UserDefault for my isFirstTime variable. If yes, I then execute the rest of my program, but if not, I start up my intro sequence's View Controller.
if UserDefaults.standard.object(forKey: "isFirstTime") != nil{
// Not the first time app is opened
isFirstTime = false // I use isFirstTime elsewhere in my code too.
} else {
let introVC = self.storyboard?.instantiateViewController(withIdentifier: "intro")
self.present(introVC!, animated: false, completion: nil)
}
In my intro sequence View Controller, when I am done with my gathering the user's basic information, I do two things: the first is changing the value of isFirstTime and setting it as a UserDefault, and the second is dismissing the View Controller.
isFirstTime = false
UserDefaults.standard.set(isFirstTime, forKey: "isFirstTime")
dismiss(animated: false, completion: nil)
You can achieve it easily. This is code which I have used for it.
Step 1 First create a file called UserDefaultManager.swift
import UIKit
// User Defaults Manager Constants
let kIsFirstTimeLaunch = "IsFirstTimeLaunch"
class UserDefaultsManager: NSObject {
// MARK: Setter Methods
class func setIsFirstTimeLaunch(flag: Bool) {
NSUserDefaults.standardUserDefaults().setBool(flag, forKey:kIsFirstTimeLaunch)
NSUserDefaults.standardUserDefaults().synchronize()
}
// MARK: Getter Methods
class func isFirstTimeLaunch() -> Bool {
return NSUserDefaults.standardUserDefaults().boolForKey(kIsFirstTimeLaunch)
}
// MARK: Reset Methods
class func resetIsFirstTimeLaunch() {
NSUserDefaults.standardUserDefaults().removeObjectForKey(kIsFirstTimeLaunch)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
Step 2: In your Implementation file check it like below :
if(!UserDefaultsManager.isFirstTimeLaunch()) {
// Your code here.
let introVC = self.storyboard?.instantiateViewController(withIdentifier: "intro")
self.present(introVC!, animated: false, completion: nil)
// Update value in user defaults
UserDefaultsManager.setIsFirstTimeLaunch(true)
}
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
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.
Is there a way to extend the Quick Look Framework on iOS to handle an unknown file type like on Mac? I don't want to have to switch to my app to preview the file, much like viewing image files in email or iMessage. I would like to remove the step of having to select what app to use to open the file.
On Mac they call it a Quick Look Generator, but I can't find a way to do it on iOS
This is how you use Quick Look Framework in iOS
Xcode 8.3.2. Swift 3
First goto Build Phases and add new framework QuickLook.framework under Link Binary with Libraries.
Next import QuickLook in your ViewController Class
Next set delegate method of QuickLook to your ViewController class to access all the methods of QUickLook.framework (see below).
class ViewController: UIViewController , QLPreviewControllerDataSource {
}
Next create instance of QLPreviewController in your class as below:
let quickLookController = QLPreviewController()
Now set datasource in your viewdidload method:
override func viewDidLoad() {
super.viewDidLoad()
quickLookController.dataSource = self
}
Now create an fileURLs array to store all the documents path which you need to pass later to QLPreviewController via delegate methods.
var fileURLs = [URL]()
Now add below methods to your class to tell QLPreviewController about your total number of documents.
func numberOfPreviewItemsInPreviewController(controller: QLPreviewController) -> Int {
return fileURLs.count
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return fileURLs[index] as QLPreviewItem
}
#available(iOS 4.0, *)
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return fileURLs.count
}
Finally the method which shows your docs. You can also check if the type of document you want to Preview is possible to preview or not as below.
func showMyDocPreview(currIndex:Int) {
if QLPreviewController.canPreview(fileURLs[currIndex] as QLPreviewItem) {
quickLookController.currentPreviewItemIndex = currIndex
navigationController?.pushViewController(quickLookController, animated: true)
}
}
For now, if you want to show a preview of a file of a type not handled by the standard QLPreviewController, you have to write something yourself in your own app. You cannot write a custom Quick Look plugin like you can on the Mac.