Related
I'm trying to make a AVPlayerViewController go to full screen mode programmatically, coming from "embedded" mode, however this does not appear to be possible with the published API.
Is there a workaround that I'm missing? I'm interested in obtaining the same animation to the one that you get when the user presses the full screen button on the bottom right of the controls.
Using MPMoviePlayerController is not a viable alternative since I might have more than one video playing at a time.
Thanks.
AVPlayerViewController is a subclass of UIViewController, so it is presentable like any other view controller subclass. Are you able to use presentViewController:animated:completion?
self.avPlayerController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[self presentViewController:self.avPlayerController animated:YES completion:nil];
This then shows the "Done" button in the top left-hand corner.
Updated for iOS 11
There is no supported way to programmatically go fullscreen with AVPlayerViewController (a bit of an oversight in my opinion).
However, AVPlayerViewController does contain a private method that does exactly that. You'll have to decide for yourself whether you'd want to use it or not given you're not supposed to call private methods.
AVPlayerViewController+Fullscreen.h
#import <AVKit/AVKit.h>
#interface AVPlayerViewController (Fullscreen)
-(void)goFullscreen;
#end
AVPlayerViewController+Fullscreen.m
#import "AVPlayerViewController+Fullscreen.h"
#implementation AVPlayerViewController (Fullscreen)
-(void)goFullscreen {
NSString *selectorForFullscreen = #"transitionToFullScreenViewControllerAnimated:completionHandler:";
if (#available(iOS 11.3, *)) {
selectorForFullscreen = #"transitionToFullScreenAnimated:interactive:completionHandler:";
} else if (#available(iOS 11.0, *)) {
selectorForFullscreen = #"transitionToFullScreenAnimated:completionHandler:";
}
SEL fsSelector = NSSelectorFromString([#"_" stringByAppendingString:selectorForFullscreen]);
if ([self respondsToSelector:fsSelector]) {
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:fsSelector]];
[inv setSelector:fsSelector];
[inv setTarget:self];
NSInteger index = 2; //arguments 0 and 1 are self and _cmd respectively, automatically set
BOOL animated = YES;
[inv setArgument:&(animated) atIndex:index];
index++;
if (#available(iOS 11.3, *)) {
BOOL interactive = YES;
[inv setArgument:&(interactive) atIndex:index]; //arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
index++;
}
id completionBlock = nil;
[inv setArgument:&(completionBlock) atIndex:index];
[inv invoke];
}
}
#end
UPDATE: Swift 4 version of ToddH's answer:
private func enterFullscreen(playerViewController: AVPlayerViewController) {
let selectorName: String = {
if #available(iOS 11.3, *) {
return "_transitionToFullScreenAnimated:interactive:completionHandler:"
} else if #available(iOS 11, *) {
return "_transitionToFullScreenAnimated:completionHandler:"
} else {
return "_transitionToFullScreenViewControllerAnimated:completionHandler:"
}
}()
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
if playerViewController.responds(to: selectorToForceFullScreenMode) {
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
}
}
In iOS11 there are 2 new properties for AVPlayerViewController: entersFullScreenWhenPlaybackBegins and exitsFullScreenWhenPlaybackEnds. You can enable full screen mode right after playback begins and disable it when playback ends with these properties. If you need to enable fullscreen mode after some delay you can use private API methods as ToddH mentioned in his answer. However in iOS11 _transitionToFullScreenViewControllerAnimated:completionHandler: method is not available anymore, there is the same method called _transitionToFullScreenAnimated:completionHandler:. The second method accepts the same arguments as the first one.
I can show an example how to use it. First of all you need to create AVPlayerViewController instance in your UIViewController:
private let playerController : AVPlayerViewController = {
if let urlForPlayer = URL(string: "your_video_url") {
$0.player = AVPlayer(url: urlForPlayer)
}
return $0
} (AVPlayerViewController())
Then you need to setup view for AVPlayerViewController and add it to your current controller view. Function setupAVplayerController can do it for you:
private func setupAVplayerController() {
self.addChildViewController(self.playerController)
self.playerController.view.frame = CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0)
self.view.addSubview(self.playerController.view)
self.playerController.didMove(toParentViewController: self)
}
Function enterFullscreen forces full screen mode for AVPlayerViewController:
private func enterFullscreen(playerViewController:AVPlayerViewController) {
let selectorName : String = {
if #available(iOS 11, *) {
return "_transitionToFullScreenAnimated:completionHandler:"
} else {
return "_transitionToFullScreenViewControllerAnimated:completionHandler:"
}
}()
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
if playerViewController.responds(to: selectorToForceFullScreenMode) {
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
}
}
And now you need to call all these functions where you need it, for example in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//Your code
self.setupAVplayerController()
self.playerController.player?.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.enterFullscreen(playerViewController:self.playerController)
}
}
Don't forget that this solution based on private API calls that is not recommended to use.
As a little iOS 14 update to ToddH's answer: the private API to call is enterFullScreenAnimated:completionHandler:. So here's an extension on AVPlayerViewController to enter full screen.
extension AVPlayerViewController {
func enterFullScreen(animated: Bool) {
perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
func exitFullScreen(animated: Bool) {
perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
}
This implementation doesn't offer a completion callback. If you pass a Swift closure to the completionHandler parameter, it crashes in the underlying Obj-C API. I haven't investigated how to pass the closure to make it work.
Swift 3 version for the answer of ToddH:
extension AVPlayerViewController {
func goFullScreen() {
let selector = NSSelectorFromString("_transitionToFullScreenViewControllerAnimated:completionHandler:")
if self.responds(to: selector) {
// first argument is animated (true for me), second is completion handler (nil in my case)
self.perform(selector, with: true, with: nil)
}
}
}
You can just set the videoGravity property of AVPlayerViewController.
if(fullscreen)
{
[self.avPlayerController
setVideoGravity:AVLayerVideoGravityResizeAspectFill];
}
else
{
[self.avPlayerController
setVideoGravity:AVLayerVideoGravityResizeAspect];
}
For an 'embedded' AVPlayerViewController instance, it is quite easy to programmatically have it start playback in full screen mode, and without hacking anything (calling private methods). You just need to set its entersFullScreenWhenPlaybackBegins property to true.
You need to add the controller as a child VC to the main VC, and that's basically it. In viewDidAppear(_:) you need to call play() method on the controller's player property - playback will be automatically started in fullscreen.
It's often best to check Apple sample code for these kind of tricky APIs; I think this one might be useful for a lot of AVPlayer use cases: Using AVKit in iOS.
I did not have the need to use any restricted code.
For this, I am assuming that you have added the AVPlayerViewController as a child view controller.
Then for that you will first have to remove the child view controller and then present it again as a fullscreen controller as well attach the AVPlayer view properly to it's parent view.
Here is how I did it. Please note that I am using a library called Easy Peasy for restoring the playerVC.view constraints - one can do that with proper constraints as well.
#objc func fullscreenButtonClicked() {
playerVC.willMove(toParentViewController: nil)
playerVC.view.removeFromSuperview()
playerVC.removeFromParentViewController()
self.present(self.playerVC, animated: false, completion: {
self.playerVC.view.easy.layout(Top(), Right(), Left(), Bottom())
})
}
Its pretty simple, just set
playerViewController.videoGravity = .resizeAspectFill
and it goes full screen:)
In the UIWebView, if an input element containing text has focus, and a button is pressed that causes the input to lose focus, then subsequently double-tapping on the input to regain focus and selecting Cut (or Copy or Paste) from the popup bar that appears causes the UIWebView to crash with the error:
-[UIWebView cut:]: unrecognized selector sent to instance 0x10900ca60
Demo project: https://github.com/guarani/WebViewDoubleTapTestTests.git
I think this must be a UIWebView bug, any ideas?
For completeness, here are the contents of my web view,
<html>
<head>
</head>
<body>
<br><br>
<input type="text">
<input type="button">
</body>
</html>
Filed a Bug Report at Apple: 15894403
Update 2019/05/30: Bug still present in iOS 12.0 (16E226)
This is an Apple bug. The problem is the cut: action is sent incorrectly in the responder chain, and ends up being sent to the UIWebView instance instead of the internal UIWebDocumentView, which implements the method.
Until Apple fixes the bug, let's have some fun with the Objective C runtime.
Here, I subclass UIWebView with the purpose of supporting all UIResponderStandardEditActions methods, by forwarding them to the correct internal instance.
#import ObjectiveC;
#interface CutCopyPasteFixedWebView : UIWebView #end
#implementation CutCopyPasteFixedWebView
- (UIView*)_internalView
{
UIView* internalView = objc_getAssociatedObject(self, "__internal_view_key");
if(internalView == nil && self.subviews.count > 0)
{
for (UIView* view in self.scrollView.subviews) {
if([view.class.description hasPrefix:#"UIWeb"])
{
internalView = view;
objc_setAssociatedObject(self, "__internal_view_key", view, OBJC_ASSOCIATION_ASSIGN);
break;
}
}
}
return internalView;
}
void webView_implement_UIResponderStandardEditActions(id self, SEL selector, id param)
{
void (*method)(id, SEL, id) = (void(*)(id, SEL, id))[[self _internalView] methodForSelector:selector];
//Call internal implementation.
method([self _internalView], selector, param);
}
- (void)_prepareForNoCrashes
{
NSArray* selectors = #[#"cut:", #"copy:", #"paste:", #"select:", #"selectAll:", #"delete:", #"makeTextWritingDirectionLeftToRight:", #"makeTextWritingDirectionRightToLeft:", #"toggleBoldface:", #"toggleItalics:", #"toggleUnderline:", #"increaseSize:", #"decreaseSize:"];
for (NSString* selName in selectors)
{
SEL selector = NSSelectorFromString(selName);
//This is safe, the method will fail if there is already an implementation.
class_addMethod(self.class, selector, (IMP)webView_implement_UIResponderStandardEditActions, "");
}
}
- (void)awakeFromNib
{
[self _prepareForNoCrashes];
[super awakeFromNib];
}
#end
Use this subclass in your storyboard.
Have fun.
If you don't mind that there is no callout for cut/paste/etc. in the case, when the UIWebview is wrongly becoming first responder, then you can also fix it with this category. This does not prohibit cut/paste/etc. when the UIWebDocumentView (correctly) becomes first responder.
#implementation UIWebView (NoWrongPerformWebview)
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
return NO;
}
#end
// Swift 4 compliant version
import UIKit
extension UIWebView {
override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
// Should be respond to a certain Selector ??
return responds(to: action)
}
}
If anyone is interested, here's the swift version of Leo Natans method :
import Foundation
import ObjectiveC
var AssociatedObjectHandle: UInt8 = 0
class CustomWebView: UIWebView {
func _internalView() -> UIView? {
var internalView:UIView? = objc_getAssociatedObject(self, "__internal_view_key") as? UIView
if internalView == nil && self.subviews.count > 0 {
for view: UIView in self.scrollView.subviews {
if view.self.description.hasPrefix("UIWeb") {
internalView = view
objc_setAssociatedObject(self, "__internal_view_key", view, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
}
}
}
return internalView
}
override func awakeFromNib() {
super.awakeFromNib()
self._prepareForNoCrashes()
}
func _prepareForNoCrashes() {
let selectors = ["cut:", "copy:", "paste:", "select:", "selectAll:", "delete:", "makeTextWritingDirectionLeftToRight:", "makeTextWritingDirectionRightToLeft:", "toggleBoldface:", "toggleItalics:", "toggleUnderline:", "increaseSize:", "decreaseSize:"]
for selName: String in selectors {
let selector = NSSelectorFromString(selName)
//This is safe, the method will fail if there is already an implementation.
let swizzledMethod:IMP = class_getInstanceMethod(CustomWebView.self, #selector(CustomWebView.webView_implement_UIResponderStandardEditActions))
class_addMethod(CustomWebView.self, selector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
}
}
func webView_implement_UIResponderStandardEditActions(this:AnyObject, selector:Selector, param:AnyObject)
{
let method = {(val1: UIView?, val2: Selector, val3: AnyObject) -> Void in
self._internalView()?.methodForSelector(selector)
}
method(self._internalView(), selector, param);
}
}
- (UIView *)_internalView {
UIView *internalView = nil;
if (internalView == nil && self.subviews.count > 0) {
for (UIView *view in self.scrollView.subviews) {
if([view.class.description hasPrefix:#"UIWeb"]) {
internalView = view;
break;
}
}
}
return internalView;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
struct objc_method_description methodDescription = protocol_getMethodDescription(#protocol(UIResponderStandardEditActions), aSelector, NO, YES);
if (methodDescription.name == aSelector) {
UIView *view = [self _internalView];
if ([view respondsToSelector:aSelector]) {
return view;
}
}
return [super forwardingTargetForSelector:aSelector];
}
I'm getting some strange behaviour with presentViewController:animated:completion. What I'm making is essentially a guessing game.
I have a UIViewController (frequencyViewController) containing a UITableView (frequencyTableView). When the user taps on the row in questionTableView containing the correct answer, a view (correctViewController) should be instantiate and its view should slide up from the bottom of the screen, as a modal view. This tells the user they have a correct answer and resets the frequencyViewController behind it ready for the next question. correctViewController is dismissed on a button press to reveal the next question.
This all works correctly every time, and the correctViewController's view appear instantly as long as presentViewController:animated:completion has animated:NO.
If I set animated:YES, correctViewController is initialized and makes calls to viewDidLoad. However viewWillAppear, viewDidAppear, and the completion block from presentViewController:animated:completion are not called. The app just sits there still showing frequencyViewController until I make a second tap. Now, viewWillAppear, viewDidAppear and the completion block are called.
I investigated a bit more, and it's not just another tap that will cause it to continue. It seems if I tilt or shake my iPhone this can also cause it to trigger the viewWillLoad etc. It's like it's waiting to any other bit of user input before it will progress. This happens on a real iPhone and in the simulator, which I proved by sending the shake command to the simulator.
I'm really at a loss as to what to do about this... I'd really appreciate any help anyone can provide.
Thanks
Here's my code. It's pretty simple...
This is code in questionViewController that acts as the delegate to the questionTableView
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
{
// If guess was wrong, then mark the selection as incorrect
NSLog(#"Incorrect Guess: %#", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
[cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];
}
else
{
// If guess was correct, show correct view
NSLog(#"Correct Guess: %#", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
self.correctViewController = [[HFBCorrectViewController alloc] init];
self.correctViewController.delegate = self;
[self presentViewController:self.correctViewController animated:YES completion:^(void){
NSLog(#"Completed Presenting correctViewController");
[self setUpViewForNextQuestion];
}];
}
}
This is the whole of the correctViewController
#implementation HFBCorrectViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
// Custom initialization
NSLog(#"[HFBCorrectViewController initWithNibName:bundle:]");
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
NSLog(#"[HFBCorrectViewController viewDidLoad]");
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(#"[HFBCorrectViewController viewDidAppear]");
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)close:(id)sender
{
NSLog(#"[HFBCorrectViewController close:sender:]");
[self.delegate didDismissCorrectViewController];
}
#end
Edit:
I found this question earlier: UITableView and presentViewController takes 2 clicks to display
And if I change my didSelectRow code to this, it works very time with animation... But it's messy and doesn't make sense as to why it doesn't work in the first place. So I don't count that as an answer...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
{
// If guess was wrong, then mark the selection as incorrect
NSLog(#"Incorrect Guess: %#", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
[cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];
// [cell setAccessoryType:(UITableViewCellAccessoryType)]
}
else
{
// If guess was correct, show correct view
NSLog(#"Correct Guess: %#", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
////////////////////////////
// BELOW HERE ARE THE CHANGES
[self performSelector:#selector(showCorrectViewController:) withObject:nil afterDelay:0];
}
}
-(void)showCorrectViewController:(id)sender
{
self.correctViewController = [[HFBCorrectViewController alloc] init];
self.correctViewController.delegate = self;
self.correctViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:self.correctViewController animated:YES completion:^(void){
NSLog(#"Completed Presenting correctViewController");
[self setUpViewForNextQuestion];
}];
}
I've encountered the same issue today. I dug into the topic and it seems that it's related to the main runloop being asleep.
Actually it's a very subtle bug, because if you have the slightest feedback animation, timers, etc. in your code this issue won't surface because the runloop will be kept alive by these sources. I've found the issue by using a UITableViewCell which had its selectionStyle set to UITableViewCellSelectionStyleNone, so that no selection animation triggered the runloop after the row selection handler ran.
To fix it (until Apple does something) you can trigger the main runloop by several means:
The least intrusive solution is to call CFRunLoopWakeUp:
[self presentViewController:vc animated:YES completion:nil];
CFRunLoopWakeUp(CFRunLoopGetCurrent());
Or you can enqueue an empty block to the main queue:
[self presentViewController:vc animated:YES completion:nil];
dispatch_async(dispatch_get_main_queue(), ^{});
It's funny, but if you shake the device, it'll also trigger the main loop (it has to process the motion events). Same thing with taps, but that's included in the original question :) Also, if the system updates the status bar (e.g. the clock updates, the WiFi signal strength changes etc.) that'll also wake up the main loop and present the view controller.
For anyone interested I wrote a minimal demonstration project of the issue to verify the runloop hypothesis: https://github.com/tzahola/present-bug
I've also reported the bug to Apple.
Check this out: https://devforums.apple.com/thread/201431
If you don't want to read it all - the solution for some people (including me) was to make the presentViewController call explicitly on the main thread:
Swift 4.2:
DispatchQueue.main.async {
self.present(myVC, animated: true, completion: nil)
}
Objective-C:
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:myVC animated:YES completion:nil];
});
Probably iOS7 is messing up the threads in didSelectRowAtIndexPath.
I bypassed it in Swift 3.0 by using the following code:
DispatchQueue.main.async {
self.present(UIViewController(), animated: true, completion: nil)
}
Calling [viewController view] on the view controller being presented did the trick for me.
I'd be curious to see what [self setUpViewForNextQuestion]; does.
You could try calling [self.correctViewController.view setNeedsDisplay]; at the end of your completion block in presentViewController.
I've wrote extension (category) with method swizzling for UIViewController that solves the issue. Thanks to AXE and NSHipster for implementation hints (swift/objective-c).
Swift
extension UIViewController {
override public class func initialize() {
struct DispatchToken {
static var token: dispatch_once_t = 0
}
if self != UIViewController.self {
return
}
dispatch_once(&DispatchToken.token) {
let originalSelector = Selector("presentViewController:animated:completion:")
let swizzledSelector = Selector("wrappedPresentViewController:animated:completion:")
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
func wrappedPresentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
dispatch_async(dispatch_get_main_queue()) {
self.wrappedPresentViewController(viewControllerToPresent, animated: flag, completion: completion)
}
}
}
Objective-C
#import <objc/runtime.h>
#implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = #selector(presentViewController:animated:completion:);
SEL swizzledSelector = #selector(wrappedPresentViewController:animated:completion:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)wrappedPresentViewController:(UIViewController *)viewControllerToPresent
animated:(BOOL)flag
completion:(void (^ __nullable)(void))completion {
dispatch_async(dispatch_get_main_queue(),^{
[self wrappedPresentViewController:viewControllerToPresent
animated:flag
completion:completion];
});
}
#end
Check if your cell in the storyboard has Selection = none
If so, change it to blue or grey and it should work
XCode Vesion : 9.4.1, Swift 4.1
In my case this happen, when I tap cell and move for the another view. I debug into the deeper and it seems that happen inside viewDidAppear because of contains following code
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
then I added above code segment inside prepare(for segue: UIStoryboardSegue, sender: Any?) and work perfect.
Within my experience, my solution is, If we'll hope to do any new changes(eg. table reload, deselect selected cell etc.) for the tableview when again come back from second view, then use delegate instead of viewDidAppear and using above tableView.deselectRow code segment before moving second view controller
I basically have a subclass (RRView) of UIView with a delegate protocol. The RRViews are connected via outlets to the view controller which implements the delegate protocol as well as the delegates.
The code where I ask the delegate for additional info is implemented like this.
- (void)setDelegate:(id<RRViewDelegate>)delegate {
if (_delegate != delegate) {
_delegate = delegate;
if (_delegate && [_delegate respondsToSelector:#selector(cornersForRRView:)]) {
self.corners = [_delegate cornersForRRView:self];
}
if (_delegate && [_delegate respondsToSelector:#selector(cornerRadiusForRRView:)]) {
CGFloat maxRadius = floorf(self.bounds.size.height/2);
CGFloat radius = [_delegate cornerRadiusForRRView:self];
self.cornerRadius = MIN(radius, maxRadius);
}
if (_delegate && [_delegate respondsToSelector:#selector(rotationTypeForRRView:)]) {
self.type = [_delegate rotationTypeForRRView:self];
}
if (_delegate && [_delegate respondsToSelector:#selector(fontSizeForRRView:)]) {
self.fontSize = [_delegate fontSizeForRRView:self];
self.textFont = [UIFont fontWithName:self.textFont.fontName size:self.fontSize];
}
}
}
The problem I face is that I have a bunch of RRViews so I try to distinguish them like so, but the outlets are not yet connected.
- (UIRectCorner)cornersForRRView:(RRView *)view {
if ([view isEqual:self.dimensionsInfoView]) {
return UIRectCornerTopLeft | UIRectCornerTopRight;
}
else if ([view isEqual:self.oneHundredPercentInfoView]) {
return UIRectCornerTopRight;
}
else if ([view isEqual:self.deviceApperanceInfoView]) {
return UIRectCornerTopLeft;
}
else {
return 0;
}
}
Is there another way to accomplish this?
First of all, you can safely use pointer comparison here - view == self.dimensionsInfoView instead of [view isEqual:self.dimensionsInfoView]. You want to check whether the references contain the same objects, not check the internal states of the objects.
Also, you don't have to check if delegate is nil (_delegate &&), this is not Java, calling a method on nil delegate will just return NO.
The anwer for you question is rather simple.
Option 1: Don't set the delegate in xib, set it in controller's viewDidLoad, when the IBOutlets are already connected.
Option 2: Don't call the delegate methods in setDelegate, call them in some other method, e.g. [UIView layoutSubviews].
Option 3: Drop the setup methods from the delegate and call them directly in viewDidLoad, e.g. [self.dimensionsInfoView setFontSize:10.0f], [self.dimensionsInfoView setCorners:... withRadius:...];
I would definitely go with Option 3. You are just overcomplicate matters with a delegate. Why calling delegate methods for something that doesn't change?
Edit: Updated to make question more obvious
Edit 2: Made question more accurate to my real-world problem. I'm actually looking to take action if they tap anywhere EXCEPT in an on-screen text-field. Thus, I can't simply listen for events within the textfield, I need to know if they tapped anywhere in the View.
I'm writing unit tests to assert that a certain action is taken when a gesture recognizer recognizes a tap within certain coordinates of my view. I want to know if I can programmatically create a touch (at specific coordinates) that will be handled by the UITapGestureRecognizer. I'm attempting to simulate the user interaction during a unit test.
The UITapGestureRecognizer is configured in Interface Builder
//MYUIViewControllerSubclass.m
-(IBAction)viewTapped:(UITapGestureRecognizer*)gesture {
CGPoint tapPoint = [gesture locationInView:self.view];
if (!CGRectContainsPoint(self.textField, tapPoint)) {
// Do stuff if they tapped anywhere outside the text field
}
}
//MYUIViewControllerSubclassTests.m
//What I'm trying to accomplish in my unit test:
-(void)testThatTappingInNoteworthyAreaTriggersStuff {
// Create fake gesture recognizer and ViewController
MYUIViewControllerSubclass *vc = [[MYUIViewControllersSubclass alloc] init];
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer initWithView: vc.view];
// What I want to do:
[[ Simulate A Tap anywhere outside vc.textField ]]
[[ Assert that "Stuff" occured ]]
}
There is a much simpler way to trigger a touch for a UITapGestureRecognizer in a unit test using a single line. Assuming you have a var that holds a reference to the tap gesture recognizer all you need is the following:
singleTapGestureRecognizer?.state = .ended
I think you have multiple options here:
May be the simplest would be to send a push event action to your view but i don't think that what you really want since you want to be able to choose where the tap action occurs.
[yourView sendActionsForControlEvents: UIControlEventTouchUpInside];
You could use UI automation tool that is provided with XCode instruments. This blog explains well how to automate your UI tests with script then.
There is this solution too that explain how to synthesize touch events on the iPhone but make sure you only use those for unit tests. This sounds more like a hack to me and I will consider this solution as the last resort if the two previous points doesn't fulfill your need.
What you attempt to do is very hard (but not entirely impossible) while staying on the (iTunes-)legal path.
Let me first draft the right way;
The proper way out for doing this is using UIAutomation. UIAutomation does exactly what you ask for, it simulates user behaviour for all kinds of tests.
Now that hard way;
The issue that your problems boils down to is to instantiate a new UIEvent. (Un)fortunately UIKit does not offer any constructors for such events due to obvious security reasons. There are however workarounds that did work in the past, not sure if they still do.
Have a look at Matt Galagher's awesome blog drafting a solution on how to synthesise touch events.
If used in tests you can use either a test library called SpecTools which helps with all this and more or use it's code directly:
// Return type alias
public typealias TargetActionInfo = [(target: AnyObject, action: Selector)]
// UIGestureRecognizer extension
extension UIGestureRecognizer {
// MARK: Retrieving targets from gesture recognizers
/// Returns all actions and selectors for a gesture recognizer
/// This method uses private API's and will most likely cause your app to be rejected if used outside of your test target
/// - Returns: [(target: AnyObject, action: Selector)] Array of action/selector tuples
public func getTargetInfo() -> TargetActionInfo {
var targetsInfo: TargetActionInfo = []
if let targets = self.value(forKeyPath: "_targets") as? [NSObject] {
for target in targets {
// Getting selector by parsing the description string of a UIGestureRecognizerTarget
let selectorString = String.init(describing: target).components(separatedBy: ", ").first!.replacingOccurrences(of: "(action=", with: "")
let selector = NSSelectorFromString(selectorString)
// Getting target from iVars
let targetActionPairClass: AnyClass = NSClassFromString("UIGestureRecognizerTarget")!
let targetIvar: Ivar = class_getInstanceVariable(targetActionPairClass, "_target")
let targetObject: AnyObject = object_getIvar(target, targetIvar) as! AnyObject
targetsInfo.append((target: targetObject, action: selector))
}
}
return targetsInfo
}
/// Executes all targets on a gesture recognizer
public func execute() {
let targetsInfo = self.getTargetInfo()
for info in targetsInfo {
info.target.performSelector(onMainThread: info.action, with: nil, waitUntilDone: true)
}
}
}
Both, library as well as the snippet use private API's and will probably cause a rejection if used outside of your test suite ...
Answer by #Ondrej updated to Swift 4:
// Return type alias
typealias TargetActionInfo = [(target: AnyObject, action: Selector)]
// UIGestureRecognizer extension
extension UIGestureRecognizer {
// MARK: Retrieving targets from gesture recognizers
/// Returns all actions and selectors for a gesture recognizer
/// This method uses private API's and will most likely cause your app to be rejected if used outside of your test target
/// - Returns: [(target: AnyObject, action: Selector)] Array of action/selector tuples
func getTargetInfo() -> TargetActionInfo {
guard let targets = value(forKeyPath: "_targets") as? [NSObject] else {
return []
}
var targetsInfo: TargetActionInfo = []
for target in targets {
// Getting selector by parsing the description string of a UIGestureRecognizerTarget
let description = String(describing: target).trimmingCharacters(in: CharacterSet(charactersIn: "()"))
var selectorString = description.components(separatedBy: ", ").first ?? ""
selectorString = selectorString.components(separatedBy: "=").last ?? ""
let selector = NSSelectorFromString(selectorString)
// Getting target from iVars
if let targetActionPairClass = NSClassFromString("UIGestureRecognizerTarget"),
let targetIvar = class_getInstanceVariable(targetActionPairClass, "_target"),
let targetObject = object_getIvar(target, targetIvar) {
targetsInfo.append((target: targetObject as AnyObject, action: selector))
}
}
return targetsInfo
}
/// Executes all targets on a gesture recognizer
func sendActions() {
let targetsInfo = getTargetInfo()
for info in targetsInfo {
info.target.performSelector(onMainThread: info.action, with: self, waitUntilDone: true)
}
}
}
Usage:
struct Automator {
static func tap(view: UIView) {
let grs = view.gestureRecognizers?.compactMap { $0 as? UITapGestureRecognizer } ?? []
grs.forEach { $0.sendActions() }
}
}
let myView = ... // View under UI Logic Test
Automator.tap(view: myView)
I was facing the same issue, trying to simulate a tap on a table cell to automate a test for a view controller which handles tapping on a table.
The controller has a private UITapGestureRecognizer created as below:
gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(didRecognizeTapOnTableView)];
The unit test should simulate a touch so that the gestureRecognizer would trigger the action as it was originated from the user interaction.
None of the proposed solutions worked in this scenario, so I solved it decorating UITapGestureRecognizer, faking the exact methods called by the controller. So I added a "performTap" method that call the action in a way the controller itself is unaware of where the action is originated from. This way, I could make a test unit for the controller independent of the gesture recognizer, just of the action triggered.
This is my category, hope it helps someone.
CGPoint mockTappedPoint;
UIView *mockTappedView = nil;
id mockTarget = nil;
SEL mockAction;
#implementation UITapGestureRecognizer (MockedGesture)
-(id)initWithTarget:(id)target action:(SEL)action {
mockTarget = target;
mockAction = action;
return [super initWithTarget:target action:action];
// code above calls UIGestureRecognizer init..., but it doesn't matters
}
-(UIView *)view {
return mockTappedView;
}
-(CGPoint)locationInView:(UIView *)view {
return [view convertPoint:mockTappedPoint fromView:mockTappedView];
}
-(UIGestureRecognizerState)state {
return UIGestureRecognizerStateEnded;
}
-(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point {
mockTappedView = view;
mockTappedPoint = point;
[mockTarget performSelector:mockAction];
}
#end
Okay, I've turned the above into a category that works.
Interesting bits:
Categories can't add member variables. Anything you add becomes static to the class and thus is clobbered by Apple's many UITapGestureRecognizers.
So, use associated_object to make the magic happen.
NSValue for storing non-objects
Apple's init method contains important configuration logic; we could guess at what is set (number of taps, number of touches, what else?
But this is doomed. So, we swizzle in our init method that preserves the mocks.
The header file is trivial; here's the implementation.
#import "UITapGestureRecognizer+Spec.h"
#import "objc/runtime.h"
/*
* With great contributions from Matt Gallagher (http://www.cocoawithlove.com/2008/10/synthesizing-touch-event-on-iphone.html)
* And Glauco Aquino (http://stackoverflow.com/users/2276639/glauco-aquino)
* And Codeshaker (http://codeshaker.blogspot.com/2012/01/calling-original-overridden-method-from.html)
*/
#interface UITapGestureRecognizer (SpecPrivate)
#property (strong, nonatomic, readwrite) UIView *mockTappedView_;
#property (assign, nonatomic, readwrite) CGPoint mockTappedPoint_;
#property (strong, nonatomic, readwrite) id mockTarget_;
#property (assign, nonatomic, readwrite) SEL mockAction_;
#end
NSString const *MockTappedViewKey = #"MockTappedViewKey";
NSString const *MockTappedPointKey = #"MockTappedPointKey";
NSString const *MockTargetKey = #"MockTargetKey";
NSString const *MockActionKey = #"MockActionKey";
#implementation UITapGestureRecognizer (Spec)
// It is necessary to call the original init method; super does not set appropriate variables.
// (eg, number of taps, number of touches, gods know what else)
// Swizzle our own method into its place. Note that Apple misspells 'swizzle' as 'exchangeImplementation'.
+(void)load {
method_exchangeImplementations(class_getInstanceMethod(self, #selector(initWithTarget:action:)),
class_getInstanceMethod(self, #selector(initWithMockTarget:mockAction:)));
}
-(id)initWithMockTarget:(id)target mockAction:(SEL)action {
self = [self initWithMockTarget:target mockAction:action];
self.mockTarget_ = target;
self.mockAction_ = action;
self.mockTappedView_ = nil;
return self;
}
-(UIView *)view {
return self.mockTappedView_;
}
-(CGPoint)locationInView:(UIView *)view {
return [view convertPoint:self.mockTappedPoint_ fromView:self.mockTappedView_];
}
//-(UIGestureRecognizerState)state {
// return UIGestureRecognizerStateEnded;
//}
-(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point {
self.mockTappedView_ = view;
self.mockTappedPoint_ = point;
// warning because a leak is possible because the compiler can't tell whether this method
// adheres to standard naming conventions and make the right behavioral decision. Suppress it.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.mockTarget_ performSelector:self.mockAction_];
#pragma clang diagnostic pop
}
# pragma mark - Who says we can't add members in a category?
- (void)setMockTappedView_:(UIView *)mockTappedView {
objc_setAssociatedObject(self, &MockTappedViewKey, mockTappedView, OBJC_ASSOCIATION_ASSIGN);
}
-(UIView *)mockTappedView_ {
return objc_getAssociatedObject(self, &MockTappedViewKey);
}
- (void)setMockTappedPoint_:(CGPoint)mockTappedPoint {
objc_setAssociatedObject(self, &MockTappedPointKey, [NSValue value:&mockTappedPoint withObjCType:#encode(CGPoint)], OBJC_ASSOCIATION_COPY);
}
- (CGPoint)mockTappedPoint_ {
NSValue *value = objc_getAssociatedObject(self, &MockTappedPointKey);
CGPoint aPoint;
[value getValue:&aPoint];
return aPoint;
}
- (void)setMockTarget_:(id)mockTarget {
objc_setAssociatedObject(self, &MockTargetKey, mockTarget, OBJC_ASSOCIATION_ASSIGN);
}
- (id)mockTarget_ {
return objc_getAssociatedObject(self, &MockTargetKey);
}
- (void)setMockAction_:(SEL)mockAction {
objc_setAssociatedObject(self, &MockActionKey, NSStringFromSelector(mockAction), OBJC_ASSOCIATION_COPY);
}
- (SEL)mockAction_ {
NSString *selectorString = objc_getAssociatedObject(self, &MockActionKey);
return NSSelectorFromString(selectorString);
}
#end
CGPoint tapPoint = [gesture locationInView:self.view];
should be
CGPoint tapPoint = [gesture locationInView:gesture.view];
because the cgpoint should be retrieved from exactly where the gesture target is rather than trying to guess where in the view it's in