I have a long register form that consists of 4 steps (the content is not relevant), here are the mockups:
My problem is that I need to share a progress view between multiple views. This view should have an animation of growth. What would be the right and clean way to do this with UIKit? Should I create a custom Navigation Bar with that progress? Or use child controllers in some way?
I've been searching over here but the other questions I found are very old (like 7 years ago) and I don't know if there could be better solutions.
Thanks a lot!
There are various ways to do this...
One common approach is to set the "progress view" as the navigation bar's Title View -- but that won't show it below the navigation bar.
So, another approach is to subclass UINavigationController and add a "progress view" as a subview. Then, implement willShow viewController and/or didShow viewController to update the progress.
As a quick example, assuming we have 4 "steps" to navigate to...
We'll start with defining a "base" view controller, with two properties that our custom nav controller class will use:
class MyBaseVC: UIViewController {
// this will be read by ProgressNavController
// to calculate the "progress percentage"
public let numSteps: Int = 4
// this will be set by each MyBaseVC subclass,
// and will be read by ProgressNavController
public var myStepNumber: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// maybe some stuff common to the "step" controllers
}
}
Then, each "step" controller will be a subclass of MyBaseVC, and will set its "step number" (along with anything else specific to that controller):
class Step1VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 1
// maybe some other stuff specific to this "step"
}
}
class Step2VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 2
// maybe some other stuff specific to this "step"
}
}
class Step3VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 3
// maybe some other stuff specific to this "step"
}
}
class Step4VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 4
// maybe some other stuff specific to this "step"
}
}
Then we can setup our custom nav controller class like this (it's not really as complicated as it may look):
class ProgressNavController: UINavigationController, UINavigationControllerDelegate {
private let outerView = UIView()
private let innerView = UIView()
private var pctConstraint: NSLayoutConstraint!
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// for this example, we're using a simple
// green view inside a red view
// as our "progress view"
// we set it up here, but we don't add it as a subview
// until we navigate to a MyBaseVC
// we know we're setting
// outerView height to 20
// innerView height to 12 (4-points top/bottom "padding")
// so let's round the ends of the innerView
innerView.layer.cornerRadius = 8.0
outerView.backgroundColor = .systemRed
innerView.backgroundColor = .systemGreen
outerView.translatesAutoresizingMaskIntoConstraints = false
innerView.translatesAutoresizingMaskIntoConstraints = false
outerView.addSubview(innerView)
// initialize pctConstraint
pctConstraint = innerView.widthAnchor.constraint(equalTo: outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
NSLayoutConstraint.activate([
innerView.topAnchor.constraint(equalTo: outerView.topAnchor, constant: 4.0),
innerView.leadingAnchor.constraint(equalTo: outerView.leadingAnchor, constant: 4.0),
innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor, constant: -4.0),
pctConstraint,
])
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// if the next VC to show
// is a MyBaseVC subclass
if let _ = viewController as? MyBaseVC {
// add the "progess view" if we're coming from a non-MyBaseVC controller
if outerView.superview == nil {
view.addSubview(outerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 4.0),
outerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
outerView.heightAnchor.constraint(equalToConstant: 20.0),
])
// .alpha to Zero so we can "fade it in"
outerView.alpha = 0.0
// we just added the progress view,
// so we'll let didShow "fade it in"
// and update the progress width
} else {
self.updateProgress(viewController)
}
} else {
if outerView.superview != nil {
// we want to quickly "fade-out" and remove the "progress view"
// if the next VC to show
// is NOT a MyBaseVC subclass
UIView.animate(withDuration: 0.1, animations: {
self.outerView.alpha = 0.0
}, completion: { _ in
self.outerView.removeFromSuperview()
self.pctConstraint.isActive = false
self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
self.pctConstraint.isActive = true
})
}
}
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// if the VC just shown
// is a MyBaseVC subclass
// AND
// outerView.alpha < 1.0 (meaning it was just added)
if let _ = viewController as? MyBaseVC, outerView.alpha < 1.0 {
self.updateProgress(viewController)
}
// otherwise, updateProgress() is called from willShow
}
private func updateProgress(_ viewController: UIViewController) {
if let vc = viewController as? MyBaseVC {
// update the innerView width -- the "progress"
let nSteps: CGFloat = CGFloat(vc.numSteps)
let thisStep: CGFloat = CGFloat(vc.myStepNumber)
var pct: CGFloat = .leastNonzeroMagnitude
// sanity check
// avoid error/crash if either values are Zero
if nSteps > 0.0, thisStep > 0.0 {
pct = thisStep / nSteps
}
// don't exceed 100%
pct = min(pct, 1.0)
// we can't update the multiplier directly, so
// deactivate / update / activate
self.pctConstraint.isActive = false
self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: pct, constant: -8.0)
self.pctConstraint.isActive = true
// if .alpha is already 1.0, this is effectively ignored
UIView.animate(withDuration: 0.1, animations: {
self.outerView.alpha = 1.0
})
// animate the "bar width"
UIView.animate(withDuration: 0.3, animations: {
self.outerView.layoutIfNeeded()
})
}
}
}
So, when we navigate to a new controller:
we check to see if it is an instance of MyBaseVC
if Yes
add the progress view (if it's not already there)
get the step number from the new controller
update the progress
if Not
remove the progress view
I put up a complete example you can check out and inspect here: https://github.com/DonMag/ProgressNavController
Related
I've got some subviews in my navigationBar. A black square, some titles and a blue bar. When pushed to a detail viewController, I want my subviews to hide (animated).
I added the subways to the navigation bar inside the main view controller using
self.navigationController?.navigationBar.addsubView(mySubview).
Currently, it looks like his:
What is the right way to hide (animated) those subviews in the detail viewController?
Your question is very interesting. I never thought about the navigation bar.
When UINavigationController is used as the root controller, all UIViewControllers are stored in its stack, meaning that all UIViewControllers share a navigationBar.
You added mySubView to navigationBar in the first UIViewController. If you want to hide it on the details page, you can search for subviews directly.
The first step, you need to give mySubView a tag, which can be a tag, or it can be a custom type, which is convenient for later judgment.
On the first page
import SnapKit
mySubView = UIView()
mySubView?.tag = 999
mySubView?.backgroundColor = .red
mySubView?.translatesAutoresizingMaskIntoConstraints = false
let navBar = navigationController?.navigationBar
navBar!.addSubview(mySubView!)
mySubView!.snp.makeConstraints { (make) in
make.left.equalTo(navBar!).offset(100)
make.centerY.equalTo(navBar!)
make.width.height.equalTo(50)
}
On the details page, I deleted isHidden and saved navigationBar with the attribute because navigationBar = nil during the gesture. If SnapKit is unfamiliar, take a few more minutes to learn.
var mySubView: UIView? = nil
var navBar: UINavigationBar?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Do any additional setup after loading the view.
navigationController?.interactivePopGestureRecognizer?.addTarget(self, action: #selector(backGesture))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
homeNavigationBarStatus()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
homeNavigationBarStatus()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if mySubView == nil {
for view: UIView in (navigationController?.navigationBar.subviews)! {
if view.tag == 999 {
mySubView = view
}
}
}
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseOut, animations: {
self.detailNavigationBarStatus()
}, completion: nil)
}
func detailNavigationBarStatus(){
changingNavigationBarStatus(progress: 0)
}
func homeNavigationBarStatus(){
changingNavigationBarStatus(progress: 1.0)
}
func changingNavigationBarStatus(progress:CGFloat){
mySubView?.alpha = progress
mySubView?.snp.updateConstraints({ (make) in
make.left.equalTo(navBar!).offset(100 * progress)
})
}
#objc func backGesture(sender: UIScreenEdgePanGestureRecognizer) {
switch sender.state {
case .changed:
let x = sender.translation(in: view).x
progress = x / view.frame.width
changingNavigationBarStatus(progress: progress)
default:
break
}
}
However, using tag values is not elegant enough, you can create a specific class for mySubView, which can also be judged by class.
I want to present a fullscreen ViewController "A" to cover our loading process across ViewControllers "B" AND "C", or in other words, 1) I present ViewController A from ViewController B, 2) segue from ViewController B to ViewController C while ViewController A is showing, 3) dismiss the ViewController A into ViewController C that ViewController B segued into.
If I push from the presenter ViewController B, the presented ViewController A will disappear as well. So my question is, what's the best way to change the ViewControllers B and C in the background, while another one (ViewController A) is presented on top of them?
Thanks.
You can do this in two ways:
1.Using a navigation controller
if let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourVCName") as? JunctionDetailsVC {
if let navigator = navigationController {
navigator.pushViewController(viewController, animated: false)
}
}
2.Present modally from you initial VC
if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourVCName") as? LoginVC
{
present(vc, animated: false, completion: nil)
}
Remember to not animate because you said that users shouldn't notice the transition.
Without knowing anything else about your app, I think you'd be better off redesigning the flow and User Experience, but here is one approach to do what you want.
We start with VC-B as the root VC of a UINavigationController
On button-tap, we add a "cover view" to the navigation controller's view hierarchy
We initially position that view below the bottom of the screen
Animate it up into view
Make desired changes to VC-B's view
Instantiate and Push VC-C
Do what's needed to setup the UI on VC-C
Animate the cover view down and off-screen
Remove the cover view from the hierarchy
And here's the code. Everything is done via code - even the initial Nav Controller setup - so No Storyboard needed (go to Project General Settings and delete anything in the Main Interface field).
AppDelegate.swift
//
// AppDelegate.swift
//
// Created by Don Mag on 8/30/19.
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
// instantiate a UINavigationController
let navigationController = UINavigationController();
// instantiate a NavBViewController
let vcB = NavBViewController();
// set the navigation controller's first controller
navigationController.viewControllers = [ vcB ];
self.window?.rootViewController = navigationController;
self.window?.makeKeyAndVisible()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
}
func applicationDidEnterBackground(_ application: UIApplication) {
}
func applicationWillEnterForeground(_ application: UIApplication) {
}
func applicationDidBecomeActive(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
}
}
ViewControllers.swift - contains CoverView, NavBViewController and NavCViewController classes
//
// ViewControllers.swift
//
// Created by Don Mag on 8/30/19.
//
import UIKit
class CoverView: UIView {
let theSpinner: UIActivityIndicatorView = {
let v = UIActivityIndicatorView()
v.translatesAutoresizingMaskIntoConstraints = false
v.style = .whiteLarge
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.textColor = .white
v.text = "Please Wait"
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .blue
// add an Activity Spinner and a label
addSubview(theSpinner)
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
theSpinner.centerXAnchor.constraint(equalTo: theLabel.centerXAnchor),
theSpinner.bottomAnchor.constraint(equalTo: theLabel.topAnchor, constant: -100.0),
])
theSpinner.startAnimating()
}
}
class NavBViewController: UIViewController {
// this view will be added or removed while the "coverView" is up
let newViewToChange: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.textColor = .white
v.textAlignment = .center
v.text = "A New View"
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "View Controller B"
return v
}()
let theButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Tap Me", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add a button and a label
view.addSubview(theButton)
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theLabel.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any) {
// get
// the neavigation controller's view,
if let navView = navigationController?.view {
// create a "cover view"
let coverView = CoverView()
coverView.translatesAutoresizingMaskIntoConstraints = false
// add the coverView to the neavigation controller's view
navView.addSubview(coverView)
// give it a tag so we can find it from the next view controller
coverView.tag = 9999
// create a constraint with an .identifier so we can get access to it from the next view controller
let startConstraint = coverView.topAnchor.constraint(equalTo: navView.topAnchor, constant: navView.frame.height)
startConstraint.identifier = "CoverConstraint"
// position the coverView so its top is at the bottom (hidden off-screen)
NSLayoutConstraint.activate([
startConstraint,
coverView.heightAnchor.constraint(equalTo: navView.heightAnchor, multiplier: 1.0),
coverView.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
coverView.trailingAnchor.constraint(equalTo: navView.trailingAnchor),
])
// we need to force auto-layout to put the coverView in the proper place
navView.setNeedsLayout()
navView.layoutIfNeeded()
// change the top constraint constant to 0 (top of the neavigation controller's view)
startConstraint.constant = 0
// animate it up
UIView.animate(withDuration: 0.3, animations: ({
navView.layoutIfNeeded()
}), completion: ({ b in
// after animation is complete, we'll change something in this VC's UI
self.doStuff()
}))
}
}
func doStuff() -> Void {
// if newView is already there, remove it
// else, add it to the view
// this will happen *while* the coverView is showing
if newViewToChange.superview != nil {
newViewToChange.removeFromSuperview()
} else {
view.addSubview(newViewToChange)
NSLayoutConstraint.activate([
newViewToChange.bottomAnchor.constraint(equalTo: view.bottomAnchor),
newViewToChange.leadingAnchor.constraint(equalTo: view.leadingAnchor),
newViewToChange.trailingAnchor.constraint(equalTo: view.trailingAnchor),
newViewToChange.heightAnchor.constraint(equalToConstant: 80.0),
])
}
// simulate it taking a full second
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// instantiate and push the next VC
// again, this will happen *while* the coverView is showing
let vc = NavCViewController()
self.navigationController?.pushViewController(vc, animated: false)
}
}
}
class NavCViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "View Controller C"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
// add a label
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
// do whatever else needed to setup this VC
// simulate it taking 1 second to setup this view
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// get
// the neavigation controller's view,
// the view with tag 9999 (the "coverView")
// the top constraint of the coverView
if let navView = self.navigationController?.view,
let v = navView.viewWithTag(9999),
let c = (navView.constraints.first { $0.identifier == "CoverConstraint" }) {
// change the top constant of the coverView to the height of the navView
c.constant = navView.frame.height
// animate it "down"
UIView.animate(withDuration: 0.3, animations: ({
navView.layoutIfNeeded()
}), completion: ({ b in
// after animation is complete, remove the coverView
v.removeFromSuperview()
}))
}
}
}
}
When you run it, it will look like this:
Tapping "Tap Me" will slide-up a "cover view" and a new red view will be added (but you won't see it):
The sample has 2-seconds worth of delay, to simulate whatever your app is doing to set up its UI. After 2-seconds, the cover view will slide down:
Revealing the pushed VC-C (confirmed by the Back button on the Nav Bar).
Tapping Back takes you back to VC-B, where you see the new red view that was added:
So, by animating the position of the cover view, we emulate the use of present() and dismiss(), and allow the push to take place behind it.
I'm trying to add a child view controller to a parent view controller in a swift ios application, but when I add the child view controller, the activityIndicatorView doesn't appear. What could I be missing?
Here is a snippet that can be tried in a playground:
import PlaygroundSupport
import Alamofire
class LoadingViewController: UIViewController {
private lazy var activityIndicator = UIActivityIndicatorView(style: .gray)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// We use a 0.5 second delay to not show an activity indicator
// in case our data loads very quickly.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) { [weak self] in
self?.activityIndicator.startAnimating()
}
}
}
// methods for adding and removing child view controllers.
extension UIViewController {
func add(_ child: UIViewController, frame: CGRect? = nil) {
addChild(child)
if let frame = frame {
child.view.frame = frame
}
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
// Just to be safe, we check that this view controller
// is actually added to a parent before removing it.
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
class MyViewController : UITabBarController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.view = view
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let loadingViewController = LoadingViewController()
add(loadingViewController, frame: view.frame)
AF.request("http://www.youtube.com").response { response in
print(String(describing: response.response))
loadingViewController.remove()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
The loadingViewController view fills the parent view, and i've tried to change the background colour at different points and that works. but activityIndicator or any other subview i try to add just doesn't appear.
Try add the line
activityIndicator.startAnimating()
in your viewDidLoad() method from your LoadingViewController class
I have this UIViewController:
import UIKit
class ViewController: UIViewController {
var object: DraggableView?
override func viewDidLoad() {
super.viewDidLoad()
// Create the object
object = DraggableView(parent: self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Add subview
object?.setup()
}
}
And I have this class to add the view in this VC:
import UIKit
class DraggableView {
var parent: UIViewController!
let pieceOfViewToShow: CGFloat = 30.0
init(parent: UIViewController) {
self.parent = parent
}
func setup() {
let view = UIView(frame: parent.view.frame)
view.backgroundColor = UIColor.red
parent.view.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
view.heightAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.heightAnchor).isActive = true
// I need to show only a piece of the view at bottom, so:
view.topAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.topAnchor, constant: parent.view.frame.height - pieceOfViewToShow).isActive = true
}
}
Problem
Everything is correct but when the device rotates it loses the constraint and the added view is lost.
I think the problem is in the next line that is not able to update the correct height [parent.view.frame.height] when the device is rotated.
view.topAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.topAnchor, constant: parent.view.frame.height - pieceOfViewToShow).isActive = true
How could I make to update this constant when rotating?
I'm using Swift 3.
You can try using traitCollectionDidChange callback on the UIView to update the constraint when a rotation changes, for that to work you'll need to make DraggableView a subclass of the UIView:
import UIKit
class DraggableView: UIView {
var parent: UIViewController!
let pieceOfViewToShow: CGFloat = 30.0
// keep the constraint around to have access to it
var topConstraint: NSLayoutConstraint?
init(parent: UIViewController) {
super.init(frame: parent.view.frame)
self.parent = parent
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.backgroundColor = UIColor.red
parent.view.addSubview(self)
self.translatesAutoresizingMaskIntoConstraints = false
self.leadingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
self.heightAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.heightAnchor).isActive = true
// keep a reference to the constraint
topConstraint = self.topAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.topAnchor, constant: parent.view.frame.height - pieceOfViewToShow)
topConstraint?.isActive = true
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// update the constraints constant
topConstraint?.constant = parent.view.frame.height - pieceOfViewToShow
}
}
I'm looking for the best way to invert the place of two UIView, with animation if possible (first i need to change the place, animation is optionnal).
viewController :
So, i want view1 to invert his place with the view2. The views are set with autolayout in the storyboard at the init.
if state == true {
view1 at the top
view2 at the bot
} else {
view 2 at the top
view 1 at the top
}
I've tried to take view.frame.(x, y, width, height) from the other view and set it to the view but it doesn't work.
I think the best way to do this would be to have a topConstraint for both views connected to the header and then change their values and animate the transition. You can do it in a way similar to this:
class MyViewController: UIViewController {
var view1TopConstraint: NSLayoutConstraint!
var view2TopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view1TopConstraint = view1.topAnchor.constraintEqualToAnchor(header.bottomAnchor, constant: 0)
view1TopConstraint.isActive = true
view2TopConstraint = view2.topAnchor.constraintEqualToAnchor(header.bottomAnchor, constant: view1.frame.height)
view2TopConstraint.isActive = true
}
func changeView2ToTop() {
UIView.animateWithDuration(0.2, animations: { //this will animate the change between 2 and 1 where 2 is at the top now
self.view2TopConstraint.constant = 0
self.view1TopConstraint.constant = view2.frame.height
//or knowing that view2.frame.height represents 30% of the total view frame you can do like this as well
// self.view1TopConstraint.constant = view.frame.height * 0.3
self.view.layoutIfNeeded()
})
}
You could also create the NSLayoutConstraint in storyboard and have an outlet instead of the variable I have created or set the top constraint for both views in storyboard at "remove at build time" if you are doing both. This way you won't have 2 top constraints and no constraint warrning
I Made an Example: https://dl.dropboxusercontent.com/u/24078452/MovingView.zip
I just created a Storyboard with 3 View, Header, View 1 (Red) and View 2 (Yellow). Then I added IBoutlets and animated them at viewDid Appear, here is the code:
import UIKit
class ViewController: UIViewController {
var position1: CGRect?
var position2: CGRect?
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
position1 = view1.frame
position2 = view2.frame
UIView.animate(withDuration: 2.5) {
self.view1.frame = self.position2!
self.view2.frame = self.position1!
}
}
}
Hello #Makaille I have try to resolve your problem.
I have made an example, which will help you for your required implementation.
Check here: Source Code
I hope, it will going to help you.
#IBAction func toggle(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
if(sender.isSelected){
let topPinConstantValue = layout_view2TopPin.constant
layout_view1TopPin.constant = topPinConstantValue
layout_view2TopPin.priority = 249
layout_view1BottomPin_to_View2Top.priority = 999
layout_view1TopPin.priority = 999
layout_view2BottomPin_ToView1Top.priority = 249
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: { (value) in
if(value){
sender.isUserInteractionEnabled = true
}
})
} else {
let topPinConstantValue = layout_view1TopPin.constant
layout_view2TopPin.constant = topPinConstantValue
layout_view1TopPin.priority = 249
layout_view2BottomPin_ToView1Top.priority = 999
layout_view2TopPin.priority = 999
layout_view1BottomPin_to_View2Top.priority = 249
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: { (value) in
if(value){
sender.isUserInteractionEnabled = true
}
})
}
sender.isSelected = !sender.isSelected
}