Opposite of what everyone else seems to be asking. I have a collectionView with cells into which I am adding a loaded .xib as a subview. Easy enough.
However, the size of the collectionView cells change at run-time based on different criteria so I need the subview to be properly constrained to the size of the collectionViewCell's contentView. In an attempt to do this, I have this code when I add the subview:
/**
Used to present a view in the collectionView. WidetView is a subclass of UIView
*/
class WidgetCell: UICollectionViewCell, Reusable {
var widgetView: WidgetView! {
didSet {
for view in contentView.subviews {
view.removeFromSuperview()
}
contentView.addSubview(widgetView)
let views = ["view": widgetView as Any]
var constraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[view]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: views)
constraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[view]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: views))
contentView.addConstraints(constraints)
}
}
}
Unfortunately, what's presented isn't a set of views that are properly constrained. The added UIView is the same size as what's defined in the .xib.
How can I fix this? If you want a sample project (Swift 3) look here: https://github.com/AaronBratcher/FitToCell
It's a simple case of calling contentView.activateConstraints(constraints) with your existing code and then at the point where the cells are shown, cell.layoutIfNeeded(). This forces the cell to recalculate the bounds of it's subviews.
Since the expected size of the cell is known for layout purposes, pass that size to the WidgetCell instance and then set the view's frame size.
final class WidgetCell: UICollectionViewCell {
var cellSize: CGSize?
var widgetView: WidgetView! {
didSet {
for view in contentView.subviews {
view.removeFromSuperview()
}
let frame: CGRect
if let cellSize = cellSize {
frame = CGRect(origin: CGPoint(x: 0, y: 0), size: cellSize)
} else {
frame = contentView.frame
}
widgetView.frame = frame
widgetView.setNeedsLayout()
contentView.addSubview(widgetView)
}
}
}
Related
I'd like to use a scrollView to move the nested view content up when the keyboard appears. (Maybe you know a better solution ?)
So, I put a UIScrollView into my UIViewController and a UIImageView into my UIScrollView. The problem is my UIScrollView is as large as my image size despite constraints.
I put the following constraints :
scrollView.addConstraintsWithFormat(format: "H:|[v0]|", views: backgroundImage)
scrollView.addConstraintsWithFormat(format: "V:|[v0]|", views: backgroundImage)
self.view.addConstraintsWithFormat(format: "H:|[v0]|", views: scrollView)
self.view.addConstraintsWithFormat(format: "V:|[v0]|", views: scrollView)
Someone have a solution ?
This is my full UIViewController code :
import UIKit
class HomeViewController: UIViewController {
let scrollView: UIScrollView = {
let screenSize = UIScreen.main.bounds
let scrollView = UIScrollView()
scrollView.backgroundColor = .red
scrollView.contentSize = CGSize(width: screenSize.width, height: screenSize.height)
return scrollView
}()
let backgroundImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "BACKGROUND_ASIA")
imageView.alpha = 0.5
return imageView
}()
override func viewDidLoad() {
setupHomeView()
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func setupHomeView() {
self.view.backgroundColor = UIColor.black
self.view.addSubview(scrollView)
self.view.addConstraintsWithFormat(format: "H:|[v0]|", views: scrollView)
self.view.addConstraintsWithFormat(format: "V:|[v0]|", views: scrollView)
scrollView.addSubview(backgroundImage)
scrollView.addConstraintsWithFormat(format: "H:|[v0]|", views: backgroundImage)
scrollView.addConstraintsWithFormat(format: "V:|[v0]|", views: backgroundImage)
}
}
extension UIView {
func addConstraintsWithFormat(format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
You should call super first in viewDidLoad.
You should read up on how scrollViews work.
Here's what you need:
The ScrollView needs constraints for left/right/top/bottom.
This will determine the size of the presentable portion of the scrollview. This is the part that you would resize when the keyboard shows.
Then, you need to set the size of the ScrollView's content. This is the content that can be scrolled. You will need to manually set the size of your imageView, or setup equality between your imageView and views that exist outside of your scrollview. (eg imageView.width == view.width).
Hope this points in the right direction. You might want to consider using Interface Builder to set this up so you can see all the constraints and get warning when things aren't set up properly.
Thanks for your answer PEEJWEEJ, but I found another alternative to my problem. I used the NotificationCenter to notify keyboard opening and I made a view.animate() to scroll my view. By this way I avoid to use a scrollView or a tableView.
I've got a custom UIView that I instantiate in a view controller with this function, displayedTimer is an iVar of the view controller:
func changeViewModeTo(mode: String){
if mode == "settings" {
addSettingsModeConstraints()
animatedLayoutIfNeeded(removeView: true)
}
if mode == "timer" {
displayedTimer = TimerView.init()
displayedTimer.frame = CGRect(x: (self.view.bounds.size.width)/2 - 50, y: (self.view.bounds.size.height)/2 - 80, width: 100, height: 160)
let colors = timer.getColorScheme()
displayedTimer.setColorScheme(colorLight: colors["lightColor"]!, colorDark: colors["darkColor"]!)
displayedTimer.setTimeRemainingLabel(timer.duration)
displayedTimer.setCountDownBarFromPercentage(1.0)
displayedTimer.layer.zPosition = 100 //make sure the timer view sits on top of the settings panel
displayedTimer.timerLabel.hidden = false
displayedTimer.translatesAutoresizingMaskIntoConstraints = false
let pinchGestureRecogniser = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchDetected(_:)))
displayedTimer.addGestureRecognizer(pinchGestureRecogniser)
self.view.addSubview(displayedTimer)
addTimerModeConstraints()
animatedLayoutIfNeeded(removeView: false)
}
}
If the mode is set to timer then it creates a subclass of UIView and sets an instance variable to it, constraints are added to make it full screen and then an animated layoutIfNeeded() is called. If the mode being set is settings then it deactivates the timerConstraints, adds new constraints to shrink the view, calls an animated layoutIfNeeded and then removes the view from the superView.
func animatedLayoutIfNeeded(removeView removeView: Bool){
UIView.animateWithDuration(0.2, delay: 0, options: [UIViewAnimationOptions.CurveEaseIn] , animations: {
self.view.layoutIfNeeded()
}) { (true) in
if removeView == true {
self.displayedTimer.removeFromSuperview()
}
}
}
The constraints are added and removed with these methods (settingsConstraints and timerConstraints are iVars of the view controller):
//MARK: - Layout Constraints
func addSettingsModeConstraints() {
let views = ["timerView": displayedTimer]
let timerHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-75-[timerView]-75-|",
options: [],
metrics: nil,
views: views)
settingsConstraints += timerHorizontalConstraints
let timerVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-105-[timerView]-85-|",
options: [],
metrics: nil,
views: views)
settingsConstraints += timerVerticalConstraints
NSLayoutConstraint.deactivateConstraints(timerConstraints)
NSLayoutConstraint.activateConstraints(settingsConstraints)
}
func addTimerModeConstraints() {
let views = ["timerView": displayedTimer]
let timerHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-0-[timerView]-0-|",
options: [],
metrics: nil,
views: views)
timerConstraints += timerHorizontalConstraints
let timerVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-0-[timerView]-0-|",
options: [],
metrics: nil,
views: views)
timerConstraints += timerVerticalConstraints
NSLayoutConstraint.activateConstraints(timerConstraints)
}
changeViewModeTo is called from a pinch gesture recogniser (negative pinch sets one mode, positive pinch sets another mode).
The first time I pinch, the view is created and goes full screen. I then reverse pinch and the view shrinks and is removed. Then when I pinch again to start the process over the app crashes, there are no console errors but there is a red error over the line of code: NSLayoutConstraint.activateConstraints(timerConstraints)
I'm guessing removing the subview has caused the reference to NSConstraints to disappear?
Any thoughts would be great as I can't figure it out.
So turns out this is a simple fix, call removeAll() on settingsConstraints and timerConstraints before recreating them and activating them solves the problem.
I'm trying to build a reusable UIView whose width should equal to its superview in Swift.
Since the size of its superview varies, I think I have to set constraints for it with auto layout.
But I can't figure out how to do it programmatically in Swift.
Here is the code for the reusable subview:
import UIKit
class bottomMenu: UIView {
#IBOutlet var bottomMenu: UIView!
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
NSBundle.mainBundle().loadNibNamed("bottomMenu", owner: self, options: nil)
bottomMenu.backgroundColor = UIColor.redColor()
//How to make the width of the bottom Menu equal to its superview?
self.addSubview(self.bottomMenu)
}
}
Can anyone show me how to make it?
Thanks
You can override didMoveToSuperview() method in your UIView subclass and add the constraints there:
override func didMoveToSuperview() {
super.didMoveToSuperview()
backgroundColor = UIColor.redColor()
let views = ["view" : self];
self.setTranslatesAutoresizingMaskIntoConstraints(false)
self.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[view]|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
self.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
}
Add constraints for element inside. For each element add constraint for top, bottom, left and right. If you have any images that needs to be same size add width and height as well. If you can post the screenshot of the UIView I will add more information and will be able to be more helpful.
Also take a look at http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-1 if you are new to autolayout.
The following code gives the loaded view the same height and width as the super view. (not sure what you wanted for height)
bottomMenu.setTranslatesAutoresizingMaskIntoConstraints(false)
//Views to add constraints to
let views = Dictionary(dictionaryLiteral: ("bottomMenu",bottomMenu))
// Horizontal constraints
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[bottomMenu]|", options: nil, metrics: nil, views: views)
self.addConstraints(horizontalConstraints)
// Vertical constraints
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[bottomMenu]|", options: nil, metrics: nil, views: views)
self.addConstraints(verticalConstraints)
How do you get the width and height of a UIView who's size and position are set using Auto Layout and Apple's Visual Format Language?
Here's the code (view is just the variable from UIViewController):
// Create and add the view
var stageView = UIView()
stageView.setTranslatesAutoresizingMaskIntoConstraints(false) // Since I'm using Auto Layout I turn this off
view.addSubview(stageView)
// Create and add constraints to the containing view
let viewsDictionary = ["stageView":stageView]
let horizontalConstraints: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("H:|-150-[stageView]-150-|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDictionary)
let verticalConstraints: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("V:|-100-[stageView]-150-|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: viewsDictionary)
view.addConstraints(horizontalConstraints)
view.addConstraints(verticalConstraints)
println("stageView.frame=\(stageView.frame)")
and got:
stageView.frame=(0.0,0.0,0.0,0.0)
so I tried:
let fittingSize = stageView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
println("fittingSize=\(fittingSize)")
and got:
fittingSize=(0.0,0.0)
I can't seem to find a way to get the size. I'm able to add subviews to stageView that place just fine using Auto Layout and Visual Format Language, but I can't get width and height for stageView which I need to further position those subviews.
Any ideas?
You have a few options:
You can force the layout engine to size the views immediately by calling setNeedsLayout and then call layoutIfNeeded. This is not recommended because it's inefficient and any manual frames required for layout might not have been set yet. You can read more about this approach on my answer to this question.
You can also wait until the subviews have been updated in the view controller:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
println("stageView.frame = \(stageView.frame)")
}
If you want to know within a UIView subclass (or more often, a UITableViewCell subclass, you can check after layoutSubviews has run:
override func layoutSubviews() {
super.layoutSubviews()
println("self.frame = \(self.frame)")
}
You need to check the frame inside viewDidLayoutSubviews.
This function run after constraint calculation
Its suppose to look something like this
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
//Print frame here
}
I need to create a view with a scroll view and a page control in it, and place 7 views inside scroll view.
To lay out subviews inside the scroll view I use pure Auto layout Approach, that is described here.
So I have my controller with XIB file (I don't use storyboards here) that is pretty simple: it's a UIScrollView and UIPageControl with all constraints set up.
And I have a XIB for a UIView subclass Slide which has 2 UIImageViews and 1 UILabel, and there's also some constraints.
To add some views to UIScrollView I use this code in viewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
var pSlide: Slide?
for var i = 0; i < 7; i++ {
var slide = Slide(frame: self.view.bounds, imageName: "slide-\(i+1)-bg", text: NSLocalizedString("slides_\(i+1)", comment: ""))
slide.setTranslatesAutoresizingMaskIntoConstraints(false)
scrollView.addSubview(slide)
var dict: [NSObject : AnyObject] = ["currentSlide" : slide]
if let previousSlide = pSlide {
dict["previousSlide"] = previousSlide
let constraintsHorizontal = NSLayoutConstraint.constraintsWithVisualFormat("H:[previousSlide][currentSlide]", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsHorizontal)
let constraintsVertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|[currentSlide]|", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsVertical)
} else {
let constraintsVertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|[currentSlide]|", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsVertical)
let constraintsLeft = NSLayoutConstraint.constraintsWithVisualFormat("H:|[currentSlide]", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsLeft)
}
if i == 6 {
let constraintsRight = NSLayoutConstraint.constraintsWithVisualFormat("H:[currentSlide]|", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsRight)
}
pSlide = slide
}
pageControl.numberOfPages = numberOfSlides
view.layoutSubviews()
}
In this piece of code I create a Slide instance, and set all necessary constraints to it, according to pure Auto Layout approach.
init() method of the Slide class looks like this:
init(frame: CGRect, imageName: String, text: String) {
super.init(frame: frame)
NSBundle.mainBundle().loadNibNamed("Slide", owner: self, options: nil)
self.addSubview(self.view)
self.view.frame = frame
self.layoutIfNeeded()
println("Frame is \(frame); view.frame is \(self.view.frame)")
backgroundImage.image = UIImage(named: imageName)
textLabel.text = text
}
I hoped that
self.view.frame = frame
self.layoutIfNeeded()
will help me but no. The problem is, on 3.5 inch screen all my UIScrollView subviews have the height of 568, which is the normal height for 4 inch display, but not for 3.5 inch.
I'm checking the height in viewDidAppear(animated:) method. But, in init() method of Slide class the height appears to be ok — 480.
I'm trying to solve it for second day already, and still nothing works. I know that this may be much more simple to implement without using Auto Layout and Interface Builder, but I need to do it with these.
I used UIPageViewController instead of all this mess, and it works just fine.