Mac Catalyst popover set/limit size problem - ios

Is it possible to limit/fix (min/max) popover size on Mac Catalyst? See the attached video.
Example video

Yes, but it's a bit of a hack. Big Sur is loading these presented view controllers as their own windows, so we can grab the window's windowScene and set its sizeRestrictions. The best (?) place to do this is in the presented view controller's viewWillLayoutSubviews method:
class MyPresentedViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if #available(macCatalyst 14, *) {
view.window?.windowScene?.sizeRestrictions?.minimumSize = CGSize(width: 500, height: 500)
view.window?.windowScene?.sizeRestrictions?.maximumSize = CGSize(width: 800, height: 800)
}
}
}
If you don’t want the presented view to be resizable at all, just set the minimumSize and maximumSize to the same value.
I don't love using viewWillLayoutSubviews like this, but the windowScene is still nil in viewDidLoad and viewWillAppear, and while it is non-nil in viewDidAppear, setting sizeRestrictions there will cause a visible resize on screen.
The good news is that this problem may be fixed in Big Sur 11.1. According to the beta release notes, macOS 11.1 will respect preferredContentSize and they won't be resizable by default:
When you present a view controller with page sheet or form sheet presentation style, the size of the view controller’s root view is, by default, determined by the value returned from the presented view controller’s preferredContentSize method and the view is not resizable. You can arrange for the presented view controller to be resizable by using Auto Layout to specify the maximum and minimum sizes of its root view. To enable this, set the canResizeToFitContent property of the application’s main window to YES. One way to do this is to override the willMove(toWindow:) or didMoveToWindow() methods of a view in the main view controller. (65254666)

Related

iOS Personal Hotspot breaks layout when navigation bar is hidden

I'm writing an application for iOS with Swift and I'm using the auto layout in all my View Controllers but When the personal Hotspot is activated, the view doesn't resize correctly and The bottom of the view goes below the screen. I found It doesn't happen to my all views, except in the views that I have this line of code:
navigationController?.navigationBar.isHidden = true
How can I handle this situation?
I found a solution. I always have this problem when I add a child view controller and the bottom of the child view goes below the screen. I found when the blue bar appears, the height of my parent view controller will be lower than the device screen's height. So I need to change the child view position in the situation.
if let parentHeight = parent?.view.frame.height, parentHeight < UIScreen.main.bounds.height {
view.frame.origin.y = UIScreen.main.bounds.height-childViewHeight-8
}

Custom container does not adjust correctly layout to in-call status bar

I am using my own custom container view controller that is modally
presented from a root view controller (so the container is not the root view controller itself). Root view controller gets its layout handled properly when the in-call status bar is shown. Now if I modally present the custom container with its child (lets say instance of SomeViewController), the child is laid out as expected. If an in-call status bar is shown while the custom container is already presented, the child adjusts correctly.
The problem arises when the in-call status bar is shown while the custom container is not yet presented. If I present the custom container while there is an in-call status bar, the bottom of the child view gets cropped by the size of the extended status bar (i.e., 20 points) - it seems like either the size of the frame is not correct, or there is an offset set to it. If I dismiss in-call status bar, the top gets adjusted to the newly gained space, but the bottom stays cropped.
Following shows the relevant code of the container view:
class ContainerController: UIViewController {
var selectedViewController: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
if let selectedViewController = selectedViewController {
initialTransition(to: selectedViewController)
}
}
fileprivate func initialTransition(to viewController: UIViewController) {
guard self.isViewLoaded else {
return
}
self.addChildViewController(viewController)
viewController.view.frame = self.view.frame
self.view.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
}
// rest of the code omitted
}
The container view is presented using this code in the root view controller:
let container = ContainerController()
trainingContainer.selectedViewController = SomeViewController()
self.present(trainingContainer, animated: true, completion: nil)
While there are several SO questions about similar issues (SO question, another SO question, etc.), most of them suggest either solutions that did not work (e.g., old wantsFullScreenLayout and its successors), or seemed a bit too heavyweight (observing status bar did change to adapt layout), especially considering that when the child view controller was presented directly, it was behaving correctly.
After playing around I was able to determine that there was a problem with the frame being set - the frame of the container view controller seemed to be offseted but not resized when the initialTransition(to:) was called (in container's viewDidLoad), thus causing the child the get a frame that overlapped the bottom of the screen by the offset - 20 points.
My first approach was to add setting the frame once again in container's viewDidAppear, which in the end solved the problem, but cause a glitch - for a moment the bottom seemed cropped, and then viewDidAppear was called and the layout was adjusted correctly. This glitch looked bad.
I finally achieved what I wanted by overriding container's viewDidLayoutSubviews and setting the frame of the child there (thus when the container gets notified to adjust its frame to the status bar, the information about a new frame gets passed to the child).
override func viewDidLayoutSubviews() {
self.selectedViewController?.view.frame = self.view.frame
super.viewDidLayoutSubviews()
}

Custom camera view overlay size problems when app rotates

I am using a custom camera view overlay in Swift 3. When I move the camera from landscape to portrait, it cuts the camera view size down. Is there a way to check the device orientation and change the frame bounds? Right now the line of code I'm using is
previewLayer?.frame = self.view.bounds
You got several options for this. The main idea is to get a function to be called whenever there is a change in the view.
Option 1, iOS8+
As of iOS 8, all rotation-related methods are deprecated. Instead, rotations are treated as a change in the size of the view controller’s view and are therefore reported using the viewWillTransition(to:with:) method. When the interface orientation changes, UIKit calls this method on the window’s root view controller. That view controller then notifies its child view controllers, propagating the message throughout the view controller hierarchy.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
var previewLayer:CALayer?
if let layer = previewLayer {
layer.frame = self.view.bounds //or:
layer.frame.size = size //If coordinates is (x:0,y:0) you only need to update the size, and that is provided through the function
}
}
Option 2, iOS 6 & 7
In iOS 6 and iOS 7, your app supports the interface orientations defined in your app’s Info.plist file. A view controller can override the supportedInterfaceOrientations method to limit the list of supported orientations. Typically, the system calls this method only on the root view controller of the window or a view controller presented to fill the entire screen; child view controllers use the portion of the window provided for them by their parent view controller and no longer participate directly in decisions about what rotations are supported. The intersection of the app's orientation mask and the view controller's orientation mask is used to determine which orientations a view controller can be rotated into.
When a rotation occurs for a visible view controller, the willRotate(to:duration:), willAnimateRotation(to:duration:), and didRotate(from:) methods are called during the rotation. The viewWillLayoutSubviews() method is also called after the view is resized and positioned by its parent. If a view controller is not visible when an orientation change occurs, then the rotation methods are never called. However, the viewWillLayoutSubviews() method is called when the view becomes visible. Your implementation of this method can call the statusBarOrientation method to determine the device orientation.
When the orientation changes this method will be called and therefor you can update previewLayer.frame there.
override func viewDidLayoutSubviews() {
var previewLayer:CALayer?
if let layer = previewLayer {
layer.frame = self.view.bounds
}
}
or
override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
var previewLayer:CALayer?
if let layer = previewLayer {
layer.frame = self.view.bounds
}
}

Size a UIViewController's view to parent UIWindow's bounds

I want to give a UIViewController's view a size that is different from the device's screen size. I know I can usually achieve this by adding the view controller as a child view controller of another parent UIViewController that has defined a frame for the child, but I am in a situation that seems a little different.
I have a UIWindow that only takes up a portion of the screen (it's got a frame that's basically (0, 0, DEVICE_WIDTH, HEIGHT_LESS_THAN_DEVICE_HEIGHT). This window shows up with the proper sizing and positioning when presented. I am setting a view controller as the rootViewController of the window, and then presenting the window by setting its hidden value to false. When this happens, the view controller's view ends up sized to fill the device's screen (i.e. a frame of (0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)).
What I would like is for the view controller to inherit its sizing from the UIWindow it is set as the root view controller of. Is there a way to do this?
I have also tried overriding loadView() and returning a custom-sized view there. Logging the view shows that the view controller's view object is correctly sized during viewDidLoad, but is overwritten with the default size by viewWillAppear:. I would be open to using loadView() to size the view controller if inheriting sizing from the window isn't possible, but I don't know how to make the custom size stick.
Note: The reason why I am trying to add a view controller to the window is because I want to take advantage of the view controller lifecycle methods such as viewDidAppear:, which is why I am not just creating a simple UIView and adding it as a subview of the window.
As counter intuitive as it may seem, if you set set self.view.frame on viewWillAppear (IOS 8) or viewDidAppear (IOS 7) you will be able to make it work.
Swift code (IOS 8):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Banner style size, for example
self.view.frame = CGRectMake(0, 0, 320, 50)
}
For IOS 7, I had to use viewDidAppear, which is obviously an unsatisfactory solution. So I had to start the view with alpha = 0.0 and set alpha = 1.0 on viewDidAppear, after modifying self.view.frame.
Hope it helps.

iPhone, iOS 8: How to presentViewController smaller than original view controller?

I have two view controllers, I want to present view controller(VC) of the first VC. Second VC have smaller size. I want to show second view controller over the first one. Like a popover. You can imagine it, if we add another view controller that slides from bottom to top but stops at navigation bar.
Here is my code:
#IBAction func showSecondVC(sender: AnyObject) {
let secondViewController = storyboard?.instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
secondViewController.view.frame = CGRect(origin: CGPoint(x: view.frame.origin.x, y: header.frame.height), size: CGSize(width: view.frame.width, height: view.frame.height - header.frame.height))
secondViewController.definesPresentationContext = true
secondViewController.providesPresentationContextTransitionStyle = true
secondViewController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.presentViewController(secondViewController, animated: true, completion:nil)
}
You can see that I set frame for secondViewController. I want that it will be with this frame, but if I add completion block and show it's frame after animation, it will be same as first View Controller have.
Edit
I also want to mention, that I try to make this in Portrait orientation.
If you try this in iPhone, the built-in root view of presented view controller will always be full screen, so your efforts to resize it will not be successful. (iPad is a different story)
Just let the root view as it is (i.e. don't fight the fact it's full screen size ), but make its background color clear color. If you want to present some kind of customised view/content/whatever it is..for example a smaller view with some warning..or some options. Just add it as a subview of the root view.
UPDATE:
As the UIViewController class documentation says:
"In a horizontally compact environment, the presented view is always full screen."
The only combination I can imagine for this to work is Iphone6+ in landscape mode where the horizontal size class is then larger then compact. So you are out of luck because you want portrait.
To add the Earl Grey's response, after setting the ViewController's background color to clear. Create a segue with the following properties set:
Kind to "Present Modally"
Presentation to "Over Current Context"

Resources