mastodon-ios/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift

216 lines
9.1 KiB
Swift
Raw Normal View History

2021-09-24 13:58:50 +02:00
//
// 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)
}
}
}