Why does parent UIStackView not recognize child StackView's height? - swift

I am trying to load a UIStackView from a xib that contains multiple labels into a parent UIStackView. The labels in the child stack get populated with values (or hidden) after the ViewController in the viewDidLoad method via a model object.
I expect that the parent stackview would recognize the change in intrinsic height of the child stackview, and thus move sibling views down. However, the next view (button) covers the content of the child stack. Why does the parent not recognize this change to a subview's height? I am not seeing any error messages, ambiguous constraints, or conflicts.
Here is how the error renders and how the views appear in the View Hierarchy.
I have tried:
Adding a UIView wrapper around the child stack view to see if the parent would register the change in it's height.
Moved ALL the code into the parent view controller, avoiding xib loading and that does appear to work, at the cost of losing modularity and separation of concerns.
Added optional height constraints on the child stackview based on other article recommendations, resulting in the positions not flexing with the content, or not resolving the original issue.
Here is the storyboard layout
Parent View Controller
class AcceptTermsViewController: RegistrationViewController {
#IBOutlet weak var infoView: StackViewInBorderedView!
#IBOutlet weak var termsView: TermsTextView!
#IBOutlet weak var masterStack: UIStackView!
var registration: Registration?
override func viewDidLoad() {
super.viewDidLoad()
self.configView()
self.setupNavBar()
}
func configView() {
if let reg = registration {
infoView.configView(reg)
}
}
}
Child StackView Code
class StackViewInBorderedView: UIStackView {
// loaded from NIB
private weak var view: UIView!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
#IBOutlet weak var locationLabel: UILabel!
#IBOutlet weak var postalLabel: UILabel!
#IBOutlet weak var idLabel: UILabel!
#IBOutlet weak var npiLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
loadViewFromNib()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadViewFromNib()
}
fileprivate func loadViewFromNib() {
self.view = Bundle (for: type(of: self)).loadNibNamed(
"StackViewInBorderedView", owner: self, options: nil)! [0] as? UIView
view.frame = bounds
view.autoresizingMask = [.flexibleWidth]
self.addSubview(view)
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = 10
let shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.frame.height / 12).cgPath
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.gray.cgColor
shadowLayer.shadowOffset = CGSize.zero
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = 3
shadowLayer.masksToBounds = false
self.layer.insertSublayer(shadowLayer, at: 0)
}
func configView(_ reg: Registration) {
configLabels(reg)
isLayoutMarginsRelativeArrangement = true
directionalLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 0)
}
func configLabels(_ reg: Registration) {
// Name line
if let degree = reg.degree {
nameLabel.text = "\(reg.firstName!) \(reg.lastName!), \(degree)"
} else {
nameLabel.text = "\(reg.firstName!) \(reg.lastName!)"
}
// Title line
if let title = reg.title {
titleLabel.text = title
} else {
titleLabel.isHidden = true
}
// Email line
if let email = reg.email {
emailLabel.text = email
} else {
emailLabel.isHidden = true
}
// Location line
if let city = reg.city, let state = reg.state, let postal = reg.postalCode,
!city.isEmpty, !state.isEmpty, !postal.isEmpty {
postalLabel.text = "\(city), \(state) \(postal)"
} else if let postal = reg.postalCode {
postalLabel.text = postal
}
// NPI line
if let licenseId = reg.licenseId {
switch reg.countryCode {
case "BR":
idLabel.text = "ID"
default:
idLabel.text = "NPI"
}
npiLabel.text = licenseId
} else {
self.idLabel.isHidden = true
self.npiLabel.isHidden = true
}
}
}

A colleague of mine found the answer.
When loading the view from the nib, I needed to call self.addArrangedSubview instead of self.addSubview which provides the intended behavior.

Related

Change default possition of view in Xcode

I'm trying to change a default position of my view for a user.
First I tried it with constraint like this:
class ShopSummaryViewController: BaseViewController, OverlayViewController {
// MARK: properties
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
let overlaySize: CGSize? = CGSize(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
#IBOutlet weak var trayView: UIView!
override func viewDidLoad() {
let onePercent = UIScreen.main.bounds.height / 100
heightConstraint.constant = onePercent * 4
}
which work, but for my purpose of UIGestureRecognizer it would be better to not use constraint, but something like this
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
let overlaySize: CGSize? = CGSize(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
#IBOutlet weak var trayView: UIView!
override func viewDidLoad() {
trayView.center = CGPoint(x: trayView.frame.size.width/2, y: trayView.frame.size.height/2)
}
which doesn't work. Could someone help me move the default position of the view by around 50% down

I cannot set background picture (Swift)

I made a simple timer app and I want to add a background picture or video in the background. However, I cannot do it and I do not know why.
Can someone tell me why the picture does not show up in the background? I set the background in the viewDidLoad method. Maybe the #IBOutlet var backgroundView: UIView! part is wrong? I connected it with the given UIView when I made this Xcode project.
class ViewController: UIViewController, UITableViewDelegate {
let mainStopwatch = Stopwatch()
let lapStopwatch = Stopwatch()
var isPlay: Bool = false
var laps: [String] = []
//UI components
#IBOutlet weak var lapRestButton: UIButton!
#IBOutlet weak var playPauseButton: UIButton!
#IBOutlet weak var lapTimerLabel: UILabel!
#IBOutlet weak var timerLabel: UILabel!
#IBOutlet weak var lapsTableView: UITableView!
#IBOutlet var backgroundView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.view.addBackground(name: "nathan-dumlao-iXXyrEwZgiU-unsplash")
lapRestButton.isEnabled = false
lapsTableView.dataSource = self
lapsTableView.delegate = self
}
//UI Settings
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}
//Actions
#IBAction func playPauseTimer(_ sender: Any) {
lapRestButton.isEnabled = true
changeButton(lapRestButton, title: "Lap", color: .black)
}
}
And this is another swift file for setting the background picture.
extension UIView {
func addBackground(name: String) {
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let imageViewBackground = UIImageView(frame: CGRect(x: 0, y: 0, width: width, height: height))
imageViewBackground.image = UIImage(named: name)
imageViewBackground.contentMode = .scaleAspectFill
self.addSubview(imageViewBackground)
self.sendSubviewToBack(imageViewBackground)
}
var timer = Timer()
var counter = 0 // the counter is use for the index of array in below code
var imageArr = ["image","image1","image2"] //Image arr
override func viewDidLoad() {
super.viewDidLoad()
self.addBackground(name: "image") // set default image on your view
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) // timer will start and it will change view background image after one min you can change time according to your requirement.
}
// called every time interval from the timer
#objc func timerAction() {
self.addBackground(name: self.imageArr[counter]) // here we are change our view background image when timer function is run on every min
counter += 1 // every-time counter will increment by one so the image array index will also increment and we will get our new image
}
// that's your code only you need to change one line that last one
func addBackground(name: String) {
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let imageViewBackground = UIImageView(frame: CGRect(x: 0, y: 0, width: width, height: height))
imageViewBackground.image = UIImage(named: name)
imageViewBackground.contentMode = .scaleAspectFit
self.viewref.addSubview(imageViewBackground) // here we will set the image view to our view
}
hope this will help you for change your background of view.
Thanks.
Seems like you've don't using your backgroundView to anywhere in class code , So first
Remove backgroundView from storyBoard and also remove code from class file
viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
self.addBackground(name: "nathan-dumlao-iXXyrEwZgiU-unsplash") //Update this line
lapRestButton.isEnabled = false
lapsTableView.dataSource = self
lapsTableView.delegate = self
}
Update your extension code to below one
extension YourViewController {
func addBackground(name: String) {
let width = self.view.frame.size.width
let height = self.view.frame.size.height
let imageViewBackground = UIImageView(frame: CGRect(x: 0, y: 0, width: width, height: height))
imageViewBackground.image = UIImage(named: name)
imageViewBackground.contentMode = .scaleAspectFill
self.view.addSubview(imageViewBackground)
// self.sendSubviewToBack(imageViewBackground) //Comment this line
}
}

Does anyone know how to get this to work i am trying to use pagecontrol to make a slideshow but index shows a error

class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var scrollView: UIScrollView!
var images: [String] = ["0","1","2"]
var frame = CGRect(x:0,y:0,width:0,height:0)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
pageControl.numberOfPages = images.count
for idex in 0..<images.count {
frame.origin.x = scrollView.frame.size.width
* CGFloat(index)
// the code above me shows a error
frame.size = scrollView.frame.size
let imgView = UIImageView(frame: frame)
imgView.image = UIImage(named: images[index])
this code above as well
self.scrollView.addSubview(imgView)
}
scrollView.contentSize = CGSize(width:(scrollView.frame.size.width * CGFloat(images.count)), height: scrollView.frame.size.height)
scrollView.delegate = self
}
Have you tried replacing the actual loop by this loop :
for index in 0..<images.count {
...

Thread 1 EXC_BAD_ACCESS when loading custom UIView from .xib file

I am trying to load a number of custom UIViews from a .xib file into a UIScrollView and I keep getting "Thread 1: EXC_BAD_ACCESS".
Since it's the first time when I'm actually trying to load a custom UIView (done this many times for UICollectionView and UITableView cells), I've been following this tutorial.
class PreSignupDataQuestionView : NibView
{
#IBOutlet weak var vMainContainer: UIView!
#IBOutlet weak var vQuestionViewContainer: UIView!
#IBOutlet weak var ivQuestionViewImage: UIImageView!
#IBOutlet weak var lblQuestion: UILabel!
}
class NibView : UIView
{
var view: UIView!
override init(frame: CGRect)
{
super.init(frame: frame)
xibSetup()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
xibSetup()
}
}
private extension NibView
{
func xibSetup()
{
backgroundColor = UIColor.clear
view = loadNib()
view.frame = bounds
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[childView]|",
options: [],
metrics: nil,
views: ["childView": view!]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[childView]|",
options: [],
metrics: nil,
views: ["childView": view!]))
}
}
extension UIView
{
func loadNib() -> UIView
{
let bundle = Bundle(for: type(of: self))
let nibName = type(of: self).description().components(separatedBy: ".").last!
let nib = UINib(nibName: nibName, bundle: bundle)
// Error pops here
return nib.instantiate(withOwner: self, options: nil).first as! UIView
}
}
Inside my UIViewController I have a function that sets up the UIScrollView and it works fine until I try to load the custom UIView.
for index in 0..<self.screenData.count
{
let xCoordinate = (CGFloat(index) * (width)) + firstSpace
let viewFrame = CGRect(x: xCoordinate, y: 0, width: width - spaceBetweenView, height: height)
let childView = PreSignupDataQuestionView().loadNib() as! PreSignupDataQuestionView
childView.frame = viewFrame
childView.tag = index
childView.isUserInteractionEnabled = true
childView.ivQuestionViewImage.image = UIImage(named: "PlaceholderImage")
childView.lblQuestion.text = "Lorep Ipsum"
self.svQuestions.addSubview(childView)
}
However, I have no problem loading a UIView if there is no .xib involved:
let xCoordinate = (CGFloat(index) * (width)) + firstSpace + lastSpace
let viewFrame = CGRect(x: xCoordinate, y: 0, width: width - spaceBetweenView, height: height)
let childView = UIView()
childView.tag = index
childView.isUserInteractionEnabled = true
if index % 2 == 0 { childView.backgroundColor = .red }
else { childView.backgroundColor = .green }
childView.frame = viewFrame
self.scrollView.addSubview(childView)
Any ideas? I've been trying to load a custom UIView into a UIScrollView for some time now and can't seem to figure this out.
I know this is a common issue but so far nothing worked for me.
That tutorial isn't quite right...
When loading the custom view - your PreSignupDataQuestionView - via code, change this line:
let childView = PreSignupDataQuestionView().loadNib() as! PreSignupDataQuestionView
to simply:
let childView = PreSignupDataQuestionView()
That should take care of it.
EDIT:
Try this in a new view controller, to rule out any other possible issues:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let childView = PreSignupDataQuestionView()
childView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(childView)
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
childView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50),
childView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50),
childView.heightAnchor.constraint(equalToConstant: 300),
])
}
}
EDIT 2:
I posted a complete sample project at https://github.com/DonMag/XIBLoadExtension
See if that works for you. If so, then compare to what you've got to see what's different.

How to fix AutoLayout for my ScrollView?

I have a view controller that has a UIView(ParentView) and then UIScrollview(ScrollView). The ParentView is anchored to the leading, trailing, top and bottom of the ViewController. The ScrollView is anchored to the leading, trailing, top and bottom of the ParentView. The structure is like this:
-UIView
--ParentView
---ScrollView
Then I created a xib file that has an image and a label. The xib will be added dynamically to the ScrollView. The xib frame height and width are equal to the ScrollView height and width.
When I run the simulator on iphone 6 plus the scroll works perfectly; the width of the xib is exactly the width of the screen. But when i run the simulator with iphone 6, the width of the xib is the same size of the screen, there is an extra space when scrolling to the next item. What should I do to removed it!?
This is a screen shot for iphone 6 plus
This is a screen shot for iphone 6. Notice the extra space of the second screen
And here is my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var ParentView: UIView!
#IBOutlet weak var ScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let photos = ["joe", "john"]
let userNames = ["joe", "john"]
self.ScrollView.frame = CGRectMake(0, 0, self.ParentView.frame.size.width, self.ParentView.frame.size.height)
self.ScrollView.contentSize = CGSizeMake(self.ParentView.frame.size.width * CGFloat(photos.count), self.ParentView.frame.size.height)
self.ScrollView.pagingEnabled = true
var i: Int = 0
while i < 2 {
if let userXib = NSBundle.mainBundle().loadNibNamed("User", owner: self, options: nil)[0] as? User {
userXib.frame = CGRect(x: self.ScrollView.frame.size.width * CGFloat(i), y: 0, width: self.ScrollView.frame.size.width, height: userXib.frame.size.height)
userXib.assignValues(photos[i], myName: userNames[i])
self.ScrollView.addSubview(userXib)
}
i = i + 1
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
UPDATE #1 - Coder1000 solution:
I tried implementing #3 & #4 in your answer by adding the UIView layer inside the scroll. I called it ScrollMainView. In the storyboard, I set its leading, trailing, top and bottom to fill its superview: Scrollview and then in the loop added those elements to it instead of the scrollview. But now the scroll doesn't scroll! My code and final display look like this:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var ParentView: UIView!
#IBOutlet weak var ScrollView: UIScrollView!
#IBOutlet weak var ScrollMainView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let photos = ["joe", "john"]
let userNames = ["joe", "john"]
self.ScrollView.frame = CGRectMake(0, 0, self.ParentView.frame.size.width, self.ParentView.frame.size.height)
self.ScrollView.contentSize = CGSizeMake(self.ParentView.frame.size.width * CGFloat(photos.count), self.ParentView.frame.size.height)
self.ScrollView.pagingEnabled = true
self.ScrollMainView.frame = CGRectMake(0, 0, self.ScrollView.frame.size.width, self.ScrollView.frame.size.height)
print("the frame for parentview is: \(self.ParentView.frame)")
var i: Int = 0
while i < 2 {
if let userXib = NSBundle.mainBundle().loadNibNamed("User", owner: self, options: nil)[0] as? User {
userXib.frame = CGRect(x: self.ScrollMainView.frame.size.width * CGFloat(i), y: 0, width: self.ScrollMainView.frame.size.width, height: userXib.frame.size.height)
userXib.assignValues(photos[i], myName: userNames[i])
self.ScrollMainView.addSubview(userXib)
}
i = i + 1
}
self.ScrollView.addSubview(ScrollMainView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
There are several solutions, in viewDidLoad the frame is not set correctly, so when you ask it, it is wrong.
The parentView and your scrollView are adjusted by the autolayout constraint later, but you User view is not...
A simple solution may be to put the code in:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.ScrollView.frame = CGRectMake(0, 0, self.ParentView.frame.size.width, self.ParentView.frame.size.height)
self.ScrollView.contentSize = CGSizeMake(self.ParentView.frame.size.width * CGFloat(photos.count), self.ParentView.frame.size.height)
self.ScrollView.pagingEnabled = true
for subView: UIView in self.ScrollView.subviews {
if subView is User { subView.removeFromSuperview()
}
var i: Int = 0
while i < 2 {
if let userXib = NSBundle.mainBundle().loadNibNamed("User", owner: self, options: nil)[0] as? User {
userXib.frame = CGRect(x: self.ScrollView.frame.size.width * CGFloat(i), y: 0, width: self.ScrollView.frame.size.width, height: userXib.frame.size.height)
userXib.assignValues(photos[i], myName: userNames[i])
self.ScrollView.addSubview(userXib)
}
i = i + 1
}
}
Or you can add constraints to your custom xib programmatically.
Or you can keep an array of your costom xibs and just adjust their frames, AND THE SCROLLVIEW CONTENTSIZE, in viewDidLayoutSubviews()
Or you can put the code in viewDidAppear()