Transparency on a UITableView causes navigation animation to look bad - ios

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

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

Why does my UIView.Transition not work unless wrapped in a UIView.Animate block?

I am using a UIView.transition to flip a card over.
While troubleshooting it not working, I stumbled upon a way to make it work - but I have no idea why. I am hoping that someone can look at the two code blocks below and help me understand why one works and the other doesn't. It seems very strange to me.
First, here is the code block that actually works. The card flip is visually flawless.
UIView.animate(withDuration: 0.01) {
imageView.alpha = 1.0
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
}
But I don't understand why I seem to need to wrap the UIView.transition() into the completion handler of a call to UIView.animate() in order for the flip animation to work.
*(Note: If I pull the "imageView.alpha = 1.0" out of the animate() block and place it immediately BEFORE calling UIView.animate() - the flip animation does not occur (with or without the layoutIfNeeded() call). It just toggles the images. *
Now, here is the code that I expected to work - but when I use this code instead of the above code, there is no "flip" transition. The card image just immediately toggles between the face up and face down image. The "UIView.transition" call here is identical to the one in the above code. The only difference here is that it's NOT wrapped into a 0.01 second UIView.animate completion block.
imageView.alpha = 1.0
imageView.layoutIfNeeded()
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // same behaviour with and without this line
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
This transition ends my flipTicket() function. The code that precedes this transition is identical in both cases. I wasn't going to include it because I don't think it's necessary to understand the problem - but then again - what do I know? Here's the stuff that came before the above clips:
func flipTicket() {
let originalTicketNumAlpha = self.tickNumLabel.alpha
self.tickNumLabel.alpha = 0.0
let tempFaceDownImage:UIImage = self.dealTicketState.faceDownImage
let tempFaceUpImage:UIImage = getCurrentFaceUpImage()
var endingImage:UIImage = self.dealTicketState.faceDownImage
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
imageView.alpha = 0.0
self.coveringLabel.alpha = 1.0
self.coveringLabel.backgroundColor = .black
self.coveringLabel.layoutIfNeeded()
var animation:UIView.AnimationOptions = .transitionFlipFromLeft
if faceDownView.alpha == 1.0 {
animation = .transitionFlipFromRight
imageView.image = tempFaceDownImage
endingImage = tempFaceUpImage
} else {
animation = .transitionFlipFromLeft
imageView.image = tempFaceUpImage
}
self.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: self.topAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
imageView.layoutIfNeeded()
Background:
This code is part of a custom UI control which represents a playing card. It consists of several subviews. The topmost subview was originally a UIImageView which held the image of the back of the card. This let me simply toggle the alpha for that top view to display the card as either face up or face down. And then I added one more topMost view to the control - a UILabel with "background = .clear" and attached a UITapGestureRecognizer to it. When the control is tapped, this function which is meant to animate the flipping over of the card is called.
To construct the animation, I call the getCurrentFaceUp() function which temporarily sets the alpha of the card's faceDownView to 0 (so I can take a snapshot of the card underneath it as it is currently configured). It returns a UIImage of the "face up" view of the card. I already have a UIImage of the faceDown view. These are the 2 images I need for the transition.
So...then I set the background color of that topMost UILabel to .black, create a new temporary UIImageView and place it on top of the existing control. I set the temporary imageView to initially display whichever one of the 2 images is currently visible on the control. And then I run the flip transition, change the configuration of the background control to match the new state, change the label background back to .clear and dispose of the temporary UIImageView.
(If there's a better way to accomplish this, I'm open to hearing it but the main purpose of this post is to understand why my code appears to be acting strangely.)
When I was looking for a way to animate a card flip, I found a YouTube video that demonstrated the UIView.transition() with the flip animation. It did not require using a UIView.animate() wrapper in order to make it work - so I'm pretty certain that it's me that did something wrong -but I've spent hours testing variations and searching for someone else with this problem and I haven't been able to find the answer.
Thanks very much in advance to anybody that can help me understand what's going on here...
A little tough to tell (didn't try to actually run your code), but I think you may be doing more than you need to.
Take a look at this...
We'll start with two "Card View" subclasses - Front and Back (we'll "style" them in the next step):
class CardFrontView: UIView {
}
class CardBackView: UIView {
}
Then, a "Playing Card View" class, that contains a "Front" view (cyan) and a "Back" view (red). On init, we add the subviews and set the "Front" view hidden. On tap, we'll run the flip transition between the Front and Back views:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .cyan
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and then here's a simple Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
The result:
So, next step, we'll add a little styling to the Front and Back views -- no changes to the PlayingCardView functionality... just a couple new lines to set the styling...
Card Front View - with rounded corners, a border and labels at the corners and center:
class CardFrontView: UIView {
var theLabels: [UILabel] = []
var cardID: Int = 0 {
didSet {
theLabels.forEach {
$0.text = "\(cardID)"
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
for i in 1...5 {
let v = UILabel()
v.font = .systemFont(ofSize: 24.0)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
switch i {
case 1:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 2:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 4:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
default:
v.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
theLabels.append(v)
}
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
}
}
Looks like this:
Card Back View - with rounded corners, a border and a cross-hatch pattern:
class CardBackView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
layer.masksToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
// simple cross-hatch pattern
let hReplicatorLayer = CAReplicatorLayer()
let vReplicatorLayer = CAReplicatorLayer()
let line = CAShapeLayer()
let pth = UIBezierPath()
pth.move(to: CGPoint(x: 0.0, y: 0.0))
pth.addLine(to: CGPoint(x: 20.0, y: 20.0))
pth.move(to: CGPoint(x: 20.0, y: 0.0))
pth.addLine(to: CGPoint(x: 0.0, y: 20.0))
line.strokeColor = UIColor.yellow.cgColor
line.lineWidth = 1
line.path = pth.cgPath
var instanceCount = Int((bounds.maxX + 0.0) / 20.0)
hReplicatorLayer.instanceCount = instanceCount
hReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
instanceCount = Int((bounds.maxY + 0.0) / 20.0)
vReplicatorLayer.instanceCount = instanceCount
vReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, 20, 0)
hReplicatorLayer.addSublayer(line)
vReplicatorLayer.addSublayer(hReplicatorLayer)
layer.addSublayer(vReplicatorLayer)
}
}
Looks like this:
Playing Card View - only change is setting the Front Card background color to white, and setting its "ID" to 5:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .white
cardFront.cardID = 5
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and finally, the same Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
and here's the new result:

Animate UIView transform happens instantly instead of over duration value

I am trying to animate a view on screen in a similar way to how action sheets appear.
My router presents CustomCardViewController which has an overlay background.
After a short delay I'd like containerView too animate into view from the bottom.
Instead what is happening however is it just appears in place. There is no animation between the transition.
final class CustomCardViewController: UIViewController {
private let backgroundMask: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .init(white: 0, alpha: 0.3)
return view
}()
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.transform = .init(translationX: 0, y: view.frame.height)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
backgroundMask.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTapToDismiss)))
modalPresentationStyle = .overFullScreen
[backgroundMask, containerView].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
backgroundMask.topAnchor.constraint(equalTo: view.topAnchor),
backgroundMask.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundMask.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backgroundMask.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -24),
containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200)
])
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.containerView.transform = .identity
}, completion: nil)
}
}
private extension CustomCardViewController {
#objc func onTapToDismiss() {
dismiss(animated: false, completion: nil)
}
}
You need to call your animation block from another lifecycle method. You can't trigger this from viewDidLoad as the view has only loaded, there is nothing on screen yet.
Try using viewDidAppear
For animation, you need to update the constant of the constrain that you want to animate. Here, since you're trying to animate from the bottom, you need to update a vertical constrain in this case, the bottom constraint. Here's the code for animation:
final class CustomCardViewController: UIViewController {
//..
override func viewDidLoad() {
super.viewDidLoad()
NSLayoutConstraint.activate([
// remove bottom constraint from here
])
containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 250)
containerViewBottomConstraint?.isActive = true
}
var containerViewBottomConstraint: NSLayoutConstraint? // declare bottom constraint
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
containerViewBottomConstraint?.constant = -24
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
}
}

How can implement two vertical button in swipe to delete in ios?

I am trying to implement swipe to delete feature with two options in tableview, one is to delete and another one is to Update.The things I want is these options should be vertical rather than horizontal.I have checked so many question but nothing find.
Thanks in advance for support.
.
As I mentioned in the comments, here is one approach:
add your buttons to the cell
add a "container" view to the cell
constrain the container view so it overlays / covers the buttons
add a Pan gesture recognizer to the container view so you can drag it left / right
as you drag it left, it will "reveal" the buttons underneath
You lose all of the built-in swipe functionality, but this is one approach that might give you the design you're going for.
First, an example of creating a "drag view":
class DragTestViewController: UIViewController {
let backgroundView = UIView()
let containerView = UIView()
// leading and trailing constraints for the drag view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(60.0)
private let origTrailing = CGFloat(-60.0)
private var currentLeading = CGFloat(60.0)
private var currentTrailing = CGFloat(-60.0)
override func viewDidLoad() {
super.viewDidLoad()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .cyan
backgroundView.clipsToBounds = true
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .red
// add a label to the container view
let exampleLabel = UILabel()
exampleLabel.translatesAutoresizingMaskIntoConstraints = false
exampleLabel.text = "Drag Me"
exampleLabel.textColor = .yellow
containerView.addSubview(exampleLabel)
backgroundView.addSubview(containerView)
view.addSubview(backgroundView)
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: origTrailing)
NSLayoutConstraint.activate([
// constrain backgroundView top to top + 80
backgroundView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80.0),
// constrain backgroundView leading / trailing to leading / trailing with 40-pt "padding"
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
// constrain height to 100
backgroundView.heightAnchor.constraint(equalToConstant: 100.0),
// constrain containerView top / bottom to backgroundView top / bottom with 8-pt padding
containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 8.0),
containerView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -8.0),
// activate leading / trailing constraints
leadingConstraint,
trailingConstraint,
// constrain the example label centered in the container view
exampleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
exampleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the containerView - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
// update current vars
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
default:
break
}
}
}
That code will produce this:
A red view with a centered label, inside a cyan view. You can drag the red "container" view left and right.
Add a view controller to a new project and assign its Custom Class to DragTestViewController from the above code. There are no #IBOutlet or #IBAction connections, so you should be able to run it as-is. See if you can drag the red view.
Using that as a starting point, we can get this:
with this code:
// simple rounded-corner shadowed view
class ShadowRoundedView: UIView {
let shadowLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.layer.addSublayer(shadowLayer)
clipsToBounds = false
backgroundColor = .clear
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowRadius = 4.0
shadowLayer.shadowOpacity = 0.6
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
}
override func layoutSubviews() {
super.layoutSubviews()
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: 16.0)
shadowLayer.path = pth.cgPath
}
}
// simple rounded button
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class DragRevealCell: UITableViewCell {
// callback closure for button taps
var callback: ((Int) -> ())?
// this will hold the "visible" labels, and will initially cover the buttons
let containerView: ShadowRoundedView = {
let v = ShadowRoundedView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// this will hold the buttons
let buttonsView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.clipsToBounds = true
return v
}()
// a "delete" button
let deleteButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete", for: [])
v.setTitleColor(.blue, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .white
return v
}()
// an "update" button
let updateButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Update", for: [])
v.setTitleColor(.white, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
// single label for this example cell
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
return v
}()
// leading and trailing constraints for the container view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(8.0)
private let origTrailing = CGFloat(-8.0)
private var currentLeading = CGFloat(0.0)
private var currentTrailing = CGFloat(0.0)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// cell background color
backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// add buttons to buttons container view
buttonsView.addSubview(deleteButton)
buttonsView.addSubview(updateButton)
// add label to container view -- this is where you would add all your labels, stack views, image views, etc.
containerView.addSubview(myLabel)
// add buttons view first
addSubview(buttonsView)
// add container view second - this will "overlay" it on top of the buttons view
addSubview(containerView)
// containerView leading / trailing constraints - these will be updated as we drag
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: origTrailing)
// needed to avoid layout warnings
let bottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0)
bottomConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
leadingConstraint,
trailingConstraint,
bottomConstraint,
myLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
myLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
myLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
myLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8.0),
myLabel.heightAnchor.constraint(equalToConstant: 120.0),
buttonsView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
buttonsView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
deleteButton.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: 0.0),
deleteButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
deleteButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: 0.0),
updateButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
updateButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.topAnchor.constraint(equalTo: deleteButton.bottomAnchor, constant: 12.0),
updateButton.heightAnchor.constraint(equalTo: deleteButton.heightAnchor),
updateButton.widthAnchor.constraint(equalTo: deleteButton.widthAnchor),
deleteButton.widthAnchor.constraint(equalToConstant: 120.0),
deleteButton.heightAnchor.constraint(equalToConstant: 40.0),
])
// delete button border
deleteButton.layer.borderColor = UIColor.blue.cgColor
deleteButton.layer.borderWidth = 1.0
// targets for button taps
deleteButton.addTarget(self, action: #selector(self.deleteTapped(_:)), for: .touchUpInside)
updateButton.addTarget(self, action: #selector(self.updateTapped(_:)), for: .touchUpInside)
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the container view - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
// don't allow drag-to-the-right
if currentLeading + translation.x <= origLeading {
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
}
default:
// if the drag-left did not fully reveal the buttons, animate the container view back in place
if containerView.frame.maxX > buttonsView.frame.minX {
self.leadingConstraint.constant = self.origLeading
self.trailingConstraint.constant = self.origTrailing
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
}, completion: { _ in
//self.dragX = 0.0
})
}
}
}
#objc func deleteTapped(_ sender: Any?) -> Void {
callback?(0)
}
#objc func updateTapped(_ sender: Any?) -> Void {
callback?(1)
}
}
class DragRevealTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DragRevealCell.self, forCellReuseIdentifier: "DragRevealCell")
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "DragRevealCell", for: indexPath) as! DragRevealCell
c.myLabel.text = "Row \(indexPath.row)" + "\n" + "This is where you would populate the cell's labels, image views, any other UI elements, etc."
c.selectionStyle = .none
c.callback = { value in
if value == 0 {
print("Delete action")
} else {
print("Update action")
}
}
return c
}
}
Add a UITableViewController the project and assign its Custom Class to DragRevealTableViewController from the above code. Again, there are no #IBOutlet or #IBAction connections, so you should be able to run it as-is.
NOTE: This is example code only, and should not be considered "production ready"!!! It is only partially implemented and will likely need quite a bit more work. But, it may give you a good starting point.

How to add a Container View programmatically

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

Resources