I have a MainViewController that contains a ContainerViewController.
The ContainerViewController starts out showing childViewControllerA, and dynamically switches it out to childViewControllerB when a button in the childViewControllerA is clicked:
func showNextViewContoller() {
let childViewControllerB = ChildViewControllerB()
container.addViewController(childViewControllerB)
container.children.first?.remove() // Remove childViewControllerA
}
Here’s a diagram:
The second view controller (ViewControllerB) has an image view that I’d like to show in the center. So I assigned it the following constraints:
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6),
])
The problem I’m running into is the imageView is not centered vertically: It’s lower than it should be.
When I run the app so that ContainerVeiwController shows childViewControllerB first, then it works as intended. The issue occurs only when childViewControllerB is switched in dynamically after childViewControllerA:
To help debug, I added the following code to all three ViewControllers:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("MainViewController bounds = (self.view.bounds)")
}
And this gave an interesting print out (running this on an iPhone 13 mini simulator):
MainViewController bounds = (0.0, 0.0, 375.0, 812.0) //iPhone 13 mini screen is 375 x 812
ChildViewControllerA bounds = (0.0, 0.0, 375.0,738.0). // 812 - 50 safety margin - 24 titlebar = 738.
Now, the switch happens after the button was clicked and childViewControllerB is added:
ChildViewControllerB bounds = (0.0, 0.0, 375.0, 812.0)
It seems like ChildViewControllerB is assuming a full screen size and ignoring the bounds of it’s parent view controller (ContainerViewController). So, the imageView’s hightAnchor is based on the full screen height, causing it to appear off center.
So, I changed the constraints on the imageView to:
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),
])
Next, I tried to force a layout update by adding any of these lines after the switch happens in showNextViewController() function above:
container.children.first?.view.layoutSubviews()
//and
container.children.first?.view.setNeedsLayout()
None of them worked.
How do I get ChildViewControllerB to respect the bounds of ContainerViewController?
If it helps, the imageView only needs to be in the center initially. It’ll eventually have a pan, pinch and rotate gesture attached, so the user can move it anywhere they want.
Edit 01:
This is how I’m adding and removing a child view controller:
extension UIViewController {
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
guard parent != nil else { return }
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
Edit 02:
On recommendation by a few commentators, I updated the addViewController() function:
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.view.topAnchor.constraint(equalTo: self.view.topAnchor),
child.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
child.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
])
child.didMove(toParent: self)
}
This didn’t seem to work, I got errors saying "Unable to simultaneously satisfy constraints." Unfortunately I have very little knowledge on how to decipher the error messages…
Edit 03: Simplified Project:
Here’s a simplified project. There are four files plus AppDelegate (I’m not using a storyboard):
- MainViewController
- ViewControllerA
- ViewControllerB
- Utilities
- AppDelegate
MainViewController:
import UIKit
class MainViewController: UIViewController {
let titleBarView = UIView(frame: .zero)
let container = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
setup()
layout()
}
func setup() {
titleBarView.backgroundColor = .gray
view.addSubview(titleBarView)
addViewController(container)
showViewControllerA()
}
func layout() {
titleBarView.translatesAutoresizingMaskIntoConstraints = false
container.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
titleBarView.heightAnchor.constraint(equalToConstant: 24),
container.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor, constant: 0),
container.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
container.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
container.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
func showViewControllerA() {
let viewControllerA = ViewControllerA()
viewControllerA.delegate = self
container.children.first?.remove()
container.addViewController(viewControllerA)
}
func showViewControllerB() {
let viewControllerB = ViewControllerB()
container.children.first?.remove()
container.addViewController(viewControllerB)
}
}
extension MainViewController: ViewControllerADelegate {
func nextViewController() {
showViewControllerB()
}
}
ViewController A:
protocol ViewControllerADelegate: AnyObject {
func nextViewController()
}
class ViewControllerA: UIViewController {
let nextButton = UIButton()
weak var delegate: ViewControllerADelegate?
override func viewDidLoad() {
super.viewDidLoad()
setup()
layout()
view.backgroundColor = .gray
}
func setup() {
nextButton.setTitle("next", for: .normal)
nextButton.addTarget(self, action: #selector(nextButtonPressed), for: .primaryActionTriggered)
view.addSubview(nextButton)
}
func layout() {
nextButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
nextButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc func nextButtonPressed() {
delegate?.nextViewController()
}
}
ViewController B:
import UIKit
class ViewControllerB: UIViewController {
let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
setup()
layout()
}
func setup() {
view.addSubview(imageView)
blankImage()
}
func layout() {
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.magnificationFilter = CALayerContentsFilter.nearest;
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),
])
view.layoutSubviews()
}
func blankImage() {
let ciImage = CIImage(cgImage: createBlankCGImage(width: 32, height: 64)!)
imageView.image = cIImageToUIImage(ciimage: ciImage, context: CIContext())
}
}
Utilities:
import Foundation
import UIKit
func createBlankCGImage(width: Int, height: Int) -> CGImage? {
let bounds = CGRect(x: 0, y:0, width: width, height: height)
let intWidth = Int(ceil(bounds.width))
let intHeight = Int(ceil(bounds.height))
let bitmapContext = CGContext(data: nil,
width: intWidth, height: intHeight,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
if let cgContext = bitmapContext {
cgContext.saveGState()
let r = CGFloat.random(in: 0...1)
let g = CGFloat.random(in: 0...1)
let b = CGFloat.random(in: 0...1)
cgContext.setFillColor(red: r, green: g, blue: b, alpha: 1)
cgContext.fill(bounds)
cgContext.restoreGState()
return cgContext.makeImage()
}
return nil
}
func cIImageToUIImage(ciimage: CIImage, context: CIContext) -> UIImage? {
if let cgimg = context.createCGImage(ciimage, from: ciimage.extent) {
return UIImage(cgImage: cgimg)
}
return nil
}
extension UIViewController {
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
guard parent != nil else { return }
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
AppDelegate:
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = .white
window?.makeKeyAndVisible()
window?.rootViewController = MainViewController()
return true
}
}
2
Answers
Update as following:
In MainViewViewController update the layout() function:
Asteroid is on the right track, but couple other issues…
You are not giving the views of the child controllers any constraints, so they load at their "native" size.
Changing your
addViewController(...)
func as advised by Asteroid solves theA
andB
missing constraints, but…You are calling that same func for your
container
controller and adding constraints to its view inlayout()
, so you end up with conflicting constraints.One solution would be to change your
addViewController
func to this:then in
setup()
:while leaving your other "add view controller" calls like this:
The other thing that may throw you off is the extraneous
super.
in your image view constraints: