How to add a Container View programmatically - ios

A Container View can be easily added into a storyboard through Interface Editor. When added, a Container View is of a placeholder view, an embed segue, and a (child) view controller.
However, I am not able to find a way to add a Container View programmatically. Actually, I am not even able to find a class named UIContainerView or so.
A name for the class of Container View is surely a good start. A complete guide including the segue will be much appreciated.
I am aware of View Controller Programming Guide, but I do not regard it as the same as the way Interface Builder does for Container Viewer. For example, when the constraints are properly set, the (child) view will adapts to the size changes in Container View.

A storyboard "container view" is just a standard UIView object. There is no special "container view" type. In fact, if you look at the view hierarchy, you can see that the "container view" is a standard UIView:
To achieve this programmatically, you employ "view controller containment":
Instantiate the child view controller by calling instantiateViewController(withIdentifier:) on the storyboard object.
Call addChild in your parent view controller.
Add the view controller's view to your view hierarchy with addSubview (and also set the frame or constraints as appropriate).
Call the didMove(toParent:) method on the child view controller, passing the reference to the parent view controller.
See Implementing a Container View Controller in the View Controller Programming Guide and the "Implementing a Container View Controller" section of the UIViewController Class Reference.
For example, in Swift 4.2 it might look like:
override func viewDidLoad() {
super.viewDidLoad()
let controller = storyboard!.instantiateViewController(withIdentifier: "Second")
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
controller.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10)
])
controller.didMove(toParent: self)
}
Note, the above doesn't actually add a "container view" to the hierarchy. If you want to do that, you'd do something like:
override func viewDidLoad() {
super.viewDidLoad()
// add container
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
])
// add child view controller view to container
let controller = storyboard!.instantiateViewController(withIdentifier: "Second")
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: containerView.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
controller.didMove(toParent: self)
}
This latter pattern is extremely useful if ever transitioning between different child view controllers and you just want to make sure one child's view is in the same location and the previous child's view (i.e. all the unique constraints for the placement are dictated by the container view, rather than needing to rebuild these constraints each time). But if just performing simple view containment, the need for this separate container view is less compelling.
In the examples above, I’m setting translatesAutosizingMaskIntoConstraints to false defining the constraints myself. You obviously can leave translatesAutosizingMaskIntoConstraints as true and set both the frame and the autosizingMask for the views you add, if you’d prefer.
See previous revisions of this answer for Swift 3 and Swift 2 renditions.

#Rob's answer in Swift 3:
// add container
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
])
// add child view controller view to container
let controller = storyboard!.instantiateViewController(withIdentifier: "Second")
addChildViewController(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: containerView.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
controller.didMove(toParentViewController: self)

Here is my code in swift 5.
class ViewEmbedder {
class func embed(
parent:UIViewController,
container:UIView,
child:UIViewController,
previous:UIViewController?){
if let previous = previous {
removeFromParent(vc: previous)
}
child.willMove(toParent: parent)
parent.addChild(child)
container.addSubview(child.view)
child.didMove(toParent: parent)
let w = container.frame.size.width;
let h = container.frame.size.height;
child.view.frame = CGRect(x: 0, y: 0, width: w, height: h)
}
class func removeFromParent(vc:UIViewController){
vc.willMove(toParent: nil)
vc.view.removeFromSuperview()
vc.removeFromParent()
}
class func embed(withIdentifier id:String, parent:UIViewController, container:UIView, completion:((UIViewController)->Void)? = nil){
let vc = parent.storyboard!.instantiateViewController(withIdentifier: id)
embed(
parent: parent,
container: container,
child: vc,
previous: parent.children.first
)
completion?(vc)
}
}
Usage
#IBOutlet weak var container:UIView!
ViewEmbedder.embed(
withIdentifier: "MyVC", // Storyboard ID
parent: self,
container: self.container){ vc in
// do things when embed complete
}
Use the other embed function with non-storyboard view controller.

Details
Xcode 10.2 (10E125), Swift 5
Solution
import UIKit
class WeakObject {
weak var object: AnyObject?
init(object: AnyObject) { self.object = object}
}
class EmbedController {
private weak var rootViewController: UIViewController?
private var controllers = [WeakObject]()
init (rootViewController: UIViewController) { self.rootViewController = rootViewController }
func append(viewController: UIViewController) {
guard let rootViewController = rootViewController else { return }
controllers.append(WeakObject(object: viewController))
rootViewController.addChild(viewController)
rootViewController.view.addSubview(viewController.view)
}
deinit {
if rootViewController == nil || controllers.isEmpty { return }
for controller in controllers {
if let controller = controller.object {
controller.view.removeFromSuperview()
controller.removeFromParent()
}
}
controllers.removeAll()
}
}
Usage
class SampleViewController: UIViewController {
private var embedController: EmbedController?
override func viewDidLoad() {
super.viewDidLoad()
embedController = EmbedController(rootViewController: self)
let newViewController = ViewControllerWithButton()
newViewController.view.frame = CGRect(origin: CGPoint(x: 50, y: 150), size: CGSize(width: 200, height: 80))
newViewController.view.backgroundColor = .lightGray
embedController?.append(viewController: newViewController)
}
}
Full sample
ViewController
import UIKit
class ViewController: UIViewController {
private var embedController: EmbedController?
private var button: UIButton?
private let addEmbedButtonTitle = "Add embed"
override func viewDidLoad() {
super.viewDidLoad()
button = UIButton(frame: CGRect(x: 50, y: 50, width: 150, height: 20))
button?.setTitle(addEmbedButtonTitle, for: .normal)
button?.setTitleColor(.black, for: .normal)
button?.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button!)
print("viewDidLoad")
printChildViewControllesInfo()
}
func addChildViewControllers() {
var newViewController = ViewControllerWithButton()
newViewController.view.frame = CGRect(origin: CGPoint(x: 50, y: 150), size: CGSize(width: 200, height: 80))
newViewController.view.backgroundColor = .lightGray
embedController?.append(viewController: newViewController)
newViewController = ViewControllerWithButton()
newViewController.view.frame = CGRect(origin: CGPoint(x: 50, y: 250), size: CGSize(width: 200, height: 80))
newViewController.view.backgroundColor = .blue
embedController?.append(viewController: newViewController)
print("\nChildViewControllers added")
printChildViewControllesInfo()
}
#objc func buttonTapped() {
if embedController == nil {
embedController = EmbedController(rootViewController: self)
button?.setTitle("Remove embed", for: .normal)
addChildViewControllers()
} else {
embedController = nil
print("\nChildViewControllers removed")
printChildViewControllesInfo()
button?.setTitle(addEmbedButtonTitle, for: .normal)
}
}
func printChildViewControllesInfo() {
print("view.subviews.count: \(view.subviews.count)")
print("childViewControllers.count: \(childViewControllers.count)")
}
}
ViewControllerWithButton
import UIKit
class ViewControllerWithButton:UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
private func addButon() {
let buttonWidth: CGFloat = 150
let buttonHeight: CGFloat = 20
let frame = CGRect(x: (view.frame.width-buttonWidth)/2, y: (view.frame.height-buttonHeight)/2, width: buttonWidth, height: buttonHeight)
let button = UIButton(frame: frame)
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
}
override func viewWillLayoutSubviews() {
addButon()
}
#objc func buttonTapped() {
print("Button tapped in \(self)")
}
}
Results

Related

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

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
}
}
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 the A and B missing constraints, but...
You are calling that same func for your container controller and adding constraints to its view in layout(), so you end up with conflicting constraints.
One solution would be to change your addViewController func to this:
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)
}
then in setup():
func setup() {
titleBarView.backgroundColor = .red
view.addSubview(titleBarView)
// change this
//addViewController(container)
// to this
addViewController(container, constrainToSuperview: false)
showViewControllerA()
}
while leaving your other "add view controller" calls like this:
container.addViewController(viewControllerA)
container.addViewController(viewControllerB)
The other thing that may throw you off is the extraneous super. in your image view constraints:
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),
])
Update as following:
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)])
}
In MainViewViewController update the layout() function:
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),
])
}

Transparency on a UITableView causes navigation animation to look bad

I'm using a UITableView with a Navigation Controller and I have made the former partially transparent, which looks great.
The problem I am running into is that when I press a button, the transition animation (to change to another view) looks odd because the old view that is sliding behind the new one is visible for a time.
I have tried things like temporarily shutting off transparency (either suddenly, or gradually), and while it looks a little better, overall the experience still isn't great.
I guess it might be possible to do a custom animation, but this seems like a bad idea since it will likely look different than the built-in OS animation. Actually, even with a custom animation I am not sure how I would do it since I think I would run into the same issue.
Does anyone have any ideas how I can make things look cleaner?
UPDATE: adding more detail based on questions asked in the comments
The UI is a pretty complex set of pieces but I'll try to describe the relevant parts here.
There is a UISplitViewController [A], and I have created a UIVisualEffectView (with UIBlurEffect) that is attached as a subview of A's parent. My menu consists of a UINavigationController [B], and a UITableViewController [C] that is the top level of the menu. [B] is added as a subview as the content view of the blur effect view.
Two other UITableViewControllers [D] and [E] are transitioned to when button [1] or [2] are pressed on [C].
There are a few other view controllers that are subviews of [A] (or [A]'s parent) that are showing through, blurred, but that is the design and there is no issue there.
The problem is for the transition animations from [C]->[D], [D]->[C] (via back button), [C]->[E], or [E]->[C], you can see the controller that is moving away behind the controller that is coming in. So if you do [C]->[D] (via pressing button [1] on [C]) then you will see [C] going behind [D] as it slides in, and [C] eventually disappears.
The actual showing of [D] or [E] is done via a line of code like this (inside the custom class of [C])
self.navigationController?.show(myVC, sender: self)
where navigationController is [B] and myVC is [D].
The transition back to [C] is done via popViewController().
OK - trying to (minimally) emulate your setup description...
View controller with an image view filling the entire view
Navigation controller added as a child VC
Two VCs for the nav controller...
both with transparent background
"Page 1" pushes to "Page 2"
So I assume you mean you have a current "push/pop" transition that looks like this with simulator Debug -> Slow Animations to exaggerate the effect (these are kinda "heavy" gifs, so open them in a new browser tab if the animation isn't running):
And your goal is something close or similar to this:
You will likely need to use a custom transition.
I was able to get those results using the code from this article unedited: Simple, custom navigation transitions -- note: this is not mine - just found it from quick searching.
Here's the code for the full example -- everything is done via code, no #IBOutlet or #IBAction connections needed. Just assign a new view controller's custom class as NavSubVC :
class NavSubVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
guard let img = UIImage(named: "navBKG") else {
DispatchQueue.main.async {
let a = UIAlertController(title: "Alert", message: "Could not load \"navBKG\" image", preferredStyle: .alert)
self.present(a, animated: true, completion: nil)
}
return
}
let imgView = UIImageView(image: img)
imgView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgView)
let rvc = Page1VC()
let navC = UINavigationController(rootViewController: rvc)
self.addChild(navC)
guard let navView = navC.view else { return }
view.addSubview(navView)
navC.didMove(toParent: self)
navView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
navView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
navView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
navView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
navView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
// let's have a gray nav bar always showing
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithTransparentBackground()
navigationBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navigationBarAppearance.backgroundColor = .systemGray
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().compactAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
// let's add a border to the navigation controller view
// so we can see its frame (since the controllers have clear backgrounds)
navView.layer.borderWidth = 2
navView.layer.borderColor = UIColor.yellow.cgColor
// un-comment this line to see the custom transition
//navC.addCustomTransitioning()
}
}
class PageBaseVC: UIViewController {
var labels: [UILabel] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
for i in 1...6 {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.textColor = .white
v.translatesAutoresizingMaskIntoConstraints = false
v.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
v.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
labels.append(v)
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
labels[0].topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
labels[0].leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
labels[1].centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
labels[1].leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
labels[2].bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
labels[2].leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
labels[3].topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
labels[3].trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
labels[4].centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
labels[4].trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
labels[5].bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
labels[5].trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
}
}
class Page1VC: PageBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Page 1"
labels.forEach { v in
v.backgroundColor = .systemBlue
}
let b = UIButton()
b.backgroundColor = .systemGreen
b.setTitle("Push to Page 2", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(b)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
b.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
b.centerXAnchor.constraint(equalTo: g.centerXAnchor),
b.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
b.heightAnchor.constraint(equalToConstant: 60.0),
])
b.addTarget(self, action: #selector(doPush(_:)), for: .touchUpInside)
}
#objc func doPush(_ sender: Any?) {
let vc = Page2VC()
self.navigationController?.pushViewController(vc, animated: true)
}
}
class Page2VC: PageBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Page 2"
labels.forEach { v in
v.backgroundColor = .systemRed
}
}
}
// Custom Navigation Transition
// from: https://ordinarycoding.com/articles/simple-custom-uinavigationcontroller-transitions/
final class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
// 1
let presenting: Bool
// 2
init(presenting: Bool) {
self.presenting = presenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
// 3
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 4
guard let fromView = transitionContext.view(forKey: .from) else { return }
guard let toView = transitionContext.view(forKey: .to) else { return }
// 5
let duration = transitionDuration(using: transitionContext)
// 6
let container = transitionContext.containerView
if presenting {
container.addSubview(toView)
} else {
container.insertSubview(toView, belowSubview: fromView)
}
// 7
let toViewFrame = toView.frame
toView.frame = CGRect(x: presenting ? toView.frame.width : -toView.frame.width, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
let animations = {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5) {
toView.alpha = 1
if self.presenting {
fromView.alpha = 0
}
}
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1) {
toView.frame = toViewFrame
fromView.frame = CGRect(x: self.presenting ? -fromView.frame.width : fromView.frame.width, y: fromView.frame.origin.y, width: fromView.frame.width, height: fromView.frame.height)
if !self.presenting {
fromView.alpha = 0
}
}
}
UIView.animateKeyframes(withDuration: duration,
delay: 0,
options: .calculationModeCubic,
animations: animations,
completion: { finished in
// 8
container.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
final class TransitionCoordinator: NSObject, UINavigationControllerDelegate {
// 1
var interactionController: UIPercentDrivenInteractiveTransition?
// 2
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return TransitionAnimator(presenting: true)
case .pop:
return TransitionAnimator(presenting: false)
default:
return nil
}
}
// 3
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
extension UINavigationController {
// 1
static private var coordinatorHelperKey = "UINavigationController.TransitionCoordinatorHelper"
// 2
var transitionCoordinatorHelper: TransitionCoordinator? {
return objc_getAssociatedObject(self, &UINavigationController.coordinatorHelperKey) as? TransitionCoordinator
}
func addCustomTransitioning() {
// 3
var object = objc_getAssociatedObject(self, &UINavigationController.coordinatorHelperKey)
guard object == nil else {
return
}
object = TransitionCoordinator()
let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &UINavigationController.coordinatorHelperKey, object, nonatomic)
// 4
delegate = object as? TransitionCoordinator
// 5
let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
edgeSwipeGestureRecognizer.edges = .left
view.addGestureRecognizer(edgeSwipeGestureRecognizer)
}
// 6
#objc func handleSwipe(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
guard let gestureRecognizerView = gestureRecognizer.view else {
transitionCoordinatorHelper?.interactionController = nil
return
}
let percent = gestureRecognizer.translation(in: gestureRecognizerView).x / gestureRecognizerView.bounds.size.width
if gestureRecognizer.state == .began {
transitionCoordinatorHelper?.interactionController = UIPercentDrivenInteractiveTransition()
popViewController(animated: true)
} else if gestureRecognizer.state == .changed {
transitionCoordinatorHelper?.interactionController?.update(percent)
} else if gestureRecognizer.state == .ended {
if percent > 0.5 && gestureRecognizer.state != .cancelled {
transitionCoordinatorHelper?.interactionController?.finish()
} else {
transitionCoordinatorHelper?.interactionController?.cancel()
}
transitionCoordinatorHelper?.interactionController = nil
}
}
}

self.perform segue causes view to not conform

I have programmatically created a view layout with a content view, when this controller is the initial view controller I get something like this.
:
When I have another view controller and segue to this viewcontroller, I get something like this:
Attached is my View Class:
class TestView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.addSubview(contentView)
}
func setupConstraints() {
self.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0).isActive = true
// check what safearealayoutguide is
if #available(iOS 11.0, *) {
print("IOS11 Screen")
contentView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
} else {
contentView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor, constant: 0).isActive = true
}
contentView.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor, constant: 0).isActive = true
if #available(iOS 11.0, *) {
contentView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
} else {
contentView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor, constant: 0).isActive = true
}
}
let contentView: UIView = {
let view = UIView(frame: CGRect(x: 100, y: 100, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width))
//view.layer.backgroundColor = CGColortran
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.green.cgColor
return view
}()
}
then my view controller:
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func loadView() {
self.view = TestView(frame: UIScreen.main.bounds)
}
}
When this is the only screen in the application, it works. when I add another view controller and do a self.performSegue(withIdentifier: "LoginUser", sender: nil) it seems to break the view. What's going on here?
Comment
self.translatesAutoresizingMaskIntoConstraints = false
as currently you mix setting a frame TestView(frame: UIScreen.main.bounds) with constraints , the above line is used only for a view when you set constraints to it as it invalidates the frame

How to add a view as subview for certain controllers

I have multiple storyboards in my app. I want to add a view on always on the top just below the navigation bar for some of the controllers. How Can I achieve this?
I already used navigation delegate and add a view in the window but no luck. Steps to show the gray view in the attached image is.
1. On click of a button on that view controller; a gray view should show and remain on the top of the controllers until all the scanning of the device is not done whether the user should go any of the viewControllers.
You can create a UINavigationController subclass and add the view in it.
class NavigationController: UINavigationController {
let customView = UIView()
let iconImgView = UIImageView()
let msgLbl = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
customView.isHidden = true
customView.translatesAutoresizingMaskIntoConstraints = false
customView.backgroundColor = .gray
view.addSubview(customView)
iconImgView.contentMode = .scaleAspectFit
iconImgView.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(iconImgView)
msgLbl.numberOfLines = 0
msgLbl.lineBreakMode = .byWordWrapping
msgLbl.textColor = .white
msgLbl.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(msgLbl)
customView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
customView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
customView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
iconImgView.widthAnchor.constraint(equalToConstant: 40).isActive = true
iconImgView.heightAnchor.constraint(equalToConstant: 40).isActive = true
iconImgView.centerYAnchor.constraint(equalTo: customView.centerYAnchor).isActive = true
iconImgView.leadingAnchor.constraint(equalTo: customView.leadingAnchor, constant: 15).isActive = true
iconImgView.trailingAnchor.constraint(equalTo: msgLbl.leadingAnchor, constant: 15).isActive = true
msgLbl.topAnchor.constraint(equalTo: customView.topAnchor, constant: 10).isActive = true
msgLbl.bottomAnchor.constraint(equalTo: customView.bottomAnchor, constant: 10).isActive = true
msgLbl.trailingAnchor.constraint(equalTo: customView.trailingAnchor, constant: -15).isActive = true
msgLbl.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
}
func showCustomView(message: String, icon: UIImage) {
msgLbl.text = message
iconImgView.image = icon
customView.isHidden = false
}
func hideCustomView() {
customView.isHidden = true
}
}
Embed all your view controllers in this navigation controller. When you want to show/hide the gray view in a view controller use
Show
(self.navigationController as? NavigationController)?.showCustomView(message: "Any Message", icon: UIImage(named: "anyImage")!)
Hide
(self.navigationController as? NavigationController)?.hideCustomView()
When you push another view controller from the same navigation controller the view won't be hidden until you call the hide method
You can simply create a custom UIView with the relevant frame and call addSubview() on the view you want to add it to.
lazy var customView: UIView = {
let customView = UIView(frame: CGRect.init(x: 0, y: self.view.safeAreaInsets.top, width: UIScreen.main.bounds.width, height: 100))
customView.backgroundColor = .gray
return customView
}()
#IBAction func onTapButton(_ sender: UIButton) {
self.view.addSubview(customView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.customView.removeFromSuperview()
}
To add it below the navigationBar, use y position of frame as self.view.safeAreaInsets.top. With this your customView will always be aligned below the navigationBar.
You can create the view with the height as per your requirement. I've used height = 100.
Give the correct frame and you can add any view as a subView to another view.

How can I create reusable view controllers inside of UIScrollView?

I am trying to create a full page sized horizontally scrolling UIScrollView. On each page I am adding instances of the same UIViewController class. I would like to create some kind of reusable functionality to conserve both memory and processor use needed. Below is a basic implementation I have created with some toying around with how reusability might work although Im not quite sure. Thank you for any help you can offer.
Current UIScroll ViewController Model
let scrollView:UIScrollView = {
let scrollView = UIScrollView(frame: CGRect.zero)
scrollView.isPagingEnabled = true
scrollView.backgroundColor = UIColor.gray
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = true
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
scrollView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size)
self.view.addSubview(scrollView)
scrollView.delegate = self
scrollView.contentSize = CGSize(width: 3 * self.view.frame.width, height: self.view.frame.height)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0),
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0),
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0)
])
let viewController1 = UIViewController()
viewController1.view.backgroundColor = UIColor.red
let viewController2 = UIViewController()
viewController2.view.backgroundColor = UIColor.blue
let viewController3 = UIViewController()
viewController3.view.backgroundColor = UIColor.green
self.addChild(viewController1)
viewController1.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController1.view)
addViewControllerContraints(viewController: viewController1, index: 0)
viewController1.didMove(toParent: self)
self.addChild(viewController2)
viewController2.view.frame = CGRect(origin: CGPoint(x: self.view.bounds.width, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController2.view)
addViewControllerContraints(viewController: viewController2, index: 1)
viewController2.didMove(toParent: self)
self.addChild(viewController3)
viewController3.view.frame = CGRect(origin: CGPoint(x: 2 * self.view.bounds.width, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController3.view)
addViewControllerContraints(viewController: viewController3, index: 2)
viewController3.didMove(toParent: self)
}
func addViewControllerContraints( viewController: UIViewController, index:Int){
guard let view = viewController.view else{
print("View found nil")
return
}
view.translatesAutoresizingMaskIntoConstraints = false
let offset:CGFloat = UIScreen.main.bounds.width * CGFloat(index)
print("Offset: \(offset)")
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0),
view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: offset),
view.heightAnchor.constraint(equalToConstant: self.view.bounds.height),
view.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
Is there a good way to create some type of reuse functionality this is something I was playing around with based on This Answer although I realize that is primarily for UIPageViewControllers where allocation and deallocation of UIViewController's is handled for you.
Possible Reuse Functionality
var reuseableViewControllers:[UIViewController] = [UIViewController]()
private func unusedViewController() -> UIViewController {
let unusedViewControllers = reuseableViewControllers.filter { $0.parent == nil }
if let someUnusedViewController = unusedViewControllers.first {
return someUnusedViewController
} else {
let newViewController = UIViewController()
reuseableViewControllers.append(newViewController)
return newViewController
}
}

Resources