I have a #IBDesignable UIView (ContainerView) that has one subView (also #IBDesignable). I would very much like to be able to update the constraints of the subView in the code in a way were they are automatically updated in InterfaceBuilder. Example:
#IBDesignable class ContainerView: UIView {
#IBOutlet var mySubView: MyView!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
self.setup()
}
private func setup() {
self.mySubView.leadingConstraint.constant = MyResource.sizes.defaultLeading
}
}
This will work just fine at runtime, but it crashes the IBDesignablesAgent because mySubView is nil when running prepareForInterfaceBuilder.
I want to do it this way to be able to set my constraints globally in some constants, but keep the view representation in my xib files.
Does anyone have a work around for this, or am I reaching for the impossible here?
Related
ViewController:
class ViewController: UIViewController, ViewSpecificController {
typealias RootView = CustomView
override func viewDidLoad() {
super.viewDidLoad()
view().configure()
}
override func loadView() { self.view = CustomView() }
}
UIView:
class CustomView: UIView {
func configure() {
backgroundColor = .orange
translatesAutoresizingMaskIntoConstraints = false
addConstraints()
}
func addConstraints() {
var constraints = [NSLayoutConstraint]()
constraints.append(self.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor))
constraints.append(self.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor))
constraints.append(self.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor))
constraints.append(self.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor))
NSLayoutConstraint.activate(constraints)
}
}
Executing this code results in an error "[LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want."
I tried to initialize UIView, the same error appeared there. How to fix it?
From what i can see it looks like your CustomView class is trying to set constraints to itself. The constraints aren't needed as the ViewController will handle sizing it automatically once you replace the original in loadView(). Removing your addConstraints() method from configure() should solve your problem. See if that works...
I'm working on an app that involves a whole bunch of buttons that will be formatted exactly the same way -- same size, layout, color, text labels, etc. Similar to how it works with "prototype" cells in Table Views and Container Views, I'm trying to create one instance of the "prototype" button as a custom class of UIButton in an external xib that I can pull into the main view controller and repeat as necessary.
I'm not having any trouble bringing in the buttons, and I haven't had any problems customizing the text labels, etc. So this doesn't feel like an issue where the "prototype" isn't outletted correctly.
What doesn't seem to be working are the actual button taps.
I have two buttons -- button1 and button2, which have been assigned tag=1 and tag=2. In order to test the button action, I've set up a print statement that prints the tag number of the sender to the console.
If I create a simple, plain button in the main storyboard, and give it a tag number, the button behaves correctly. But the custom-class buttons don't send anything to the console.
Am I missing a step? My gut is that there's an extra detail in the external xib that I'm missing...but I would think that the specifics (tag numbers, etc.) need to be associated with each instance, not with the prototype.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var button1: customButton!
#IBOutlet weak var button2: customButton!
override func viewDidLoad() {
super.viewDidLoad()
button1.customTitle.text = "This is Button 1"
button2.customTitle.text = "This is Button 2"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func buttonPressed(_ sender: UIButton) {
print(sender.tag)
}
}
EDIT: here is the code from the xib where the button is generated.
import UIKit
class customButton: UIButton {
#IBOutlet var contentView: UIView!
#IBAction func buttonPressed(_ sender: UIButton) {
}
override init(frame: CGRect) { // for using CustomView in code
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) { // for using CustomView in IB
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("customButton", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
}
I now see in your edited answer, you have a contentView added as a subview. Set it's interaction enabled status to false so it wont care about the touches itself, and button action should be fired.
In your button file, maybe in commonInit:
contentView.isUserInteractionEnabled = false
I created a nib file with a custom collectionViewCell and attached to a viewController
class CustomCollectionView: UICollectionViewCell{}
Now I have to use the exact cell inside a tableView. I created a new nib file and viewController
class CustomTableView: UITableViewCell{}
and I copied the hole code of CustomCollectionView on it. every thing is working fine but I believe that it dose not make sense to copy the hole exact code of CustomCollectionView into CustomTableView and to use the exact same nib file but with a tableViewCell instead of collectionViewCell on it. Is there any way to optimize what I did?
As you said in a comment in suhit's answer, you can do this by using a common view in both the CollectionViewCell and TableViewCell subclasses. You don't need a ViewController since it adds extra overhead. A simple UIView is enough. Some code to show what I mean:
class CustomTableViewCell: UITableViewCell {
var customView: CustomView!
func awakeFromNib() {
super.awakeFromNib()
customView = CustomView()
customView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(customView)
customView.fillSuperview()
}
}
class CustomCollectionViewCell: UICollectionViewCell {
var customView: CustomView!
func awakeFromNib() {
super.awakeFromNib()
customView = CustomView()
customView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(customView)
customView.fillSuperview()
}
}
extension UIView {
func fillSuperview() {
guard let superview = superview else {
return print("no superview")
}
topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
rightAnchor.constraint(equalTo: contentVisuperviewew.rightAnchor).isActive = true
}
}
A sample implementation for the CustomView class:
class CustomView: UIView {
func initialize() {
//...
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
}
If you wish to create your custom view in a xib that's also fine, but it's a little trickier. This is beyond the scope of the question but I'm just going to leave a link here in case you need it.
If you want to use same view then its better to use similar type view i.e. use collectionView at both places so that you can use the CustomCollectionViewCell in both ViewControllers. UICollectionView is highly customisable so you can do whatever you want to do with UITableView in UICollectionView as well.
I am subclassing NSLayoutConstraint to provide custom spacing depending on a flag. Is it possible to have this reflected in the Interface Builder? I tried using IBDesignable and prepareForInterfaceBuilder, but it has no effect on the NSLayoutConstraint in the Interface Builder (only at runtime does it reflect).
Here's what I'm trying:
#IBDesignable
class CustomLayoutConstraint: NSLayoutConstraint {
#IBInspectable var doubleConstant: Bool = false
override func awakeFromNib() {
super.awakeFromNib()
handleSelection()
}
override func prepareForInterfaceBuilder() {
handleSelection()
}
func handleSelection() {
if doubleConstant {
constant *= 2
}
}
}
I have a flag in IBInspectable that will double the constant at runtime if set. Is there a way for this to be reflected in the StoryBoard so it's clear something will be happening?
I've got view controller (using Storyboards if matters). Controller got custom view inside it let's call it AView. The view is laid out on storyboard as UIView object with custom class set. AView's contents are on separate XIB because I need this highly reusable. Here's how code looks like:
class VC: UIViewController {
#IBOutlet weak var aView: AView!
override func viewDidLoad() {
super.viewDidLoad()
aView.setup(false) //doesn't work
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
aView.setup(false) //doesn't work
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
aView.setup(false) //do work but glitches
}
}
class AView: UIView {
required init?(coder aDecoder: NSCoder) {
//init stuff: loading nib, adding view from it
}
#IBOutlet weak var someView: UIView! //this view has all constraints which are required and additional rightConstraint which is inactive, for future use
#IBOutlet var leftConstraint: NSLayoutConstraint!
#IBOutlet var rightConstraint: NSLayoutConstraint!
func setup(shouldBeOnLeft: Bool) {
leftConstraint.active = true
rightConstraint.active = false
self.layoutIfNeeded()
}
}
I need to setup this view before it appears, based on some parameters. I'm modifying only its internal content from inside. If I call aView.setup(shouldBeOnLeft:) in viewDidLoad or viewWillAppear constraints don't update or maybe do but I don't see changes. If I move it to viewDidAppear it works but obviously I see misplaced views for a while (state before setup).
The question is: how to get it work as intended and without view's manipulation form view controller and independent on how and where setup method is called unless it's inside or right after VC's viewDidLoad? Only thing that VC needs to know is to call setup with parameter.
You should call it inside viewDidLayoutSubviews(), at this point it have set the view parameters, and you can manipulate the view before is presented to the user (so, no glitches)
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instm/UIViewController/viewDidLayoutSubviews
Discussion
Called to notify the view controller that its view has just laid out its subviews.
OK I figured that out - but I don't actually like it in 100%
class VC: UIViewController {
#IBOutlet weak var aView: AView!
override func viewDidLoad() {
super.viewDidLoad()
aView.setup(false) //now it does work
}
}
class AView: UIView {
required init?(coder aDecoder: NSCoder) {
//init stuff: loading nib, adding view from it
}
#IBOutlet weak var someView: UIView! //this view has all constraints which are required and additional rightConstraint which is inactive, for future use
#IBOutlet var leftConstraint: NSLayoutConstraint!
#IBOutlet var rightConstraint: NSLayoutConstraint!
func setup(shouldBeOnLeft: Bool) {
self.yesIfLeft = shouldBeOnLeft
}
private var yesIfLeft = false {
didSet{
self.updateLayout()
}
}
private func updateLayout() {
leftConstraint.active = self.yesIfLeft
rightConstraint.active = !self.yesIfLeft
self.layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
self.updateLayout()
}
}
Whole point is to keep reference to settings that cause layout change and update them also in layoutSubviews(). Doing this way VC doesn't have to know about internals of AView (that's a big plus for me) but requires that AView holds it's configuration which is some extra work to do. I'm not marking this answer as accepted for now because maybe someone will have some better idea.