IGListKit/Examples/Examples-iOS/IGListKitExamples/Views/PostCell.swift

291 lines
11 KiB
Swift
Raw Normal View History

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import UIKit
// MARK: - UICollectionViewCell
// In IGListKit, cells are regular UICollectionViewCells
// There's no special base class - IGListKit works with standard UIKit components
final class PostCell: UICollectionViewCell {
// MARK: - UI Components
private let headerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 20
imageView.clipsToBounds = true
imageView.backgroundColor = .systemGray5
return imageView
}()
private let usernameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.boldSystemFont(ofSize: 15)
return label
}()
private let optionsButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "ellipsis"), for: .normal)
button.tintColor = .darkGray
return button
}()
private let postImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = .systemGray6
return imageView
}()
private let actionStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 10
return stackView
}()
private let likeButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "heart"), for: .normal)
button.tintColor = .label
return button
}()
private let commentButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "message"), for: .normal)
button.tintColor = .label
return button
}()
private let shareButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "paperplane"), for: .normal)
button.tintColor = .label
return button
}()
private let likesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.boldSystemFont(ofSize: 14)
return label
}()
private let captionStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 5
return stackView
}()
private let captionUsernameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.boldSystemFont(ofSize: 14)
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return label
}()
private let captionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 14)
label.numberOfLines = 3
return label
}()
private let timestampLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .gray
return label
}()
// MARK: - Properties
// Closure for handling the options button tap
// This allows the section controller to respond to UI events in the cell
var optionsButtonTapped: ((UIButton) -> Void)?
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupActions()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Clean up cell for reuse - important for efficient cell recycling
override func prepareForReuse() {
super.prepareForReuse()
avatarImageView.image = nil
postImageView.image = nil
usernameLabel.text = nil
captionUsernameLabel.text = nil
captionLabel.text = nil
likesLabel.text = nil
timestampLabel.text = nil
}
// MARK: - Setup
private func setupViews() {
optionsButton.accessibilityIdentifier = "optionsButton"
contentView.backgroundColor = .systemBackground
// Add subviews
contentView.addSubview(headerView)
headerView.addSubview(avatarImageView)
headerView.addSubview(usernameLabel)
headerView.addSubview(optionsButton)
contentView.addSubview(postImageView)
contentView.addSubview(actionStackView)
actionStackView.addArrangedSubview(likeButton)
actionStackView.addArrangedSubview(commentButton)
actionStackView.addArrangedSubview(shareButton)
contentView.addSubview(likesLabel)
contentView.addSubview(captionStackView)
captionStackView.addArrangedSubview(captionUsernameLabel)
captionStackView.addArrangedSubview(captionLabel)
contentView.addSubview(timestampLabel)
// Layout constraints
NSLayoutConstraint.activate([
// Header view
headerView.topAnchor.constraint(equalTo: contentView.topAnchor),
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
headerView.heightAnchor.constraint(equalToConstant: 50),
avatarImageView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 10),
avatarImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
avatarImageView.heightAnchor.constraint(equalToConstant: 40),
usernameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 10),
usernameLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
optionsButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -10),
optionsButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
optionsButton.widthAnchor.constraint(equalToConstant: 30),
optionsButton.heightAnchor.constraint(equalToConstant: 30),
// Post image
postImageView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
postImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
postImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
postImageView.heightAnchor.constraint(equalTo: contentView.widthAnchor), // Square aspect ratio
// Action buttons
actionStackView.topAnchor.constraint(equalTo: postImageView.bottomAnchor, constant: 8),
actionStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
likeButton.widthAnchor.constraint(equalToConstant: 30),
likeButton.heightAnchor.constraint(equalToConstant: 30),
commentButton.widthAnchor.constraint(equalToConstant: 30),
commentButton.heightAnchor.constraint(equalToConstant: 30),
shareButton.widthAnchor.constraint(equalToConstant: 30),
shareButton.heightAnchor.constraint(equalToConstant: 30),
// Likes
likesLabel.topAnchor.constraint(equalTo: actionStackView.bottomAnchor, constant: 5),
likesLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
// Caption
captionStackView.topAnchor.constraint(equalTo: likesLabel.bottomAnchor, constant: 5),
captionStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
captionStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
// Timestamp
timestampLabel.topAnchor.constraint(equalTo: captionStackView.bottomAnchor, constant: 5),
timestampLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
timestampLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
])
}
private func setupActions() {
optionsButton.addTarget(self, action: #selector(handleOptionsButtonTap), for: .touchUpInside)
}
// MARK: - Actions
@objc private func handleOptionsButtonTap() {
optionsButtonTapped?(optionsButton)
}
// MARK: - Configuration
// Configure the cell with data from a Post model
// In IGListKit, cells are configured by their section controllers
func configure(with post: Post) {
usernameLabel.text = post.username
captionUsernameLabel.text = post.username
captionLabel.text = post.description
likesLabel.text = "\(post.likes) likes"
// Format timestamp
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
timestampLabel.text = formatter.localizedString(for: post.timeStamp, relativeTo: Date())
if let avatarURL = post.userAvatarURL {
URLSession.shared.dataTask(with: avatarURL) { [weak self] data, _, _ in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.avatarImageView.image = image
}
}
}.resume()
}
if let imageURL = post.imageURL {
URLSession.shared.dataTask(with: imageURL) { [weak self] data, _, _ in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.postImageView.image = image
}
}
}.resume()
}
}
}