We can create formsheet views of non-standard size using the following bit of code (below), but the resulting view isn't centered -- the x-coordinate is always where a standard sized formsheet view would be. Changing the center property of the view and superview doesn't affect anything in a useful way. How can we use a custom formsheet size that is correctly centered?
Add the following code to the view controller that is being presented as a UIModalPresentationPageSheet:
#implementation MySpecialFormsheet {
CGRect _realBounds;
}
- (void)viewDidLoad {
// code below works great, but the resulting view isn't centered.
_realBounds = self.view.bounds;
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.view.superview.bounds = _realBounds;
}
I've only found success changing the frame after presenting it. I actually do it like so:
GameSetupViewController *gameSetup = [[GameSetupViewController alloc] init];
[gameSetup setDelegate:self];
[gameSetup setModalPresentationStyle:UIModalPresentationPageSheet];
[self presentModalViewController:gameSetup animated:YES];
[gameSetup.view.superview setAutoresizingMask:(UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth)];
[gameSetup.view.superview setFrame:CGRectMake(64, 64, 896, 640)];
But perhaps you could get away with using viewDidAppear or some other handle.
Related
I'm new to iOS layouts, and I'm trying to programmatically centre a custom view (actually a React Native RCTRootView which extends UIView, if that's relevant) which has intrinsically sized, dynamic content (defined over in javascript land).
I've seen solutions overriding intrinsicallySizedContent but that seems to be only relevant to auto-layout, which isn't used by RN. The only way I've been able measure the size of the RCTRootView's content is by adding it to a view, and waiting over several passes of layoutSubViews until its frame has a size. That led me to the following effort:
Attempt: Wrap and set wrapper size in layoutSubViews
I'm wrapping the RCTRootView in another UIView and trying to override layoutSubViews, setting the size of my wrapping frame once I have the size of the react native content.
My wrapper is created and added to the navigation bar in my UIViewController like this:
RCTBridge *bridge = ((RCTRootView*)self.view).bridge;
RCTRootView *reactView = [[RCTRootView alloc] initWithBridge:bridge moduleName:navBarCustomView initialProperties:initialProps];
RCCCustomTitleView *titleView = [[TitleWrapperView alloc] initWithFrame:self.navigationController.navigationBar.bounds subView:reactView];
self.navigationItem.titleView = titleView;
self.navigationItem.titleView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
And the wrapper is implemented like this:
#import "TitleWrapperView.h"
#import <React/RCTRootView.h>
#interface TitleWrapperView ()
#property (nonatomic, strong) RCTRootView *subView;
#end
#implementation TitleWrapperView // Extends UIView
-(instancetype)initWithFrame:(CGRect)frame subView:(RCTRootView*)subView {
self = [super initWithFrame:frame];
if (self) {
self.subView = subView;
self.subView.sizeFlexibility = RCTRootViewSizeFlexibilityWidthAndHeight;
self.clipsToBounds = true;
// Prevent the wrapper from being so large initially as to squash the '< Back' button down to '<'
self.frame = CGRectZero;
[self addSubview:subView];
}
return self;
}
-(void)layoutSubviews {
[super layoutSubviews];
CGRect contentViewFrame = self.subView.contentView.frame;
if (contentViewFrame.size.width > 0) {
// Once we have a measurement for the sub-view's content, set the wrapper's frame
if (self.frame.size.width != contentViewFrame.size.width) {
self.frame = contentViewFrame;
}
}
}
#end
That works a treat on iOS 11+, but not on iOS 10.3, where when the screen containing the custom nav (the red box) is pushed, the navigation transition animates the titleView over towards the top left, rather than into the centre (the end position, which is correct) as I'd hope:
Other approaches?
There might be a way to overcome the iOS 10 glitch above (and I'd love to hear it) but I'm fairly certain there must be a better approach. Is is possible to allow an arbitrary, complex view to lay itself out and get its measurement before adding it to a parent? I've also tried overriding sizeThatFits in my wrapper, which is called by the navigation bar, and requesting the size of my RCTRootView using [subView sizeThatFits: myLargeContainer], but I get 0,0 back.
I'm at a loss - any pointers are appreciated.
I have an project using autolayout,
And I notice that after viewWillAppear, viewWillLayoutSubViews and viewDidLayoutSubViews pair will be called several times on iOS 8, for my case, it is 2-3 times usually.
The fist viewDidLayoutSubViews will get incorrect frame size, so I have to avoid for first viewDidLayoutSubViews, and init my views afterwards.
However, when I tested it on iOS 7, I found that only ONE viewWillLayoutSubViews and viewDidLayoutSubViews pair got called, so my code broke again.
My question is, what is changed on iOS 8 for this behaviour?
EDIT:
I have pasted my demo code here:
In the code, _pieChart will be added to self.ChartViewCanvas, and self.ChartViewCanvas is using autolayout. _pieChart is from old project code, which is drawn without auto layout.
I was required to draw the pie chart before viewDidAppear, because drawing in viewDidAppear will have a 1 sec delay compare to other views in storyboard. This is not allowed for me.
Is there any way to know when is the final viewDidLayoutSubViews? Calling [self.ChartViewCanvas addSubview:_pieChart]; multiple times will lead to lower performance, and sometimes _pieChart's drawInRect will not be called every time, so the chart is not update.
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_pieChart.delegate = self;
if (!_pieChart) {
_pieChart = [[PieChartView alloc] initWithFrame:CGRectMake(0, 0, pieRadius * 2, pieRadius * 2)];
}else {
[_pieChart setFrame:CGRectMake(0, 0, pieRadius * 2, pieRadius * 2)];
}
//_pieChart.translatesAutoresizingMaskIntoConstraints = NO;
if ([_pieChart superview]) {
[_pieChart removeFromSuperview];
}
[self.ChartViewCanvas addSubview:_pieChart];
}
Probably only Apple knows, but I won't deal with that too much if everything is working fine. In iOS8 Apple changed a lot view controllers (again) in they way they are presented from containers VC as for rotation and UITraitCollections.
For instance UIAlertView is now a view controller, when you show one you trigger all the mechanism related to present a VC.
If this fact is creating an issue it must be said that you should not rely on how many times those methods are called because they were always be unpredictable there are too many variables to be taken into account.
A quick and dirty solution could be wrap your code in a dispatch_once if you want that it will be called only one time.
If you add your view using auto layout correctly you won't see any sort of bug.
[EDIT]
Here is a little snippet about how it might look your viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
//.. your stuff
//We don't need any frame autolayout wil take care of calculating it on its pass
_pieChart = [[PieChartView alloc]initWithFrame:CGRectZero];
_pieChart.delegate = self;
_pieChart.translatesAutoresizingMaskIntoConstraints = NO;
[self.ChartViewCanvas addSubview:_pieChart];
NSDictionary *bindings = NSDictionaryOfVariableBindings(_pieChart);
// We create constraints to tell the view that it needs to sctretch its bounds to the superview
NSString *formatTemplate = #"%#:|[_pieChart]|";
for (NSString * axis in #[#"H",#"V"]) {
NSString * format = [NSString stringWithFormat:formatTemplate,axis];
NSArray * constraints = [NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:nil views:bindings];
[_pieChart.superview addConstraints:constraints];
}
// Do any additional setup after loading the view.
}
Of course that is going to call drawRect:, draw rect is called when a view is marked as dirty in the display pass, but before display is usually called the autolayout engine to calculate frames of views in needs for layout.
I tried this out on my application and found the same as you: 1 call on iOS7 and 3 on iOS8. From the stack traces this seems to be down to doing double layout after viewWillAppear and an extra layout following viewDidAppear not seen on iOS7.
My suggestion would be that you add any views in viewDidLoad (or viewWillAppear), then only do layout adjustments in the layout subview runs. Based on your updated post something like:
- (void)viewDidLoad{
[super viewDidLoad];
_pieChart = [[PieChartView alloc] initWithFrame:CGRectMake(0, 0, pieRadius * 2, pieRadius * 2)];
[self.ChartViewCanvas addSubview:_pieChart];
_pieChart.delegate = self;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[_pieChart setFrame:CGRectMake(0, 0, pieRadius * 2, pieRadius * 2)];
}
For interest the difference between iOS7 and 8 calling sequence was:
iOS7
i) viewWillAppear is called.
ii) layout of subviews is called. From the stack this seems to relate to the navigation bar and animation.
ii) viewDidAppear is called.
iOS8
i) viewWillAppear is called.
ii) layout of subviews is called. From the stack this seems to relate to the navigation bar and animation.
iii) exact same layout with exact same stack is called again. So something in the stack must request a rerun from some point.
iv) viewDidAppear is called.
v) An extra layout of subviews is called. This seems driven from a transaction pushed onto the run loop.
i am trying to get x,y coordinates and height,width of an imageview of the view controller in another class..but i failed ...please tell me where did i made mistake..?
this is my code of view controller and class that i given to the view controller
view controller.m
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
dot1=[[UIImageView alloc]initWithFrame:CGRectMake(145, 30, 20, 20)];
dot1.backgroundColor=[UIColor blackColor];
dot1.layer.cornerRadius=11.0;
dot2=[[UIImageView alloc]initWithFrame:CGRectMake(80, 130, 20, 20)];
dot2.backgroundColor=[UIColor blackColor];
dot2.layer.cornerRadius=11.0;
NSLog(#" (x==%f,y==%f,w==%f,h==%f)",dot1.frame.origin.x,dot1.frame.origin.y,dot1.frame.size.width,dot1.frame.size.height);//here i got the correct output..
}
and this code belongs to the class view.m....
- (void)drawRect:(CGRect)rect
{
ViewController*vc=[[ViewController alloc]init];
NSLog(#"(x==%f,y==%f,w==%f,h==%f)",vc.dot1.frame.origin.x,vc.dot1.frame.origin.y,vc.dot1.frame.size.width,vc.dot1.frame.size.height);//but here i got all points 0.
}
note: i gives class to the view controller is view.
All points of imageView frame will be 0 as you are allocating the imageView dot1 and dot2 inside - (void)viewDidLoad which won't be called on alloc and init. So if you want to get the imageView's proper frame you need to set it in init method which is the only method you are calling from your drawRect: function.
Have a cgrect and pass the frame
CGRect requiredRect;
-(void)initWithRect:(CGRect) rectLocal
{
requiredRect = rectLocal;
}
I am building an app that displays an image in landscape and portrait modes. Rotating works perfectly. The image is also perfectly positioned in landscape mode. However it keeps its landscape coordinates in portrait, which misplace it as a result. Please find my code below. Could you let me know what I'm missing? Is there also a way to achieve this strictly from a Xib file?
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[[self navigationController] setNavigationBarHidden:YES animated:NO];
UIImage *startImage = [UIImage imageNamed:#"title.png"];
UIImageView *startImageView = [[UIImageView alloc] initWithImage:startImage];
if (curOrientation == UIInterfaceOrientationPortrait || curOrientation == UIInterfaceOrientationPortraitUpsideDown) {
[startImageView setFrame:CGRectMake(-128, 0, 1024, 1024)];
}else{
[startImageView setFrame:CGRectMake(0, -128, 1024, 1024)];
}
[self.view addSubview:startImageView];
}
Currently you are only calling this code when the view is first loaded. You actually need to call it
whenever the view appears onscreen (in case the device was rotated while it was offscreen)
whenever the device is rotated
but you should keep the view creation code in viewDidLoad, as you only want to create it once.
Make a property to keep a pointer to the view so that you can refer to it from all of these places in your codeā¦
#property (nonatomic, weak) UIImageView* startImageView;
Create it in viewDidLoad (but don't worry then about the geometry, as you can do this in viewWillAppear):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[[self navigationController] setNavigationBarHidden:YES animated:NO];
UIImage *startImage = [UIImage imageNamed:#"title.png"];
UIImageView *startImageView = [[UIImageView alloc] initWithImage:startImage];
self.startImageView = startImageView;
[self.view addSubview:startImageView];
}
Make a generic orientation method:
- (void) orientStartImageView
{
UIInterfaceOrientation curOrientation = [UIApplication sharedApplication].statusBarOrientation;
if (curOrientation == UIInterfaceOrientationPortrait || curOrientation == UIInterfaceOrientationPortraitUpsideDown) {
[self.startImageView setFrame:CGRectMake(-128, 0, 1200, 1200)];
}else{
[self.startImageView setFrame:CGRectMake(0, -128, 1200, 1200)];
}
}
Call it from viewWillAppear (triggered every time the view comes onscreen):
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self orientStartImageView];
}
Call it from viewWillLayoutSubviews (triggered every time the view IS onscreen and the device rotates):
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self orientStartImageView];
}
By the way, I am not sure your frames are correct - in portrait you are shifting the left edge offscreen, in landscape you are shifting the top edge offscreen. Is that what you want? It may well be that you can achieve what you want in Interface Builder, but it is not clear from your code what that is - maybe you could post a picture. Also check that you have Autolayout disabled (checkbox in Interface Builder's file inspector) to simplify issues.
update
You may be able to do this from the Xib with no code: centre the imageView in it's superView, set it's size to your final size (eg 1200x1200), disable Autolayout, deselect all springs and struts, set your View Mode appropriately (eg center or scaleToFill)
I have a couple of modal view controllers of certain size. I'm trying to avoid the use of custom views (creating full screen black translucent overlay over current view, add the modal view over that view, do the animations, etc) to present it because there is no modalPresentationStyle that fits the size of my controllers.
Now I'm using UIModalPresentationPageSheet but my view is smaller in height and I have an ugly blank space
Desired presentation
_______________
| _______ |
| | | |
| | MyVC | |
| | | |
| ------- |
---------------
Actual presentation
_______________
| | | |
| | MyVC | |
| | | |
| |-------| |
| | blank | |
---------------
If I use the UIModalPresentationFormSheet the container is smaller in width.
I'm trying to figure out how to do it but I don't know if it's possible. What is the solution to the problem of presenting a modal VC smaller than any of the presentationStyles? The only solution is to arrange a "custom modal view controller engine"? Popovers doesn't fit my design requirements :(
As some other users here, I also had the problem that the origin of the modal view controller was not correct. After some experiments, I found a solution that worked for me:
- (void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.view.superview.bounds = CGRectMake(0, 0, <width>, <height>);
}
I obtained the following result (link text) by using:
self.modalPresentationStyle = UIModalPresentationFormSheet;
and by presenting it as a modal view controller. Let me know if you need further help.
All these answers will not work on ios8 SDK.
This will work:
AboutViewController * _aboutViewController = [[AboutViewController alloc] init];
_aboutViewController.modalPresentationStyle = UIModalPresentationFormSheet;
if(IS_IOS8)
{
_aboutViewController.preferredContentSize = CGSizeMake(300, 300);
}
[self presentViewController:_aboutViewController animated:YES completion:nil];
In AboutViewController.m
- (void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
if(!IS_IOS8)
{
self.view.superview.bounds = CGRectMake(0, 0, 300, 300);
}
}
IS_IOS8
#define IS_IOS8 ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8)
In iOS 8 you can also use UIPresentationController which gives you more customization options.
I had an issue getting the custom sized modal to center until I figured out what ViewController.view.superview.frame was set to initially. It is determined based on the UIModalPresentation type. superview.center isn't even needed(as far as my testing shows).
Also, I set the coordinates dynamically using [UIScreen mainScreen].applicationFrame, only supporting landscape orientation for now. You have to do a conditional check to support both portrait and landscape by flipping the X/Y and Height/Width values.
Here was my first working solution for iOS 6.0:
ViewController.modalPresentationStyle = UIModalPresentationFormSheet;
ViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:ViewController animated:YES completion:nil];
ViewController.view.superview.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
ViewController.view.superview.frame = CGRectMake(
// Calcuation based on landscape orientation (width=height)
([UIScreen mainScreen].applicationFrame.size.height/2)-(320/2),// X
([UIScreen mainScreen].applicationFrame.size.width/2)-(320/2),// Y
320,// Width
320// Height
);
And then I looked at the previous answer and face palmed. Shortens my solution a tad by replacing the last line with:
ViewController.view.superview.bounds = CGRectMake(0, 0, 320, 320);
EDIT for final solution and remarks.
The best way to do this is to change the bounds property on the superview of the view being presented inside the formsheet. When you present a view modally, the presented view is embedded inside another view.
You should set the bounds property of the superview to the same rect as the view's bounds. First add a private ivar in your class:
#implementation MySpecialFormsheet {
CGRect _realBounds;
}
It's best to do this in viewWillAppear:. Add the following code to the view controller that is being presented modally:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.view.superview.bounds = _realBounds;
}
You need to cache _realBounds in your viewDidLoad method:
- (void)viewDidLoad {
_realBounds = self.view.bounds;
[super viewDidLoad];
}
even for reducing the forma sheet height and width you can use
[self.view.superview setBounds:CGRectMake(0, 0, 450, 480)];
This is my solution for a NOT autolayout view (never tried with autolayout) and compliant with iOS 7 and iOS 8.
At first be sure to call the modal view in this way:
CloudSettingsViewController *cloudController = [[CloudSettingsViewController alloc] initWithNibName:#"CloudSettingsView" bundle:nil];
CGRect originalBounds = cloudController.view.bounds;
cloudController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
cloudController.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentViewController:cloudController animated:YES completion:nil];
With iOS 8 set the preferredContentSize in the viewDidLoad method of the modal view.
- (void)viewDidLoad {
[super viewDidLoad];
// backup of the original size (selected in interface builder)
height = self.view.frame.size.height;
width = self.view.frame.size.width;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8) // versioni successive ios 8
self.preferredContentSize = CGSizeMake(width, height); // this is necessary to get the correct size of the modal view
}
This works ALSO when the modal view needs to display the keyboard on iPad.
With previous versions of iOS 8 you should set the bounds after presentViewController call:
[currentController presentViewController:controlPanelController animated:YES completion:nil];
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8)
cloudController.view.superview.bounds = originalBounds;//it's important to do this after
The approaches described above need to be tweaked for iOS7:
// set desired bounds, then call -presentFromViewController:
#implementation PresentedViewController
{
CGRect _presentationBounds;
}
- (void)presentFromViewController:(UIViewController *)viewController
{
self.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
self.modalPresentationStyle = UIModalPresentationFormSheet;
_presentationBounds = self.view.bounds;
[viewController presentViewController:self animated:YES completion:nil];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
if (!CGRectIsEmpty(_presentationBounds))
{
self.view.superview.bounds = _presentationBounds;
_presentationBounds = CGRectZero;
}
}
(This code also works on iOS6.)
Even I was struggling with the same issue for quite a while. After giving few tries from many of the answers given here I came up with a solution that worked for me pretty well.
Here is the code while presenting the view controller.
SomeViewController *sovcObj = [[SomeViewController alloc] initWithNibName:#"SyncOptionsViewController" bundle:nil];
someObj.modalPresentationStyle = UIModalPresentationFormSheet;
someObj.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:someObj animated:YES completion:nil];
someObj.view.superview.bounds = CGRectMake(0, 0, 400, 400); // whatever width and height you want
In the xib (if you are using one) in the 'Simulated Metrics' section select 'Freeform' size.
Apart from this you need to write the below code in your presented view controller (i.e.., SomeViewController in this example)
- (void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.view.superview.bounds = CGRectMake(0, 0, 400, 400); // whatever width and height you want
}
This code also works for iOS 7 as well
One solution is to use UIModalPresentationPageSheet to present a page sheet and then immediately resize the modal view. This is fully explained in this answer to another question.
Resize the modal view "after" it is presented using presentModalViewController method.See this link to resize modal View https://coderwall.com/p/vebqaq
You can use MZFormSheetController like this:
MZFormSheetController *formSheet = [[MZFormSheetController alloc] initWithSize:customSize viewController:presentedViewController];
[presentingViewController mz_presentFormSheetController:formSheet animated:YES completionHandler:nil];
This solution worked for me as I had the same issue.
My modal view structure is the following (I use UIModalPresentationCurrentContext at both presenting and presented controllers to achieve transparency in the presented VC)
ModalViewController
> UIView (view) - resized to the same size as the presenting controller but has semi-translucent background - done automatically
>> UIView (contentView) - the true view with the content of the popup - this is the one that got consistently placed at the top of the screen all the time
In ModalViewController I overwrote the following method to fix the positioning issue:
- (void) viewWillLayoutSubviews(){
[super viewWillLayoutSubviews];
CGRect r = self.view.bounds();
self.contentView.center = CGPointMake(r.size.width/2,r.size.height/2);
}
I actually create and size the content view first but in different method.
Hope this helps someone.