I created a custom UIMenuController in a UIWebView but it seems to get rid of the "Speak Selection" option in the UIMenuController after that. The speak selection option is turned on in Preferences on all test devices and it appears in other apps, including non-Apple apps. Is there an accessibility service or part of the sharedMenuController that I can call to get this item?
UIMenuItem *copyMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(#"Copy", #"Copy menu item") action:#selector(myappCopy:)];
UIMenuItem *highlightMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(#"Highlight", #"Highlight menu option") action:#selector(myappHighlight:)];
UIMenuItem *unhighlightMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(#"Remove Highlight", #"Remove Highlight menu option")
action:#selector(myappRemoveHighlight:)];
UIMenuItem *noteMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(#"Note", #"Note menu options") action:#selector(myappNote:)];
[UIMenuController sharedMenuController].menuItems = [NSArray arrayWithObjects:copyMenuItem, highlightMenuItem, unhighlightMenuItem, noteMenuItem, nil];
[copyMenuItem release];
[highlightMenuItem release];
[unhighlightMenuItem release];
[noteMenuItem release];
I even tried to parse the existing shared menu items at the start, but I don't see anything dumped in the log. The method is getting called on app launch.
Tried this at top of method:
for (UIMenuItem *menuItem in [UIMenuController sharedMenuController].menuItems) {
NSLog(#"title: %#", menuItem.title);
NSLog(#"action: %#", menuItem.action);
}
Any help is much appreciated! Thanks - Eric
I've found some interesting things with this bug. Basically, when speak selection is enabled, after you make the first selection the UIMenuController is emptied of menuItems. The solution, though hacky, is simple.
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
NSString *selectorString = NSStringFromSelector(action);
BOOL isAccessibilitySelector = [selectorString isEqualToString:#"_accessibilitySpeak:"] || [selectorString isEqualToString:#"_accessibilityPauseSpeaking:"];
if (isAccessibilitySelector && [super canPerformAction:action withSender:sender]) {
//(re)add menuItems to UIMenuController
return YES;
}
return NO;
}
I should note that you must re-add the menuItems after their canPerformAction...() has been called.
I have submitted this as radar:12931434. Update: DUP'ed to 13060693.
Some of the UIMenuController items can be found in UIResponder.h in UIKit framework.
#interface NSObject(UIResponderStandardEditActions) // these methods are not implemented in NSObject
- (void)cut:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)copy:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)paste:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)select:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)selectAll:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)delete:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_2);
- (void)makeTextWritingDirectionLeftToRight:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0);
- (void)makeTextWritingDirectionRightToLeft:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0);
#end
But there is no speak text option there. It turns out if you override "canPerformAction: WithSelector:", within a subclass of your UIWebView or UITextField as listed below, you will also get a listing of all of the actions sent to self including the UIMenuController options.
// Override
- (BOOL) canPerformAction:(SEL)action withSender:(id)sender
{
NSLog(#"%#",NSStringFromSelector(action));
//if you are customizing your menu, return NO except for your specific selectors
return YES;
}
You'll find several methods that may interest you, including _accessibilitySpeak: and _accessibilityPauseSpeaking: and _define: (please note these three selectors are iOS 5 only). The underscore means that they are private, so also keep in mind that you can't directly call them with the classic [class selector] syntax.
Remember, these are system menuItems, which means Apple will stick them in front of any menu items you add, often leaving your menu items in a second layer accessed by tapping the > arrow. If you want to control the order in which the items are display, and/or mix Apple's system items with your items, you will need to create custom menu items for these actions that call a method in your class like this:
- (void) myAppSpeak: (UIMenuController*) sender
{
[super performSelector:#selector(_accessibilitySpeak:)];
}
Keep in mind that these methods need to be implemented in a subclass of a class that implements these already, such as a sub class of UIWebView....not a subclass of UIWebViewController.
Then inside the controller, or wherever you build your UIMenuController, create the custom button that calls this method. Be sure if you are in a web view, you are referencing an object of type of your subclass, and not the generic webview. Otherwise, it won't work.
UIMenuItem *speakMenuItem = [[UIMenuItem alloc] initWithTitle:#"Speak" action:#selector(myAppSpeak:)];
[UIMenuController sharedMenuController].menuItems = [NSArray arrayWithObjects:speakMenuItem, etc. etc., nil];
Even though you are adding it to the your menu items, it will not appear unless you return YES for the selector in your canPerformAction: WithSelector: in your subclass of your web view or text field. So feel free to add items here that may be circumstantial otherwise. You can use logic in your subclassed view to sort that out.
Related
I have a UIWebView with a contentEditable div in order to implement some kind of rich text editor. I need to trimm the copy & cut options in the UIMenuController that appears in the web view once the user selects any piece of text.
There seems to be a lot of solutions around the web, but for some reason, non of them applies for my scenario.
I've subclassed the UIWebView and implemented the canPerformAction:(SEL)action withSender: to remove the copy and cut, but once the user chooses "Select" or "Select All", a new menu appears, and apparently, the web view does not intercept this action and the canPerform method is not being called.
Is there a way to trimm actions for this cases?
I will adapt another answer of mine for your case.
The canPerformAction: is actually called on the internal UIWebDocumentView instead of the UIWebView, which you cannot normally subclass. With some runtime magic, it's possible.
We create a class which has one method:
#interface _SwizzleHelper : UIView #end
#implementation _SwizzleHelper
-(BOOL)canPerformAction:(SEL)action
{
//Your logic here
return NO;
}
#end
Once you have a web view which you want to control the actions of, you iterate its scroll view's subviews and take the UIWebDocumentView class. We then dynamically make the superclass of the class we created above to be the subview's class (UIWebDocumentView - but we cannot say that upfront because this is private API), and replace the subview's class to our class.
#import "objc/runtime.h"
-(void)__subclassDocumentView
{
UIView* subview;
for (UIView* view in self.scrollView.subviews) {
if([[view.class description] hasPrefix:#"UIWeb"])
subview = view;
}
if(subview == nil) return; //Should not stop here
NSString* name = [NSString stringWithFormat:#"%#_SwizzleHelper", subview.class.superclass];
Class newClass = NSClassFromString(name);
if(newClass == nil)
{
newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0);
if(!newClass) return;
Method method = class_getInstanceMethod([_SwizzleHelper class], #selector(canPerformAction:));
class_addMethod(newClass, #selector(canPerformAction:), method_getImplementation(method), method_getTypeEncoding(method));
objc_registerClassPair(newClass);
}
object_setClass(subview, newClass);
}
I'm using TTLauncherView as a sort of home screen for my app and I only have one page's worth of icons. How can I make it so the TTLauncherView won't let you drag icons to "the next page"? I want to set a maximum number of pages (in this case one.)
(EDIT: long story short, I subclassed beginEditing, see the answer below.)
I see where why it adds an extra page when beginEditing is called, but I don't want to edit the framework code. (That makes it hard to update to newer versions.) I'd also prefer not to subclass and override that one method, if I have to rely on how it's implemented. (I'm not against subclassing or adding a category if it's clean.)
I tried setting scrollView.scrollEnabled to NO in the callback method launcherViewDidBeginEditing in my TTLauncherViewDelegate, but that doesn't work while it's in editing mode and I don't know why.
I tried adding a blocker UIView to the scrollview to intercept the touch events by setting userInteractionEnabled=NO, which works OK. I still have to disable the dragging of TTLauncherItems to the next page somehow.
I also tried setting the contentSize of the scrollview to it's bounds in launcherViewDidBeginEditing, but that didn't seem to work either.
Is there a better way?
Tried to block gestures:
- (void)setLauncherViewScrollEnabled:(BOOL)scrollEnabled {
if (scrollEnabled) {
[self.scrollViewTouchInterceptor removeFromSuperview];
self.scrollViewTouchInterceptor = nil;
} else {
// iter through the kids to get the scrollview, put a gesturerecognizer view in front of it
UIScrollView *scrollView = [launcherView scrollViewSubview];
self.scrollViewTouchInterceptor = [UIView viewWithFrame:scrollView.bounds]; // property retains it
UIView *blocker = self.scrollViewTouchInterceptor;
[scrollView addSubview:scrollViewTouchInterceptor];
[scrollView sendSubviewToBack:scrollViewTouchInterceptor];
scrollViewTouchInterceptor.userInteractionEnabled = NO;
}
}
For reference: TTLauncherView.m:
- (void)beginEditing {
_editing = YES;
_scrollView.delaysContentTouches = YES;
UIView* prompt = [self viewWithTag:kPromptTag];
[prompt removeFromSuperview];
for (NSArray* buttonPage in _buttons) {
for (TTLauncherButton* button in buttonPage) {
button.editing = YES;
[button.closeButton addTarget:self action:#selector(closeButtonTouchedUpInside:)
forControlEvents:UIControlEventTouchUpInside];
}
}
// Add a page at the end
[_pages addObject:[NSMutableArray array]];
[_buttons addObject:[NSMutableArray array]];
[self updateContentSize:_pages.count];
[self wobble];
if ([_delegate respondsToSelector:#selector(launcherViewDidBeginEditing:)]) {
[_delegate launcherViewDidBeginEditing:self];
}
}
I think overriding beginEditing in TTLauncherView is your best bet. Since you'd only really be touching one method (and only a few lines in that method), upgrading it when the time comes shouldn't be too bad. Since that method explicitly adds the extra page, I'm not sure how you'd get around it w/o editing that specific piece of code
As Andrew Flynn suggested in his answer, I was able to make it work by subclassing and overriding the beginEditing method to remove the extra page TTLauncherView adds when it goes into editing mode.
One problem I'm having is I can't figure out how to remove the warning I get for calling the (private) method updateContentSize on my subclass. I tried casting it to id, but that didn't remove the warning. Is it possible?
edit: I was able to remove the warning by using performSelector to send the message to the private class. (I had previously create a category method performSelector:withInt that wraps NSInvocation so that I can pass primitives via performSelector methods, which makes this very convenient.)
// MyLauncherView.h
#interface MyLauncherView : TTLauncherView {
NSInteger _maxPages;
}
#property (nonatomic) NSInteger maxPages;
#end
// MyLauncherView.m
#implementation MyLauncherView
#synthesize maxPages = _maxPages;
- (void)beginEditing {
[super beginEditing];
// ignore unset or negative number of pages
if (self.maxPages <= 0) {
return;
}
// if we've got the max number of pages, remove the extra "new page" that is added in [super beginEditing]
if ([_pages count] >= self.maxPages ) {
[_pages removeLastObject];
[self updateContentSize:_pages.count]; // this has a compiler warning
// I added this method to NSObject via a category so I can pass primitives with performSelector
// [self performSelector:#selector(updateContentSize:) withInt:_pages.count waitUntilDone:NO];
}
}
I'm pretty new to the objective-c language (less than three months) but it is something that i really need to understand.
Suppose there is a controller (in a iOS environment) that manages a table view for input data from the user. The table must have editable cells and some features to make the value selection easier, for example a button that shows a popover with the possible values for a field.
Suppose there is a field to store country names. The popover first shows a list of continents; when the user selects a continent, the controller of the popover must show the countries of the previews selected continent.
Now, this popover appears in many places in the app so it will be nice if I can encapsulate it for later use. What i will expect for this popover is something like this:
...
#protocol MyPopoverDelegate<NSObject> {
-(void)didSelectCountry:(NSString *)countryName;
{
...
MyPopoverController *dataSelector = [[MyPopoverController] alloc] init];
dataSelector.dataType = CountryDataType;
dataSelector.delegate = self;
[dataSelector show];
[dataSelector release];
...
The problem here is the line [dataSelector release] because the code for managing the popover must stay alive until the country is selected. That's means the dataSelector variable must be a property of the caller class and that sucks.
The question then is:
How can i organize situations like this to have a reusable controller?
Thanks
Edited after vodkhang answer:
Ok, that's a good one, but dataSelector still is a property.
What if i do:
#implementation MyPopoverController
- (id)init {
...
[self retain];
...
}
- (void)popoverControllerDidDismissPopover: (UIPopoverController *)popoverController {
...
[delegate didFinishSelectingCountry:countryName];
[self release];
}
#end
I never see this behavior in objective-c, i feel that this is not the idea.
Why is it wrong?.
One of the way you can do for delegate method is to have:
MyPopOverDelegate
- (void)didFinishSelectingCountry:(NSString *)countryName popOver:(MyPopOver *)popOver;
Caller.m
// the caller
- (void)viewDidLoad {
MyPopoverController *dataSelector = [[MyPopoverController] alloc] init];
dataSelector.dataType = CountryDataType;
dataSelector.delegate = self;
[dataSelector show];
}
- (void)didFinishSelectingCountry:(NSString *)countryName popOver:(MyPopOver *)popOver {
// finish stuff
[popOver release];
}
This way is used a lot like NSUrlConnection, UIImagePickerController
If you want some unique object reusable across an entire app from anywhere in the view hierarchy, you can make it a property of the app delegate, and let the app delegate own it (retain it when live, release it during memory warnings, etc.).
A self retained object may eventually run into problems if you ever port your code to a garbage collected environment.
I am using ShareKit (www.getsharekit.com) to share my URL's to Twitter and Facebook. I want to be able to remove all of the additional social network it points to, but am not sure where to edit?
in SHK.m find this method
+ (NSArray *)favoriteSharersForType:(SHKShareType)type
and change
switch (type)
{
case SHKShareTypeURL:
favoriteSharers = [NSArray arrayWithObjects:#"SHKTwitter",#"SHKFacebook",#"SHKReadItLater",nil];
break;
case SHKShareTypeImage:
favoriteSharers = [NSArray arrayWithObjects:#"SHKTwitter",#"SHKFacebook",#"SHKReadItLater",nil];
break;
case SHKShareTypeText:
favoriteSharers = [NSArray arrayWithObjects:#"SHKTwitter",#"SHKFacebook",#"SHKReadItLater",nil];
break;
case SHKShareTypeFile:
favoriteSharers = [NSArray arrayWithObjects:#"SHKTwitter",#"SHKFacebook",#"SHKReadItLater",nil];
break;
to the following for each instance of the switch statement:
favoriteSharers = [NSArray arrayWithObjects:#"SHKFacebook", nil];
or what ever other options you want to support (ie if you only want twitter and facebook add #"SHKTwitter", to the array)
that will eliminate the other options but the action sheet that displays the options wont reflect the change and it will still give the more option, which we also need to disable.
So to do that go to SHKActionSheet.m
in this method you can change the title from "Share" to something more specific (this part is optional) ie "Share with Facebook and Twitter"
+ (SHKActionSheet *)actionSheetForType:(SHKShareType)type
change
SHKActionSheet *as = [[SHKActionSheet alloc] initWithTitle:SHKLocalizedString(#"PUT YOUR NEW TITLE HERE")
delegate:self
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:nil];
as.item = [[[SHKItem alloc] init] autorelease];
as.item.shareType = type;
than in that same method, delete this line
// Add More button
[as addButtonWithTitle:SHKLocalizedString(#"More...")];
that will remove the more button but now the code will now confuse the more button with the cancel button, so to fix that, go to this method:
- (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated
and delete the following else if statement
// More
else if (buttonIndex == sharers.count)
{
SHKShareMenu *shareMenu = [[SHKCustomShareMenu alloc] initWithStyle:UITableViewStyleGrouped];
shareMenu.item = item;
[[SHK currentHelper] showViewController:shareMenu];
[shareMenu release];
}
what this method is susposted to do is take the button that is normally the more button and open the more options. So by deleting it the code has no associated action with the cancel button so it just closes and release the action sheet, effectively creating a cancel button
From ShareKit/Core/SHK.m:
[SHK setFavorites: (NSArray *)favs forType:(SHKShareType)type]
+ (void)setFavorites:(NSArray *)favs forType:(SHKShareType)type
{
[[NSUserDefaults standardUserDefaults] setObject:favs forKey:[NSString stringWithFormat:#"%#%i", SHK_FAVS_PREFIX_KEY, type]];
}
favs is like :
[NSArray arrayWithObjects:#"SHKTwitter",#"SHKFacebook",#"SHKReadItLater",nil]
type is SHKShareType:
typedef enum
{
SHKShareTypeUndefined,
SHKShareTypeURL,
SHKShareTypeText,
SHKShareTypeImage,
SHKShareTypeFile
} SHKShareType;
The new way to do this with latest version of ShareKit 2.0 is to overwrite the following methods in your SHKConfigurator (extending DefaultSHKConfigurator.m)
// SHKActionSheet settings
- (NSNumber*)showActionSheetMoreButton {
return [NSNumber numberWithBool:true];// Setting this to true will show More... button in SHKActionSheet, setting to false will leave the button out.
}
/*
Favorite Sharers
----------------
These values are used to define the default favorite sharers appearing on ShareKit's action sheet.
*/
- (NSArray*)defaultFavoriteURLSharers {
return [NSArray arrayWithObjects:#"SHKTwitter",#"SHKFacebook", #"SHKReadItLater", nil];
}
- (NSArray*)defaultFavoriteImageSharers {
return [NSArray arrayWithObjects:#"SHKMail",#"SHKFacebook", #"SHKCopy", nil];
}
- (NSArray*)defaultFavoriteTextSharers {
return [NSArray arrayWithObjects:#"SHKMail",#"SHKTwitter",#"SHKFacebook", nil];
}
See: http://github.com/ideashower/ShareKit/issues/closed#issue/30
For the unwanted services not to appear on the action sheet you can simply remove the respective sharer services classes from the project.
Go to project--> Sharers --> Services --> select the .h and .m files of the respective service and delete it.
For example Facebook option can be removed by deleting SHKFacebook.h and SHKFacebook.m.
Note: You will have to check the class import to avoid errors.
See my answer on the other thread with more thorough explanation.
It is now easily configurable without changing ShareKit's code, if you use ShareKit 2.0
Basically, if you use only Facebook and Twitter, it is easier not to mess with ShareKit's code. You can easily
create your own UIActionSheet with two buttons, and call ShareKit's convenience share methods.
Easiest way is to edit "SHKSharers.plist" and keep the services you need.
Does any one know, how can i disable cut, copy and paste option on iPhone 3.0?
Thanks for your help and time.
I, too, couldn't find much documentation on using canPerformAction:withSender: for this purpose. So, I settled for clearing the pasteboard when exiting the application. In my AppDelegate.m:
- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(#"application terminating");
// Clear pasteboard to prevent pasting into other applications:
UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
pasteBoard.items = nil;
}
This worked well for my user-annotated reference application. I don't mind users copying and pasting within my application, but I'd rather they not republish my original content.
At some point I'd like more fine-grained control, perhaps with canPerformAction:withSender:, so that I can allow users to copy/paste the content they do create themselves.
Override this method in the controller class.
// Hide cut/copy/paste menu
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if ( [UIMenuController sharedMenuController] )
{
[UIMenuController sharedMenuController].menuVisible = NO;
}
return NO;
}
Any responder (UIView or UIWindow subclass) can override the canPerformAction:withSender: method, so you could just return NO for all the actions you don't want to permit.
See the UIResponder documentation...