1

I am trying to place an UIPageViewController inside a UIView with no predetermined height. What I am trying to achieve is build a responsive In-app Message that has one or more components (buttons, texts, carousels, etc) that has its height determined by its children.

The modal window works fine with all components except the UIPageViewController. The UIView resizes based on whatever components I put into it, but not when I place an UIPageViewController. It seems like there are some constraints missing.

The general hierarchy is:

UIView (centered vertically on the screen with trailing and leading margins, no height)
|  UIStackView (inside this UIStackView I place any number of components I wish)
|  |  UILabel (example component)
|  |  UIButton (example component)
|  |  UIPageViewController (example component)
|  |  |  UIStackView (inside this UIStackView I place any other number of components)
|  |  |  |  UILabel (example component)
|  |  |  |  UIButton (example component)
…

The following log excerpt belongs to an In-app Message with these characteristics:

UIView (centered vertically on the screen with trailing and leading margins, no height)
|  UIStackView (leading and trailing anchors equal to parent, top and bottom anchors equal to children)
|  |  UIPageViewController (will display bellow the first slide content)
|  |  |  UIStackView
|  |  |  |  UIImageView
|  |  |  |  UILabel
|  |  |  |  UILabel

Logs:

(lldb) po [[0x7feeb7a78e30 superview ] recursiveDescription]
<UIView: 0x7feeb76c44f0; frame = (0 0; 334 0); layer = <CALayer: 0x600002afd1c0>>
   | <_UIPageViewControllerContentView: 0x7feeb7a78e30; frame = (0 0; 334 0); clipsToBounds = YES; opaque = NO; autoresize = W+H; layer = <CALayer: 0x600002ae1500>>
   |    | <_UIQueuingScrollView: 0x7feeafbc6800; frame = (0 0; 334 0); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x600002773600>; layer = <CALayer: 0x600002ae0e80>; contentOffset: {334, 0}; contentSize: {1002, 0}; adjustedContentInset: {0, -334, 0, -334}>
   |    |    | <UIView: 0x7feeb7a79b30; frame = (0 0; 334 0); layer = <CALayer: 0x600002ae04a0>>
   |    |    | <UIView: 0x7feeb7a79ca0; frame = (334 0; 334 0); layer = <CALayer: 0x600002ae0880>>
   |    |    |    | <UIView: 0x7feeb6f25990; frame = (0 0; 757 219.667); autoresize = W+H; layer = <CALayer: 0x600002aed700>>
   |    |    |    |    | <UIStackView: 0x7feeb6fb7a10; frame = (0 0; 757 219.667); layer = <CALayer: 0x600002aed8c0>>
   |    |    |    |    |    | <UIImageView: 0x7feeb76ceb70; frame = (0 0; 757 131.667); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x600002aff560>>
   |    |    |    |    |    | <UILabel: 0x7feeb76ced40; frame = (0 141.667; 757 29.6667); text = 'This is the first slide t...'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000bddb30>>
   |    |    |    |    |    | <UILabel: 0x7feeb6fb7c60; frame = (0 181.333; 757 18.3333); text = 'This is the description f...'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000bca0d0>>
   |    |    | <UIView: 0x7feeb7a79e10; frame = (668 0; 334 0); layer = <CALayer: 0x600002ae0840>>
   |    | <UIPageControl: 0x7feeb76cb9b0; frame = (10 -25; 314 20); autoresize = W; userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x600002693180>; layer = <CALayer: 0x600002afdac0>>
   |    |    | <_UIPageControlContentView: 0x7feeb6fb4b10; frame = (111.667 -3; 91 26); layer = <CALayer: 0x600002aed0c0>>
   |    |    |    | <_UIPageControlIndicatorContentView: 0x7feeb6fb73b0; frame = (14.3333 0; 62.6667 26); autoresize = H; layer = <CALayer: 0x600002aec080>>
   |    |    |    |    | <_UIPageIndicatorView: 0x7feeb6f27570; frame = (0.166667 8.16667; 9.66667 9.66667); opaque = NO; userInteractionEnabled = NO; tintColor = UIExtendedGrayColorSpace 0.333333 1; layer = <CALayer: 0x600002ae5b00>>
   |    |    |    |    | <_UIPageIndicatorView: 0x7feeb0809630; frame = (17.5 8.16667; 9.66667 9.66667); opaque = NO; userInteractionEnabled = NO; tintColor = UIExtendedGrayColorSpace 0.666667 1; layer = <CALayer: 0x600002a01000>>
   |    |    |    |    | <_UIPageIndicatorView: 0x7feeb6cd34c0; frame = (35.5 8.16667; 9.66667 9.66667); opaque = NO; userInteractionEnabled = NO; tintColor = UIExtendedGrayColorSpace 0.666667 1; layer = <CALayer: 0x600002a01080>>
   |    |    |    |    | <_UIPageIndicatorView: 0x7feeb6fc6100; frame = (53.1667 8.16667; 9.66667 9.66667); opaque = NO; userInteractionEnabled = NO; tintColor = UIExtendedGrayColorSpace 0.666667 1; layer = <CALayer: 0x600002ae0140>>

Here are two examples. The first is a working In-app Message with no UIPageViewController. The second is what I am trying to achieve, but as a working example on Android.

Working In-app Message without UIPageViewController Working example of carousel on Android

Based on these logs, can anyone point me in the right direction of correcting the constraints? What happens is that the views higher in the hierarchy have 0 height, though their children have not. This results in a 0 height In-app Message.

Edit

I am adding the modal view as a subview to the main view and I add the UIPageViewController to the top controller when it is present in the hierarchy:

All is done via code. I am not adding a view controller, only a subview to my main view:

UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
if (keyWindow != nil) {
    [keyWindow addSubview:self.parentView]; // self.parentView = blackish background + vertically centered UIView (the actual modal view) + UIStackView for the components
    if (hasCarousel){
        UIViewController *topController = [UIApplication getPresentedViewController];
        if (topController != nil)
            [topController embedViewController:pageViewController inView:carouselWrapper]; // carouselWrapper = UIView containing the pageViewController that is added to the UIStackView above

    }
}
rgoldenb
  • 13
  • 3
  • So, you're showing a modal view controller, currently laid-out correctly with a `UIImageView`... and you want to replace the image view with a `UIPageViewController`? but... you want the page view controller to size itself based on the view controllers (the "pages") it's loading? Are you laying this out in Storyboard or all via code? – DonMag Nov 16 '20 at 21:43
  • Thank you for your response. Actually the content inside the modal view is completely arbitrary. I can have buttons, texts, images etc on the outer-most UIStackView and one of these components can be a UIPageViewController and you are correct, I want to resize itself based on its view controllers (pages) each of them containing an UIStackView. It is all done through code – rgoldenb Nov 17 '20 at 13:33
  • OK - the question is: *"resize itself based on its view controllers (pages)"* ... Are you thinking that as you swipe from page-to-page, the height of the Page View Controller (and thus the height of your modal view) is going to keep changing? Or, can each "page" have the same height as the (dynamically sized) first page? – DonMag Nov 17 '20 at 14:02
  • The intention is to resize the UIPageViewController based on its bigger child, so the modal won't keep changing when you swipe and will be guaranteed to accommodate all its children – rgoldenb Nov 17 '20 at 14:21
  • Are you designing your views in Storyboard, or via code only? Is this a **presented** view controller? Or a view added as a subview to your main view? – DonMag Nov 17 '20 at 14:36
  • I just edited the question to add more information. I have a blackish background with a vertically centered UIView with a UIStackView as subview created in Storyboard that is added as subview to the main view of the app. The remaining components are all added through code (UIPageViewController as well) inside the UIStackView – rgoldenb Nov 17 '20 at 14:53
  • One approach... For each "page" view controller: - get its height based on its dynamic content using `.systemLayoutSizeFitting(...)` - use the max height from those pages as your page view controller's view height. I can post some example code as an answer in a bit. – DonMag Nov 17 '20 at 15:30
  • Thanks @DonMag. I'll try that out and will compare to the code you'll post. I'll let you know if it works out – rgoldenb Nov 17 '20 at 16:17

1 Answers1

0

To provide an answer based on our comments...

A UIPageViewController loads and sets the view size of its "page" controllers. So, trying to do it the other way around - setting the Page View Controller's size to match the "page" size is a little tricky.

In addition, if we tried to do that for each "page," the frame would keep changing and would (likely) be a bad user experience.

We can, however, pre-load the pages and calculate the maximum page height to setup our controller.

Overview code would be:

MutableArray *pages = [NSMutableArray new];
// however we load and initialize our page controllers
[pages addObject:FirstPageVC()];
[pages addObject:SecondPageVC()];
// etc

CGFloat maxHeight = 0;

CGSize fitSize = CGSizeMake(_carouselWrapper.frame.size.width, UILayoutFittingCompressedSize.height);

for (UIViewController *vc in pages) {
    // get size of page view
    CGSize sz = [vc.view systemLayoutSizeFittingSize:fitSize withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityDefaultLow];
    maxHeight = MAX(sz.height, maxHeight);
}

// if using the built-in PageControl
//  add height of PageControl + 1.5 (control is shown 1.5-pts below the view)
UIPageControl *c = [UIPageControl new];
maxHeight += [c sizeForNumberOfPages:1].height + 1.5;

// here we can set the height of the view frame for our PageViewController

I played around with it a bit and put a working example project here: https://github.com/DonMag/DynamicPGVC

Basic idea of what you showed, although I made the "parentView" (I call it an OverlayView) a separate view subclass to keep a lot of the activity out of the main view controller.

Here's how it looks:

enter image description here

Tapping "Label" button (the thin rectangle shows the frame of the "carouselView"):

enter image description here

Tapping "Image" button:

enter image description here

Tapping "Page View Controller" button - first "page" (The "pages" are simple - vertical stack view with title and content labels):

enter image description here

and second page, showing the max height of the content:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • thank you for the thorough response! I appreciated the example. I got it working with your insights. I guess the key was to have the subclass of UIStackView and using the correct lifecycle callbacks to get the correct measurement of each page coupled with the systemLayoutSizeFitting, which I didn't get right.That was great, couldn't expect a better answer. All the best – rgoldenb Nov 19 '20 at 02:36