import Messages import UIKit final class StickerBrowserViewController: MSStickerBrowserViewController { private struct Pack { let savedPack: SavedPack let thumbnail: UIImage? let stickers: [MSSticker] } private var packs: [Pack] = [] private var selectedPackIndex: Int? // nil = all packs private var displayedStickers: [MSSticker] = [] private var tabCollectionView: UICollectionView! private let tabSize: CGFloat = 36 private let tabSpacing: CGFloat = 6 private let tabInset: CGFloat = 8 // MARK: - Lifecycle init() { super.init(stickerSize: .small) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } override func viewDidLoad() { super.viewDidLoad() setupTabBar() } // MARK: - Data func reloadStickers() { packs = StickerStore.shared.savedPacks .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } .compactMap { pack in let urls = StickerStore.shared.stickerFileURLs(forPack: pack.name) let stickers = urls.compactMap { url in try? MSSticker(contentsOfFileURL: url, localizedDescription: pack.title) } guard !stickers.isEmpty else { return nil } let thumbnail = StickerStore.shared.thumbnail(forPack: pack.name) return Pack(savedPack: pack, thumbnail: thumbnail, stickers: stickers) } if let index = selectedPackIndex, index >= packs.count { selectedPackIndex = nil } updateDisplayedStickers() tabCollectionView?.reloadData() } private func updateDisplayedStickers() { if let index = selectedPackIndex { displayedStickers = packs[index].stickers } else { displayedStickers = packs.flatMap(\.stickers) } stickerBrowserView.reloadData() } // MARK: - Tab bar private func setupTabBar() { let tabBarHeight = tabSize + tabInset * 2 let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumInteritemSpacing = tabSpacing layout.minimumLineSpacing = tabSpacing layout.sectionInset = UIEdgeInsets(top: 0, left: tabInset, bottom: 0, right: tabInset) layout.itemSize = CGSize(width: tabSize, height: tabSize) tabCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) tabCollectionView.translatesAutoresizingMaskIntoConstraints = false tabCollectionView.backgroundColor = .secondarySystemBackground tabCollectionView.showsHorizontalScrollIndicator = false tabCollectionView.dataSource = self tabCollectionView.delegate = self tabCollectionView.register(TabCell.self, forCellWithReuseIdentifier: TabCell.reuseID) let separator = UIView() separator.translatesAutoresizingMaskIntoConstraints = false separator.backgroundColor = .separator view.addSubview(tabCollectionView) view.addSubview(separator) NSLayoutConstraint.activate([ tabCollectionView.topAnchor.constraint(equalTo: view.topAnchor), tabCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tabCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tabCollectionView.heightAnchor.constraint(equalToConstant: tabBarHeight), separator.topAnchor.constraint(equalTo: tabCollectionView.bottomAnchor), separator.leadingAnchor.constraint(equalTo: view.leadingAnchor), separator.trailingAnchor.constraint(equalTo: view.trailingAnchor), separator.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale), ]) additionalSafeAreaInsets = UIEdgeInsets(top: tabBarHeight, left: 0, bottom: 0, right: 0) } private func selectPack(at index: Int?) { guard selectedPackIndex != index else { return } selectedPackIndex = index updateDisplayedStickers() tabCollectionView.reloadData() } // MARK: - MSStickerBrowserViewDataSource override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int { displayedStickers.count } override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker { displayedStickers[index] } } // MARK: - UICollectionViewDataSource extension StickerBrowserViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { packs.count + 1 // +1 for "All" tab } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCell.reuseID, for: indexPath) as! TabCell if indexPath.item == 0 { let isSelected = selectedPackIndex == nil cell.configureAsAll(isSelected: isSelected) } else { let pack = packs[indexPath.item - 1] let isSelected = selectedPackIndex == indexPath.item - 1 cell.configure(image: pack.thumbnail, isSelected: isSelected) } return cell } } // MARK: - UICollectionViewDelegate extension StickerBrowserViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.item == 0 { selectPack(at: nil) } else { selectPack(at: indexPath.item - 1) } } } // MARK: - TabCell private final class TabCell: UICollectionViewCell { static let reuseID = "TabCell" private let imageView = UIImageView() private let label = UILabel() override init(frame: CGRect) { super.init(frame: frame) contentView.layer.cornerRadius = 8 contentView.clipsToBounds = true imageView.contentMode = .scaleAspectFit imageView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(imageView) label.font = .systemFont(ofSize: 11, weight: .medium) label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false label.isHidden = true contentView.addSubview(label) let inset: CGFloat = 4 NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset), imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset), imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset), imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset), label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } func configure(image: UIImage?, isSelected: Bool) { imageView.image = image imageView.isHidden = false label.isHidden = true contentView.backgroundColor = isSelected ? .systemGray5 : .clear imageView.alpha = isSelected ? 1.0 : 0.5 } func configureAsAll(isSelected: Bool) { imageView.isHidden = true label.isHidden = false label.text = "All" label.textColor = isSelected ? .label : .secondaryLabel contentView.backgroundColor = isSelected ? .systemGray5 : .clear } override func prepareForReuse() { super.prepareForReuse() imageView.image = nil imageView.isHidden = false label.isHidden = true } }