Support Orientation Change On Some of UI but Not All - ios

I am making a drawing app and I would like the user to be able to rotate their device and draw on the canvas in any orientation. The toolbar of brushes/colors/etc needs to change orientation to always be on the top of the screen HOWEVER the drawing canvas needs to NOT change orientation (so as to preserve the drawing's orientation; imagine rotating a piece of paper with a drawing on it - the drawing would sometimes be sideways or upside down but your pencils would be still right in front of you).
I have tried several attempts and have concluded that I DO want to support iOS's default orientation changes because things like UIAlert popups need to be oriented correctly. Implementing override var supportedInterfaceOrientations: UIInterfaceOrientationMask on the view controller is not the best option.
I have gotten closer by subscribing to device orientation changed notifications and rotating the drawing canvas in the opposite direction to compensate for the default UI orientation rotation. In this case I am applying a CGAffineTransform rotation of 90, -90, 0, or 180 degrees to the canvas container view but its subviews are not rotating correctly with it.
Any ideas that I may be missing to get the behavior I want?
This is what I want. Notice how the toolbar is always being oriented to the top after rotation but the drawing stays glued to the device.
Before orientation change:
After orientation change:

I did a quick test app to see if what you experienced (with regards to rotating the canvas view and not having its subviews rotate as well) and I cannot replicate what you're seeing.
I simply have an observer set up on viewDidLoad from the main view:
NotificationCenter.default.addObserver(self, selector:#selector(self.orientationChanged(_:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object:nil)
And the orientation change notification is handled like this:
func orientationChanged(_ n:Notification) {
let orientation = UIDevice.current.orientation
if orientation == UIDeviceOrientation.portrait {
// No rotation
vwCanvas.transform = CGAffineTransform(rotationAngle:0)
} else if orientation == UIDeviceOrientation.portraitUpsideDown {
// Rotate canvas 180 degrees
vwCanvas.transform = CGAffineTransform(rotationAngle:CGFloat.pi)
} else if orientation == UIDeviceOrientation.landscapeLeft {
// Rotate canvas 90 degrees counterclockwise
vwCanvas.transform = CGAffineTransform(rotationAngle:CGFloat.pi/2.0)
} else if orientation == UIDeviceOrientation.landscapeRight {
// Rotate canvas 90 degrees clockwise
vwCanvas.transform = CGAffineTransform(rotationAngle:-CGFloat.pi/2.0)
}
}
Here's my portrait orientation screen:
And here's the rotated version:
As you'll notice, the sub-views are rotated in the above too. So just wondering what the difference is between my version and yours (since I don't know what your code is like) that you don't have the subviews rotate when you rotate the canvas ...

Related

Get screen dimension (per scanline) in pixels for iPhone X (10) ios 11

Considering that iPhone X Notch has smooth curves (no rectangle around notch), How to find if a pixel (x,y) on captured screenshot is part of draw buffer covered by the notch (vs actual display buffer) on iPhone X?
I am trying to log start X and end Y (pixel) position for actual pixels drawn for each of the horizontal scan lines when iPhone X is in landscape mode.
Is there a way to use inbuilt api to get this information? [[UIScreen mainScreen] nativeBounds] won't suffice in this case.
Hardcoding values or proving an image mask to detect it is possible. I am wondering if there is a more robust way to find width in pixel for scan line A/B/C in attached pic.
// For a given scan-line for a given Y = any-value between 0 & window.screen.height (Landscape orientation)
+ (CGPoint)printScanlineForY:(NSUInteger )y {
// what goes here?
...
// Case 1: expected for non-notch scan-line, return CGPointMake(0, screen-width)
// Case 2: expected for notch affected scan-line, return (non-0, screen-width)
// non-0 as determined by notch draw curve.
}
// Returning nativeBounds CGRect would return larger bounding box for entire screen.
// Output on iPhone X simulator: Native Screen Bounds: {{0, 0}, {1125, 2436}}
+ (CGRect)printScreenBounds {
CGRect bounds = [[UIScreen mainScreen] nativeBounds];
NSLog(#"Native Screen Bounds: %#", NSStringFromCGRect(bounds));
return bounds;
}
Related question with helpful details: Detect if the device is iPhone X
You can reference the documentation here which has some nice images
https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/
If you align your views to layout margins the system should take care of putting your views in visible areas.
If you want to figure out exactly where the notch is you can empirically calculate it by creating a view taking the full screen bounds, and doing a hit hit test for each coordinate , checking if it is "touchable". Collect the coordinates to some sort of array and then use them for future reference.
For instance something like:
#implementation FindNotch
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
{
UIView *hitView = [super hitTest...];
if (view == null) { ... } // the notch isn't "touchable"
}
#end
FindNotch *notch = [FindNotch alloc] initWithFrame:[UIScreen mainScreen].bound];
static UIWindow *window; window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds;]
window.windowLevel = UIWindowLevelStatusBar + 1; // make sure the view is above anything else in the notch area. Might require some tweaking
Notice that it isn't tested and you may need to raise the window level above the status bar or else it may be "underneath" and not respond to touches

Swapped width and height values

Explanation
For some reason, width and height values are being swapped in my game (as you can see below) that is set to be in landscape orientation. This way, the sprite that should be centered in the screen, is totally off the right position.
Code
You can download it here.
GameScene
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
print("width:", self.frame.width)
print("height:", self.frame.height)
//Sprite
let sprite = SKSpriteNode (imageNamed: "sprite")
sprite.anchorPoint = CGPointZero
sprite.position = CGPoint(x: self.frame.width / 2 - sprite.frame.width / 2, y: self.frame.height / 2 - sprite.frame.height / 2)
addChild(sprite)
}
}
GameViewController
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Set view size.
let scene = GameScene(size: view.bounds.size)
// Configure the view.
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .Landscape
} else {
return .Landscape
}
}
}
Thanks in advance,
Luiz.
If you go to targets-General in your xCode project (where you set version number, bundle ID etc) did you untick portrait? You also might have to check your info.plist to see if all portrait entries are removed for both devices.
You approach unfortunately is bad practice a lot of tutorials teach you and I would not continue like this.
If you use "scaleMode ResizeFill" or set the scene size to "view.bounds" your game will never look consistent on all devices. Furthermore all your values (sprite sizes, font sizes, physics values etc) will also not be the same on devices different to the one you are testing on.
Basically you will have to adjust for all of this on like 5-6 devices and its madness, especially using xCode simulator. Trust me I have been there before with 2 games and it nearly killed me. Its a game of game of "yeah this looks about right".
Dont go through this, so what you should do is
1) Change your scene size to the default scene size used by xCode
GameScene(size: CGSize(width: 1024, height: 768)) // swap if portrait
Note: Check update at bottom
If you dont do this, and leave it at view.bounds.size, point 2 will not work.
2) Change your scene scale mode to .AspectFill (also default xCode settings).
This way all your stuff will look great on all iPhones. Your values will scale correctly and you save yourself a lot of work.
On iPads you will have some extra space at the top and bottom (landscape) or left and right (portrait) which you usually just cover with some more background and have as a non playable area
Your scene basically looks like this now.
The red area is your whole scene size (iPad) but if you run on iPhones you will only see the green bit. In Portrait mode that red area would be on the left and right side.
Thats why y positioning is usually done from the centre because if you use frame.minY or frame.maxY you would be in the red zone and won't see the sprite on iPhones. That red zone you just cover with some more background (your background full screen images should be iPad size).
This also make your game more balanced, because if your would just stretch up your game than it will be easier on iPads because you have more space.
So design your game within the green zone. Than on iPads the only thing you might have to do is move up some UI, like pause button or score label, when you want to show them at the top edge. I do it this way.
// Some button
...
someButton.position = CGPoint(x: frame.midX, y: frame.midY + 200)
addChild(someButton)
if UIDevice.current.userInterfaceIdiom == .pad {
// set for iPad
}
// Some label
someLabel.positon = CGPoint(x: frame.midX - 100, someButton.position.y) // because its y position is the same as someButton I only have to adjust someButton.position.y on iPads.
addChild(someLabel)
I would not continue with your approach, even if it means redoing a lot of work.
UPDATE:
It seems with xCode 8 apple changed the default scene size from 1024x768 (768x1024) to 1334x750 (750x1334). I have only played around with those settings for a bit and they seem confusing for a universal game.
So you scene would now look like this and stuff on the xAxis on iPads is offscreen.
That makes no sense as the iPad is clearly not more widescreen than an iPhone.
I actually opened a bug report to see what apple says about this so for now I would probably keep it at 1024x768.
Hope this helps
Override viewWillLayoutSubviews in your GameViewController to ensure you get the proper width/height of the view. The implication with this also is that any objects you may instantiate would need to wait until this method has been called.
So you would want to do something like
class GameViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// NOTE: Add code to do this only once!
// Set view size.
let scene = GameScene(size: view.bounds.size)
// Configure the view.
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
Note I didn't try and compile this and it is missing a trailing }. Additionally, you would want to do this only once, I haven't added the code to do that. In my game, my scene is a property so I just check to see if it is nil and if it is, then I create the scene and present.
Following your code and according to how do you want to construct your sprite and position in GameScene you simply must change the scene.scaleMode to AspectFill or AspectFit (depend by what do you want to achieve):
sources:
#available(iOS 7.0, *)
public enum SKSceneScaleMode : Int {
case Fill /* Scale the SKScene to fill the entire SKView. */
case AspectFill /* Scale the SKScene to fill the SKView while preserving the scene's aspect ratio. Some cropping may occur if the view has a different aspect ratio. */
case AspectFit /* Scale the SKScene to fit within the SKView while preserving the scene's aspect ratio. Some letterboxing may occur if the view has a different aspect ratio. */
case ResizeFill /* Modify the SKScene's actual size to exactly match the SKView. */
}

iPhone 5 screen in landscape proportion when system says it is not landscape

I have an app that needs to change it's dimensions when it is changed from landscape to portrait or vice-versa. I have registered a notification like so:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "rotated", name: UIDeviceOrientationDidChangeNotification, object: nil);
And I get the notification but there is one scenario where it fails. When I pick the phone up, switch it to landscape, then set it down flat on the table without tilting it, I get a notification that it has changed out of landscape but I get dimensions that are still the old dimensions. This is how I am checking:
let isLandscape = UIDeviceOrientationIsLandscape(currentDevice.orientation);
Here is a log:
Start in portrait:
Landscape: false
Bounds (0.0, 0.0, 320.0, 568.0)
Turn phone on it's side:
Landscape: true
Bounds (0.0, 0.0, 568.0, 320.0)
Lay flat on table:
Landscape: false
Bounds (0.0, 0.0, 568.0, 320.0)
I am brand new to this so perhaps I am using the wrong API.
This was a silly mistake. I assumed there were only two orientations, but I looked at the UIDeviceOrientation enumeration and there are several:
case Portrait // Device oriented vertically, home button on the bottom
case PortraitUpsideDown // Device oriented vertically, home button on the top
case LandscapeLeft // Device oriented horizontally, home button on the right
case LandscapeRight // Device oriented horizontally, home button on the left
case FaceUp // Device oriented flat, face up
case FaceDown // Device oriented flat, face down
So my assumption that "not landscape means portrait" was not correct.

Problems with Azimuth calculation when iOS device is in Landscape Right

I have written an Augmented reality app for iOS which uses location services and GPS, everything works fine when the device is in landscape left but when the devices rotation is landscape right the center azimuth does not get calculated correctly, I am currently calculating this using the true heading in the didUpdateHeading method the subtracting a heading adjustment of 90 degrees. Should I be checking if < 0??
Thanks.
This is a pretty annoying issue and it seems that setting the headingOrientation property doesn't actually do anything.
The code below works for a landscape left orientation (home button on the right):
orientation = (float) manager.heading.magneticHeading;
orientation += 90.0f;
if(orientation > 360.0f)
orientation -= 360.0f;
So for a landscape right orientation, this should do the trick:
orientation = (float) manager.heading.magneticHeading;
orientation -= 90.0f;
if(orientation < 0.0f)
orientation += 360.0f;

ZBar for landscape orientation in iPad

I've worked on the Zbar in iPhone and also in iPad, it works fine without any issues, but not with the landscape mode in iPad. When I present the ZBarReaderViewController in iPad with a popover in landscape mode, the view is 90 degree shifted as in the below image,
where the bag is on the table and the image is captured with iPad in landscape mode. I want the bag image not as shifted.
I've already tried setting the supportedOrientationsMask as
reader.supportedOrientationsMask = ZBarOrientationMask(UIInterfaceOrientationLandscapeLeft || UIInterfaceOrientationLandscapeRight);
But its not showing in the correct orientation, but is a 90 degree shifted. Can someone help me solve this issue? Any timely help is much more appreciated. Thanks.
I had almost the same issue, and I got it resolved by adding the below code. My app supports only Landscape orientation:
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if (UIDeviceOrientationLandscapeLeft == orientation) {
//Rotate 90
reader.cameraViewTransform = CGAffineTransformMakeRotation (3*M_PI/2.0);
} else if (UIDeviceOrientationLandscapeRight == orientation) {
//Rotate 270
reader.cameraViewTransform = CGAffineTransformMakeRotation (M_PI/2.0);
}
The thing with this solution is that is fixes only the visual part of the problem. The user sees the right orientation, however the ZBarReader still 'sees' the same image, because you're transforming the preview image. What works is this:
[self.readerView willRotateToInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation] duration:0];
in the viewDidLoad method of your ViewController containing the ZBarReaderView.

Resources