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.
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
}
}
I need to create WebAR using iPhone 12's LiDAR sensor.
Is that possible to get permission or API to access it?
Kindly suggest me the good reference for my requirement.
AR QuickLook content implementation
In 2019 Apple released AR Quick Look framework allowing you to create a web-based Augmented Reality experience browsing Safari. QuickLook is based on RealityKit engine, it's easy to implement and conveniently to use. It automatically uses LiDAR Scanner if your iPhone has it. If there's no LiDAR Scanner on-board, it runs regular plane detection feature.
Here's a Swift sample code for native Xcode project:
import ARKit
import QuickLook
extension ViewController: QLPreviewControllerDelegate,
QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController,
previewItemAt index: Int) -> QLPreviewItem {
guard let path = Bundle.main.path(forResource: "file", ofType: "usdz")
else { fatalError("Couldn't find a model") }
let url = URL(fileURLWithPath: path)
return url as QLPreviewItem
}
}
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let previewController = QLPreviewController()
previewController.delegate = self
previewController.dataSource = self
self.present(previewController, animated: true, completion: nil)
}
}
Web AR content implementation
To activate your USDZ model through web resource, use the following HTML tags:
<div>
<a rel="ar" href="/assets/models/bicycle.usdz">
<img src="/assets/models/bicycle-image.jpg">
</a>
</div>
You cannot access the LiDAR scanner's parameters when you enable AR Quick Look via iOS Safari. If your iPhone has a LiDAR on-board, it will be used automatically.
I am trying to reproduce what Apple does with Safari, where there is a sharing button that allows to share the current page but also provides some extra (and internal) options to do with the page.
For example, we are given options to Add to the reading list, add as bookmark, etc.
These options are exclusive to Safari, as they are not part of the default Share Sheet. How can I hide some of my own app's functionality under a Share Sheet like that?
I did some research and I only found out about custom activities which (as I understand) are the squared buttons that are in the second row of the share sheet (partially hidden in my screenshot).
I also found out about extensions, but I don't think that helps my case, as that allows to customize the share sheet globally for every app, and I need to add options in my app's runtime.
When you initialise a share sheet (aka UIActivityController), you are asked for an array of applicationActivities, that is where your custom internal/share actions will go.
You should just subclass UIActivity, and override the required members as specified by the documentation:
activityType
activityTitle
activityImage
canPerform(withActivityItems:)
prepare(withActivityItems:)
activityCategory
To make your UIActivity appear as a menu button at the bottom of the share sheet, make sure you return .action for activityCategory. If you return .share, it will appear as a square button at the top.
Example:
// dummy implementation
class Foo: UIActivity {
override var activityTitle: String? { "Foo" }
override var activityType: UIActivity.ActivityType? { UIActivity.ActivityType("Foo") }
override var activityImage: UIImage? { UIImage(systemName: "doc.on.doc.fill") }
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
true
}
override class var activityCategory: UIActivity.Category { .action }
override func prepare(withActivityItems activityItems: [Any]) {
print("Preparing Foo!")
}
override func perform() {
print("Performed Foo!")
}
}
...
let shareSheet = UIActivityViewController(activityItems: [stuffToShare], applicationActivities: [Foo()])
Output:
If I use .share instead:
I need to set a number of files that can be selected by enabling the UIDocumentPickerViewController allowsMultipleSelection, but I didn't find any properties that I could use to set this.
It is possible?
In order to do this, you'll need to implement UIDocumentPickerDelegate's didPickDocumentsAt. It would look something like this:
class YourViewController: UIDocumentPickerViewController {
let maxDocs = 3
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension YourViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
// check to make sure you haven't hit your document cap specified above
guard urls.count < maxDocs else { return }
// if you pass the guard, business as usual
}
}
I haven't worked with this class before, so there may be some rough edges, but that's the physics for poets way of how you'd do it. You may need to refine it a bit, as I'm not sure where the URLs for the delegate method come from. You could throw in a breakpoint when this method is called and do po urls to see what's in there.
In looking at the delegate methods available for this class, I don't see one for selecting an individual document, so you'll need to tinker around to see what happens with the array of URLs that's a parameter in the delegate method and figure out how much bookkeeping you need to do to handle state toggling between selected and !selected.
You'll need to use :
UIDocumentBrowserViewController
instead of UIDocumentPickerViewController. It will allow you to select multiple items :
let document = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: ["public.text", "com.apple.iwork.pages.pages", "public.data"])
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
print("result.........\(documentURLs)")
}
override func viewDidLoad() {
super.viewDidLoad()
document.delegate = self
document.allowsPickingMultipleItems = true
}
And don't forget to add UIDocumentBrowserViewControllerDelegate to your VC.
I have two UITableViewController, the first one:
protocol FetchUserProfileData {
func getNumberOfRequests()
}
class ListEvents: UITableViewController{
var fetchInfo:FetchUserProfileData?
func getNumberOfRequests() -> Int{
return 12
}
and the UIViewController:
class UserProfileDetails:UIViewController, FetchUserProfileData {
var listEvents: UserListEvents?
func getNumberOfRequests(){
}
override func viewDidLoad(){
listEvents?.fetchInfo = self
print(listEvents?.getNumberOfRequests())
and this line: print(listEvents?.getNumberOfRequests()) gives me a nil value instead of 12... What's wrong here?
---- edit
Ok, now I see that listEvents is empty... So my question is how can I pass that data from ListEvents to UserProfileDetails?
In this code, listEvents is probably nil.
But, the way you use the protocol looks odd to me. I would expect:
getNumberOfRequests in the protocol to return Int
ListEvents should be implementing the protocol, not UserProfileDetails
The empty getNumberOfRequests() in UserProfileDetails should be deleted
You did not set listEvents. When you are using story boards then you should set the fetchInfo not earlier than in (overwriting) prepareForSegue. Google for examples, the web is full of them. When you segue programmatically then you can set the property not before you actually instanticated the new view controller. You are better of using listEvents!.fetchInfo = self because in that case you'll get an exception when listEvents is nil.
I made some change your code and this will pass data from ListEvents to UserProfileDetails.
protocol FetchUserProfileDelegate {
func getNumberOfRequests()->Int
}
class ListEvents: UITableViewController,FetchUserProfileDelegate{
var userProfile: UserProfileDetails?
override func viewDidLoad() {
userProfile = UserProfileDetails()
userProfile?.delegate = self
}
// MARK: FetchUserProfileDelegate
func getNumberOfRequests() -> Int{
return 12 // return as your target Int
}
}
class UserProfileDetails:UIViewController {
var delegate:FetchUserProfileDelegate?
override func viewDidLoad() {
if let _ = delegate{
let resultInt = delegate?.getNumberOfRequests() // get the Int form ListEvents
print(resultInt)
}
}
}
The idea of moving data from one controller to another is very common. Most of the time this is done using a segue. A controller can have a function called prepareForSegue. This function gets called before the transition happens. Inside the prepareForSegue function, the system gives you destination controller object. You take that object and set your data in it. When the transition happens, and your destination controller comes up, it already has the data you want to give to it.
Use Xcode and make a new project. Choose "Master-Detail Application". This will generate the code for you and it is a good example of how to pass data between controllers.