Why are viewDidLayoutSubviews and viewWillLayoutSubviews called two times? - ios

import UIKit
class ViewController: UIViewController {
// MARK: -property
// lazy var testBtn: UIButton! = {
// var btn: UIButton = UIButton()
// btn.backgroundColor = UIColor.red
// print("testBtn lazy")
// return btn
// }()
// MARK: -life cycle
override func viewDidLoad() {
super.viewDidLoad()
print("View has loaded")
// set the superView backgroudColor
// self.view.backgroundColor = UIColor.blue
// add testBtn to the superView
// self.view.addSubview(self.testBtn)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("View will appear")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("View has appeared")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("View will disappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("View has desappeared")
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("SubViews will layout")
// layout subViews
// 'CGRectMake' is unavailable in Swift
// self.testBtn.frame = CGRectMake(100, 100, 100, 100)
// self.testBtn.frame = CGRect(x: 100, y: 100, width: 100, height: 100) // CGFloat, Double, Int
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("SubViews has layouted")
// let testBtn_Width = self.testBtn.frame.width
// print("testBtn's width is \(testBtn_Width)")
}
}
The result:
View has loaded
View will appear
SubViews will layout
SubViews has layouted
SubViews will layout
SubViews has layouted
View has appeared
As you see, I have created a new project and type some simple code.
I didn't change the size of the viewController's view.
Why are "SubViews has layouted" and "SubViews will layout" console two times?
Why are viewDidLayoutSubviews and viewWillLayoutSubviews called two times?

Because whenever setNeedsLayout or setNeedsDisplayInRect is being called internally, LayoutSubviews is also being called (once per run loop) on any given view. This applies for example if the view has been added, scrolled, resized, reused etc.

Related

`UIScrollView` `bounds` are different in `viewDidAppear` than last `viewDidLayoutSubviews`

In attempt to improve the UI for smaller devices, when a UIScrollView contentSize.height is greater than it's bounds.height (I'm calling this an overflow) I want to tweak the UI.
My issue is: I'm finding the layout of the views is different after appearing on screen (in viewDidAppear(:)) than it is in the last viewDidLayoutSubviews() (could be called multiple times).
In order to determine if a scrollView is overflowing, we need to know both the scrollView.bounds and scrollView.contentSize. Which I thought would be determined in the last call of viewDidLayoutSubviews().
Below examples my scenario. It's running an interface built in xib with the scrollView set up there too. Note there are no hiding/showing of views to interfere with the layout values, simply runs through.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
printOverflow(type: #function)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
printOverflow(type: #function)
}
private func printOverflow(type: String) {
let isOverflowY = scrollView.contentSize.height > scrollView.bounds.height
debugPrint("[Overflow] \(type) \(isOverflowY ? "Overflow" : "Fits")")
}
// After running prints:
// "[Overflow] viewDidLayoutSubviews() Fits"
// "[Overflow] viewDidAppear(_:) Overflow"
My question is: why can I not get the final layout of the subviews in the last viewDidLayoutSubviews()?
I also tried adding a view.layoutIfNeeded() in viewWillAppear(:), indeed this fires another viewDidLayoutSubviews() as you'd expect but the bounds properties are still different to that in viewDidAppear(:) (despite actually now printing "Overflow")
From Apple's docs (italics are mine):
func viewDidLayoutSubviews()
When the bounds change for a view controller'€™s view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view'€™s subviews have been adjusted. Each subview is responsible for adjusting its own layout.
So, viewDidLayoutSubviews() can (and will) be called before subviews have been completely configured by auto-layout. That's also why we see viewDidLayoutSubviews() being called multiple times during loading / displaying.
If you subclass UIScrollView like this:
class MyScrollView: UIScrollView {
override func layoutSubviews() {
super.layoutSubviews()
printOverflow(type: #function)
}
private func printOverflow(type: String) {
let isOverflowY = contentSize.height > bounds.height
debugPrint("MyScrollView {Content Size: \(contentSize.height) Bounds Height: \(bounds.height)} [Overflow] \(type) \(isOverflowY ? "Overflow" : "Fits")")
}
}
You'll see that you get the correct heights.
What exactly are trying to do to "improve the UI for smaller devices"? There may be other (better?) options...
Edit
Here's a very simple example showing the sizing at each stage. The custom view changes its height anchor (if necessary) in its layoutSubviews() function:
import UIKit
class MySizingView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
if frame.height != 200.0 {
heightAnchor.constraint(equalToConstant: 200.0).isActive = true
}
print(#function, "self height:", frame.height)
}
}
class SizingTestViewController: UIViewController {
let myView: MySizingView = {
let v = MySizingView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(myView)
NSLayoutConstraint.activate([
// constrain width and center X & Y
// its height will be set by its own height constraint
myView.widthAnchor.constraint(equalToConstant: 300.0),
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(#function, "myView height:", myView.frame.height)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print(#function, "myView height:", myView.frame.height)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(#function, "myView height:", myView.frame.height)
}
}
Debug console output should be:
viewWillAppear(_:) myView height: 0.0
viewDidLayoutSubviews() myView height: 0.0
layoutSubviews() self height: 0.0
viewDidLayoutSubviews() myView height: 200.0
layoutSubviews() self height: 200.0
viewDidAppear(_:) myView height: 200.0

How to access UIView in ViewController when calling function from another file?

Situation: I paint stuff through a button-click on a subview. Then I add the subview to the view.
Problem: When calling drawNew from another swift file, the if-loop is not triggered. Probably because the view with the tag 777 does not get found.
Question: How do I tell the code to look for the view in ViewController?
When do I call the function drawNew()? I call it from another file with override func touchesEnded(...)
//###########################
// ViewController.swift
//###########################
var dv = Painting() // in here are functions to create a cg path
//
// IBActions
//
#IBAction func buttonPressed(_ sender: UIButton) {
// painting
if sender.tag == 1 {
drawNew()
} else if sender.tag == 2 {
CanvasView.clearCanvas()
}
}
//
// FUNCTIONS
//
func drawNew() {
dv.makeMeSomePath()
if let foundView = view.viewWithTag(777) { // problem lies here maybe
dv.tag = 111
foundView.addSubview(dv)
print("added subview")
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// prepare the canvas for
let newView = CharacterView(frame: CGRect(x: 0,
y:0,
width: HandwritingCanvasView.frame.width,
height: HandwritingCanvasView.frame.height))
newView.tag = 777
HandwritingCanvasView.addSubview(newView)
// self.view.addSubview(demoView)
}
Then this is the other file from where I call the drawNew() func
// HandwritingCanvasView.swift
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
ViewController.drawNew()
}
It will be easier to have reference on newView in ViewController.
Try to do next
//###########################
// ViewController.swift
//###########################
weak var subView: UIView? //Add reference on your view
//
// FUNCTIONS
//
func drawNew() {
dv.makeMeSomePath()
if let foundView = subView { // Use the reference instead of searching view with tag
dv.tag = 111
foundView.addSubview(dv)
print("added subview")
}
}
override func viewDidAppear(_ animated: Bool) {
// Your code is here
// ...
HandwritingCanvasView.addSubview(newView)
subView = newView
//...
}

How i get subView using a Rect in Swift 2.0

There is a Main View with subview. I need to get these subviews.
For example, I created a view:
With the Swift Code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Check if has view in rect area
let rect1 = CGRect(x: 0, y: 0, width: 100, height: 100)
if CGRectContainsPoint(rect1, rect1.origin) { // return true
print("TRUE")
} else {
print("FALSE")
}
}
}
I need to get the view you are "touching" the Rect in size or origin.
In the above example, I created three subviews with different colors between them.
I need to get the view and make a print() with the backgroundColor.
Can someone help me?
This func fix the problem:
func intersectingViewWithView(panView: UIView) -> UIView? {
for view in self.view.subviews {
if view != panView {
if CGRectIntersectsRect(panView.frame, view.frame) {
return view
}
}
}
return nil
}

Button click crashes the whole application

I am currently building an application's UI without using IB or storyboard. I have the following hierarchy:
1. ViewController: which is the main view controller.
2. DrawerViewController: which is the view controller for the lower drawer menu.
The main view controller adds as a subview the drawerViewController's view.
3. Add Button: this is a UIButton created and added programatically to the drawerViewController's view.
The problem is if I clicked on the add button, the application crashes, giving absolutely no error messages in the output, except (lldb).
I have tried the following:
Change the selector name and the method name.
Checked the name of the selector method.
Added and removed the parameter of the selector method.
Changed the name of the button.
Added the button inside another view.
There's no way to trace the bug using breakpoints, because the application is compiling just fine, and the app crashes only if you click on the button.
Here's a link to a testing project I created to show the problem:
GitHub
ViewController:
class ViewController: UIViewController //, DrawerDelegate
{
//Lower Drawer.
var drawerView: DrawerView!
var drawerViewHidden: Bool = false
var addButton: UIButton!
var viewHeight: CGFloat! = 0
var viewWidth : CGFloat! = 0
var shownDrawerViewY: CGFloat!
var hiddenDrawerViewY: CGFloat!
init()
{
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Setup UI
setUpUI()
}
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated);
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK:- UI initialization.
/**
Private function that setsup the UI.
*/
private func setUpUI()
{
setUpDrawer()
}
//MARK: Drawer setup.
/**
A private function that programatically creataes a drawer and add it to the view.
*/
private func setUpDrawer()
{
var controller = DrawerViewController(width: self.view.frame.width, height: self.view.frame.height)
self.view.addSubview(controller.view)
//Hide
// toggleDrawer()
}
//MARK:- Delegates.
//MARK: DrawerDelegate methods.
func addButtonClicked(sender: UIButton)
{
// toggleDrawer()
}
}
DrawerViewController
class DrawerViewController: UIViewController
{
//Drawer variables.
var drawerViewHidden: Bool = false
var addPinDrawer: AddPinDrawer!
var viewHeight: CGFloat! = 0
var viewWidth : CGFloat! = 0
var shownDrawerViewY: CGFloat?
var hiddenDrawerViewY: CGFloat?
var addButton: UIButton?
init(width: CGFloat, height: CGFloat)
{
super.init(nibName: nil, bundle: nil)
viewWidth = width
viewHeight = height
setUpUI()
}
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
required override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?)
{
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
//MARK:- UI setup
func setUpUI()
{
setUpDimensions()
setUpDrawerElements()
// toggleDrawer()
}
func setUpDimensions()
{
//Determine the y in the case drawer is shown
shownDrawerViewY = viewHeight * 2.0/8.0
//Determine the height of the drawer.
let drawerHeight = viewHeight * 6.0/8.0
//Determine the y in the case drawer is hidden
hiddenDrawerViewY = viewHeight * 7.4/8.0
//Create the frame, starting with the drawer shown.
let frame = CGRectMake(0, shownDrawerViewY!, viewWidth, drawerHeight)
//Create a new Drawer View.
self.view = UIView(frame: frame)
setUpAddButton(frame)
//Setup the background image of the drawer.
self.view.backgroundColor = UIColor(patternImage: UIImage(named: Constants.drawerBackgroundImage)!)
}
func setUpDrawerElements()
{
//Setup the button.
setUpAddPinDrawer()
}
func setUpAddPinDrawer()
{
addPinDrawer = AddPinDrawer(frame: CGRectMake(0, 0, self.view.frame.width, self.view.frame.height))
// self.view.addSubview(addPinDrawer)
}
//MARK: Handling drawer toggles
func toggleDrawer()
{
//Toggle the variable.
drawerViewHidden = !drawerViewHidden
//If the drawer must be hidden.
if drawerViewHidden
{
hideDrawer()
}
//If the drawer must be shown
else
{
showDrawer()
}
}
func hideDrawer()
{
//Hide the drawer
UIView.animateWithDuration(0.6, animations: { () -> Void in
self.view.frame.origin.y = self.hiddenDrawerViewY!
})
// drawerView.hideDrawer()
}
func showDrawer()
{
UIView.animateWithDuration(0.6, animations: { () -> Void in
self.view.frame.origin.y = self.shownDrawerViewY!
})
// drawerView.showDrawer()
}
func setUpAddButton(frame: CGRect!)
{
//Determine the button dimensions.
let width:CGFloat = 75.0//1.0/10.0 * viewHeight
let x = frame.width/2.0 - width/2.0
//Button background image.
let background = UIImage(named: Constants.addButtonBackgroundImage)!
//Create the button.
addButton = UIButton(frame: CGRectMake(x, -width/2.0, width, width)) as UIButton
// addButton!.setImage(background, forState: UIControlState.Normal)
addButton?.backgroundColor = UIColor.yellowColor()
//Add the event handler.
addButton!.addTarget(self, action: "buttonAdd:", forControlEvents: .TouchUpInside)
//Set it rotated.
// self.addButton!.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4))
//Add the button to the subview.
self.view.addSubview(addButton!)
// println(addButton!.targetForAction("test", withSender: addButton)!)
}
func buttonAdd(sender: UIButton)
{
println("Helloooo asdf")
}
}
I have solved the problem. There's a very little mistake in the initiation of the DrawerViewController, In the main view controller, I am creating a variable called DrawerViewController and initiating its view. This variable will be deallocated once the method is done, which means all events will not be handled. Causing the app to crash since there's no method to handle the button's events.
The solution:
Make the DrawerViewController an instance variable and initialise it in the main view controller.

Swift Fade In Animation for Background Animation between A Single View to a PageView Controller

So I am trying to create a fade in animation for a UI Image that is in a page view scroller after a button is clicked in the main view controller. This is my main storyboard.
class MainWorkoutViewController: UIViewController {
// Outlet used in storyboard
#IBOutlet var scrollView: UIScrollView?;
override func viewDidLoad() {
super.viewDidLoad();
func scrollViewDidEndDragging(MainWorkoutViewController: UIScrollView,
withVelocity: CGPoint,
targetContentOffset : UnsafeMutablePointer<CGPoint>){
}
// 1) Create the three views used in the swipe container view
var AVc :AViewController = AViewController(nibName: "AViewController", bundle: nil);
var BVc :BViewController = BViewController(nibName: "BViewController", bundle: nil);
var CVc :CViewController = CViewController(nibName: "CViewController", bundle: nil);
// 2) Add in each view to the container view hierarchy
// Add them in opposite order since the view hieracrhy is a stack
self.addChildViewController(CVc);
self.scrollView!.addSubview(CVc.view);
CVc.didMoveToParentViewController(self);
self.addChildViewController(BVc);
self.scrollView!.addSubview(BVc.view);
BVc.didMoveToParentViewController(self);
self.addChildViewController(AVc);
self.scrollView!.addSubview(AVc.view);
AVc.didMoveToParentViewController(self);
// 3) Set up the frames of the view controllers to align
// with eachother inside the container view
var adminFrame :CGRect = BVc.view.frame;
adminFrame.origin.x = adminFrame.width;
AVc.view.frame = adminFrame;
var BFrame :CGRect = AVc.view.frame;
BFrame.origin.x = 2*BFrame.width;
CVc.view.frame = BFrame;
// 4) Finally set the size of the scroll view that contains the frames
var scrollWidth: CGFloat = 3 * self.view.frame.width
var scrollHeight: CGFloat = self.view.frame.size.height
self.scrollView!.contentSize = CGSizeMake(scrollWidth, scrollHeight)
var frame: CGRect = self.view.frame
frame.origin.x = frame.size.width * CGFloat(1);
frame.origin.y = 0;
self.scrollView!.scrollRectToVisible(frame, animated: false)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(animated: Bool) {
self.tabBarController?.selectedIndex = 2
}
}
This is the .swift file for the first Viewcontroller that I have, which contains the image I want to fade in when the screen loads.
import UIKit
class AViewController: UIViewController {
#IBOutlet var Background: UIImageView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(1.5, animations: {
self.Background.alpha = 1.0
})
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I looked around for a way to do it and that is why I have the animatedWithDuration, but instead all I am getting is the screen swiping up from the bottom of the phone. Any ideas?
Try setting the alpha to 0.0 before the UIView.animateWithDuration function.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.background.alpha = 0.0
UIView.animateWithDuration(1.5, animations: {
self.background.alpha = 1.0
})
}
Please note that viewDidAppear is not called after you have scrolled to a view in the scrollview. It is called when the view is added to the view hierarchy. You have to manually write the code for making the label appear using scrollView delegate functions if you want to make it appear when you scroll with your fingers or scroll automatically later.
You can do a cross dissolve animation instead of scrolling by doing
CATransaction.flush()
self.scrollView.contentOffset = CGPointMake(self.view.frame.width,0)
UIView.transitionWithView(self.scrollView, duration: 1.0, options:
.TransitionCrossDissolve, animations: { () -> Void in
}) { (finished) -> Void in
}
Either use CATransactionFlush or put the code inside viewDidAppear.
Remove the scrollView.scrollRectToVisible code.

Resources