// // SidebarListContentView.swift // Mastodon // // Created by Cirno MainasuK on 2021-9-24. // import os.log import UIKit import MetaTextKit import FLAnimatedImage final class SidebarListContentView: UIView, UIContentView { let logger = Logger(subsystem: "SidebarListContentView", category: "UI") let imageView = UIImageView() let animationImageView = FLAnimatedImageView() // for animation image let headlineLabel = MetaLabel(style: .sidebarHeadline(isSelected: false)) let subheadlineLabel = MetaLabel(style: .sidebarSubheadline(isSelected: false)) private var currentConfiguration: ContentConfiguration! var configuration: UIContentConfiguration { get { currentConfiguration } set { guard let newConfiguration = newValue as? ContentConfiguration else { return } apply(configuration: newConfiguration) } } init(configuration: ContentConfiguration) { super.init(frame: .zero) _init() apply(configuration: configuration) } required init?(coder: NSCoder) { super.init(coder: coder) _init() } } extension SidebarListContentView { private func _init() { let imageViewContainer = UIView() imageViewContainer.translatesAutoresizingMaskIntoConstraints = false addSubview(imageViewContainer) NSLayoutConstraint.activate([ imageViewContainer.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), imageViewContainer.centerYAnchor.constraint(equalTo: centerYAnchor), ]) imageViewContainer.setContentHuggingPriority(.defaultLow, for: .horizontal) imageViewContainer.setContentHuggingPriority(.defaultLow, for: .vertical) animationImageView.translatesAutoresizingMaskIntoConstraints = false imageViewContainer.addSubview(animationImageView) NSLayoutConstraint.activate([ animationImageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), animationImageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), animationImageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), animationImageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), ]) animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical) animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) imageView.translatesAutoresizingMaskIntoConstraints = false imageViewContainer.addSubview(imageView) NSLayoutConstraint.activate([ imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), imageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), imageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), ]) imageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical) imageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) let textContainer = UIStackView() textContainer.axis = .vertical textContainer.translatesAutoresizingMaskIntoConstraints = false addSubview(textContainer) NSLayoutConstraint.activate([ textContainer.topAnchor.constraint(equalTo: topAnchor, constant: 10), textContainer.leadingAnchor.constraint(equalTo: imageViewContainer.trailingAnchor, constant: 10), textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: 12), ]) textContainer.addArrangedSubview(headlineLabel) textContainer.addArrangedSubview(subheadlineLabel) headlineLabel.setContentHuggingPriority(.required - 9, for: .vertical) headlineLabel.setContentCompressionResistancePriority(.required - 9, for: .vertical) subheadlineLabel.setContentHuggingPriority(.required - 10, for: .vertical) subheadlineLabel.setContentCompressionResistancePriority(.required - 10, for: .vertical) NSLayoutConstraint.activate([ imageViewContainer.heightAnchor.constraint(equalTo: headlineLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), imageViewContainer.widthAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), ]) animationImageView.isUserInteractionEnabled = false headlineLabel.isUserInteractionEnabled = false subheadlineLabel.isUserInteractionEnabled = false imageView.contentMode = .scaleAspectFit animationImageView.contentMode = .scaleAspectFit imageView.tintColor = Asset.Colors.brandBlue.color animationImageView.tintColor = Asset.Colors.brandBlue.color } private func apply(configuration: ContentConfiguration) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard currentConfiguration != configuration else { return } currentConfiguration = configuration guard let item = configuration.item else { return } // configure state imageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color animationImageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color headlineLabel.setup(style: .sidebarHeadline(isSelected: item.isSelected)) subheadlineLabel.setup(style: .sidebarSubheadline(isSelected: item.isSelected)) // configure model imageView.isHidden = item.imageURL != nil animationImageView.isHidden = item.imageURL == nil imageView.image = item.image.withRenderingMode(.alwaysTemplate) animationImageView.setImage( url: item.imageURL, placeholder: animationImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink scaleToSize: nil ) animationImageView.layer.masksToBounds = true animationImageView.layer.cornerCurve = .continuous animationImageView.layer.cornerRadius = 4 headlineLabel.configure(content: item.headline) if let subheadline = item.subheadline { subheadlineLabel.configure(content: subheadline) subheadlineLabel.isHidden = false } else { subheadlineLabel.isHidden = true } } } extension SidebarListContentView { struct Item: Hashable { // state var isSelected: Bool = false // model let image: UIImage let imageURL: URL? let headline: MetaContent let subheadline: MetaContent? static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool { return lhs.isSelected == rhs.isSelected && lhs.image == rhs.image && lhs.imageURL == rhs.imageURL && lhs.headline.string == rhs.headline.string && lhs.subheadline?.string == rhs.subheadline?.string } func hash(into hasher: inout Hasher) { hasher.combine(isSelected) hasher.combine(image) imageURL.flatMap { hasher.combine($0) } hasher.combine(headline.string) subheadline.flatMap { hasher.combine($0.string) } } } struct ContentConfiguration: UIContentConfiguration, Hashable { let logger = Logger(subsystem: "SidebarListContentView.ContentConfiguration", category: "ContentConfiguration") var item: Item? func makeContentView() -> UIView & UIContentView { SidebarListContentView(configuration: self) } func updated(for state: UIConfigurationState) -> ContentConfiguration { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") var updatedConfiguration = self if let state = state as? UICellConfigurationState { updatedConfiguration.item?.isSelected = state.isHighlighted || state.isSelected } else { assertionFailure() updatedConfiguration.item?.isSelected = false } return updatedConfiguration } static func == ( lhs: ContentConfiguration, rhs: ContentConfiguration ) -> Bool { return lhs.item == rhs.item } func hash(into hasher: inout Hasher) { hasher.combine(item) } } }