This is my first iOS app and I'm using Swift. I'm following tutorials on CodeWithChris.com.
The user will be tapping on a series of icons to determine what device model they have. So far I've used a handful of constraints to get the icons placed properly on an iPhone 6 display. When using iPhone 5S/5c/5 or 4S/4 display sizes the icons and text still render at the full size for iPhone 6 and they go off the screen.
Here's my process for building the layout:
Grab and parse JSON to NSArray. Returns ["iPhone", "Android", "Windows", "iPad"]
Create UIView object for each item in NSArray. Set Name variable to the associated name.
Place each UIView using addSubview()
Apply height, width, and bottom-margin constraints to the UIView
Set UIView position constraints based on its position in the row
Add the icons (UIImageViews) to each UIView
Set height and width constraints based on the type of device
Add text labels
I'm using a bunch of constraints to place the UIViews and the UIImageViews. How can I make these constraints dynamic based on the device's screen size?
iPhone 6 with contentView in blue and UIViews in red
iPhone 6
iPhone 5/5S/5c
iPhone 4S/4
Here's where I add the device UIViews and apply constraints:
// Loop through the array of DeviceTypes to add each to the UIView
for index in 0...deviceTypes.count-1 {
// Grab the current device
var thisDevice:DeviceType = deviceTypes[index]
// Add to the view!
contentView.addSubview(thisDevice)
thisDevice.setTranslatesAutoresizingMaskIntoConstraints(false)
// How tall is the Device UIView
var heightConstraint:NSLayoutConstraint = NSLayoutConstraint(item: thisDevice, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: 200)
// How wide is the device UIView
var widthConstraint:NSLayoutConstraint = NSLayoutConstraint(item: thisDevice, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: 130)
// How far is the bottom of the UIView from the top of the contentView
var bottomMarginConstraint:NSLayoutConstraint = NSLayoutConstraint(item: thisDevice, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 100)
// Add thisDevice's UIView constraints
thisDevice.addConstraints([heightConstraint, widthConstraint])
contentView.addConstraint(bottomMarginConstraint)
thisDevice.backgroundColor = UIColor.redColor()
// set UIView position constraints based on place in row
if (index > 0) {
// device is second or further in row
var deviceOnTheLeft = deviceTypes[index-1]
var leftMarginConstraint:NSLayoutConstraint = NSLayoutConstraint(item: thisDevice, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: deviceOnTheLeft, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 10)
contentView.addConstraint(leftMarginConstraint)
}
else {
// device is first in row
var leftMarginConstraint:NSLayoutConstraint = NSLayoutConstraint(item: thisDevice, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.Left, multiplier: 1, constant: 0)
// Add constraint
contentView.addConstraint(leftMarginConstraint)
}
Here are the constraints that I apply to the icon images:
func addDeviceImages() {
// Set right image based on deviceType name
self.frontImageView.image = UIImage(named: self.Name!)
// Set translates autoresizing mask to fault
self.frontImageView.setTranslatesAutoresizingMaskIntoConstraints(false)
// Add the imageview to the view
self.addSubview(self.frontImageView)
if (self.Name == "Windows") {
self.addImageSizeConstraints(90, height: 160)
}
else if (self.Name == "iPad"){
self.addImageSizeConstraints(120, height: 180)
}
else if (self.Name == "iPhone"){
self.addImageSizeConstraints(85, height: 175)
}
else if (self.Name == "Android"){
self.addImageSizeConstraints(95, height: 185)
}
}
func addImageSizeConstraints(width: CGFloat, height: CGFloat) {
// Set the size constraints for the imageview based on the values passed in
var heightConstraint:NSLayoutConstraint = NSLayoutConstraint(item: self.frontImageView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: height)
var widthConstraint:NSLayoutConstraint = NSLayoutConstraint(item: self.frontImageView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: width)
self.frontImageView.addConstraints([heightConstraint, widthConstraint])
// Set the position of the imageview in the UIView
var verticalConstraint:NSLayoutConstraint = NSLayoutConstraint( item: self.frontImageView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: -25)
var horizontalConstraint:NSLayoutConstraint = NSLayoutConstraint( item: self.frontImageView, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Left, multiplier: 1, constant: 5)
self.addConstraints([horizontalConstraint,verticalConstraint])
}
You are using the wrong kinds of constraints. Your width constraints are absolute - you are setting a fixed constant. Instead, make the constant 0, and the widths depend on the height, using a value for the multiplier that fixes the aspect ratio correctly. In this way, as the height changes, the width will change to match. Do the same sort of thing with the separation between the images - make the separation depend on the width of the superview, as a multiplier.
Related
I want to place header view on top of screen with NSLayoutConstraint (I must use NSLayoutConstraint). When I do it like in below code, view places corruptly in somewhere else and also controllers background color turns black and nothing works. Where am I doing wrong?
I searched below posts for not opening a duplicate post but nothing fixed it:
Programmatically creating constraints bound to view controller margins
Programmatically Add CenterX/CenterY Constraints
EDIT: This controller is inside navigation controller but I'm not sure If It is related.
override func viewDidLoad(){
self.view.backgroundColor = UIColor.white
boxView.backgroundColor = Color.Common.welcomeScreenBackgroundColor.withAlphaComponent(0.5)
boxView.translatesAutoresizingMaskIntoConstraints = false
self.view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubView(boxView)
}
override func viewDidLayoutSubviews() {
//Header = 20 from left edge of screen
let cn1 = NSLayoutConstraint(item: boxView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 0)
//Header view trailing end is 20 px from right edge of the screen
let cn2 = NSLayoutConstraint(item: boxView, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0)
//Header view height = constant 240
let cn3 = NSLayoutConstraint(item: boxView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant:240)
//Header view vertical padding from the top edge of the screen = 20
let cn5 = NSLayoutConstraint(item: boxView, attribute: .top, relatedBy: .equal, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: 0)
self.view.addConstraints([cn1,cn2,cn3,cn5])
}
The problem was setting translatesAutoresizingMaskIntoConstraints to false on Superview. So I deleted the;
self.view.translatesAutoresizingMaskIntoConstraints = false
and this solves the problem. I think this causes app creates constraint for superview.
I created a .xib with freeform and implement it like this
if let alertView = Bundle.main.loadNibNamed(Constants.XIB.titleImageLabelThreeButtonsAlertView, owner: self, options: nil)?.first as? TitleImageLabelThreeButtonsAlertView {
view.addSubview(alertView)
alertView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
view.addConstraint(NSLayoutConstraint(item: alertView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 20))
view.addConstraint(NSLayoutConstraint(item: alertView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 20))
alertView.autoresizingMask = [UIViewAutoresizing.flexibleLeftMargin, UIViewAutoresizing.flexibleRightMargin, UIViewAutoresizing.flexibleTopMargin, UIViewAutoresizing.flexibleBottomMargin]
self.view.layoutIfNeeded()
}
I call this code in viewDidAppear. The center thing seems to work, but it seems that the trailing and leading don't have any effect. I want them with a distance of 20, my alertView should have a fixed height and appear in center.
The xib has always the same size (see screenshots)
My originally targeted was to get a xib that I can implement in every view for every device. So what is the best way to get this?
my xib file
simulator iphone 7
simulator iphone 4
You are mixing up auto layout and fixed placement (with autoresizing mask). What you want to do is completely use auto layout so that the view will adjust its layout automatically. You say you want a horizontal distance of 20, a fixed height and to be centred so I would do this:
if let alertView = Bundle.main.loadNibNamed(Constants.XIB.titleImageLabelThreeButtonsAlertView, owner: self, options: nil)?.first as? TitleImageLabelThreeButtonsAlertView {
view.addSubview(alertView)
// Start using auto layout
alertView.translatesAutoresizingMaskIntoConstraints = false
// Set the leading and trailing constraints for horizontal placement
view.addConstraint(NSLayoutConstraint(item: alertView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: -20))
view.addConstraint(NSLayoutConstraint(item: alertView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 20))
// Centre it vertically
view.addConstraint(NSLayoutConstraint(item: alertView, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0))
// Set the fixed height constraint
let fixedHeight: CGFloat = 100
view.addConstraint(NSLayoutConstraint(item: alertView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 0, constant: fixedHeight))
}
That will get you what you want no matter how the device, superview, orientation, etc changes.
I have added a UIView to a table cell through my storyboard with the following constraints:
Then I have the following code to programmatically add a UIImageView to the UIView above and size it according to the orientation of the screen.
//Use half the screen size width when on an iPhone and on Landscape
let image: UIImage = UIImage(named: HEADER_IMAGE_BATH)!
imageView = UIImageView(image: image)
imageView!.frame = CGRectMake(0 , 0, self.view.frame.width / 2, 185)
imageView!.contentMode = .ScaleAspectFit
//center image
let centerXConst = NSLayoutConstraint(item: imageView!, attribute: .CenterX, relatedBy: .Equal, toItem: imageWrapperView, attribute: .CenterX, multiplier: 1, constant: 1)
let centerYConst = NSLayoutConstraint(item: imageView!, attribute: .CenterY, relatedBy: .Equal, toItem: imageWrapperView, attribute: .CenterY, multiplier: 1, constant: 1)
NSLayoutConstraint.activateConstraints([centerXConst, centerYConst])
//add to sub view
imageWrapperView.addSubview(imageView!)
However, my image does not get centered when in landscape. My image is only half the width of the screen and I would like to center it inside my UIView. What am I missing? Thanks
You should use Auto Layout instead of frame for width and height of imageView
You have to add imageView to imageWrapperView before add constraints
You have to set imageView.translatesAutoresizingMaskIntoConstraints to false
Then, the final code is:
//Use half the screen size width when on an iPhone and on Landscape
let image: UIImage = UIImage(named: "key.png")!
let imageView = UIImageView(image: image)
imageView.contentMode = .ScaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
//add to sub view
imageWrapperView.addSubview(imageView)
//center image
let centerXConst = NSLayoutConstraint(item: imageView, attribute: .CenterX, relatedBy: .Equal, toItem: imageWrapperView, attribute: .CenterX, multiplier: 1.0, constant: 0.0)
let centerYConst = NSLayoutConstraint(item: imageView, attribute: .CenterY, relatedBy: .Equal, toItem: imageWrapperView, attribute: .CenterY, multiplier: 1.0, constant: 0.0)
let heightConstraint = NSLayoutConstraint(item: imageView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 185.0)
let widthConstraint = NSLayoutConstraint(item: imageView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: self.view.frame.width / 2)
imageView.addConstraints([heightConstraint, widthConstraint])
NSLayoutConstraint.activateConstraints([centerXConst, centerYConst])
When you're using autolayout, do not try to change the frame of the views. If you need to change the frame, better take the outlets of the constraints and change them programatically.
In your question, to center align the imageView, you can do it by putting 4 constraints. Set the height and width of the imageView and the other two are center horizontally and center vertically constraints.
This is the basic way of center aligning any type of view.
I am using the ios-charts library by Daniel Gindi to try and create a bar chart.
I programatically create a BarChartView with a red background, and fill it with hard-coded data. The view is sized correctly, however the bar chart doesn't scale properly and is cut off when it reaches the top of the view.
My view looks like this:
This view controller is instantiated inside a scrollview using the storyboard method instantiateViewControllerWithIdentifier. However when I make this view controller the initial view controller, the chart scales correctly, and looks like this:
Why does my chart scale incorrectly?
I would also like to note that if I set the leftAxis.axisMaxValue property of the incorrectly-scaled graph to something large, like 100, the graph looks like this:
I will also provide the code I used to create the graph, minus the 30+ lines I used to set properties and the data of the graph.
override func viewDidLoad(){
var chart : UIView?
let gBFrame = self.graphBackground.frame
let frame = CGRect(origin: gBFrame.origin, size: CGSize(width: gBFrame.width, height: gBFrame.height-25))
chart = BarChartView(frame: frame)
self.view.addSubview(chart!)
constrainChart()
}
func constrainChart(){
if type == "Bar Chart"{
chart!.translatesAutoresizingMaskIntoConstraints = false
let leftConstraint = NSLayoutConstraint(item: self.chart!, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: self.graphBackground, attribute: NSLayoutAttribute.Left, multiplier: 1, constant: 0)
let rightConstraint = NSLayoutConstraint(item: self.chart!, attribute: NSLayoutAttribute.Right, relatedBy: NSLayoutRelation.Equal, toItem: self.graphBackground, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 0)
let topConstraint = NSLayoutConstraint(item: self.chart!, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.graphBackground, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: self.chart!, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.graphBackground, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: -25)
self.view.addConstraints([leftConstraint,rightConstraint,topConstraint,bottomConstraint])
} else if type == "Horizontal Bar Chart"{
} else if type == "Pie Chart"{
} else {
}
chart?.layoutIfNeeded()
}
I can provide any additional information if it is helpful.
Any ideas on what could be the problem? Or what to try next?
EDIT:
when I instantiate the view controller inside the scrollview, I use NSLayoutConstraints to position it such that its left boundary is 2*self.view.frame.width from the left boundary of the scrollview.
I find that if I set that constraint to 0, such that the view controller with the chart appears in the leftmost frame of the scrollview, the chart appears correctly. However if I change that constraint at all (like by one unit), the chart scales incorrectly again.
Right after I instantiate the view controller using the aforementioned storyboard method, I position it using the method whose code is shown below:
func setUpQuestionFrame(newViewController: UIViewController){
var frame = newViewController.view.frame
frame.origin.x = self.view.frame.size.width*2
newViewController.view.frame = frame
self.addChildViewController(newViewController)
self.scrollView.addSubview(newViewController.view)
newViewController.didMoveToParentViewController(self)
newViewController.view.translatesAutoresizingMaskIntoConstraints = false
let widthConstraintVCBAR = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: newViewController.view, attribute: NSLayoutAttribute.Width, multiplier: 1, constant: 0)
view.addConstraint(widthConstraintVCBAR)
let heightConstraintVCBAR = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: newViewController.view, attribute: NSLayoutAttribute.Height, multiplier: 1, constant: 0)
view.addConstraint(heightConstraintVCBAR)
let horizontalConstraintVCBAR = NSLayoutConstraint(item: newViewController.view, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 5)//self.view.frame.width*2)
view.addConstraint(horizontalConstraintVCBAR)
let verticalConstraintVCBAR = NSLayoutConstraint(item: newViewController.view, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0)
view.addConstraint(verticalConstraintVCBAR)
}
The issue was solved by adding chart!.notifyDataSetChanged(), which tells the chart to reconfigure once it receives new data.
It was the same solution that was used in this question.
I have multiple buttons separated by some 10px padding (leadingPadding), in case of English the items must start from left to right, and in case of Arabic from right to left and centralized.
This is the code used to draw the scrollView:
var previousView: UIView = self.itemsScrollView!
for i in 0..<self.items.count {
let sectionButton = UIButton(frame: CGRectZero)
sectionButton.titleLabel.text = "i = " + i
sectionButton.translatesAutoresizingMaskIntoConstraints = false
self.itemsScrollView!.addSubview(sectionButton)
// Left Constraint
let leftConstraint = NSLayoutConstraint(
item: sectionButton,
attribute: .Leading,
relatedBy: .Equal,
toItem: toView,
attribute: (toView === self.itemsScrollView!) ? .Leading : .Trailing,
multiplier: 1.0,
constant: leadingPadding);
self.itemsScrollView! .addConstraint(leftConstraint)
// Top Constraint
let topConstraint = NSLayoutConstraint(
item: sectionButton,
attribute: .Top,
relatedBy: .Equal,
toItem: self.itemsScrollView!,
attribute: .Top,
multiplier: 1.0,
constant: self.itemPaddingTopBottom);
self.itemsScrollView! .addConstraint(topConstraint)
// Width Constraint
let widthConstraint = NSLayoutConstraint(
item: sectionButton,
attribute: .Width,
relatedBy: .Equal,
toItem: nil,
attribute: .NotAnAttribute,
multiplier: 1.0,
constant: buttonWidth);
self.itemsScrollView! .addConstraint(widthConstraint)
// Height Constraint
let heightConstraint = NSLayoutConstraint(
item: sectionButton,
attribute: .Height,
relatedBy: .Equal,
toItem: nil,
attribute: .NotAnAttribute,
multiplier: 1.0,
constant: itemHeight);
self.itemsScrollView! .addConstraint(heightConstraint)
previousView = sectionButton
}
// add Trailing constraint on scroll
if (previousView !== self.itemsScrollView!) {
let leftConstraint = NSLayoutConstraint(
item: previousView,
attribute: .Trailing,
relatedBy: .Equal,
toItem: self.itemsScrollView!,
attribute: .Trailing,
multiplier: 1.0,
constant: 0.0);
self.itemsScrollView! .addConstraint(leftConstraint)
}
It works great when the language orientation is LTR.
And a weird behavior when the language orientation is RTL (They need to be centralized like the picture above)
I'm assuming this is something related to the Trailing constraint with the scrollView but I'm not sure. If yes, is there any way to fix that?
Thanks
You should not add multiple views directly in scrollview. Instead you should add a single container view ,horizontally centered in scrollview, which should include all the necessary subview. The width of the container view must be determined by its subviews, so the first item leading is aligned with container leading and the last item trailing is aligned with container trailing in LTR mode or first item trailing is aligned with container trailing and last item leading is aligned with container leading in RTL mode