6

I am working on a photography app that allow photos to be taken in portrait or landscape. Due to the requirements of the project, I cannot let the device orientation autorotate, but rotation does need to be supported.

When using the following orientation methods:

override func shouldAutorotate() -> Bool {
    return true
}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    if self.orientation == .Landscape {
        return UIInterfaceOrientationMask.LandscapeRight
    } else {
        return UIInterfaceOrientationMask.Portrait
    }
}

override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
    if self.orientation == .Landscape {
        return UIInterfaceOrientation.LandscapeRight
    } else {
        return UIInterfaceOrientation.Portrait
    }
}

I am able to set rotation correctly at launch. By changing the orientation value and calling UIViewController.attemptRotationToDeviceOrientation() I am able to support rotation to the new desired interface. However, this rotation only occurs when the user actually moves their device. I need it to happen automatically.

I am able to call: UIDevice.currentDevice().setValue(targetOrientation.rawValue, forKey: "orientation") to force the change, but that causes other side effects because UIDevice.currentDevice().orientation only returns the setValue from that point on. (and it's extremely dirty)

Is there something I'm missing? I've looked into closing and launching a new view controller, but that has other issues such as a constant UI glitch when dismissing and immediately presenting a new view controller.

EDIT:

The following methods did not work for me:

EDIT 2:

Thoughts on potential solutions:

  1. set orientation directly (with setValue) and deal with all the side effects this presents on iOS 9 (not acceptable)

  2. I can use the current solution and indicate that the user needs to rotate the device. Once the device has been physically rotated, the UI rotates and then locks in place correctly. (poor UI)

  3. I can find a solution that forces the refresh of orientation and rotates without physical action. (what I'm asking about, and looking for)

  4. Do it all by hand. I can lock the interface in portrait or landscape, and manually rotate and resize the container view. This is 'dirty' because it forgoes all of the size class autolayout features and causes much heavier code. I am trying to avoid this.

Community
  • 1
  • 1
Nathan Tornquist
  • 6,468
  • 10
  • 47
  • 72
  • 1
    This is an Objective-C based answer: http://stackoverflow.com/questions/2689598/forcing-uiinterfaceorientation-changes-on-iphone?rq=1, but I believe the API is the same for Swift; will it work for you? – fullofsquirrels Jun 17 '16 at 16:24
  • 1
    It would have, however those methods are deprecated in iOS 9. There are plenty of solutions that used to exist, I'm having trouble finding one that still functions. Our needs are very defined, and make sense, but they are slightly against the current design pattern (with reason). In this case, letting the user select orientation is not viable. – Nathan Tornquist Jun 17 '16 at 16:25
  • Ha, just saw the deprecation now, bummer. Is your VC contained in a Nav Controller or Tab Bar? The answer by Korey Hinton here: http://stackoverflow.com/questions/26357162/how-to-force-view-controller-orientation-in-ios-8 attempts to address that specific case, but I haven't tried it. – fullofsquirrels Jun 17 '16 at 21:00
  • Nope, it's completely standalone. I've considered rotating the entire interface manually, but I'd like to avoid that if possible. It's a dirtier solution than I'd like. – Nathan Tornquist Jun 17 '16 at 21:14
  • I think it's not clear: what do you mean about "extremely dirty". Do you refeer about bounds dimensions after rotation? Please explain your question with some screenshots so anyone can help you. The risk to fall in a "too broad" question or question "with multiple solutions" is very high. – Alessandro Ornano Jun 22 '16 at 16:55
  • I can: 1/ set orientation directly and deal with all the side effects this presents on iOS 9 (not acceptable) 2/ I can use the current solution and indicate that the user needs to rotate the device (poor UI) 3/ I can find a solution that forces the refresh of orientation (what I'm asking about) 4/ do it all by hand. I can lock the interface in portrait or landscape, and manually rotate and resize the container view. This is 'dirty' because it forgoes all of the size class autolayout features and causes much heavier code. I am trying to avoid this. – Nathan Tornquist Jun 22 '16 at 17:04

3 Answers3

4

I was able to find a solution with the assistance of this answer: Programmatic interface orientation change not working for iOS

My base orientation logic is as follows:

// Local variable to tracking allowed orientation. I have specific landscape and
// portrait targets and did not want to remember which I was supporting
enum MyOrientations {
    case Landscape
    case Portrait
}
var orientation: MyOrientations = .Landscape

// MARK: - Orientation Methods

override func shouldAutorotate() -> Bool {
    return true

}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    if self.orientation == .Landscape {
        return UIInterfaceOrientationMask.LandscapeRight
    } else {
        return UIInterfaceOrientationMask.Portrait
    }
}

override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
    if self.orientation == .Landscape {
        return UIInterfaceOrientation.LandscapeRight
    } else {
        return UIInterfaceOrientation.Portrait
    }
}

// Called on region and delegate setters
func refreshOrientation() {
    if let newOrientation = self.delegate?.getOrientation() {
        self.orientation = newOrientation
    }
}

Then when I want to refresh the orientation, I do the following:

// Correct Orientation
let oldOrientation = self.orientation
self.refreshOrientation()
if self.orientation != oldOrientation {
    dispatch_async(dispatch_get_main_queue(), {
        self.orientationRefreshing = true

        let vc = UIViewController()

        UIViewController.attemptRotationToDeviceOrientation()

        self.presentViewController(vc, animated: false, completion: nil)

        UIView.animateWithDuration(0.3, animations: {
            vc.dismissViewControllerAnimated(false, completion: nil)
        })
    })
}

This solution has the side effect of causing view[Will/Did]Appear and view[Will/Did]Disappear to fire all at once. I'm using the local orientationRefreshing variable to manage what aspects of those methods are called again.

Community
  • 1
  • 1
Nathan Tornquist
  • 6,468
  • 10
  • 47
  • 72
3

I've encountered this exact problem in the past, myself. I was able to solve it using a simple work around (and GPUImage). My code is in Objective-C but i'm sure you'll have no problem translating it to Swift.

I began by setting the project's supported rotations to all that I hoped to support and then overriding the same UIViewController methods:

-(BOOL)shouldAutorotate { 
    return TRUE; 
}

-(NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

Which allows the device to rotate but will persist in Portrait mode. Then began observing for Device rotations:

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(adjustForRotation:)
                                             name:UIDeviceOrientationDidChangeNotification
                                           object:nil];

Then updated the UI if the device was in Portrait mode or landscape:

-(void)adjustForRotation:(NSNotification*)notification
{
    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];

    switch (orientation) {
        case UIDeviceOrientationLandscapeLeft:
        {
            // UPDATE UI
        }
            break;
        case UIDeviceOrientationLandscapeRight:
        {
            // UPDATE UI
        }
            break;
        default: // All other orientations - Portrait, Upside Down, Unknown
        {
            // UPDATE UI
        }
            break;
    }
} 

And finally, GPUImage rendered the image based on the device's orientation.

[_gpuImageStillCamera capturePhotoAsImageProcessedUpToFilter:last_f
                                              withCompletionHandler:^(UIImage *processedImage, NSError *error) { 
    // Process the processedImage 
}];
esreli
  • 4,993
  • 2
  • 26
  • 40
  • In your experience, how did you handle the updating of the UI? Are you manually scaling and rotating the container UI? – Nathan Tornquist Jun 22 '16 at 22:53
  • I would apply a CGAffineTransform on the UI elements @NathanTornquist, so a rotation – esreli Jun 23 '16 at 00:23
  • Also, I would highly recommend using GPUImage. It's one of the greater contributions to the open source iOS ecosystem by far! If you're serious about using this method, i'd be happy to outline my solution in more detail. – esreli Jun 23 '16 at 00:25
  • I have not used CGAffineTransformations on entire views before. If I understand this correctly, I would have to rotate and resize the view, and then push in the target size class so that I can still make use of autolayout constraints within the interface? Do you have experience with that/know if it works? – Nathan Tornquist Jun 23 '16 at 14:50
  • I should have been more clear. I used CGAffineTransformations on the view's subviews. There's also the possibility of UIView.frame adjustments. Think of it this way, the UIViewController will still think it's in Portrait mode but you modify the subview contents in code for the landscape orientation yourself. It worked smoothly for me. – esreli Jun 23 '16 at 17:00
  • @achi suppose if i want to present UIActivityViewController from this view controller, In device's landscape mode, don't you think it would create a issue as view controller itself is still in portrait mode only? – mayur Aug 16 '17 at 14:32
  • @mayur you're right though this answer is tailored to the needs of the question, no? – esreli Aug 16 '17 at 22:32
0

So I looked at the private headers of UIDevice, and it appears that there are two setters, and two property definitions, for orientation, which is currently baffling me. This is what I saw...

@property (nonatomic) int orientation;
@property (nonatomic, readonly) int orientation; 

- (void)setOrientation:(int)arg1;
- (void)setOrientation:(int)arg1 animated:(BOOL)arg2;

So when I saw that you used setValue:forKey:, I wanted to see there was a synthesized setter and getter, and am honestly not 100% sure as to which one is being set, and which one is being acknowledged by the device... I attempted in a demo app to use setValue:forKey: to no avail, but used this trick from one of my past applications, and it did the trick right away :) I hope this helps

UIDevice.currentDevice().performSelector(Selector("setOrientation:"), withObject: UIInterfaceOrientation.Portrait.rawValue)
AntonTheDev
  • 899
  • 6
  • 13
  • While that works, it's a private API and a very solid way to get my app rejected. – Nathan Tornquist Jun 23 '16 at 21:00
  • I've actually squeaked by the review process with this one before, about three years ago on TMGGO, when forcing a video player to orient itself. Technically, `setValue:forKey:`, is actually calling `setOrientation:` in the background, so to me I'd be surprised that it would be rejected since you are technically doing the same thing, odd enough the results are different :/ – AntonTheDev Jun 23 '16 at 21:05