iOS8, how to know when UIView autorotation is complete? - objective-c

I'm working with a storyboard project using size classes and autolayout. However, there are a couple instances in code where I'm adding "old school" menus and components on screen. These components are drawn correctly until the view autorotates.
I'm trying to fix the autorotation issues for controls added to UIView programmatically in iOS8. How do I determine when a UIView autorotation has completed and the view has new bounds?
There's this method which is called before rotation is completed, and view still has old size, and subviews cannot properly redraw themselves. I do not see anything along the lines of didTransition
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
//this does not seem to work - uses old size instead of new one
[introductionView setNeedsDisplay];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}

The method is called viewWillTransitionToSize:withTransitionCoordinator:. The second parameter is the transition coordinator! It tells you when the rotation is over.
Here's a typical structure from my own code (in Swift, but I'm sure you can translate mentally):
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
coordinator.animateAlongsideTransition({
_ in
// ...
}, completion: {
_ in
// ... now the transition is over! ...
})
}

Related

Stop app going to background when swiping up from bottom edge on iPhone X iOS 12

My game is going into background mode when performing a swipe from the bottom edge of the screen on iPhone X iOS 12.
As per Apple documentation overriding preferredScreenEdgesDeferringSystemGestures and calling setNeedsUpdateOfScreenEdgesDeferringSystemGestures should stop the app from going to background but this is's not working on iOS 12.
I am using Unity3D and the editor has the Defer system gestures on edges option , which is implemented as per apple documentation, but also does not work.
I am compiling the project in Xcode 10.
Does anyone else have this problem and do you have a fix?
PS: I am testing this in an empty single view iOS project, the only added code is the following:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear: animated];
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
return UIRectEdgeAll;
}
- (BOOL)prefersHomeIndicatorAutoHidden
{
return YES;
}
Update: Turns out that if I use a swift implementation it works. Too bad I cannot do this for the Unity3D 2017 generated project.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 11.0, *){
setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
}
}
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge{
return [.all];
}
}
New Update: In Unity 2019 it works by unchecking "Status Bar Hidden" in Project Stttings\Resolution and presentation and making sure you check at least one edge in Poject Settings\Other Settings\Defer system gestures on edges
Removing prefersHomeIndicatorAutoHidden makes it work in Objective C also.
This is the working example implementation, for anyone having the same problem:
- (void)viewDidLoad {
[super viewDidLoad];
if (#available(iOS 11.0, *)) {
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
return UIRectEdgeAll;
}
And for those who, like me, are using Unity3d just delete the following method from UnityViewControllerBase+iOS.mm in the generated Xcode project:
- (BOOL)prefersHomeIndicatorAutoHidden
{
return YES;
}
As per the apple documentation, preferredScreenEdgesDeferringSystemGestures doesn't stop the app from going to background, it just gives your gesture precedence over system gesture.
However, if you try to do it successively a second time, the system gesture would work. You can easily verify this by comparing with other apps.
By default the line at the bottom which helps in swiping up is black in colour and the swipe up gesture would work instantly if you do not override this method. But in your app, the line will look gray'ed out initially. If you do a swipe up, it will become black again and if you swipe up a second time, the system gesture will work.
I am putting this as an answer because of limited characters for commenting.
For Swift the answer is to override the instance property like so within your UIViewController.
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
get { .all }
}
for example.

keyDown: Not Called on NSClipView Subclass

My app is not document based, and its sole window is managed by a custom, xib-based NSWindowController subclass that I instantiate within the app delegate code:
- (void) applicationDidFinishLaunching:(NSNotification*) aNotification
{
_mainWindowController = [MainWindowController new];
// (stored in ivar just to prevent deallocation)
//[_mainWindowController showWindow:self];
// ↕︎ Not sure about the difference between these two... both seem to work.
[[_mainWindowController window] makeKeyAndOrderFront:self];
}
I have subclassed NSClipView to "center content inside a scroll view" (instead of having it pegged to the lower left corner) when it is zoomed to a size smaller than the clip view, and also implement custom functionality on mouse drag etc.
My window does have a title bar.
My window isn't borderless (I think), so I am not subclassing NSWindow.
I have overriden -acceptsFirstResponder, -canBecomeKeyView and -becomeFirstResponder in my NSClipview subclass (all return YES).
The drag events do trigger -mouseDown: etc., and if I set a breakpoint there, the first responder at that point is the same as the window hosting my clip view: [self.window firstResponder] and [self window] give the same memory address.
What am I missing?
Update
I put together a minimal project reproducing my setup.
I discovered that if my custom view is the window's main view, -keyDown: is called without problems. But if I place a scroll view and replace its clip view by my custom view (to do that, I need to change the base class from NSView to NSClipView, of course!), -keyDown: is no longer triggered.
I assume it has something to do with how NSScrollView manages events (however, as I said before, -mouseDown:, -mouseDragged: etc. seem to be unaffected).
I also discovered that I can override -keyDown: in my window controller, and that seems to work, so I have decided to do just that (still open to an answer, though). Also, since I'm trying to detect the shift key alone (not as a modifier of another key), I'd rather use:
- (void) flagsChanged:(NSEvent *) event
{
if ([event modifierFlags] & NSShiftKeyMask) {
// Shift key is DOWN
}
else{
// Shift key is UP
}
}
...instead of -keyDown: / -keyUp: (taken from this answer).

iOS 8 Orientation Change Detection

Running on iOS 8, I need to change the UI when rotating my app.
Currently I am using this code:
-(BOOL)shouldAutorotate
{
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
if (orientation != UIInterfaceOrientationUnknown) [self resetTabBar];
return YES;
}
What I do it remove the current UI and add a new UI appropriate to the orientation. However, my issue is that this method is called about 4 times every time a single rotation is made.
What is the correct way to make changes upon orientation change in iOS 8?
Timur Kuchkarov is correct, but I'll post the answer since I missed his comment the first time I checked this page.
The iOS 8 method of detecting orientation change (rotation) is implementing the following method of the view controller:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
// Do view manipulation here.
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
Note: The controller's view has not yet transitioned to that size at this time, so be careful if your sizing code relies on the view's current dimensions.
The viewWillTransitionToSize:withTransitionCoordinator: method is called immediately before the view has transitioned to the new size, as Nick points out. However, the best way to run code immediately after the view has transitioned to the new size is to use a completion block in the method:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// your code here
}];
}
Thanks to the this answer for the code and to Nick for linking to it in his comment.

How can I set the size of a UIView init with storyboard?

I am currently using iOS 6.0.
I have a custom UIView that needs to have a certain size. If I programmatically init the view and add it it's fine. However, I can't find a place where I can set the size of the view in the storyboard.
Setting its size in the storyboard doesn't work because the storyboard thinks it's empty and set it's size to zero. Setting its size in viewDidLoad or viewDidAppear doesn't work because later on the size will be overwritten by _applyISEngineLayoutValue.
You can do that in your Interface Builder. Open the storyboard where you have your view and open the utilities menu:
Then you can select a button that looks like a ruler on the top of the utilities menu:
In that menu you can set the size of your view and how you want it to expand.
Also, please make sure you setted your Class' view in the class inspector:
Image token from this site.
Finally, make sure you override the initWithFrame and initWithCoder methods:
- (id)initWithFrame:(CGRect)frame {
return [super initWithFrame:frame];
}
//Needs to be overrided when you set your size in interface builder
- (id)initWithCoder:(NSCoder *)aDecoder {
return [self initWithFrame:[self frame]];
}
I am facing the same problem, and I think the short answer is: you can't rely on the frame or bounds in the view's init method when using storyboards.
When your view is initialized by the segue, it will call the initWithCoder: method rather than other init methods, since it is deserializing your view object from the .xib file. Before storyboards, the frame and bounds would be set by the time the initWithCoder: was called, but that appears to no longer be the case with storyboards -- the iOS code sets those values later.
I've used a couple workarounds, depending on the situation:
If I know the size of the view in advance (for example a specialty view that only supports one size) I set my own frame in the initWithCoder: method.
If I don't know the size of the view, I defer initialization of size-specific things until my view's layoutSubviews method is called.
If it's more convenient, I sometimes just explicitly do the size-specific initialization in my view's ViewController. (Not pretty, but sometimes quick-and-easy. I'm not proud.)
I have the same problem as you, and when I select the view and switch to the attributes inspector , setting
"Simulated Metrics" as follows, I can resize the view in the Size inspector.
Make sure that Size is set to either "Freeform" or "None"

Custom NSScrollView doesn't scroll NSTableHeaderView

I have an NSTableView embedded within a custom NSScrollView subclass, wherein I sometimes do scrolling programmatically, like so:
[[self contentView] scrollToPoint:newOffset];
[self reflectScrolledClipView:[self contentView]];
When I do this, the NSTableView scrolls fine, but its associated NSTableHeaderView doesn't move with it. If I use my mouse and scroll the NSScrollView normally, however, they move together like they should.
I figure I'm probably just missing a single line somewhere that lets the NSTableHeaderView know that it's supposed to scroll too, but I don't know what that is. Can anyone help?
Well, I don't know precisely what kind of black magic goes on under the hood when you scroll an NSScrollView containing an NSTableHeaderView with the mouse, but it looks like it handles it internally somewhere. To circumvent this, I now only scroll the NSTableView programatically (by overriding the functions that would handle user input), and then I scroll the NSTableHeaderView myself, like so:
NSTableHeader *header = [[self documentView] headerView];
[header setBoundsOrigin:NSMakePoint(newOffset.x,[header bounds].origin.y)];
I ran into the same issue on a cell-based NSTableView with Swift 5 / MacOS 14.
The NSScrollView enclosing an NSTableView owns both the contentView and the headerView of the NSTableView (and the cornerView, which I do not use), and is normally responsible of coordinating their scrolling.
When scrolling with mouse, the NSScrollView internal magic handle correctly the scrolling of the header view.
When scrolling programmatically the NSClipView using scroll(to:) + reflectScrolledClipView, NSScrollView fails to scroll of the headerView.
I use this protocol in order to scroll programmatically the headerView too, which allows me to scroll programmatically using this protocol:
extension NSTableView : ScrollingProtocol {
func getScrollView() -> NSScrollView? {
return enclosingScrollView
}
func getVisibleOrigin() -> NSPoint? {
return enclosingScrollView?.documentVisibleRect.origin
}
func scrollToOrigin(_ targetOrigin: NSPoint) {
guard let currentOrigin = getVisibleOrigin(),
let scrollView = enclosingScrollView
else { return }
if (!NSEqualPoints(targetOrigin, currentOrigin)) {
let clipView = scrollView.contentView
clipView.scroll(to: targetOrigin)
// Workaround because NSClipView.scroll(to:) does not scroll
// the headerView of NSTableView
if let headerView = headerView {
let x = targetOrigin.x
let y = headerView.bounds.origin.y
if let headerClipView = headerView.superview as? NSClipView {
headerClipView.scroll(to: NSMakePoint(x, y))
}
}
scrollView.reflectScrolledClipView(clipView)
}
}
}