Skip to content

Commit

Permalink
Stats: Create missing ghost (loading) views (#23146)
Browse files Browse the repository at this point in the history
  • Loading branch information
staskus authored May 6, 2024
2 parents 58a864e + 42e742c commit fd795d8
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ class StatsGhostBaseCell: StatsBaseCell {
override func awakeFromNib() {
super.awakeFromNib()

commonInit()
}

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

commonInit()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}

private func commonInit() {
headingLabel.isGhostableDisabled = true
Style.configureCell(self)
}
Expand All @@ -24,7 +38,6 @@ class StatsGhostBaseCell: StatsBaseCell {

class StatsGhostGrowAudienceCell: StatsGhostBaseCell, NibLoadable { }
class StatsGhostTwoColumnCell: StatsGhostBaseCell, NibLoadable { }
class StatsGhostTopCell: StatsGhostBaseCell, NibLoadable { }
class StatsGhostTopHeaderCell: StatsGhostBaseCell, NibLoadable {
override func awakeFromNib() {
super.awakeFromNib()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import UIKit
import WordPressShared
import DesignSystem

final class StatsGhostLineChartCell: StatsGhostBaseCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

let lineChart = StatsGhostLineChartView()
lineChart.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(lineChart)
topConstraint = lineChart.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0)
topConstraint?.isActive = true
NSLayoutConstraint.activate([
lineChart.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: .DS.Padding.double),
contentView.trailingAnchor.constraint(equalTo: lineChart.trailingAnchor, constant: .DS.Padding.double),
contentView.bottomAnchor.constraint(equalTo: lineChart.bottomAnchor, constant: .DS.Padding.single),
lineChart.heightAnchor.constraint(equalToConstant: 190),
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

final class StatsGhostLineChartView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}

private func commonInit() {
backgroundColor = .clear
createMask()
}

private func createMask() {
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds

let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: bounds.maxY))

let wavePoints = [
CGPoint(x: bounds.width * 0.1, y: bounds.maxY * 0.8),
CGPoint(x: bounds.width * 0.3, y: bounds.maxY * 0.6),
CGPoint(x: bounds.width * 0.5, y: bounds.maxY * 0.4),
CGPoint(x: bounds.width * 0.7, y: bounds.maxY * 0.2),
CGPoint(x: bounds.width * 0.9, y: bounds.maxY * 0.5),
CGPoint(x: bounds.width, y: 0)
]

for (index, point) in wavePoints.enumerated() {
if index == 0 {
path.addLine(to: point)
} else {
let previousPoint = wavePoints[index - 1]
let midPointX = (previousPoint.x + point.x) / 2
path.addCurve(to: point, controlPoint1: CGPoint(x: midPointX, y: previousPoint.y), controlPoint2: CGPoint(x: midPointX, y: point.y))
}
}

path.addLine(to: CGPoint(x: bounds.width, y: 0))
path.addLine(to: CGPoint(x: bounds.width, y: bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
path.close()

maskLayer.path = path.cgPath
maskLayer.fillColor = UIColor.white.cgColor
maskLayer.fillRule = .evenOdd
layer.mask = maskLayer
backgroundColor = .clear
}

override func layoutSubviews() {
super.layoutSubviews()
layer.sublayers?.forEach { $0.frame = bounds }
createMask()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import UIKit
import WordPressShared
import DesignSystem

final class StatsGhostSingleValueCell: StatsGhostBaseCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

let ghostView = UIView()
ghostView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(ghostView)
topConstraint = ghostView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: .DS.Padding.single)
topConstraint?.isActive = true
NSLayoutConstraint.activate([
ghostView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: .DS.Padding.double),
ghostView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -.DS.Padding.double),
ghostView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.35),
ghostView.heightAnchor.constraint(equalToConstant: 36)
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ struct StatsGhostTwoColumnImmutableRow: StatsRowGhostable {

struct StatsGhostTopImmutableRow: StatsRowGhostable {
static let cell: ImmuTableCell = {
return ImmuTableCell.nib(StatsGhostTopCell.defaultNib, StatsGhostTopCell.self)
return ImmuTableCell.class(StatsGhostTopCell.self)
}()

var hideTopBorder = false
var hideBottomBorder = false
var numberOfColumns: Int = 2
var statSection: StatSection? = nil

// MARK: - Hashable
Expand All @@ -66,6 +67,7 @@ struct StatsGhostTopImmutableRow: StatsRowGhostable {
detailCell.topBorder?.isHidden = hideTopBorder
detailCell.bottomBorder?.isHidden = hideBottomBorder
detailCell.statSection = statSection
detailCell.numberOfColumns = numberOfColumns
}
}
}
Expand Down Expand Up @@ -132,3 +134,19 @@ struct StatsGhostTitleRow: StatsRowGhostable {
enum GhostCellStyle {
static let muriel = GhostStyle(beatStartColor: .placeholderElement, beatEndColor: .placeholderElementFaded)
}

struct StatsGhostSingleValueRow: StatsRowGhostable {
let statSection: StatSection?

static let cell: ImmuTableCell = {
return ImmuTableCell.class(StatsGhostSingleValueCell.self)
}()
}

struct StatsGhostLineChartRow: StatsRowGhostable {
let statSection: StatSection?

static let cell: ImmuTableCell = {
return ImmuTableCell.class(StatsGhostLineChartCell.self)
}()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import UIKit
import DesignSystem
import WordPressUI

final class StatsGhostTopCell: StatsGhostBaseCell, NibLoadable {
private let titleHeader: UIView = {
let header = UIView()
header.translatesAutoresizingMaskIntoConstraints = false
return header
}()

private let valueHeaders: UIStackView = {
let headers = UIStackView()
headers.translatesAutoresizingMaskIntoConstraints = false
headers.spacing = .DS.Padding.double
headers.distribution = .fill
headers.alignment = .fill
headers.axis = .horizontal
return headers
}()

private let contentRows: StatsGhostTopCellRow = {
let contentRows = StatsGhostTopCellRow()
contentRows.translatesAutoresizingMaskIntoConstraints = false
return contentRows
}()

var numberOfColumns: Int = 2 {
didSet {
configureCell(with: numberOfColumns)
}
}

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

contentView.addSubviews([titleHeader, valueHeaders, contentRows])

topConstraint = valueHeaders.topAnchor.constraint(equalTo: contentView.topAnchor, constant: .DS.Padding.single)
topConstraint?.isActive = true
NSLayoutConstraint.activate([
titleHeader.topAnchor.constraint(equalTo: valueHeaders.topAnchor),
titleHeader.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: .DS.Padding.double),
titleHeader.widthAnchor.constraint(equalToConstant: 50),
titleHeader.heightAnchor.constraint(equalToConstant: .DS.Padding.double),

valueHeaders.heightAnchor.constraint(equalToConstant: .DS.Padding.double),
valueHeaders.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -.DS.Padding.double),

contentRows.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0),
contentRows.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0),
contentRows.topAnchor.constraint(equalTo: valueHeaders.bottomAnchor, constant: .DS.Padding.half),
contentRows.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -.DS.Padding.single)
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func configureCell(with count: Int) {
updateHeaders(count: count)
contentRows.updateColumns(count: count)
}

private func updateHeaders(count: Int) {
valueHeaders.removeAllSubviews()
let headers = Array(repeating: UIView(), count: count-1)
headers.forEach { header in
configureHeader(header)
valueHeaders.addArrangedSubview(header)
}
}

private func configureHeader(_ header: UIView) {
header.startGhostAnimation()
header.widthAnchor.constraint(equalToConstant: Constants.columnWidth).isActive = true
}
}

final class StatsGhostTopCellRow: UIView {
private let avatarView = UIView()
private let columnsStackView = createStackView()
private let mainColumn = StatsGhostTopCellColumn()

override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
}

private static func createStackView() -> UIStackView {
let stackView = UIStackView()
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.spacing = .DS.Padding.double
return stackView
}

private func setupViews() {
[columnsStackView, mainColumn, avatarView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
setupConstraints()
}

private func setupConstraints() {
NSLayoutConstraint.activate([
avatarView.heightAnchor.constraint(equalToConstant: .DS.Padding.medium),
avatarView.widthAnchor.constraint(equalToConstant: .DS.Padding.medium),
avatarView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: .DS.Padding.double),
avatarView.topAnchor.constraint(equalTo: topAnchor, constant: .DS.Padding.double),
avatarView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -.DS.Padding.double),
mainColumn.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: .DS.Padding.double),
mainColumn.centerYAnchor.constraint(equalTo: centerYAnchor),
columnsStackView.leadingAnchor.constraint(equalTo: mainColumn.trailingAnchor, constant: .DS.Padding.double),
columnsStackView.centerYAnchor.constraint(equalTo: centerYAnchor),
columnsStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -.DS.Padding.double)
])
}

func updateColumns(count: Int) {
columnsStackView.removeAllSubviews()
mainColumn.isHidden = count <= 1

let columns = Array(repeating: StatsGhostTopCellColumn(width: StatsGhostTopCell.Constants.columnWidth), count: count-1)
columns.forEach(columnsStackView.addArrangedSubview)
}
}

private class StatsGhostTopCellColumn: UIView {
private let topView = UIView()
private let bottomView = UIView()
private let width: CGFloat?

init(width: CGFloat? = nil) {
self.width = width
super.init(frame: .zero)
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews() {
let stackView = createStackView()
addSubview(stackView)
pinSubviewToAllEdges(stackView)
setupConstraints()
}

private func createStackView() -> UIStackView {
let stackView = UIStackView(arrangedSubviews: [topView, bottomView])
stackView.axis = .vertical
stackView.spacing = .DS.Padding.half
stackView.alignment = .fill
stackView.distribution = .equalSpacing
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}

private func setupConstraints() {
var constraints = [
topView.heightAnchor.constraint(equalToConstant: .DS.Padding.medium),
bottomView.heightAnchor.constraint(equalToConstant: .DS.Padding.double)
]

if let width = width {
constraints += [
topView.widthAnchor.constraint(equalToConstant: width),
bottomView.widthAnchor.constraint(equalToConstant: width)
]
}

NSLayoutConstraint.activate(constraints)
startAnimations()
}

private func startAnimations() {
topView.startGhostAnimation(style: GhostCellStyle.muriel)
bottomView.startGhostAnimation(style: GhostCellStyle.muriel)
}

override func tintColorDidChange() {
super.tintColorDidChange()
topView.restartGhostAnimation(style: GhostCellStyle.muriel)
bottomView.restartGhostAnimation(style: GhostCellStyle.muriel)
}
}

fileprivate extension StatsGhostTopCell {
enum Constants {
static let columnWidth: CGFloat = 60
}
}
Loading

0 comments on commit fd795d8

Please sign in to comment.