forceTouchCapability returning nil
Asked Answered
R

3

10

I am trying to incorporate some 3D touch into an application and I've run into a weird issue where the forceTouchCapability check is returning nil on viewDidLoad but not in viewWillAppear/viewDidAppear.

I'm aware that this is only available on iOS 9+ so I've added checks to verify that the traitCollection property on the view controller responds to forceTouchCapability as in the following:

- (void)loadView {

   self.view = [[MyView alloc] init];
}

- (void)viewDidLoad {

   [super viewDidLoad];

   // Checking the force touch availability here
   if ([self.traitCollection respondsToSelector:@selector(forceTouchCapability)] &&
        self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {

       // This won't get called because forceTouchCapability is returning nil 
       // which corresponds to UIForceTouchCapabilityUnknown
       [self registerForPreviewingWithDelegate:self sourceView:self.view];
   }
}

In LLDB with a breakpoint at the if statement, entering po [self.traitCollection forceTouchCapability] returns nil which corresponds to UIForceTouchCapabilityUnknown. However, the traitCollection itself is not nil.

According to the documentation for UIForceTouchCapabilityUnknown:

UIForceTouchCapabilityUnknown: The availability of 3D Touch is unknown. For example, if you create a view but have not yet added it to your app’s view hierarchy, the view’s trait collection has this value.

Has the view not been added to the hierarchy by this point?

I'm curious if anyone has run into this issue before and how to work around this? I would like to avoid adding this in the viewDidAppear as this can get called quite a bit.

If it helps, I'm running this on a 6S on iOS 9.1 with Xcode 7.2

Revolver answered 16/12, 2015 at 17:23 Comment(0)
F
22

The view hasn't been added to the View Hierarchy yet. You can see this easily by checking for a superview in the debug console

(lldb) po self.view.superview
nil

If that's what you're seeing, the view hasn't been added to a hierarchy yet: so you have to put your check elsewhere.

This is kind of confusing because in Apple's ViewControllerPreview sample app it's in viewDidLoad. But it really should in traitCollectionDidChange:, because then you're sure that the view has been added to the app's hierarchy.

This is the code I use (works on iOS 8, if you don't need to support that feel free to move the outer conditional).

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
   [super traitCollectionDidChange:previousTraitCollection];

   if ([self.traitCollection respondsToSelector:@selector(forceTouchCapability)]) {
       if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
           // retain the context to avoid registering more than once
           if (!self.previewingContext) {
               self.previewingContext = [self registerForPreviewingWithDelegate:self sourceView:self.view];
           }
       } else {
           [self unregisterForPreviewingWithContext:self.previewingContext];
           self.previewingContext = nil;
       }
   }
}

The added benefit to this is that your view will be registered/unregistered if the user changes their 3D Touch settings while the app is running.

Finely answered 28/1, 2016 at 20:3 Comment(1)
@Finely good solution. Please consider recommending the usage of p instead of po. p just looks inside, po is used for executionRennin
C
2

I've also seen this issue, and found that the easiest way to check whether the device can support force touch or not is doing it via the screen instance. This kinda makes sense because the capability is a property of the screen. Doing it this way means you don't have to worry about the lifecycle of a viewcontroller or a view.

func canForceTouch() -> Bool
{
  if iOS9OrHigher // pseudocode, a function that makes sure u only do this check on ios9 or higher
  {
      return UIScreen.mainScreen().traitCollection.forceTouchCapability == .Available
  }
  return false
}
Constrictive answered 23/6, 2016 at 12:59 Comment(0)
T
-1

Like what @bpapa said, Your view hasn't added to view hierarchy yet, But my solution is different little bit:

var token:dispatch_once_t = 0
override func viewDidAppear(animated: Bool) {
    dispatch_once(&token) { 
        // Force Touch Checking
        if #available(iOS 9.0, *) {
            if self.traitCollection.forceTouchCapability == .Available {
                self.registerForPreviewingWithDelegate(self, sourceView: self.view)
            }
        }
    }
}
Theodora answered 27/6, 2016 at 14:16 Comment(2)
Why start bringing different queues into this? Actually conforming to the SDK by implementing an optional method is a far cleaner approach.Finely
@Finely We are still using the main queue (see this answer stackoverflow.com/a/25900466) ... I'm Writing this code in viewDidAppear to ensure that all views are in the hierarchy .. And dispatching once because viewDidAppear may be called many times you know that :)Theodora

© 2022 - 2024 — McMap. All rights reserved.