-
-
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 contentView
for?Example.swift: I assume this would be the proper semantic:
What did you have in mind with the tappable button? (
buttonAction
)