How to add extra options to iOS Share Sheet like Safari does - ios

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:

Related

Supporting Suggestions in Share Extension doesn't work

I am working on feature that will add my conversations to share sheet menu when user wants to share files
I did exactly what Apple said in their guide: https://developer.apple.com/documentation/foundation/app_extension_support/supporting_suggestions_in_your_app_s_share_extension
But when I am pressing on one of my conversations in apple share screen, I want to get intent from share extension ViewController, my intent is nil
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let intent = self.extensionContext?.intent as? INSendMessageIntent {
} else {
//intent is nil
}
}
I have added INSendMessageIntent to info.plist
enter image description here
My issue is exactly as this: iOS ShareContext tapping on Suggestion Intent property of extensionContext is nil

Using Markup via UIDocumentInteractionController

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
}
}

Good strategy for replacing parts of functionality in iOS ViewControllers

I have VCs in an iOS app which have quite a lot of UI controls. I would now need to replace or "mock" some of these controls when in a specific state. In some cases this would be just disabling button actions, but in some cases the actions that happen need to be replaced with something completely different.
I don't really like the idea of having this sort of check littered all around the codebase.
if condition {
...Special/disabled functionality
} else {
...Normal functionality
}
In Android, I can just subclass each Fragment/Activity and build the functionality there, and then doing the if/else when inserting Fragments or launching activities.
But on iOS with Storyboards/IBActions and Segues, UIs and VCs are really tightly coupled. You either end up duplicating UI views or adding a lot of finicky code to already large VCs.
What would be the best way to handle this in iOS?
Sample code of what I want to avoid doing:
//Before:
class SomeViewController : UIViewController {
#IBAction onSomeButton() {
checkSomeState()
doANetworkRequest(() -> {
someCompletionHandler()
updatesTheUI()
}
updateTheUIWhileLoading()
}
#IBAction onSomeOtherButton() {
checkAnotherState()
updateUI()
}
}
//After:
class SomeViewController : UIViewController {
#IBAction onSomeButton() {
if specialState {
doSomethingSimpler()
} else {
checkSomeState()
doANetworkRequest(() -> {
someCompletionHandler()
updatesTheUI()
}
updateTheUIWhileLoading()
}
}
#IBAction onSomeOtherButton() {
if specialState {
return // Do nothing
} else {
checkAnotherState()
updateUI()
}
}
}
I'd suggest using the MVVM (Model - View - ViewModel) pattern. You pass the ViewModel to your controller and delegate all actions to it. You can also use it to style your views and decide if some of them should be hidden or disabled, etc.
Let's image a shopping app in which your pro users get a 10% discount and can use a free-shipping option.
protocol PaymentScreenViewModelProtocol {
var regularPriceString: String { get }
var discountedPriceString: String? { get }
var isFreeShippingAvailable: Bool { get }
func userSelectedFreeShipping()
func buy()
}
class StandardUserPaymentScreenViewModel: PaymentScreenViewModelProtocol {
let regularPriceString: String = "20"
let discountedPriceString: String? = nil
let isFreeShippingAvailable: Bool = false
func userSelectedFreeShipping() {
// standard users cannot use free shipping!
}
func buy() {
// process buying
}
}
class ProUserPaymentScreenViewModel: PaymentScreenViewModelProtocol {
let regularPriceString: String = "20"
let discountedPriceString: String? = "18"
let isFreeShippingAvailable: Bool = true
func userSelectedFreeShipping() {
// process selection of free shipping
}
func buy() {
// process buying
}
}
class PaymentViewController: UIViewController {
#IBOutlet weak var priceLabel: UILabel!
#IBOutlet weak var discountedPriceLabel: UILabel!
#IBOutlet weak var freeShippingButton: UIButton!
var viewModel: PaymentScreenViewModelProtocol
override func viewDidLoad() {
super.viewDidLoad()
priceLabel.text = viewModel.regularPriceString
discountedPriceLabel.text = viewModel.discountedPriceString
freeShippingButton.isHidden = !viewModel.isFreeShippingAvailable
}
#IBAction func userDidPressFreeShippingButton() {
viewModel.userSelectedFreeShipping()
}
#IBAction func userDidPressBuy() {
viewModel.buy()
}
}
This approach let's you decouple your logic from your views. It's also easier to test this logic.
One thing to consider and decide is the approach as to how to inject the view model into the view controller. I can see three possibilities :
Via init - you provide a custom initializer requiring to pass the view model. This will mean you won't be able to use segue's or storyboards (you will be able to use xibs). This will let your view model be non-optional.
Via property setting with default implementation - if you provide some form of default/empty implementation of your view model you could use it as a default value for it, and set the proper implementation later (for example in prepareForSegue). This enables you to use segues, storyboards and have the view model be non-optional (it just adds the overhead of having an extra empty implementation).
Via property setting without default implementation - this basically means that your view model will need to be an optional and you will have to check for it almost everytime you access it.

How to print a UIWebView via UIActivityViewController?

I have a view controller containing a UIWebView and a toolbar with an action/share button. This initializes and presents a UIActivityViewController object. Depending on whether I supply the activityItems parameter with either the web view's URL or the URL's corresponding absoluteString, different actions are offered, but the Print option is never shown (nor offered in the "more" section).
I do know how to print the web view contents explicitly using UIPrintInfo and UIPrintInteractionController, but that would be a separate toolbar button whereas I want to simply include the system's Print option into the activity button row. I assume printing a web view does not need any explicit coding.
What can I do?
You can create Custom Activity For UIActivityCotnroller like this,
import UIKit
protocol CustomActivityDelegate : NSObjectProtocol
{
func performActionCompletion(actvity: CustomActivity)
}
class CustomActivity: UIActivity {
var delegate: CustomActivityDelegate?
override class var activityCategory: UIActivityCategory {
return .action
}
override var activityType: UIActivityType? {
guard let bundleId = Bundle.main.bundleIdentifier else {return nil}
return UIActivityType(rawValue: bundleId + "\(self.classForCoder)")
}
override var activityTitle: String? {
return "You title"
}
override var activityImage: UIImage? {
return <Your activity image >
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
return true
}
override func prepare(withActivityItems activityItems: [Any]) {
//
}
override func perform() {
self.delegate?.performActionCompletion(actvity: self)
activityDidFinish(true)
}
}
You can initialize this activity some thing like this
let customActivity = CustomActivity()
customActivity.delegate = self
And you can add this custom activity while preparing UIActivityController
let activityViewController : UIActivityViewController = UIActivityViewController(activityItems: [customActivity], applicationActivities: nil)
and you will also need to implement the call back method
func performActionCompletion(actvity: CustomActivity)
{
//Perform you task
}
Note : This is just pseudo code, might contain error or syntax problems

Quick Look Generator iOS

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.

Resources