I am trying to build (programmatically) a new view and use IBDesignable attribute to simplify this process and to show views in storyboard instead of white rectangles.
Here is a class with two subviews: UILabel and UIImageView. I am adding them dynamically to the parent view and set a couple of constraints for them:
import UIKit
#IBDesignable
class ChoiceView: UIView {
enum ChoiceState {
case empty, chosen
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViewForState(.empty)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViewForState(.empty)
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
super.layoutSubviews()
}
private func setupViewForState(_ state: ChoiceState) {
guard case .empty = state else { return } // single case for now
let placeholder = UILabel()
let choiceImage = UIImageView(image: UIImage(named: "plus_small"))
placeholder.text = "None"
placeholder.textAlignment = .right
choiceImage.contentMode = .center
let constraintFormats = [
"H:|-0-[placeholder]-10-[choice(50)]-0-|",
"V:|-0-[placeholder]-0-|",
"V:|-0-[choice]-0-|"
]
let views = ["placeholder": placeholder, "choice": choiceImage]
translatesAutoresizingMaskIntoConstraints = false
placeholder.translatesAutoresizingMaskIntoConstraints = false
choiceImage.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholder)
addSubview(choiceImage)
let constraints = constraintFormats.flatMap {
NSLayoutConstraint.constraints(
withVisualFormat: $0,
options: .directionLeadingToTrailing,
metrics: nil,
views: views)
}
NSLayoutConstraint.activate(constraints)
}
}
These are not perfect yet, but at least - shown in simulator:
But when I reload views in storyboard builder, I see this:
As you can see, the constraints are not enforced in storyboard and the image is not shown. Do you know how to fix that? Or, is it even possible to get the same picture in both storyboard and simulator?
Solution: Please look at #DonMag answer. It allows to see result I was expecting (see picture attached).
Do not set translatesAutoresizingMaskIntoConstraints = false for your Designable view itself.
let views = ["placeholder": placeholder, "choice": choiceImage]
// don't do this one
//translatesAutoresizingMaskIntoConstraints = false
placeholder.translatesAutoresizingMaskIntoConstraints = false
choiceImage.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholder)
addSubview(choiceImage)
Also, to see your "choice" image in IB:
let theBundle = Bundle(for: type(of: self))
let choiceImage = UIImageView(image: UIImage(named: "plus_small", in: theBundle, compatibleWith: self.traitCollection))
//
//let choiceImage = UIImageView(image: UIImage(named: "plus_small"))
...
Related
I have a question about the UICollection view list's separatorLayoutGuide. I saw this article and understood I need to override the function updateConstraints() in order to update the separator layout guide.
like this...
override func updateConstraints() {
super.updateConstraints()
separatorLayoutGuide.leadingAnchor.constraint(equalTo: otherView.leadingAnchor, constant: 0.0).isActive = true
}
I can see the tiny space between the cell's leading anchor and seprateguide's leading anchor like the image below and I want to fix it. (like the left side of the cell)
The problem is, however, I created a custom collection view list cell using this article and cannot change the separatorLayoutGuide leading to the custom view's leading.
I added the customListCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true in order to position the leading of separatorLayoutGuide to the customView's leading, and I get the
"UILayoutGuide:0x2822d8b60'UICollectionViewListCellSeparatorLayoutGuide'.leading"> and <NSLayoutXAxisAnchor:0x280e9cac0 "ContentView:0x15960db90.leading"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
error.
After I've done the research, I figured I didn't addSubview for the separatorLayoutGuide, but even if I add a subview to the custom view, the app crashes. Is there a way to change the separator guide's leading anchor when using the custom UIView?
class CustomListCell: UICollectionViewListCell {
var item: TestItem?
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object
var newConfiguration = ContentConfiguration().updated(for: state)
newConfiguration.name = item.name
newConfiguration.state = item.state
// Set content configuration
contentConfiguration = newConfiguration
}
}
struct ContentConfiguration: UIContentConfiguration, Hashable {
var name: String?
var state: String?
func makeContentView() -> UIView & UIContentView {
return ContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Self {
guard let state = state as? UICellConfigurationState else {
return self
}
// Updater self based on the current state
let updatedConfiguration = self
if state.isSelected {
print("is selected")
} else {
print("is deselected")
}
return updatedConfiguration
}
}
class ContentView: UIView, UIContentView {
let contentsView = UIView()
let customListCell = CustomListCell()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = ""
return label
}()
lazy var statusLabel: UILabel = {
let label = UILabel()
label.text = ""
return label
}()
lazy var symbolImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
init(configuration: ContentConfiguration) {
// Custom initializer implementation here.
super.init(frame: .zero)
setupAllViews()
apply(configuration: configuration)
}
override func updateConstraints() {
super.updateConstraints()
customListCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var currentConfiguration: ContentConfiguration!
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
guard let newConfiguration = newValue as? ContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
func setupAllViews() {
// add subviews and add constraints
}
func apply(configuration: ContentConfiguration) {
}
}
by overriding updateConstraints in a UICollectionViewListCell subclass
In your code you have a customListCell instance variable which is not necessary. Also you should not add separatorLayoutGuide as a subview anywhere.
Try to access the cell's contentView:
override func updateConstraints() {
super.updateConstraints()
if let customView = cell.contentView as? ContentView {
separatorLayoutGuide.leadingAnchor.constraint(equalTo: customView.leadingAnchor, constant: 0.0).isActive = true
}
}
Like this you can access any subview in your ContentView.
Another question is: Is it necessary to create a new constraint every time updateConstraints is called? Are constraints from previous calls still there? According to the documentation of updateConstraints:
Your implementation must be as efficient as possible. Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed. During each update pass, you must ensure that you have the appropriate constraints for the app’s current state.
Therefore I suggest this approach:
class ContentView: UIView, UIContentView {
var separatorConstraint: NSLayoutConstraint?
func updateForCell(_ cell: CustomListCell) {
if separatorConstraint == nil {
separatorConstraint = cell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 30)
}
separatorConstraint?.isActive = true
}
}
class CustomListCell: UICollectionViewListCell {
...
override func updateConstraints() {
super.updateConstraints()
(contentView as? AlbumContentView)?.updateForCell(self)
}
}
I wanted to add a simple counter of the number of objects in the table in the table header, next to its textLabel. So I created this class:
import UIKit
class CounterHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier: String = String(describing: self)
var counterLabel: UILabel
override init(reuseIdentifier: String?) {
counterLabel = UILabel()
super.init(reuseIdentifier: reuseIdentifier)
contentView.addSubview(counterLabel)
counterLabel.translatesAutoresizingMaskIntoConstraints = false
counterLabel.backgroundColor = .red
if let textLabel = self.textLabel{
counterLabel.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 6.0).isActive = true
counterLabel.topAnchor.constraint(equalTo: textLabel.topAnchor).isActive = true
counterLabel.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
}
}
required init?(coder aDecoder: NSCoder) {
counterLabel = UILabel()
super.init(coder: aDecoder)
}
}
But running this results in the following error:
'Unable to activate constraint with anchors
<NSLayoutXAxisAnchor:0x60000388ae00 "UILabel:0x7fb8314710a0.leading">
and <NSLayoutXAxisAnchor:0x60000388ae80 "_UITableViewHeaderFooterViewLabel:0x7fb8314718c0.trailing">
because they have no common ancestor.
Does the constraint or its anchors reference items in different view hierarchies?
That's illegal.'
How can I add a constraint for my counterLabel based on the already existing textLabel? Isn't textLabel already a subview of ContentView?
You're trying to use built-in textLabel, which I'm pretty sure isn't available at the init time. Try to execute your layouting code inside layoutSubviews method, right after super call. The method could be evaluated a couple of times, so you should check if you've already layouted your view (e.g. couterLabel.superview != nil)
here's how it should looks like:
final class CounterHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier: String = String(describing: self)
let counterLabel = UILabel()
override func layoutSubviews() {
super.layoutSubviews()
if counterLabel.superview == nil {
layout()
}
}
func layout() {
contentView.addSubview(counterLabel)
counterLabel.translatesAutoresizingMaskIntoConstraints = false
counterLabel.backgroundColor = .red
if let textLabel = self.textLabel {
counterLabel.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 6.0).isActive = true
counterLabel.topAnchor.constraint(equalTo: textLabel.topAnchor).isActive = true
counterLabel.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
}
}
}
Code for the Custom UIView:
Please check the video too here: https://drive.google.com/open?id=1kbrOxXWcJIi4vkiqMNer3exBr5cOWgDz
import UIKit
protocol PostAttachmentFullScreenViewDelegate: class {
func closeAttachmentFullView()
}
class PostAttachmentFullScreenView: UIView {
weak var delegate: PostAttachmentFullScreenViewDelegate?
#IBOutlet var backgroundView: UIImageView!
#IBOutlet var closeButton: UIButton!
#IBAction func closeViewAction(_ sender: Any) {
print("will call delegate to put it off")
self.delegate?.closeAttachmentFullView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let _ = commonInitialization()
backgroundView.image = UIImage(named: "ScrollImageTop1")
closeButton.isUserInteractionEnabled = true
}
override init(frame: CGRect) {
super.init(frame: frame)
let _ = commonInitialization()
backgroundView.image = UIImage(named: "ScrollImageTop1")
closeButton.isUserInteractionEnabled = true
}
func commonInitialization() -> UIView
{
let bundle = Bundle.init(for: type(of: self))
let nib = UINib(nibName: "PostAttachmentFullScreenView", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
view.frame = bounds
view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(view)
return view
}
}
usage in ViewController (I am defining an instance of the custom view and putting it inside the Scroll View):
var frame = CGRect(x:0, y:0, width:0, height:0)
let blue = PostAttachmentFullScreenView()
blue.delegate = self
blue.isUserInteractionEnabled = true
blue.backgroundColor = UIColor.blue
blue.backgroundView.image = fileAttachments[1]
frame.origin.x = attachmentsScrollView.frame.size.width * CGFloat (0)
frame.size = attachmentsScrollView.frame.size
blue.frame = frame
attachmentsScrollView.addSubview(blue)
extension NewPostViewController : PostAttachmentFullScreenViewDelegate
{
func closeAttachmentFullView() {
print("hiding attachments view")
attachmentSuperView.isHidden = true
}
}
To my surprise it doesn't even print - "will call delegate to put it off".
I am not able to understand what's wrong here. Please help me understand the issue and correct it. Thank you.
You are mixing programmatic approach and xib approach.
As you have added IBOultet and IBAction that means you are using xib for the UIView.
In that scenario you have to load the UIView xib when initialising the view.
Add an extension for UIView in your project:
extension UIView {
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
}
when you are initialising your view add it like this :
let blue : PostAttachmentFullScreenView = UIView.fromNib()
blue.delegate = self
blue.isUserInteractionEnabled = true
blue.backgroundColor = UIColor.blue
blue.backgroundView.image = fileAttachments[1]
frame.origin.x = attachmentsScrollView.frame.size.width * CGFloat (0)
frame.size = attachmentsScrollView.frame.size
blue.frame = frame
attachmentsScrollView.addSubview(blue)
and the delegate and button action methods will work.
you missed this :
You never set the target/action on your button. Somewhere you need to call addTarget(_:action:for:) to set the target/action on the button. Also, what connects the button to your PostAttachmentFullScreenView as an outlet?
This might be an obvious one but for me (Xcode 10.1) adding all missing UI constraints to the UIButton in question (at least 4 constraints) fixed the error for me in my custom view:
Make sure you add enough constraints (typically 4 constraints) or enough to have all warnings regarding missing constraints removed. After doing this and attaching the button with Ctrl + drag from View to corresponding swift code, the click was being detected and working properly.
Hope this helps.
How to show left and right image on left and right side with text in UIButton using IBDesignable?
I tried to google it to find any answer but did not find any smart answer. I want to do it through storyboard. Please Help
Required Output:
You can build on this. After setting buttons in Attributes Inspector this unbutton subclass will reset the title and place images on sides (don't forget to check all edge cases):
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var button: DoubleImageButton!
override func viewDidLoad() {
super.viewDidLoad()
button.layer.borderWidth = 2.0
button.layer.borderColor = UIColor.black.cgColor
button.layer.cornerRadius = button.bounds.height*0.5
}
}
#IBDesignable
class DoubleImageButton: UIButton {
/* Inspectable properties, once modified resets attributed title of the button */
#IBInspectable var leftImg: UIImage? = nil {
didSet {
/* reset title */
setAttributedTitle()
}
}
#IBInspectable var rightImg: UIImage? = nil {
didSet {
/* reset title */
setAttributedTitle()
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setAttributedTitle()
}
override init(frame: CGRect) {
super.init(frame: frame)
setAttributedTitle()
}
private func setAttributedTitle() {
var attributedTitle = NSMutableAttributedString()
/* Attaching first image */
if let leftImg = leftImg {
let leftAttachment = NSTextAttachment(data: nil, ofType: nil)
leftAttachment.image = leftImg
let attributedString = NSAttributedString(attachment: leftAttachment)
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
if let title = self.currentTitle {
mutableAttributedString.append(NSAttributedString(string: title))
}
attributedTitle = mutableAttributedString
}
/* Attaching second image */
if let rightImg = rightImg {
let leftAttachment = NSTextAttachment(data: nil, ofType: nil)
leftAttachment.image = rightImg
let attributedString = NSAttributedString(attachment: leftAttachment)
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
attributedTitle.append(mutableAttributedString)
}
/* Finally, lets have that two-imaged button! */
self.setAttributedTitle(attributedTitle, for: .normal)
}
}
In attributes inspector:
Result (adjust my implementation to achieve the best result):
Be sure to check cases when you'll have only right image selected for example but no left image etc. It's a quick solution and not fully tested.
Have fun and good luck! ;]
I have a collectionView cell that should either display an image or an icon that is generated as a custom UIView (lets say IconView).
Currently, I implemented this by adding an UIImageView and an IconView as subviews to a container view.
When an image is provided, the image property of UIImageView is simply updated. When a new IconView is provided it is currently always added as a subview to the container view. Therefore, before adding, it is first checked whether an IconView has already been added, and if so it is removed.
Although this implementation works, it is not very elegant and seems not efficient since it results in scrolling issues when the number of rows increase.
Would there be a better (more efficient) way to implement this for a single CollectionViewCell?
class CustomCell: UICollectionViewCell {
internal var image: UIImage? {
didSet {
self.imageView.image = image!
}
}
internal var iconView: IconView? {
didSet {
if !(self.iconContainerView.subviews.flatMap{ $0 as? IconView}.isEmpty) {
self.iconView!.removeFromSuperview()
}
self.iconView!.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.iconView!)
self.image = nil
}
}
fileprivate var imageView: UIImageView!
fileprivate var iconContainerView: UIView!
fileprivate var layoutConstraints = [NSLayoutConstraint]()
override init(frame: CGRect) {
super.init(frame: frame)
// ContainerView
self.iconContainerView = UIView()
self.iconContainerView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.iconContainerView)
// ImageView
self.imageView = UIImageView()
self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.iconContainerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
self.iconContainerView.widthAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
// Deactivate non-reusable constraints
_ = self.layoutConstraints.map { $0.isActive = false }
self.layoutConstraints = [NSLayoutConstraint]()
if let iconView = self.iconView {
self.imageView.isHidden = true
self.layoutConstraints.append(iconView.centerYAnchor.constraint(equalTo: self.iconContainerView.centerYAnchor))
self.layoutConstraints.append(iconView.centerXAnchor.constraint(equalTo: self.iconContainerView.centerXAnchor))
self.layoutConstraints.append(iconView.heightAnchor.constraint(equalToConstant: 40))
self.layoutConstraints.append(iconView.widthAnchor.constraint(equalToConstant: 40))
} else {
self.imageView.isHidden = false
self.iconView?.isHidden = true
self.layoutConstraints.append(self.imageView.leadingAnchor.constraint(equalTo: self.iconContainerView.leadingAnchor))
self.layoutConstraints.append(self.imageView.trailingAnchor.constraint(equalTo: self.iconContainerView.trailingAnchor))
self.layoutConstraints.append(self.imageView.topAnchor.constraint(equalTo: self.iconContainerView.topAnchor))
self.layoutConstraints.append(self.imageView.bottomAnchor.constraint(equalTo: self.iconContainerView.bottomAnchor))
}
_ = self.layoutConstraints.map {$0.isActive = true}
}
}
Don't ad and remove the IconView when setting. Add both in the same spot and change the isHidden, alpha, or opacity or bringSubviewToFront. This is much less main thread intensive.