Multiple UIActivityViewController placeholder items? - ios

UIActivityItemSources, it seems, can only return one kind of placeholder item? This seems strange, because I have a UIActivityItemSource that could return a string, an NSData object, or an image depending upon the activity it's given.
Is there really no way to return more than one kind of placeholder? (NSArrays don't seem to work.)
(I could imagine a solution where I instantiate a bunch of UIActivityItemProvider instances, each supporting the different datatypes mentioned above. But that seems like a lot more work than should be necessary...?)

If you add a trace inside your itemForActivityType function you will see that this function will be called multiple times. One for each activity available for share.
For example - if I want to provide different text for Twitter and mail/sms sharing I would have something like this:
- (id) activityViewController: (UIActivityViewController*) activityViewController itemForActivityType: (NSString*) activityType {
if (activityType == UIActivityTypePostToTwitter) {
return #"Sharing by Twitter";
}
else
return #"Other kind of sharing";
}
UPDATE:
If you want to provide different types of data to share (say text and images) - you need to wrote your placeholder function in a way so it returns two different kind of object when called multiple times.
- (id) activityViewControllerPlaceholderItem: (UIActivityViewController*) activityViewController {
static int step = 0;
if (step == 0) {
step = 1;
return #"text";
}
else if (step == 1) {
step = 2;
return [UIImage imageNamed: #"image"];
}
}

Related

Stop UIKeyCommand repeated actions

If a key command is registered, it's action might be called many times if the user holds down the key too long. This can create very weird effects, like ⌘N could repeatedly open a new view many times. Is there any easy way to stop this behavior without resorting to something like a boolean "already triggered" flag?
Here's how I register two different key commands:
#pragma mark - KeyCommands
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSArray<UIKeyCommand *>*)keyCommands {
return #[
[UIKeyCommand keyCommandWithInput:#"O" modifierFlags:UIKeyModifierCommand action:#selector(keyboardShowOtherView:) discoverabilityTitle:#"Show Other View"],
[UIKeyCommand keyCommandWithInput:#"S" modifierFlags:UIKeyModifierCommand action:#selector(keyboardPlaySound:) discoverabilityTitle:#"Play Sound"],
];
}
- (void)keyboardShowOtherView:(UIKeyCommand *)sender {
NSLog(#"keyboardShowOtherView");
[self performSegueWithIdentifier:#"showOtherView" sender:nil];
}
- (void)keyboardPlaySound:(UIKeyCommand *)sender {
NSLog(#"keyboardPlaySound");
[self playSound:sender];
}
#pragma mark - Actions
- (IBAction)playSound:(id)sender {
AudioServicesPlaySystemSound(1006); // Not allowed in the AppStore
}
A sample project can be downloaded here: TestKeyCommands.zip
In general, you don't need to deal with this, since the new view would usually become the firstReponder and that would stop the repeating. For the playSound case, the user would realize what is happening and take her finger off of the key.
That said, there are real cases where specific keys should never repeat. It would be nice if Apple provided a public API for that. As far as I can tell, they do not.
Given the '//Not allowed in the AppStore' comment in your code, it seems like you're OK using a private API. In that case, you could disable repeating for a keyCommand with:
UIKeyCommand *keyCommand = [UIKeyCommand ...];
[keyCommand setValue:#(NO) forKey:#"_repeatable"];
I reworked #Ely's answer a bit:
extension UIKeyCommand {
var nonRepeating: UIKeyCommand {
let repeatableConstant = "repeatable"
if self.responds(to: Selector(repeatableConstant)) {
self.setValue(false, forKey: repeatableConstant)
}
return self
}
}
Now you can have to write less code. If for example just override var keyCommands: [UIKeyCommand]? by returning a static list it can be used like this:
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(...),
UIKeyCommand(...),
UIKeyCommand(...),
UIKeyCommand(...).nonRepeating,
UIKeyCommand(...).nonRepeating,
UIKeyCommand(...).nonRepeating,
]
}
This makes the first three command repeating (like increasing font size) and the last three ones non repeating (like sending an email).
Works with Swift 4, iOS 11.
This works in iOS 12, a little bit less 'private' compared to the accepted answer:
let command = UIKeyCommand(...)
let repeatableConstant = "repeatable"
if command.responds(to: Selector(repeatableConstant)) {
command.setValue(false, forKey: repeatableConstant)
}

How can I vary the items used by UIActivityViewController in this scenario?

I have an app with a Share button. I want to customize what content is shared based on the activity type. For example, Messages might get an image and text, whereas AirDrop would just get a file.
I actually have this working perfectly, and the code I'm using has worked fine in every version of iOS through iOS 10. But I've realized I'm returning nil where I'm not supposed to, so I'm trying to figure out how to fix that.
I do something like this to set up my activity view controller:
JUNActivityProvider *fileProvider = [[JUNActivityProvider alloc] initWithPlaceholderItem:[NSObject new]];
fileProvider.objectID = objectID;
fileProvider.fileURL = fileURL;
JUNActivityProvider *textProvider = [[JUNActivityProvider alloc] initWithPlaceholderItem:[NSString new]];
textProvider.objectID = objectID;
...
UIActivityViewController *activityController = [[UIActivityViewController alloc]
initWithActivityItems:#[fileProvider,imageProvider,textProvider,urlProvider,printFormatter]
applicationActivities:nil];
Then in JUNActivityProvider, I have an item method that customizes the return value based on the activityType:
- (id)item {
if (self.fileURL) {
if ([self.activityType isEqualToString:UIActivityTypeAirDrop]) {
// Create the file
return url;
}
} else if ([self.placeholderItem isKindOfClass:[UIImage class]]) {
if ([self.activityType isEqualToString:UIActivityTypeAirDrop] == NO &&
[self.activityType isEqualToString:UIActivityTypeMail] == NO &&
[self.activityType isEqualToString:UIActivityTypePrint] == NO) {
// Create the image
return image;
}
} else if ([self.placeholderItem isKindOfClass:[NSString class]]) {
if ([self.activityType isEqualToString:UIActivityTypeMail]) {
return #"example one";
} else if ([self.activityType isEqualToString:UIActivityTypeMessage] ||
[self.activityType isEqualToString:UIActivityTypeCopyToPasteboard]) {
return #"example two";
}
}
return nil;
}
That return return nil at the end is the problem. It works fine and does exactly what I want—when it's nil that item isn't shared. The written documentation doesn't say that it must return a value, but the header file does:
- (nonnull id)item; // called on secondary thread when user selects an activity. you must subclass and return a non-nil value.
I don't want to risk a crash by returning nil when a nonnull value is expected, so I need to fix this. As far as I can tell my only option is to stop using UIActivityItemProvider, and instead implement the UIActivityItemSource protocol on my own. That protocol includes the method activityViewController:itemForActivityType:, which clearly states that you can return nil there:
May be nil if multiple items were registered for a single activity type, so long as one of the items returns an actual value.
Perfect. But here's the problem: activityViewController:itemForActivityType: is called on the main thread, which is causing problems with one of my items in particular. Here's a summary of what's happening:
I need to call some methods that run asynchronously. In order to deal with that I've tried using a dispatch semaphore. That keeps the method from returning until I've had a chance to set the return value.
Since activityViewController:itemForActivityType: is called on the main thread, that locks up while it's working.
I need to draw a UIView into an image. If I try to do that work on the main thread, nothing happens until the semaphore times out. But if I don't do it on the main thread, it crashes.
I'm at a loss for how to deal with this. Basically I need to keep the method from returning until I'm ready, but I can't lock up the main thread since I need to do some work there. This seems… impossible? Is there any way to make this work?
After filing an enhancement request I was just about to give up and settle on either returning nil or [NSNull null]. But then I realized there is absolutely a solution to this problem.
While UIActivityItemProvider includes a bunch of its own functionality, it still very much implements the UIActivityItemSource protocol. I knew that. What I hadn't considered is that this means I can just override activityViewController:itemForActivityType: and return nil there when it's appropriate.
So the last line of my item method now looks like this:
return self.placeholderItem;
You could also return [NSNull null] here, or really any object. I chose the placeholderItem because it seems a little safer—at the very least I know it's returning an object of the expected type, in case anything about the implementation ever changes.
Then all I have to do is add my own implementation of activityViewController:itemForActivityType: (where we are allowed to return nil):
- (nullable id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType {
id item = [super activityViewController:activityViewController itemForActivityType:activityType];
if ([item isEqual:self.placeholderItem]) return nil;
return item;
}
Just call super to get the item, return nil if it's something you don't want to include, or return the item if it is. Note that if your placeholderItem might ever be equal to something you actually do want to share, you will need to change this implementation a bit—but the same basic concept should work.

for loop multiple variables

I have the following code that I need to use multiple times (at least 10 times):
if([_btn_gv_tl1 isEqualToString:#"on"])
{
btn_gv_tl1.tag = 1;
UIImage *btnIMG_gv_tl1 = [UIImage imageNamed:#"stateOn.png"];
[btn_gv_tl1 setImage:btnIMG_gv_tl1 forState:UIControlStateNormal];
}
else
{
btn_gv_tl1.tag = 0;
UIImage *btnIMG_gv_tl1 = [UIImage imageNamed:#"stateOff.png"];
[btn_gv_tl1 setImage:btnIMG_gv_tl1 forState:UIControlStateNormal];
}
The problem is that I use multiple variables. _btn_gv_tl1 is a string, btw_gv_tl1 is a button, etc.
I named my variables from _btn_gv_tl1 to _btn_gv_tlx with all the variables.
How can I use the code above multiple times without copying and pasting the code? I think it is much nicer to create a loop.
Thanks!
Store those items in NSArray. You can read about them here. If you do so, it's easy know to iterate thorough this objects:
for (id object in objectsArray) {
// Do anything with ths objects
}
Update
Do the multi-dimensinal array (array of arrays) like this:
NSArray *array = #[#[object1, object11, object111],
#[object2, object22, object222],
#[object3, object33, object333]];

Should simple helper methods be a private instance method, class category or something else all together?

I have tons of little helper functions such as this littered throughout my view controller classes to help me accomplish things I do repeatedly:
- (BOOL)URLIsImgurAlbum:(NSURL *)URL {
// If the URL has "/a/" after the imgur portion it's an album.
if ([[URL.path substringWithRange:NSMakeRange(0, 3)] isEqualToString:#"/a/"]) {
return YES;
}
else {
return NO;
}
}
I feel like this is... wrong, though. Like there's a better place to put it as it's not really relevant to the view controller itself, but some of the content it's handling. I'm not using it in other classes or anything, so it's not copy and pasted across files, but still.
Would it be better as a category on NSURL (in the above case)? Something else all together?
You can make them as regular C-style functions outside of classes too:
BOOL URLIsImgurAlbum(NSURL *URL) {
return [[URL.path substringWithRange:NSMakeRange(0, 3)]
isEqualToString:#"/a/"];
}

Pass different parameters to an IBAction

My iPhone app has many buttons and I want all the buttons to call the same method, but with different parameters.
For example I want tapping one button to call the method myMethod: with the argument #"foo", and a second button should call the same method but with argument #"bar".
The so called "IBActions" must have one of these signatures:
-(void)action;
-(void)actionWithSender:(id)sender;
-(void)actionWithSender:(id)sender event:(UIEvent*)event;
You cannot add any other parameters. Nevertheless you can use sender (which is button1 or button2 in your case) to get the parameter:
-(void)actionWithSender:(UIButton*)sender {
NSString* parameter;
if (sender.tag == 1) // button1
parameter = #"foo";
else // button2
parameter = #"bar";
...
}
the real reason You cannot add additional parameter is that UIKIT will push params on the stack.
so the only way is to use tags.
A DIRTY way can be to convert a pointer to int and tagging the button with it:
myStruct params;
// fill params:
params.x=....
params.y=....
params.z=....
UIButton * btn = [UIButton......]; // create or use one from XIB
btn.tag = (int)&params;
... in Call back:
-(IBActions) doIt:(id)sender
{
myStruct * paramsPtr = (myStruct*)tag;
int i = paramsPtr->x;
NOTE: params MUST be keep static .. or allocate using malloc (more and more ugly code...).
DO NOT use a local var: it will be allocated on stack so will be removed after exiting from the setup method.
Give your various UIButton instances different tag property values.
In your IBAction method -myMethod:, you might then do something like:
- (void) myMethod:(id)sender {
switch (sender.tag) {
case (firstButtonTag):
doFooStuff;
break;
case (secondButtonTag):
doBarStuff;
break;
// etc.
}
}
The values firstButtonTag and secondButtonTag can be stored in an enum if you want to make this easy to maintain.
You can't pass parameters through an IBAction. What I usually do is give the buttons the unique tag in IB. THe tag is an integer value so I u then use a simple lookup table to convert the tag to some value.
In this case, three buttons but tags 1 to 3:
- (IBAction) buttonPressed: (UIButton*) sender
{
static const NSString* names = { #"Foo", #"Bar", #"Baz" };
id tag = [sender tag];
if (tag >= 1 && tag <= 3) {
NSLog(#"Button pressed is %#", names[tag]);
}
}
(id)Sender is shows that whatever u pass on UIButton click event is directly pass to this method and no matter that what type it is , it take automatically like if you pass button tag then it take button tag as sender.tag etc
As others have mentioned you cannot pass your custom parameter into action method. If you do not like the solution using tags you may also subclass UIButton with your custom class and add your parameter there. (By I wouldn't bother and just use tags)
You don't. The only parameter is the sender object, which you may use to have a different behavior, but what I'd do is define 2 action methods, which simply in turn call the same method with a different parameter, i.e. you'd have:
- (IBAction)button1:(id)sender
{
[self doStuff:kButton1];
}
- (IBAction)button2:(id)sender
{
[self doStuff:kButton2];
}
- (void)doStuff:(ParamType)param;
{
...
}
In defense of that method (no pun intended), I'd add that it makes clearer when you review your UI with Interface Builder to see that different buttons actually have different effects, which is harder to tell if they all call whateverAction:

Resources