Skip to content

Instantly share code, notes, and snippets.

@maxhand
Last active March 7, 2024 20:10
Show Gist options
  • Save maxhand/24f9a69dc83705cfc025cae63f86a03c to your computer and use it in GitHub Desktop.
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 }
}
@martindufort
Copy link

Thanks for this. 🤙

BackgroundView.swift: what is the let contentView for?

Example.swift: I assume this would be the proper semantic:

let backgroundViewRepresenter = ListBackgroundView(...)

What did you have in mind with the tappable button? (buttonAction)

@maxhand
Copy link
Author

maxhand commented Mar 7, 2024

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
                    [...]
        }
  )
)

@martindufort
Copy link

martindufort commented Mar 7, 2024

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...

@maxhand
Copy link
Author

maxhand commented Mar 7, 2024

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.

@martindufort
Copy link

Much better. Forgot about the backgroundView. Thanks again.

@martindufort
Copy link

Look at this beauty!!! Thanks Max. I owe you one.

CleanShot 2024-03-07 at 15 03 38@2x

@maxhand
Copy link
Author

maxhand commented Mar 7, 2024

Looks great! Happy to help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment