I have an iOS app which creates a PDF and is shared using the UIActivityViewController using the default share functionality. I had to block a few sharing facilities like Vimeo, Facebook, Twillio, etc. and also the file should not be stored on the device.
With the new iOS 11 version, Apple has provided SaveToFiles option with the default share functionality. I tried using excludedActivityTypes, and with blocking the "com.apple.CloudDocsUI.AddToiCloudDrive" option, but no success.
Can anyone help me to disable the SaveToFile option though Swift? I am using Xcode 9.3 and Swift 4.
Currently, we probably cannot disable Save to Files or Add to Shared Album by add excludedActivityTypes in activityViewController. But we can prevent when user did press two activity types will do not perform action instead we'll showing alert.
First we create custom UIActivityItemProvider
class ActivityItemProvider: UIActivityItemProvider {
override func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType) -> Any? {
// in here we'll check activityType = "com.apple.CloudDocsUI.AddToiCloudDrive" (Save to Files),
// activityType = "com.apple.mobileslideshow.StreamShareService" (Shared Album)
if(activityType.rawValue.contains("com.apple.CloudDocsUI.AddToiCloudDrive") || (activityType.rawValue.contains("com.apple.mobileslideshow.StreamShareService") {
// dismiss activityViewController first
activityViewController.dismiss(animated: true, completion: nil)
// show alert controller, we can using UIApplication.shared.keyWindow?.rootViewController to present alert
return nil
}
return self.placeholderItem
}
In where we call UIActivityController, let's using
let item = ActivityItemProvider.init(placeholderItem: {your item})
let activityViewController = UIActivityViewController.init(activityItems: item, applicationActivities: nil)
ObjectiveC version
#import "BRActivityItemProvider.h"
#import "UIViewController+Additions.h"
#implementation BRActivityItemProvider
- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType {
[super activityViewController:activityViewController itemForActivityType:activityType];
NSLog(#"itemForActivityType %#", activityType);
if([activityType containsString:#"com.apple.CloudDocsUI.AddToiCloudDrive"] || [activityType containsString:#"com.apple.mobileslideshow.StreamShareService"]) {
[activityViewController dismissViewControllerAnimated:true completion:^{
UIViewController *rootController = [UIApplication sharedApplication].keyWindow.rootViewController;
if(rootController != nil) {
NSString *str = [activityType containsString:#"com.apple.CloudDocsUI.AddToiCloudDrive"]
? #"Cannot save file to iCloud Driver"
: #"Cannot save file to Shared Album";
dispatch_async(dispatch_get_main_queue(), ^{
// this's my custom show alert controller, you can change your own
[rootController showAlertController:str action:^(UIAlertAction * _Nonnull action) {
}];
});
}
}];
return nil;
}
return self.placeholderItem;
}
In where call UIActivityViewController
BRActivityItemProvider *itemProvider = [[BRActivityItemProvider alloc]initWithPlaceholderItem:item];
UIActivityViewController *activityController = [[UIActivityViewController alloc]initWithActivityItems:itemProvider applicationActivities:nil];
activityController.popoverPresentationController.sourceView = controller.view;
// your logic code
// ....
Related
I'm trying to add a custom UIActivity to a UIActivityController. When I click on the item, it presents the view controller I want it to, but when I finish with that view controller, the original UIActivityViewController is still there. My question is, how and where do I dismiss the activity view controller? This is the code in my custom UIActivity.
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems{
self.activityTitle = #"Text Editor";
self.activityType = #"TextEdit";
self.activityImage = [UIImage imageNamed:#"Icon.png"];
if (activityItems) return YES;
else return NO;
}
- (void)prepareWithActivityItems:(NSArray *)activityItems{
for (NSString *path in activityItems) {
if ([path lastPathComponent]) {
self.file = path;
}
}
}
- (UIViewController *)activityViewController{
ACTextEditController *actec = [[ACTextEditController alloc] initWithFile:_file];
return actec;
}
EDIT
I've tried doing this, and I know it is called because I tried logging something in it and it was called
- (void)activityDidFinish:(BOOL)completed{
if (completed) {
[self.activityViewController dismissViewControllerAnimated:YES completion:nil];
}
}
however, it's still there when the view controller dismisses. Why?
Here's a working example:
http://www.apeth.com/iOSBook/ch26.html#_activity_view
Notice that we end up by calling activityDidFinish:. That is what tears down the original interface. It may be that that is the step you are omitting. If not, just compare what you're doing with what I'm doing, since what I'm doing works (on iPhone).
I've found a few questions about how to make a UIPageViewController jump to a specific page, but I've noticed an added problem with jumping that none of the answers seem to acknowledge.
Without going into the details of my iOS app (which is similar to a paged calendar), here is what I'm experiencing. I declare a UIPageViewController, set the current view controller, and implement a data source.
// end of the init method
pageViewController = [[UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:nil];
pageViewController.dataSource = self;
[self jumpToDay:0];
}
//...
- (void)jumpToDay:(NSInteger)day {
UIViewController *controller = [self dequeuePreviousDayViewControllerWithDaysBack:day];
[pageViewController setViewControllers:#[controller]
direction:UIPageViewControllerNavigationDirectionForward
animated:YES
completion:nil];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSInteger days = ((THDayViewController *)viewController).daysAgo;
return [self dequeuePreviousDayViewControllerWithDaysBack:days + 1];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
NSInteger days = ((THDayViewController *)viewController).daysAgo;
return [self dequeuePreviousDayViewControllerWithDaysBack:days - 1];
}
- (UIViewController *)dequeuePreviousDayViewControllerWithDaysBack:(NSInteger)days {
return [[THPreviousDayViewController alloc] initWithDaysAgo:days];
}
Edit Note: I added simplified code for the dequeuing method. Even with this blasphemous implementation I have the exact same problem with page order.
The initialization all works as expected. The incremental paging all works fine as well. The issue is that if I ever call jumpToDay again, the order gets jumbled.
If the user is on day -5 and jumps to day 1, a scroll to the left will reveal day -5 again instead of the appropriate day 0. This seems to have something to do with how UIPageViewController keeps references to nearby pages, but I can't find any reference to a method that would force it to refresh it's cache.
Any ideas?
Programming iOS6, by Matt Neuburg documents this exact problem, and I actually found that his solution feels a little better than the currently accepted answer. That solution, which works great, has a negative side effect of animating to the image before/after, and then jarringly replacing that page with the desired page. I felt like that was a weird user experience, and Matt's solution takes care of that.
__weak UIPageViewController* pvcw = pvc;
[pvc setViewControllers:#[page]
direction:UIPageViewControllerNavigationDirectionForward
animated:YES completion:^(BOOL finished) {
UIPageViewController* pvcs = pvcw;
if (!pvcs) return;
dispatch_async(dispatch_get_main_queue(), ^{
[pvcs setViewControllers:#[page]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO completion:nil];
});
}];
So I ran into the same problem as you where I needed to be able to 'jump' to a page and then found the 'order messed up' when I gestured back a page. As far as I have been able to tell, the page view controller is definitely caching the view controllers and when you 'jump' to a page you have to specify the direction: forward or reverse. It then assumes that the new view controller is a 'neighbor' to the previous view controller and hence automagically presents the previous view controller when you gesture back. I found that this only happens when you are using the UIPageViewControllerTransitionStyleScroll and not UIPageViewControllerTransitionStylePageCurl. The page curl style apparently does not do the same caching since if you 'jump' to a page and then gesture back it delivers the pageViewController:viewController(Before/After)ViewController: message to the data source enabling you to provide the correct neighbor view controller.
Solution:
When performing a 'jump' to page you can first jump to the neighbor page to the page (animated:NO) you are jumping to and then in the completion block of that jump, jump to the desired page. This will update the cache such that when you gesture back, the correct neighbor page will be displayed. The downside is that you will need to create two view controllers; the one you are jumping to and the one that should be displayed after gesturing back.
Here is the code to a category that I wrote for UIPageViewController:
#implementation UIPageViewController (Additions)
- (void)setViewControllers:(NSArray *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction invalidateCache:(BOOL)invalidateCache animated:(BOOL)animated completion:(void (^)(BOOL finished))completion {
NSArray *vcs = viewControllers;
__weak UIPageViewController *mySelf = self;
if (invalidateCache && self.transitionStyle == UIPageViewControllerTransitionStyleScroll) {
UIViewController *neighborViewController = (direction == UIPageViewControllerNavigationDirectionForward
? [self.dataSource pageViewController:self viewControllerBeforeViewController:viewControllers[0]]
: [self.dataSource pageViewController:self viewControllerAfterViewController:viewControllers[0]]);
[self setViewControllers:#[neighborViewController] direction:direction animated:NO completion:^(BOOL finished) {
[mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
}];
}
else {
[mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
}
}
#end
What you can do to test this is create a new 'Page-Based Application' and add a 'goto' button that will 'jump' to a certain calendar month and then gesture back. Be sure to set the transition style to scroll.
I use this function (I'm always in landscape, 2 page mode)
-(void) flipToPage:(NSString * )index {
int x = [index intValue];
LeafletPageContentViewController *theCurrentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSUInteger retreivedIndex = [self indexOfViewController:theCurrentViewController];
LeafletPageContentViewController *firstViewController = [self viewControllerAtIndex:x];
LeafletPageContentViewController *secondViewController = [self viewControllerAtIndex:x+1 ];
NSArray *viewControllers = nil;
viewControllers = [NSArray arrayWithObjects:firstViewController, secondViewController, nil];
if (retreivedIndex < x){
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
} else {
if (retreivedIndex > x ){
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:NULL];
}
}
}
Here is my Swift solution to be used for subclasses of UIPageViewController:
Assume you store an array of viewControllers in viewControllerArray and the current page index in updateCurrentPageIndex.
private func slideToPage(index: Int, completion: (() -> Void)?) {
let tempIndex = currentPageIndex
if currentPageIndex < index {
for var i = tempIndex+1; i <= index; i++ {
self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: {[weak self] (complete: Bool) -> Void in
if (complete) {
self?.updateCurrentPageIndex(i-1)
completion?()
}
})
}
}
else if currentPageIndex > index {
for var i = tempIndex - 1; i >= index; i-- {
self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Reverse, animated: true, completion: {[weak self] (complete: Bool) -> Void in
if complete {
self?.updateCurrentPageIndex(i+1)
completion?()
}
})
}
}
}
Swift version of djibouti33's answer:
weak var pvcw = pageViewController
pageViewController!.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: true) { _ in
if let pvcs = pvcw {
dispatch_async(dispatch_get_main_queue(), {
pvcs.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
})
}
}
It's important to note that this is no longer the case in iOS 10 and you no longer have to use the accepted answer solution. Just continue as always.
I can confirm this issue, and that it only happens when using UIPageViewControllerTransitionStyleScroll and not UIPageViewControllerTransitionStylePageCurl.
Workaround: Make a loop and call UIPageViewController setViewControllers for each page turn, until you reach the desired page.
This keeps the internal datasource index in UIPageViewController in sync.
This is only solution
-(void)buyAction
{
isFromBuy = YES;
APPChildViewController *initialViewController = [self viewControllerAtIndex:4];
viewControllers = [NSArray arrayWithObject:initialViewController];
[self.pageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
-(NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
if (isFromBuy) {
isFromBuy = NO;
return 5;
}
return 0;
}
I had a different approach, should be possible if your pages are designed to be updated after init:
When a manual page is selected I update a flag
- (void)scrollToPage:(NSInteger)page animated:(BOOL)animated
{
if (page != self.currentPage) {
[self setViewControllers:#[[self viewControllerForPage:page]]
direction:(page > self.currentPage ?
UIPageViewControllerNavigationDirectionForward :
UIPageViewControllerNavigationDirectionReverse)
animated:animated
completion:nil];
self.currentPage = page;
self.forceReloadNextPage = YES; // to override view controller automatic page cache
}
}
- (ScheduleViewController *)viewControllerForPage:(NSInteger)page
{
CustomViewController * scheduleViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"CustomViewController"];
scheduleViewController.view.tag = page; // keep track of pages using view.tag property
scheduleViewController.data = [self dataForPage:page];
if (self.currentViewController)
scheduleViewController.calendarLayoutHourHeight = self.currentViewController.calendarLayoutHourHeight;
return scheduleViewController;
}
and then force the the next page to reload with the correct data:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
{
CustomViewController * nextViewController = [pendingViewControllers lastObject];
// When manual scrolling occurs, the next page is loaded from UIPageViewController cache
// and must be refreshed
if (self.forceReloadNextPage) {
// calculate the direction of the scroll to know if to load next or previous page
NSUInteger page = self.currentPage + 1;
if (self.currentPage > nextViewController.view.tag) page = self.currentPage - 1;
nextViewController.data = [self dataForPage:page];
self.forceReloadNextPage = NO;
}
}
If you do not need to animate to the new page, as I didn't, the following code worked for me, called on "Value Changed" in the storyboard. Instead of changing between view controllers, I change the data associated with the current view controller.
- (IBAction)pageControlCurrentPageDidChange:(id)sender
{
self.currentIndex = self.pageControl.currentPage;
MYViewController *currentPageViewController = (MYViewController *)self.pageViewController.viewControllers.firstObject;
currentPageViewController.pageData = [self.pageDataSource dataForPage:self.currentIndex];
[currentPageViewController updateDisplay];
}
currentIndex is there so I can update the pageControl's currentPage when I swipe between pages.
pageDataSource dataForPage: returns an array of data objects that are displayed by the pages.
Here is an up-to-date Swift 3+ version of the answer by #djibouti33 with cleaned-up syntax.
weak var weakPageVc = pageVc
pageVc.setViewControllers([page], direction: .forward, animated: true) { finished in
guard let pageVc = weakPageVc else {
return
}
DispatchQueue.main.async {
pageVc.setViewControllers([page], direction: .forward, animated: false)
}
}
let orderedViewControllers = [UIViewController(),UIViewController(), UIViewController()]
let pageViewController = UIPageViewController()
let pageControl = UIPageControl()
func jump(to: Int, completion: #escaping (_ vc: UIViewController?) -> Void){
guard orderedViewControllers.count > to else{
//index of bounds
return
}
let toVC = orderedViewControllers[to]
var direction: UIPageViewController.NavigationDirection = .forward
if pageControl.currentPage < to {
direction = .forward;
} else {
direction = .reverse;
}
pageViewController.setViewControllers([toVC], direction: direction, animated: true) { _ in
DispatchQueue.main.async {
self.pageViewController.setViewControllers([toVC], direction: direction, animated: false){ _ in
self.pageControl.currentPage = to
completion(toVC)
}
}
}
}
USAGE:
self.jump(to: 5) { (vc) in
// you can do anything for new vc.
}
I was struggling with this issue for a long time myself. For me I had a UIPageViewController (I called it PageController) load from storyboard and on it I add a UIViewController 'ContentVC'.
I let the ContentVC takes care of the data to be loaded on to the content area and let PageController takes care of the sliding/goto/PageIndicator updates. The ContentVC has an ivar CurrentPageIndex and sends that value to PageController so PageController knows which page it's on. In my .m file that has PageController I have these two methods.
Note that I used set to 0 and so every time PageVC reloads it goes to the first page which I don't want, [self viewControllerAtIndex:0].
- (void)setPageForward
{
ContentVC *FirstVC = [self viewControllerAtIndex:[CurrentPageIndex integerValue]];
NSArray *viewControllers = #[FirstVC];
[PageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
This second method is PageViewController's DataSource method. presentationIndexForPageViewController will set the highlighted dot to the right page (the page you want). Note that if we return 0 here the page indicator will highlight the first dot which indicates the first page and we don't want that.
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
return [CurrentPageIndex integerValue];
}
if([MFMessageComposeViewController canSendText])
{
MFMessageComposeViewController *sms_message_vc = [[MFMessageComposeViewController alloc] init];
sms_message_vc.body = text;
sms_message_vc.recipients = recipients;
sms_message_vc.messageComposeDelegate = self;
[self presentModalViewController:sms_message_vc animated:FALSE];
[[UIApplication sharedApplication] setStatusBarHidden:TRUE];
[sms_message_vc release];
}
When this executes there's a delay of several seconds before the compose view is actually shown. What is causing this and how does one go about eliminating the delay?
EDIT 1: Clarification: Making sms_message_vc and ivar doesn't help because the ...alloc] init] process will hang the UI for seconds, regardless of where it is.
EDIT 2: Tried GCD (with different priorities) to attempt to run initialization concurrently. Did not help:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, (unsigned long)NULL), ^(void){
sms_message_vc = [[MFMessageComposeViewController alloc] init];
sms_message_vc.messageComposeDelegate = self;
});
Consider making MFMessageComposeViewController *sms_message_vc a class instance variable and calling:
MFMessageComposeViewController *sms_message_vc = [[MFMessageComposeViewController alloc] init];
earlier, along with setting the delegate to self right after initing sms_message_vc
Then just do:
sms_message_vc.body = text;
sms_message_vc.recipients = recipients;
[self presentModalViewController:sms_message_vc animated:FALSE];
[[UIApplication sharedApplication] setStatusBarHidden:TRUE];
[sms_message_vc release];
When you want to actually send the message. This shouldn't change it too much but might help some.
I have the same problem.
I tried to cache the controller in a static variable. But it did not work. behaved erratically. First time works, second time delegate called automatically without any user action and 3rd time screen goes black. Looks like you have to create the instance after each dismiss!
import Foundation
import UIKit
import MessageUI
class UIUtil {
static var messageController:MFMessageComposeViewController? = nil
static var checkedOnce = false
class func createMessageController () -> MFMessageComposeViewController? {
if checkedOnce {
return messageController
}
checkedOnce = true
if (MFMessageComposeViewController.canSendText()) {
messageController = MFMessageComposeViewController()
messageController?.recipients = [SettingsManager.shared.switchPhoneNumber]
} else {
print("SMS services are not available in this device.")
}
return messageController
}
}
usage,
func createSMSView (text:String) {
print("Sending SMS to \(SettingsManager.shared.switchPhoneNumber). Text: \(text)")
if let ctr = UIUtil.createMessageController() {
ctr.body = text
ctr.messageComposeDelegate = self
self.present(ctr, animated: true, completion: nil)
} else {
print("Could not send SMS. Text: \(text)")
}
}
I am new to Xcode (using 4.3) and am not sure how to save an image to the device's camera roll. All that I have done so far is set up an IBAction for the button to save the image. What library method or function can I use to save an image to the user's camera roll?
You use the UIImageWriteToSavedPhotosAlbum() function.
//Let's say the image you want to save is in a UIImage called "imageToBeSaved"
UIImageWriteToSavedPhotosAlbum(imageToBeSaved, nil, nil, nil);
Edit:
//ViewController.m
- (IBAction)onClickSavePhoto:(id)sender{
UIImageWriteToSavedPhotosAlbum(imageToBeSaved, nil, nil, nil);
}
Here's an answer for iOS8+ using the Photos framework.
Objective-C:
#import <Photos/Photos.h>
UIImage *snapshot = self.myImageView.image;
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:snapshot];
changeRequest.creationDate = [NSDate date];
} completionHandler:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"successfully saved");
}
else {
NSLog(#"error saving to photos: %#", error);
}
}];
Swift:
// Swift 4.0
import Photos
let snapshot: UIImage = someImage
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: snapshot)
}, completionHandler: { success, error in
if success {
// Saved successfully!
}
else if let error = error {
// Save photo failed with error
}
else {
// Save photo failed with no error
}
})
Here's a link to the Apple documentation.
Don't forget to add the appropriate key/value to your info.plist to request permission to access the photo library:
<key>NSCameraUsageDescription</key>
<string>Enable camera access to take photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Enable photo library access to select a photo from your library.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Enable photo library access to save images to your photo library directly from the app.</string>
For reference, you can save videos in a similar manner:
UISaveVideoAtPathToSavedPhotosAlbum(videoPath, nil, nil, nil);
You might want to save a video to upload to Instagram, for example:
// Save video to camera roll; we can share to Instagram from there.
-(void)didTapShareToInstagram:(id)sender {
UISaveVideoAtPathToSavedPhotosAlbum(self.videoPath, self, #selector(video:didFinishSavingWithError:contextInfo:), (void*)CFBridgingRetain(#{#"caption" : caption}));
}
- (void) video: (NSString *) videoPath
didFinishSavingWithError: (NSError *) error
contextInfo: (void *) contextInfoPtr {
NSDictionary *contextInfo = CFBridgingRelease(contextInfoPtr);
NSString *caption = contextInfo[#"caption"];
NSString *escapedString = [videoPath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; // urlencodedString
NSString *escapedCaption = [caption stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; // urlencodedString
NSURL *instagramURL = [NSURL URLWithString:[NSString stringWithFormat:#"instagram://library?AssetPath=%#&InstagramCaption=%#", escapedString, escapedCaption]];
[[UIApplication sharedApplication] openURL:instagramURL];
}
Save the image in photo library in Swift 4
Add "Privacy - Photo Library Additions Usage Description" in info.plist
Then use following code to save image
UIImageWriteToSavedPhotosAlbum(imageView.image!, nil, nil, nil)
Use asImage() to get unique content to save to the camera roll.
If you use asImage() you can save a variety of fun things to the camera roll with just a few lines of code! This can be very powerful if the object has some transparency already incorporated.
asImage() works with UITextView, WKWebView, UIImageView, UIButton, UISlider, UITableView to name some objects (but they may need to be visible when you get the image (having a non zero alpha)). I even use it to capture tiling, but that's loaded into a UIImageView already in my design. I suspect asImage() may work with many more object types too, but I only tried the ones I mentioned.
If it's a UITextView, and you set the background color to .clear then the text gets saved with a transparent background. If your text contains emoji or Memoji, you can now get those images into the camera roll, or into UIImageViews internal to your app. Having Memoji/Emoji with a transparent background in your camera roll where they can be used in any variety of applications is powerful.
Other objects may have some transparency if you crop a photo image to a circle, or set the corner radius to clip off the corners.
Note in my code, pointerToTextObjectSelected is a UITextView
var pointerToTextObjectSelected = UITextView()
// above populated elsewhere
let thisObject = pointerToTextObjectSelected.asImage()
let imageData = thisObject.pngData()
let imageToSave = UIImage(data: imageData!)
UIImageWriteToSavedPhotosAlbum(imageToSave!, nil, nil, nil)
// You will need this extension :
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(self.frame.size)
self.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}
I have seen this kind of question a lot on the internet but it seems no one really knows the answer?
I am using QLPreviewController for displaying PDF documents. I first used a UIWebView but I was recommended to use QLPreviewController instead for performance reasons with bigger documents.
what I want is 4 custom UIBarButtonItem's in the top right (so where the print button is).
I managed to get a custom toolbar at the bottom, but that's not really what I want.
Considering that it is not possible to add custom button at the place of the print button, I still want to remove the printbutton and use the custom toolbar instead.
EDIT (Solution):
I found the solution a while ago but didn't update this post so here is how I solved the problem:
I add al the buttons manually:
// Create a toolbar to have the buttons at the right side of the navigationBar
UIToolbar* toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 180, 44.01)];
[toolbar setTranslucent:YES];
// Create the array to hold the buttons, which then gets added to the toolbar
NSMutableArray* buttons = [[NSMutableArray alloc] initWithCapacity:4];
// Create button 1
button1 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:#selector(button1Pressed)];
[buttons addObject:button1];
// Create button 2
button2 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose target:self action:#selector(button2Pressed)];
[buttons addObject:button2];
// Create button 3
button3 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemBookmarks target:self action:#selector(button3Pressed)];
[buttons addObject:button3];
// Create a action button
openButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:#selector(openWith)];
[buttons addObject:openButton];
// insert the buttons in the toolbar
[toolbar setItems:buttons animated:NO];
// and put the toolbar in the navigation bar
[[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:toolbar]];
I searched for a solution to this problem for months and finally found a way to customize the navigationbar of a QLPreviewController. Previously I was also using UIWebView to display documents as I'm not allowed to display the iOS-share button for certain confidential documents within my app and this is what the QLPreviewController does. However I wanted to have those nice features such as the table of contents with the little previews and stuff. So I looked for a reliable way to get rid of this button. Like you guys I was first looking into customizing the navigationbar of the QLPreviewController. However, as others already pointed out this is absolutely not possible since iOS6. So instead of customizing the existing navigation bar what we need to do is creating an own one and placing it in front of the QL-navigationbar, thus hiding it.
So how to do this?
First of all we need to subclass QLPreviewContoller and overwrite the viewDidAppear method and viewWillLayoutSubviews like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.qlNavigationBar = [self getNavigationBarFromView:self.view];
self.overlayNavigationBar = [[UINavigationBar alloc] initWithFrame:[self navigationBarFrameForOrientation:[[UIApplication sharedApplication] statusBarOrientation]]];
self.overlayNavigationBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[self.view addSubview:self.overlayNavigationBar];
NSAssert(self.qlNavigationBar, #"could not find navigation bar");
if (self.qlNavigationBar) {
[self.qlNavigationBar addObserver:self forKeyPath:#"hidden" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
}
// Now initialize your custom navigation bar with whatever items you like...
UINavigationItem *item = [[UINavigationItem alloc] initWithTitle:#"Your title goes here"];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(doneButtonTapped:)];
item.leftBarButtonItem = doneButton;
item.hidesBackButton = YES;
[self.overlayNavigationBar pushNavigationItem:item animated:NO];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
self.overlayNavigationBar.frame = [self navigationBarFrameForOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
}
qlNavigationBar is the default navigationbar owned by the QLPreviewController, overlayNavigationBar is our custom one which will hide the default one. We also add a key-value observation to the default QL navigationbar to get notified when the default navigation bar gets hidden / reappears. In the viewWillLayoutSubviews method we take care of our custom navigationbar frame.
The next thing we should do is listen for visibility changes of the quicklook navigationbar:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// Toggle visiblity of our custom navigation bar according to the ql navigationbar
self.overlayNavigationBar.hidden = self.qlNavigationBar.isHidden;
}
So now we need to implement methods we need to get the QL navigationbar and one that always gives us the current frame for our custom navigation bar:
- (UINavigationBar*)getNavigationBarFromView:(UIView *)view {
// Find the QL Navigationbar
for (UIView *v in view.subviews) {
if ([v isKindOfClass:[UINavigationBar class]]) {
return (UINavigationBar *)v;
} else {
UINavigationBar *navigationBar = [self getNavigationBarFromView:v];
if (navigationBar) {
return navigationBar;
}
}
}
return nil;
}
- (CGRect)navigationBarFrameForOrientation:(UIInterfaceOrientation)orientation {
// We cannot use the frame of qlNavigationBar as it changes position when hidden, also there seems to be a bug in iOS7 concerning qlNavigationBar height in landscape
return CGRectMake(0.0f, self.isIOS6 ? 20.0f : 0.0f, self.view.bounds.size.width, [self navigationBarHeight:orientation]);
}
- (CGFloat)navigationBarHeight:(UIInterfaceOrientation)orientation {
if([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if(UIInterfaceOrientationIsLandscape(orientation)) {
return self.isIOS6 ? 32.0f : 52.0f;
} else {
return self.isIOS6 ? 44.0f : 64.0f;
}
} else {
return self.isIOS6 ? 44.0f : 64.0f;
}
}
What else? Well of course you need to define properties, remove the observer in dealloc as well as define and set the iOS6 property (there are plenty of examples on the web...). Also you need to customize your navigationbar and listen to the button callbacks. That's it.
I know this is a bit hacky ... hiding / replacing the default QL action button by hiding it beneath another navigationbar ...but well at least it works reliable for me and you don't access private APIs etc.
I tested my solution on all available simulators for iOS 6.0 - 7.0 as well as on iPad 2 & 3, iPhone 4S & 5 (the latter with iOS 7.0 Beta 6 installed).
Update:
This no longer works in iOS 6. Quick Look runs in another process using XPC. See here for more details. I don't foresee any way to customize QLPreviewController. The following answer remains for anyone interested for pre-iOS 6.
I answered an almost identical question the other day here. The question pertained to removing the print button, which isn't too hard. One thing to note about QLPreviewController is that it's not meant to be customized. I have built a subclass of QLPreviewController that can be customized. I've put it here on Github. It's designed to easily remove the action button, among other features too. It wouldn't take much effort at all to replace the button with a custom one.
The biggest thing to watch out for is that the action button is re-added to the navigation bar anytime a new document is displayed. You should notice this in my code. Anytime RBFilePreviewer removes the action button, you just need to re-add your custom buttons. To add your custom buttons, you should create a UIBarButtonItem that holds a custom view with four buttons in it. Then set the right bar button item as the custom UIBarButtonItem you created.
Update:
I've updated RBFilePreviewer to allow you to set a custom right bar button item right out-of-the-box. Just call -setRightBarButtonItem: on RBFilePreviewer and it just works.
I took the response from Lukas Gross and applied it in Swift on iOS 8 and came up with this solution that worked for me:
NOTE: I have the QLPreviewController embedded in a UINavigationController!
Code:
var QLNavigationBar: UINavigationBar?
var overlayNavigationBar: UINavigationBar?
func getQLNavigationBar(fromView view: UIView) -> UINavigationBar? {
for v in view.subviews {
if v is UINavigationBar {
return v as? UINavigationBar
} else {
if let navigationBar = self.getQLNavigationBar(fromView: (v as! UIView)) {
return navigationBar
}
}
}
return nil
}
func handleNavigationBar() {
self.QLNavigationBar = self.getQLNavigationBar(fromView: self.navigationController!.view)
self.overlayNavigationBar = UINavigationBar(frame: CGRectMake(0, 0, self.view.bounds.size.width, 64.0))
self.overlayNavigationBar?.autoresizingMask = UIViewAutoresizing.FlexibleWidth
if let qln = self.QLNavigationBar {
qln.addObserver(self, forKeyPath: "hidden", options: (NSKeyValueObservingOptions.New | NSKeyValueObservingOptions.Old), context: nil)
qln.superview?.addSubview(self.overlayNavigationBar!)
}
var item = UINavigationItem(title: self.navigationItem.title!)
var doneBtn = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: "doneBtnPressed")
item.leftBarButtonItem = doneBtn
item.hidesBackButton = true
self.overlayNavigationBar?.pushNavigationItem(item, animated: false)
self.overlayNavigationBar?.tintColor = .whiteColor()
self.overlayNavigationBar?.barTintColor = .blackColor()
self.overlayNavigationBar?.titleTextAttributes = [
NSForegroundColorAttributeName : UIColor.whiteColor() ]
}
And applying this code like this:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if self.QLNavigationBar!.hidden {
self.overlayNavigationBar?.hidden = self.QLNavigationBar!.hidden
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
self.QLNavigationBar?.superview?.sendSubviewToBack(self.QLNavigationBar!)
if !self.QLNavigationBar!.hidden {
self.overlayNavigationBar?.hidden = self.QLNavigationBar!.hidden
}
})
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.handleNavigationBar()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.overlayNavigationBar?.frame = CGRectMake(0, 0, self.view.bounds.size.width, 64.0)
}
I understand that this answer is a little late for this.
But I really do find a solution for this.
#import "UINavigationItem+Custome.h"
#import <QuickLook/QuickLook.h>
#import <objc/runtime.h>
#implementation UINavigationItem (Custome)
void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL);
- (void) override_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated{
if (item && [item.target isKindOfClass:[QLPreviewController class]] && item.action == #selector(actionButtonTapped:)){
QLPreviewController* qlpc = (QLPreviewController*)item.target;
[self override_setRightBarButtonItem:qlpc.navigationItem.rightBarButtonItem animated: animated];
}else{
[self override_setRightBarButtonItem:item animated: animated];
}
}
+ (void)load {
MethodSwizzle(self, #selector(setRightBarButtonItem:animated:), #selector(override_setRightBarButtonItem:animated:));
}
void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL) {
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod = class_getInstanceMethod(c, overrideSEL);
if (class_addMethod(c, origSEL, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(c, overrideSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}else{
method_exchangeImplementations(origMethod, overrideMethod);
}
}
#end
Add this as a Category and import this into your QLPreviewController's subclass and just call the
self.navigationItem.rightBarButtonItem = nil;//something you want
It works for me.
I learn this from http://nshipster.com/method-swizzling/
and thoughts from http://codego.net/507056/
Good luck, guys.
By mixing a bit out of the existing answers/comments I was able to get this working for my use case: I needed to display files inside a UINavigationController and keep the ability of hiding/showing the UINavigationBar when the file content is tapped
Based on the answer from Lukas Gross and the comment from nacross here's what I ended up doing:
Add a (subclass of) QLPreviewController as a child view controller. This will show two navigation bars: one for the main navigation controller and one from the QLPreviewController
Set up a top constraint from the container view to the top layout guide (named containerTop in the code)
Set this constraint to a negative value, equal to the UINavigationBar plus the status bar, so that the QLPreviewController's UINavigationBar remains hidden under the main UINavigationBar.
Using KVO, monitor the hidden property of the UINavigationBar so that we can (1) hide/show our main UINavigationBar and (2) reset the top constraint
I ended up with something like this:
var qlNavigationBar: UINavigationBar?
func getQLNavigationBar(fromView view: UIView) -> UINavigationBar? {
for v in view.subviews {
if v is UINavigationBar {
return v as? UINavigationBar
} else {
if let navigationBar = self.getQLNavigationBar(fromView: v) {
return navigationBar
}
}
}
return nil
}
func setObserverForNavigationBar() {
self.qlNavigationBar = self.getQLNavigationBar(fromView: self.view)
if let qln = self.qlNavigationBar {
qln.addObserver(self, forKeyPath: "hidden", options: [NSKeyValueObservingOptions.New, NSKeyValueObservingOptions.Old], context: nil)
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
self.navigationController?.setNavigationBarHidden(self.qlNavigationBar!.hidden, animated: true)
self.containerTop.constant = self.qlNavigationBar!.hidden ? self.getStatusBarHeight() * -1 : self.getFullNavigationBarHeight() * -1
UIView.animateWithDuration(0.5) {
self.view.layoutIfNeeded()
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated);
self.setObserverForNavigationBar()
self.containerTop.constant = self.getFullNavigationBarHeight() * -1
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated);
if let qln = self.qlNavigationBar {
qln.removeObserver(self, forKeyPath: "hidden")
}
}
func getFullNavigationBarHeight() -> CGFloat {
if let nav = self.navigationController {
return nav.navigationBar.frame.origin.y + nav.navigationBar.frame.size.height
}
return 0
}
func getStatusBarHeight() -> CGFloat {
return UIApplication.sharedApplication().statusBarFrame.size.height
}
The animations might need a little tweaking and it is hacky, but it's better than not having this possibility.
It should be possible to adapt this strategy to other scenarios without the UINavigationController
Note: If you have a crash when implementing the container view for the QLPreviewController from a storyboard, subclass the QLPreviewController and implement the initializer:
class MyPreviewController: QLPreviewController {
required init?(coder aDecoder: NSCoder) {
super.init(nibName: nil, bundle: nil)
}
}