-
-
Save maxhand/24f9a69dc83705cfc025cae63f86a03c to your computer and use it in GitHub Desktop.
| import UIKit | |
| class BackgroundView: UIView { | |
| private let containedView = UIStackView() | |
| private var buttonAction: (() -> ())? | |
| override init(frame: CGRect) { | |
| super.init(frame: frame) | |
| containedView.alignment = .center | |
| containedView.axis = .vertical | |
| containedView.spacing = 4 | |
| setupView() | |
| } | |
| private func setupView() { | |
| addSubview(containedView) | |
| containedView.translatesAutoresizingMaskIntoConstraints = false | |
| NSLayoutConstraint.activate([ | |
| containedView.centerXAnchor.constraint(equalTo: centerXAnchor), | |
| containedView.centerYAnchor.constraint(equalTo: centerYAnchor), | |
| containedView.widthAnchor.constraint(equalToConstant: 250) | |
| ]) | |
| } | |
| func configure(using representable: ListBackgroundViewRepresentable?) { | |
| guard let representable else { return } | |
| let symbolConfiguration = UIImage.SymbolConfiguration(scale: .large) | |
| let imageView = UIImageView(image: UIImage(systemName: representable.systemImageName)?.withConfiguration(symbolConfiguration).withTintColor(.systemGray2).withRenderingMode(.alwaysOriginal)) | |
| let titleLabel = UILabel() | |
| titleLabel.font = .preferredFont(forTextStyle: .headline) | |
| titleLabel.textColor = .systemGray | |
| titleLabel.adjustsFontForContentSizeCategory = true | |
| titleLabel.numberOfLines = 0 | |
| titleLabel.text = representable.title | |
| let secondaryLabel = UILabel() | |
| secondaryLabel.font = .preferredFont(forTextStyle: .body) | |
| secondaryLabel.text = representable.body | |
| secondaryLabel.numberOfLines = 0 | |
| secondaryLabel.textColor = .systemGray2 | |
| secondaryLabel.textAlignment = .center | |
| let accessoryButton = UIButton() | |
| var accessoryButtonConfig: UIButton.Configuration = .filled() | |
| accessoryButtonConfig.title = representable.buttonTitle | |
| accessoryButtonConfig.cornerStyle = .capsule | |
| accessoryButton.layer.cornerRadius = 15 | |
| accessoryButton.addTarget(self, action: #selector(handleButtonPressed), for: .touchUpInside) | |
| accessoryButton.configuration = accessoryButtonConfig | |
| accessoryButton.translatesAutoresizingMaskIntoConstraints = false | |
| NSLayoutConstraint.activate([ | |
| accessoryButton.widthAnchor.constraint(equalToConstant: 110), | |
| accessoryButton.heightAnchor.constraint(equalToConstant: 35) | |
| ]) | |
| var views = [imageView, titleLabel, secondaryLabel] | |
| if representable.buttonAction != nil && representable.buttonTitle != nil { | |
| views.append(accessoryButton) | |
| } | |
| buttonAction = representable.buttonAction | |
| for view in views { | |
| containedView.addArrangedSubview(view) | |
| } | |
| containedView.setCustomSpacing(8, after: imageView) | |
| containedView.setCustomSpacing(20, after: secondaryLabel) | |
| } | |
| @objc private func handleButtonPressed(sender: UIButton) { | |
| buttonAction?() | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| } |
| let backgroundView = BackgroundView() | |
| backgroundView.configure( | |
| using: ListBackgroundView( | |
| systemImageName: "line.3.horizontal.decrease", | |
| title: "These are not the results that you are looking for", | |
| buttonTitle: "Energize", | |
| buttonAction: { | |
| // Do great things | |
| } | |
| ) | |
| ) |
| struct ListBackgroundView: ListBackgroundViewRepresentable { | |
| var systemImageName: String | |
| var title: String | |
| var body: String? | |
| var buttonTitle: String? | |
| var buttonAction: (() -> ())? | |
| } |
| protocol ListBackgroundViewRepresentable { | |
| var systemImageName: String { get } | |
| var title: String { get } | |
| var body: String? { get } | |
| var buttonTitle: String? { get } | |
| var buttonAction: (() -> ())? { get } | |
| } |
It seems like contentView was supposed to be removed and wasn't. I will remove it from the gist.
Your assumption for example usage seems to be correct, it seems my example is actually incorrect. You can instantiate a ListBackgroundView and then pass in an object that conforms to ListBackgroundViewRepresentable.
I use the tappable button in any situation where I'd like the user to be able to react to the state of a list. Here's a good example that I just implemented yesterday for when a filtered list is empty. In this case, I set the tap button's action to clear any active filters.
backgroundView.configure(
using: ListBackgroundView(
systemImageName: "line.3.horizontal.decrease",
title: "Filter has no results",
buttonTitle: "Clear",
buttonAction: { in
[...]
}
)
)
Ok cool.
That looks very nice.
Then I just need to add it as a subview to my empty UICollectionViewController.collectionView and position it properly with constraints.
Will let you know how it goes...
Good luck!
I assign these views to UITableView.backgroundView with no adjustments. I haven't tested it, but you might be able to do the same with UICollectionView.backgroundView.
Much better. Forgot about the backgroundView. Thanks again.
Looks great! Happy to help.

Thanks for this. 🤙
BackgroundView.swift: what is the
let contentViewfor?Example.swift: I assume this would be the proper semantic:
What did you have in mind with the tappable button? (
buttonAction)