当容器视图控制器动态更改子视图控制器时布局不起作用

Layout not working when container view controller dynamically changes child view controller

我有一个 MainViewController,其中包含一个 ContainerViewController。

ContainerViewController 开始显示 childViewControllerA,并在单击 childViewControllerA 中的按钮时动态切换到 childViewControllerB :

func showNextViewContoller() {

    let childViewControllerB = ChildViewControllerB()

    container.addViewController(childViewControllerB)
    container.children.first?.remove()  // Remove childViewControllerA
}

这是一张图表:

第二个视图控制器 (ViewControllerB) 有一个我想在中心显示的图像视图。所以我给它分配了以下约束:

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),
    ])

我 运行 遇到的问题是 imageView 没有垂直居中:它低于应有的位置。

当我 运行 应用程序以便 ContainerVeiwController 首先显示 childViewControllerB 时,它会按预期工作。只有在 childViewControllerA:

之后动态切换 childViewControllerB 时才会出现此问题

为了帮助调试,我在所有三个 ViewController 中添加了以下代码:

override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        print("MainViewController bounds = \(self.view.bounds)")
    }

这给出了一个有趣的打印输出(运行在 iPhone 13 迷你模拟器上打印):

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.

现在,在单击按钮并添加子 ViewControllerB 后发生切换:

ChildViewControllerB bounds = (0.0, 0.0, 375.0, 812.0)

似乎 ChildViewControllerB 假设全屏大小并忽略其父视图控制器 (ContainerViewController) 的边界。所以,imageView的hightAnchor是基于全屏高度的,导致它出现偏心。

因此,我将 imageView 的约束更改为:

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),
    ])

接下来,我试图通过在上面的 showNextViewController() 函数中发生切换后添加这些行来强制更新布局:

container.children.first?.view.layoutSubviews()

//and

container.children.first?.view.setNeedsLayout()

None 他们工作了。

如何让 ChildViewControllerB 遵守 ContainerViewController 的边界?

如果有帮助,imageView 最初只需要位于中心即可。它最终会附带一个平移、捏合和旋转手势,因此用户可以将它移动到他们想要的任何地方。

编辑 01:

这就是我添加和删除子视图控制器的方式:

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()
    }
}

编辑 02:

根据一些评论员的推荐,我更新了 addViewController() 函数:

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)
        
    }

这似乎不起作用,我收到错误消息“无法同时满足约束条件。”不幸的是,我对如何破译错误信息知之甚少...

编辑 03:简化项目:

这是一个简化的项目。有四个文件加上 AppDelegate(我没有使用故事板):

主要ViewController:

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答:

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乙:

导入 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())
    }
}

实用程序:

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
    }
}

更新如下:

func addViewController(_ child: UIViewController) {
    addChild(child)
    view.addSubview(child.view)
    child.didMove(toParent: self)
        
    child.view.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint.activate([
        child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        child.view.topAnchor.constraint(equalTo: view.topAnchor),
        child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)])
}
    

MainViewViewController中更新layout()函数:

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),
    ])
        
}

小行星走在正确的轨道上,但还有一些其他问题...

您没有给子控制器的视图任何约束,因此它们以“原始”大小加载。

按照 Asteroid 的建议更改 addViewController(...) 函数可以解决 AB 缺少的约束,但是...

您正在为您的 container 控制器调用相同的函数 并且 在 [=18= 中为其视图添加约束],所以你最终会遇到约束冲突。

一个解决方案是将您的 addViewController 函数更改为:

func addViewController(_ child: UIViewController, constrainToSuperview: Bool = true) {
    addChild(child)
    view.addSubview(child.view)
    
    if constrainToSuperview {
        child.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            child.view.topAnchor.constraint(equalTo: view.topAnchor),
            child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
    
    child.didMove(toParent: self)
}

然后在 setup():

func setup() {
    
    titleBarView.backgroundColor = .red
    view.addSubview(titleBarView)

    // change this
    //addViewController(container)
    
    // to this
    addViewController(container, constrainToSuperview: false)

    showViewControllerA()
}

同时留下您的其他“添加视图控制器”调用,如下所示:

container.addViewController(viewControllerA)
container.addViewController(viewControllerB)

另一件可能让您失望的事情是图像视图约束中的无关 super.

    NSLayoutConstraint.activate([
        // change this
        //imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
        //imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
        //imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),

        // to this
        imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        imageView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6),
    ])