I want to have several objects that I can drag and drop.
Here's my code for moving one object (with lot of help from #vacawama):
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var panView: UIView!
#IBOutlet weak var panViewCenterX: NSLayoutConstraint!
#IBOutlet weak var panViewCenterY: NSLayoutConstraint!
let panRec = UIPanGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
panRec.addTarget(self, action: "draggedView:")
panView.addGestureRecognizer(panRec)
// Do any additional setup after loading the view, typically from a nib.
}
func draggedView(sender:UIPanGestureRecognizer){
self.view.bringSubviewToFront(sender.view!)
var translation = sender.translationInView(self.view)
panViewCenterY.constant += translation.y
panViewCenterX.constant += translation.x
sender.setTranslation(CGPointZero, inView: self.view)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I want to have more objects in my project:
#IBOutlet weak var panView2: UIView!
#IBOutlet weak var panView3: UIView!
#IBOutlet weak var panView4: UIView!
...
So, what is the easiest way to implement this in such way that I can have multiple panView which I can move independently around (one at the time)?
If you are using Auto Layout, you shouldn't just move a view by altering its center. Anything that causes Auto Layout to run will move your view back to its original position.
Here is an implementation of a new class called DraggableView that works with Auto Layout to provide views that can be dragged. It creates constraints in code for the width, height, X position, and Y position of the view. It uses its own UIPanGestureRecognizer to allow the view to be dragged.
DraggableView.swift can be dropped into any project that needs draggable views. Take a look at ViewController.swift below to see how easy it is to use.
DraggableView.swift
import UIKit
class DraggableView: UIView {
let superView: UIView!
let xPosConstraint: NSLayoutConstraint!
let yPosConstraint: NSLayoutConstraint!
var constraints: [NSLayoutConstraint] {
get {
return [xPosConstraint, yPosConstraint]
}
}
init(width: CGFloat, height: CGFloat, x: CGFloat, y: CGFloat, color: UIColor, superView: UIView) {
super.init()
self.superView = superView
self.backgroundColor = color
self.setTranslatesAutoresizingMaskIntoConstraints(false)
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.addTarget(self, action: "draggedView:")
self.addGestureRecognizer(panGestureRecognizer)
let widthConstraint = NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal,
toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: width)
self.addConstraint(widthConstraint)
let heightConstraint = NSLayoutConstraint(item: self, attribute: .Height, relatedBy: .Equal,
toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: height)
self.addConstraint(heightConstraint)
xPosConstraint = NSLayoutConstraint(item: self, attribute: .CenterX, relatedBy: .Equal,
toItem: superView, attribute: .Leading, multiplier: 1.0, constant: x)
yPosConstraint = NSLayoutConstraint(item: self, attribute: .CenterY, relatedBy: .Equal,
toItem: superView, attribute: .Top, multiplier: 1.0, constant: y)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveByDeltaX(deltaX: CGFloat, deltaY: CGFloat) {
xPosConstraint.constant += deltaX
yPosConstraint.constant += deltaY
}
func draggedView(sender:UIPanGestureRecognizer){
if let dragView = sender.view as? DraggableView {
superView.bringSubviewToFront(dragView)
var translation = sender.translationInView(superView)
sender.setTranslation(CGPointZero, inView: superView)
dragView.moveByDeltaX(translation.x, deltaY: translation.y)
}
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let dragView1 = DraggableView(width: 75, height: 75, x: 50, y: 50,
color: UIColor.redColor(), superView: self.view)
self.view.addSubview(dragView1)
self.view.addConstraints(dragView1.constraints)
let dragView2 = DraggableView(width: 100, height: 100, x: 150, y: 50,
color: UIColor.blueColor(), superView: self.view)
self.view.addSubview(dragView2)
self.view.addConstraints(dragView2.constraints)
let dragView3 = DraggableView(width: 125, height: 125, x: 100, y: 175,
color: UIColor.greenColor(), superView: self.view)
self.view.addSubview(dragView3)
self.view.addConstraints(dragView3.constraints)
}
}
UPDATE:
Here is a version of DraggableView.swift that supports images as a subview.
import UIKit
class DraggableView: UIView {
let superView: UIView!
let xPosConstraint: NSLayoutConstraint!
let yPosConstraint: NSLayoutConstraint!
var constraints: [NSLayoutConstraint] {
get {
return [xPosConstraint, yPosConstraint]
}
}
init(width: CGFloat, height: CGFloat, x: CGFloat, y: CGFloat, color: UIColor, superView: UIView, imageToUse: String? = nil, contentMode: UIViewContentMode = .ScaleAspectFill) {
super.init()
self.superView = superView
self.backgroundColor = color
self.setTranslatesAutoresizingMaskIntoConstraints(false)
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.addTarget(self, action: "draggedView:")
self.addGestureRecognizer(panGestureRecognizer)
let widthConstraint = NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal,
toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: width)
self.addConstraint(widthConstraint)
let heightConstraint = NSLayoutConstraint(item: self, attribute: .Height, relatedBy: .Equal,
toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: height)
self.addConstraint(heightConstraint)
xPosConstraint = NSLayoutConstraint(item: self, attribute: .CenterX, relatedBy: .Equal,
toItem: superView, attribute: .Leading, multiplier: 1.0, constant: x)
yPosConstraint = NSLayoutConstraint(item: self, attribute: .CenterY, relatedBy: .Equal,
toItem: superView, attribute: .Top, multiplier: 1.0, constant: y)
if imageToUse != nil {
if let image = UIImage(named: imageToUse!) {
let imageView = UIImageView(image: image)
imageView.contentMode = contentMode
imageView.clipsToBounds = true
imageView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.addSubview(imageView)
self.addConstraint(NSLayoutConstraint(item: imageView, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1.0, constant: 0))
self.addConstraint(NSLayoutConstraint(item: imageView, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1.0, constant: 0))
self.addConstraint(NSLayoutConstraint(item: imageView, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1.0, constant: 0))
self.addConstraint(NSLayoutConstraint(item: imageView, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1.0, constant: 0))
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveByDeltaX(deltaX: CGFloat, deltaY: CGFloat) {
xPosConstraint.constant += deltaX
yPosConstraint.constant += deltaY
}
func draggedView(sender:UIPanGestureRecognizer){
if let dragView = sender.view as? DraggableView {
superView.bringSubviewToFront(dragView)
var translation = sender.translationInView(superView)
sender.setTranslation(CGPointZero, inView: superView)
dragView.moveByDeltaX(translation.x, deltaY: translation.y)
}
}
}
The way a gesture recognizer works is that you attach it to a view. So if you have multiple views, and you want them to be draggable in the same way, attach a different pan gesture recognizer to each one.
Now let's say you want them all to have the same action handler. No problem. In the action handler, you receive the current gesture recognizer as a parameter. It has a view property; that's the view it is attached to! So now you know that that's the view you're moving.
Finally:
Found this example which explained it in a easy way:
http://www.raywenderlich.com/76020/using-uigesturerecognizer-with-swift-tutorial
So, now multiple Pan's are working..
Here's the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var boxToMove1: UIView!
#IBOutlet weak var boxToMove2: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func handlePan(objectToMove:UIPanGestureRecognizer) {
let translation = objectToMove.translationInView(self.view)
objectToMove.view!.center = CGPoint(x: objectToMove.view!.center.x + translation.x, y: objectToMove.view!.center.y + translation.y)
objectToMove.setTranslation(CGPointZero, inView: self.view)
println("\(objectToMove.view!.tag)") //Verify correct object received
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I linked up the "Pan Gesture Recognizer" from the "Object library" to each boxToMove. Then I linked the "Pan Gesture Recognizer" (both) to my #IBAction func handlePan...
This is now working!!!
Finally I understand the way this Gesture Recognizer is working!
What I'm still trying to figure out is how to do all this linking programmatically.. Any idea?
Related
I have two Swift files, one is called FileView.swift and the other is a FileViewController.swift. Using MVC Architecture, I am trying to add a subview from FileViewController.swift to FileView.swift's webViewContainer: UIView! which is an IBOutlet from a XIB File.
However, I am getting a nil result when it is called from FileViewController.swift.
class FileView: UIView {
#IBOutlet weak var webViewContainerHeight: NSLayoutConstraint!
#IBOutlet weak var webViewContainerWidth: NSLayoutConstraint!
#IBOutlet weak var closeBttnWidth: NSLayoutConstraint!
#IBOutlet weak var closeBttnHeight: NSLayoutConstraint!
#IBOutlet weak var webViewContainer: UIView!
#IBOutlet weak var closeButton: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupNIB()
}
private func setup() {
webViewContainer = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 350))
}
class FileViewController: UIViewController, WKNavigationDelegate {
var webView = WKWebView()
var campaignUrl = ""
var finalCampaignUrl = ""
lazy var customView: FileView = {
let customView = FileView()
return customView
}()
override func loadView() {
self.view = self.customView
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
webViewModal()
}
func webViewModal () {
if UIDevice.current.userInterfaceIdiom == .pad {
let deviceWidth = UIScreen.main.bounds.width/3
let deviceHeight = UIScreen.main.bounds.height/3
customView.webViewContainerWidth.constant = 290 + deviceWidth
customView.webViewContainerHeight.constant = 475 + deviceHeight
customView.closeBttnWidth.constant = 55
customView.closeBttnHeight.constant = 55
customView.closeButton.layoutIfNeeded()
customView.webViewContainer.layoutIfNeeded()
}
webView = Global.setWKWebViewInitWithConfig(frame: .zero)
customView.webViewContainer.addSubview(webView)
customView.webViewContainer.showSpinner(CGSize(width: 30 , height: 30), tintColor: UIColor.lightGray)
webView.translatesAutoresizingMaskIntoConstraints = false
let webViewHeightConstraint = NSLayoutConstraint(
item: webView,
attribute: .height,
relatedBy: .equal,
toItem: customView.webViewContainer,
attribute: .height,
multiplier: 1,
constant: 0
)
let webViewWidthConstraint = NSLayoutConstraint(
item: webView,
attribute: .width,
relatedBy: .equal,
toItem: customView.webViewContainer,
attribute: .width,
multiplier: 1,
constant: 0
)
let webViewLeftMarginConstraint = NSLayoutConstraint(
item: webView,
attribute: .leftMargin,
relatedBy: .equal,
toItem: customView.webViewContainer,
attribute: .leftMargin,
multiplier: 1,
constant: 0
)
let webViewRightMarginConstraint = NSLayoutConstraint(
item: webView,
attribute: .rightMargin,
relatedBy: .equal,
toItem: customView.webViewContainer,
attribute: .rightMargin,
multiplier: 1,
constant: 0
)
let webViewBottomMarginContraint = NSLayoutConstraint(
item: webView,
attribute: .bottomMargin,
relatedBy: .equal,
toItem: customView.webViewContainer,
attribute: .bottomMargin,
multiplier: 1,
constant: 0
)
NSLayoutConstraint.activate([webViewHeightConstraint, webViewWidthConstraint, webViewLeftMarginConstraint, webViewRightMarginConstraint, webViewBottomMarginContraint])
webView.navigationDelegate = self
Global.initialLoadWithParam(ofWebView: webView, withURL: NSURL(string: campaignUrl)!)
customView.closeButton.layer.cornerRadius = 0.9 * customView.closeButton.bounds.size.width
customView.closeButton.backgroundColor = #colorLiteral(red: 0.003921568627, green: 0.1647058824, blue: 0.2666666667, alpha: 1)
customView.closeButton.tintColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
customView.webViewContainer.layer.cornerRadius = 15
customView.webViewContainer.layer.masksToBounds = true
}
I expected that this should add
webView = Global.setWKWebViewInitWithConfig(frame: .zero)
to
customView.webViewContainer.addSubview(webView)
but it returns nil instead on customView.webViewContainer's line:
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
Should'nt webViewContainer already be instantiated when customView was loaded on override func loadView()?
The problem is that your webViewContainer is a weak reference:
class FileView: UIView {
// ...
#IBOutlet weak var webViewContainer: UIView!
// ...
}
This works fine if
you are using XIB files
someone (e.g.the view controller) keeps a strong reference to the top view
The outlets are part of the view hierarchy.
But in your case, in setup you create an instance of the view, store it in the weak reference, and then ARC (the reference counting mechanism) will find out that there are no strong references any more to the UIView and therfore clears the memory, resulting in a nil reference.
So you need to somehow keep a strong reference; in the simplest case, just skip the weak reference specifier in the outlet.
But keep in mind that you might have to do additional stuff to prevent strong reference cycles. In your case, if you do not work with xib files, you could just remove all the #IBOutlet stuff and make the NSCoder initializer just call fatalError, to prevent any creation from a xib file.
I'm trying to add views(or buttons) to UIStackView dynamically.
At first, the UIStackView has no arranged views (vertically), and
after getting from some http response, several views(buttons) are added to UIStackView.
UIStackView is also autolayout to hold a specific area.
I've tried to find dynamic adding example, but failed.
Anyone can show me the examples of adding view onto UIStackView dynamically?
It may help you. Please follow this points:
Add UIScrollView to your UIViewController in storyboard or XIB.
Initiate an NSMutableArray name it arrViews gets server response and adds view in the array.
Initialise UIStackViewpass arrView array in the init method.
After that UIStackView will be added subview of UIScrollView.
Add constraint programmatically to UIStackView. That's it.
if let response = self.serverResponse {
if let body = response.responseBody {
if let view = body.views {
arrViews = createSubViews(view)
}
}
}
let stackView = UIStackView(arrangedSubviews: arrViews)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 16
stackView.distribution = .fill
self.scrollView.addSubview(stackView)
//constraints
let leading = NSLayoutConstraint(item: stackView, attribute: .leading, relatedBy: .equal, toItem: self.scrollView, attribute: .leading, multiplier: 1.0, constant: 0)
self.scrollView.addConstraint(leading)
let trailing = NSLayoutConstraint(item: stackView, attribute: .trailing, relatedBy: .equal, toItem: self.scrollView, attribute: .trailing, multiplier: 1.0, constant: 0)
self.scrollView.addConstraint(trailing)
let top = NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: self.scrollView, attribute: .top, multiplier: 1.0, constant: 0)
self.scrollView.addConstraint(top)
let bottom = NSLayoutConstraint(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: self.scrollView, attribute: .bottom, multiplier: 1.0, constant: 0)
self.scrollView.addConstraint(bottom)
let equalWidth = NSLayoutConstraint(item: stackView, attribute: .width, relatedBy: .equal, toItem: self.scrollView, attribute: .width, multiplier: 1.0, constant: 0)
self.scrollView.addConstraint(equalWidth)
leading.isActive = true
trailing.isActive = true
top.isActive = true
bottom.isActive = true
equalWidth.isActive = true
Hope it will help you. Happy coding :)
I use this code in one of my projects:
let baseFrame = CGRect(origin: .zero, size: CGSize(width: requiredWidth, height: partitionHeight))
for instrument in instruments {
let partitionView = PartitionOnDemand(instrument: instrument, mode: playbackMode, frame: baseFrame, referenceView: partitionsAnimator)
partitionsStackView.addArrangedSubview(partitionView)
let tab = InstrumentInfoTabContainer.instantiate(with: instrument) {
self.focus(on: instrument)
}
tabsStackView.addArrangedSubview(tab)
}
While trying with answers, I happend to find how to work it.
class ViewController: UIViewController {
#IBOutlet weak var stack: UIStackView!
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.
}
#IBAction func onBtn_Create(_ sender: Any) {
createButton("new button ...")
}
#IBAction func onBtn_Delete(_ sender: Any) {
if let v = stack.arrangedSubviews.last {
stack.removeArrangedSubview(v)
v.removeFromSuperview()
}
}
func createButton(_ title: String) {
let button = UIButton()
button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
button.backgroundColor = UIColor.blue
button.setTitle(title, for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
stack.addArrangedSubview(button)
}
#objc func buttonAction(sender: UIButton!) {
print("Button tapped")
}
}
And, I anchored to UIStackView, Trailing=0, Leading=0, Top=0, Bottom=8 to TextView.Top
The subviews inside it are intact without any constraints.
Thank you.
In my UICollectionViewCell I have an image and a label. The picture takes up the whole cell (which I want) - however, the label is placed behind the image, so it's not visible. I have tried bringSubview(toFront: titleLabel), but nothing happens... I got no clue what to do really, have done a lot of searching.
This is the code for the cell, I don't use Storyboard as you can see (sorry for messy constraints, was testing different solutions to find out if this was the problem)
import UIKit
class BaseCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupBasket()
}
func setupViews() {
}
func setupBasket(){
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class VideoCell: BaseCell {
var selectedItemID : String!
static let sharedInstance = VideoCell()
var video: Video? {
didSet {
titleLabel.text = video?.title
setupThumbnailImage()
}
}
func setupThumbnailImage() {
if let thumbnailImageUrl = video?.thumbnail_image_name {
thumbnailImageView.loadImageUsingUrlString(thumbnailImageUrl)
}
}
let thumbnailImageView: CustomImageView = {
let imageView = CustomImageView()
imageView.image = UIImage(named: "taylor_swift_blank_space")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
let titleLabel: UILabel = {
let textView = UILabel()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.text = "Clothes"
textView.textColor = UIColor.lightGray
return textView
}()
let separatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
return view
}()
var titleLabelHeightConstraint: NSLayoutConstraint?
let addtoBasket = UIButton(type: .contactAdd)
override func setupViews() {
addtoBasket.frame = CGRect(x: 50, y: 0, width: 20, height: 60)
addSubview(addtoBasket)
addSubview(titleLabel)
addSubview(thumbnailImageView)
addSubview(separatorView)
addSubview(addtoBasket)
titleLabel.superview!.bringSubview(toFront: titleLabel)
//horizontal constraints
addConstraintsWithFormat("H:|-0-[v0]-0-|", views: thumbnailImageView)
//vertical constraints
addConstraintsWithFormat("V:|-1-[v0]-1-|", views: thumbnailImageView)
addConstraintsWithFormat("H:|-0-[v0]-1-|", views: separatorView)
addtoBasket.translatesAutoresizingMaskIntoConstraints = false
addtoBasket.heightAnchor.constraint(equalToConstant: 20).isActive = true
addtoBasket.widthAnchor.constraint(equalToConstant: 20).isActive = true
addtoBasket.centerXAnchor.constraint(equalTo: addtoBasket.superview!.centerXAnchor, constant: 90).isActive = true
addtoBasket.centerYAnchor.constraint(equalTo: addtoBasket.superview!.centerYAnchor, constant: -50).isActive = true
//top constraint
addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .top, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 8))
//right constraint
addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0))
//right constraint
addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 20))
//height constraint
titleLabelHeightConstraint = NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: -10)
addConstraint(titleLabelHeightConstraint!)
}
}
Try accessing the labels layer and set its zPosition.
Try titleLabel.layer.zPosition = 1
There was clearly something wrong with the constraints, now working! Thanks
I've spent hours trying to hunt down what is preventing my constraints layout from working. I have a view called ABSegment which simply holds a UILabel which should be centered and have equal height and width as its superview.
I'll include the entire class definition of this UIView subview to show what I've done.
import UIKit
class ABSegment: UIView {
let titleLabel = UILabel()
init(withTitle title: String) {
titleLabel.text = title
titleLabel.textAlignment = .center
super.init(frame: CGRect.zero)
translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
backgroundColor = UIColor.blue
}
override func layoutSubviews() {
super.layoutSubviews()
logFrames()
}
func logFrames() {
print("self.frame is \(frame)")
print("titleLabel.frame is \(titleLabel.frame)")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
logFrames()
titleLabel.removeConstraints(titleLabel.constraints)
let centerX = NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
let centerY = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: titleLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
let height = NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([centerX, centerY, width, height])
super.updateConstraints()
}
}
One natural thing to suspect is that I'm not initializing this view with initWithFrame. Rather, I'm deferring the frame setting until later in the code where these views are constructed. So code that constructs these don't set the frame. The frame may be set in Storyboard or like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let frame = CGRect(x: view.bounds.origin.x + 150, y: view.bounds.origin.y + 200, width: 100, height: 100)
segment.frame = frame
segment.layoutSubviews()
}
My understanding is that since I'm calling segment.layoutSubviews(), the ABSegment View should have a change to apply the previously activated constraints with a final frame.
There are so many different settings and things to get right in the correct order with no feedback other than not seeing the Label appear at all.
The main problems I see with your code:
You are adding and removing constraints every time updateConstraints is called. You only need to set up the constraints once when you create your view.
You are setting translatesAutoresizingMaskIntoConstraints = false on ABSegment itself. Don't do this. This tells Auto Layout that you will be specifying the size and location of ABSegment using constraints, and you clearly are using a frame for this purpose.
Here is the refactored code:
class ABSegment: UIView {
let titleLabel = UILabel()
// Computed property to allow title to be changed
var title: String {
set {
titleLabel.text = newValue
}
get {
return titleLabel.text ?? ""
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupLabel(title: "")
}
convenience init(title: String) {
self.init(frame: CGRect.zero)
self.title = title
}
// This init is called if your view is
// set up in the Storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLabel(title: "")
}
func setupLabel(title: String) {
titleLabel.text = title
titleLabel.textAlignment = .center
addSubview(titleLabel)
backgroundColor = UIColor.blue
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let centerX = NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
let centerY = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: titleLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
let height = NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([centerX, centerY, width, height])
}
override func layoutSubviews() {
super.layoutSubviews()
logFrames()
}
func logFrames() {
print("self.frame is \(frame)")
print("titleLabel.frame is \(titleLabel.frame)")
}
override func updateConstraints() {
super.updateConstraints()
logFrames()
}
}
Notes:
I moved all of the setup of the label into a function called setupLabel. This allows it to be called by both initializers.
I added a computed property called title to ABSegment which allows you to change the title at any time with mysegment.title = "new title".
I turned init(withSegment:) into a convenience init. It calls the standard init(frame:) and then sets the title. It is not a common practice to use withProperty so I changed it. You'd create an ABSegment with var segment = ABSegment(title: "some title").
I had required init?(coder aDecoder: NSCoder) call setupLabel so that ABSegment can be used with views added in the Storyboard.
The overrides of layoutSubviews and updateConstraints were left in to log the frames. That is all that they do now.
I have started creating custom views in Xcode using Swift. I decided to use the approach shown at http://www.thinkandbuild.it/building-a-custom-and-designabl-control-in-swift/, allowing me to set the control's attributes in Interface Builder.
Update: I have continued building the view, arranging the labels in the custom view's subview and aligning the subview with the view. I ended up using auto layout and constraints on both levels and managed to solved the width problem that way. I updated the code below accordingly.
Two problems remain:
As the last step, I set txtButton.setTranslatesAutoresizingMaskIntoConstraints(false) and set the constraints for the txtButton subview => the subviews' border and background disappeared.
The intrinsic size is not visible in IB, i.e. I'm getting layout issues reported in IB that suggest setting the height to 0 or 16.
Custom View Class:
import UIKit
#IBDesignable public class TextButtonView: UIView {
#IBInspectable var borderColor: UIColor = UIColor.clearColor()
#IBInspectable var borderWidth: CGFloat = 0
#IBInspectable var cornerRadius: CGFloat = 0
#IBInspectable var viewBackgroundColor: UIColor = UIColor.clearColor()
#IBInspectable var mainText: String = ""
#IBInspectable var mainTextSize: CGFloat = 15.0
#IBInspectable var mainTextColor: UIColor = UIColor.blackColor()
#IBInspectable var secText: String = ""
#IBInspectable var secTextSize: CGFloat = 15.0
#IBInspectable var secTextColor: UIColor = UIColor.blackColor()
#IBInspectable var horizMargin: CGFloat = 5.0
#IBInspectable var secHorizOffset: CGFloat = 0.0
#IBInspectable var verticalMargin: CGFloat = 3.0
#IBInspectable var lineSpacing: CGFloat = 10.0
var txtButton: UIControl!
var buttonHeight: CGFloat = 0.0
#if TARGET_INTERFACE_BUILDER
override func willMoveToSuperview(newSuperview: UIView?) {
// Build the TextButton.
txtButton = TextButton(
borderColor: self.borderColor,
borderWidth: self.borderWidth,
cornerRadius: self.cornerRadius,
viewBackgroundColor: self.viewBackgroundColor,
mainText: self.mainText,
mainTextSize: self.mainTextSize,
mainTextColor: self.mainTextColor,
secText: self.secText,
secTextSize: self.secTextSize,
secTextColor: self.secTextColor,
horizMargin: self.horizMargin,
secHorizOffset: self.secHorizOffset,
verticalMargin: self.verticalMargin,
lineSpacing: self.lineSpacing,
frame: self.bounds)
// Add the TextButton as subview of this view
self.addSubview(txtButton)
// Remember height for setting intrinsic content size.
buttonHeight = txtButton.frame.size.height
// Set remaining attributes for the container view.
self.backgroundColor = UIColor.clearColor()
// Setting constraints for the subview.
txtButton.setTranslatesAutoresizingMaskIntoConstraints(false)
self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0))
self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0))
}
#else
override public func awakeFromNib() {
super.awakeFromNib()
// Build the TextButton.
txtButton = TextButton(
borderColor: self.borderColor,
borderWidth: self.borderWidth,
cornerRadius: self.cornerRadius,
viewBackgroundColor: self.viewBackgroundColor,
mainText: self.mainText,
mainTextSize: self.mainTextSize,
mainTextColor: self.mainTextColor,
secText: self.secText,
secTextSize: self.secTextSize,
secTextColor: self.secTextColor,
horizMargin: self.horizMargin,
secHorizOffset: self.secHorizOffset,
verticalMargin: self.verticalMargin,
lineSpacing: self.lineSpacing,
frame: self.bounds)
// Add the TextButton as subview of this view.
self.addSubview(txtButton)
// Remember height for setting intrinsic content size.
buttonHeight = txtButton.frame.size.height
// Set remaining attributes for the container view.
self.backgroundColor = UIColor.clearColor()
// Setting constraints for the subview.
txtButton.setTranslatesAutoresizingMaskIntoConstraints(false)
self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0))
self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0))
}
#endif
override public func intrinsicContentSize() -> CGSize {
return CGSize(width: 250, height: buttonHeight)
}
}
Control:
import UIKit
class TextButton: UIControl {
// Designable properties and default values.
var borderColor: UIColor?
var borderWidth: CGFloat?
var cornerRadius: CGFloat?
var viewBackgroundColor: UIColor?
var mainText: String?
var mainTextSize: CGFloat?
var mainTextColor: UIColor?
var secText: String?
var secTextSize: CGFloat?
var secTextColor: UIColor?
var horizMargin: CGFloat?
var secHorizOffset: CGFloat?
var verticalMargin: CGFloat?
var lineSpacing: CGFloat?
convenience init(
borderColor: UIColor,
borderWidth: CGFloat,
cornerRadius: CGFloat,
viewBackgroundColor: UIColor,
mainText: String,
mainTextSize: CGFloat,
mainTextColor: UIColor,
secText: String,
secTextSize: CGFloat,
secTextColor: UIColor,
horizMargin: CGFloat,
secHorizOffset: CGFloat,
verticalMargin: CGFloat,
lineSpacing: CGFloat,
frame: CGRect) {
self.init(frame: frame)
self.mainText = mainText
self.mainTextSize = mainTextSize
// Button margins.
self.horizMargin = horizMargin
self.verticalMargin = verticalMargin
self.secHorizOffset = secHorizOffset
self.lineSpacing = lineSpacing
// Define the Fonts
let mainFont = UIFont(name: "Helvetica Neue", size: mainTextSize)
let secFont = UIFont(name: "Helvetica Neue", size: secTextSize)
// Create main label.
let mainLabel: UILabel = UILabel()
mainLabel.backgroundColor = UIColor.clearColor()
mainLabel.textColor = mainTextColor
mainLabel.textAlignment = .Left
mainLabel.font = mainFont
mainLabel.text = mainText
// Calculate the main label's height.
var mainLabelDummy: UILabel = mainLabel
mainLabelDummy.sizeToFit()
var mainLabelHeight: CGFloat = mainLabelDummy.frame.size.height
// Create secondary label.
let secLabel: UILabel = UILabel()
secLabel.backgroundColor = UIColor.clearColor()
secLabel.textColor = secTextColor
secLabel.textAlignment = .Left
secLabel.font = secFont
secLabel.text = secText
// Calculate the secondary label's height.
var secLabelDummy: UILabel = secLabel
secLabelDummy.sizeToFit()
var secLabelHeight: CGFloat = secLabelDummy.frame.size.height
// Add labels to view.
addSubview(mainLabel)
addSubview(secLabel)
// Set constraints for labels.
mainLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
secLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin))
self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: verticalMargin))
self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin + secHorizOffset))
self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Top, relatedBy: .Equal, toItem: mainLabel, attribute: .Bottom, multiplier: 1, constant: lineSpacing))
// Adjust frame to match content.
self.frame.size.height =
2 * verticalMargin
+ 2 * borderWidth
+ lineSpacing
+ mainLabelHeight
+ secLabelHeight
// Set remaining view properties.
self.layer.borderColor = borderColor.CGColor
self.layer.borderWidth = borderWidth
self.layer.cornerRadius = cornerRadius
self.backgroundColor = viewBackgroundColor
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
After further reading I came up with the following solution, simplifying by getting rid of the subview:
import UIKit
#IBDesignable class TextButtonView: UIControl {
// Properties accessible in Interface Builder.
#IBInspectable var borderColor: UIColor = UIColor.clearColor() {didSet { updateUI() }}
#IBInspectable var borderWidth: CGFloat = 0 {didSet { updateUI() }}
#IBInspectable var cornerRadius: CGFloat = 0 {didSet { updateUI() }}
#IBInspectable var backgrColor: UIColor = UIColor.clearColor() {didSet { updateUI() }}
#IBInspectable var mainText: String = "" {didSet { updateUI() }}
#IBInspectable var mainTextSize: CGFloat = 20.0 {didSet { updateUI() }}
#IBInspectable var mainTextColor: UIColor = UIColor.blackColor() {didSet { updateUI() }}
#IBInspectable var secText: String = "" {didSet { updateUI() }}
#IBInspectable var secTextSize: CGFloat = 12.0 {didSet { updateUI() }}
#IBInspectable var secTextColor: UIColor = UIColor.blackColor() {didSet { updateUI() }}
#IBInspectable var horizMargin: CGFloat = 0.0 {didSet { updateUI() }}
#IBInspectable var secHorizOffset: CGFloat = 0.0 {didSet { updateUI() }}
#IBInspectable var verticalMargin: CGFloat = 0.0 {didSet { updateUI() }}
#IBInspectable var lineSpacing: CGFloat = 0.0 {didSet { updateUI() }}
var mainLabel: UILabel!
var secLabel: UILabel!
var textButtonHeight: CGFloat = 0.0
var fontName: String = "Helvetica Neue"
required init(coder: NSCoder) {
super.init(coder:coder)
setupUI()
}
override init(frame: CGRect) {
super.init(frame:frame)
setupUI()
}
func setupUI() {
// Set up static properties.
mainLabel = UILabel()
mainLabel.backgroundColor = UIColor.clearColor()
mainLabel.textAlignment = .Left
secLabel = UILabel()
secLabel.backgroundColor = UIColor.clearColor()
secLabel.textAlignment = .Left
// Add labels to view.
addSubview(mainLabel)
addSubview(secLabel)
// Update variable properties.
updateUI()
}
func updateUI() {
// Set borders and background.
self.layer.borderColor = borderColor.CGColor
self.layer.borderWidth = borderWidth
self.layer.cornerRadius = cornerRadius
self.layer.backgroundColor = backgrColor.CGColor
// Update main label.
mainLabel.textColor = mainTextColor
mainLabel.font = UIFont(name: fontName, size: mainTextSize)
mainLabel.text = mainText
// Update secondary label.
secLabel.textColor = secTextColor
secLabel.font = UIFont(name: fontName, size: secTextSize)
secLabel.text = secText
// Calculate view's height.
var mainLabelCopy: UILabel = mainLabel
mainLabelCopy.sizeToFit()
var mainLabelHeight: CGFloat = mainLabelCopy.frame.size.height
var secLabelCopy: UILabel = secLabel
secLabelCopy.sizeToFit()
var secLabelHeight: CGFloat = secLabelCopy.frame.size.height
textButtonHeight =
2 * verticalMargin
+ 2 * borderWidth
+ lineSpacing
+ mainLabelHeight
+ secLabelHeight
setNeedsUpdateConstraints()
}
override func updateConstraints() {
// Set constraints for labels.
setTranslatesAutoresizingMaskIntoConstraints(false)
mainLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
secLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
removeConstraints(constraints())
self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin))
self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: verticalMargin))
self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin + secHorizOffset))
self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Top, relatedBy: .Equal, toItem: mainLabel, attribute: .Bottom, multiplier: 1, constant: lineSpacing))
self.addConstraint(NSLayoutConstraint(item: self, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: textButtonHeight))
super.updateConstraints()
}
}
At runtime this now works as expected.
In Interface Builder it still throws some warnings about misplaced views that don't really make sense. The reported actual coordinates seem to be incorrect and neither manual nor automatic correction in IB fixes it.
Nevertheless, this allows me to proceed, so I post it as a suggested answer.
I had a similar problem to this with programmatically creating constraints and the sub view not showing up at all. The way that I fixed this was by setting the height and width as a constraint. I'm pretty sure this will fix the first problem you were having.