diff --git a/BeeSwift/Cells/GoalCollectionViewCell.swift b/BeeSwift/Cells/GoalCollectionViewCell.swift index 8546baffa..3507a1a99 100644 --- a/BeeSwift/Cells/GoalCollectionViewCell.swift +++ b/BeeSwift/Cells/GoalCollectionViewCell.swift @@ -1,5 +1,5 @@ // -// GoalTableViewCell.swift +// GoalCollectionViewCell.swift // BeeSwift // // Created by Andy Brett on 4/24/15. @@ -10,73 +10,110 @@ import BeeKit import Foundation class GoalCollectionViewCell: UICollectionViewCell { - let slugLabel: BSLabel = BSLabel() - let titleLabel: BSLabel = BSLabel() - let todaytaLabel: BSLabel = BSLabel() - let thumbnailImageView = GoalImageView(isThumbnail: true) - let safesumLabel: BSLabel = BSLabel() - let margin = 8 + private lazy var cardView: CardView = { + let view = CardView() + view.backgroundColor = .secondarySystemGroupedBackground + view.layer.cornerRadius = CardLookConstants.cornerRadius + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.1 + view.layer.shadowRadius = 4 + view.layer.shadowOffset = CardLookConstants.shadowOffset + return view + }() + + private lazy var thumbnailImageView: GoalImageView = { return GoalImageView(isThumbnail: true) }() + + private lazy var titleLabel: BSLabel = { + let label = BSLabel() + label.font = UIFont.beeminder.defaultFontHeavy.withSize(17) + label.textColor = .label + return label + }() + + private lazy var slugLabel: BSLabel = { + let label = BSLabel() + label.font = UIFont.beeminder.defaultFont.withSize(15) + label.textColor = .label + return label + }() + + private lazy var countdownLabel: BSLabel = { + let label = BSLabel() + label.font = UIFont.beeminder.defaultBoldFont.withSize(13) + label.numberOfLines = 0 + return label + }() + + private lazy var todaytaLabel: BSLabel = { + let label = BSLabel() + label.textColor = .label + label.font = UIFont.beeminder.defaultFont.withSize(14) + label.textAlignment = .right + label.setContentCompressionResistancePriority(.required, for: .horizontal) + return label + }() + override init(frame: CGRect) { super.init(frame: frame) - self.contentView.addSubview(self.slugLabel) - self.contentView.addSubview(self.titleLabel) - self.contentView.addSubview(self.todaytaLabel) - self.contentView.addSubview(self.thumbnailImageView) - self.contentView.addSubview(self.safesumLabel) - self.contentView.backgroundColor = .systemBackground - - self.slugLabel.font = UIFont.beeminder.defaultFontHeavy - self.slugLabel.textColor = .label - self.slugLabel.snp.makeConstraints { (make) -> Void in - make.left.equalTo(self.margin) - make.top.equalTo(10) - make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.35) + setUpView() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setUpView() + } + + private func setUpView() { + self.contentView.backgroundColor = .clear + self.contentView.addSubview(self.cardView) + + self.cardView.snp.makeConstraints { make in make.edges.equalToSuperview().inset(6) } + + [self.thumbnailImageView, self.titleLabel, self.todaytaLabel, self.slugLabel, self.countdownLabel].forEach { + self.cardView.addSubview($0) } - self.titleLabel.font = UIFont.beeminder.defaultFont - self.titleLabel.textColor = .label - self.titleLabel.textAlignment = .left - self.titleLabel.snp.makeConstraints { (make) -> Void in - make.centerY.equalTo(self.slugLabel) - make.left.equalTo(self.slugLabel.snp.right).offset(10) - make.right.lessThanOrEqualTo(self.todaytaLabel.snp.left).offset(-10) + + self.thumbnailImageView.snp.makeConstraints { make in + make.left.top.bottom.equalToSuperview().inset(CardLookConstants.spacing) + make.width.equalTo(CGFloat(Constants.thumbnailWidth)) + make.height.equalTo(CGFloat(Constants.thumbnailHeight)) } - self.todaytaLabel.font = UIFont.beeminder.defaultFont - self.todaytaLabel.textColor = .label - self.todaytaLabel.textAlignment = .right - self.todaytaLabel.snp.makeConstraints { (make) -> Void in - make.centerY.equalTo(self.slugLabel) - make.right.equalTo(-self.margin) + + self.titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().offset(CardLookConstants.verticalPadding) + make.left.equalTo(self.thumbnailImageView.snp.right).offset(CardLookConstants.spacing) + make.right.equalTo(self.todaytaLabel.snp.left).offset(-8) } - self.todaytaLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - self.thumbnailImageView.snp.makeConstraints { (make) -> Void in - make.left.equalTo(0).offset(self.margin) - make.top.equalTo(self.slugLabel.snp.bottom).offset(5) - make.height.equalTo(Constants.thumbnailHeight) - make.width.equalTo(Constants.thumbnailWidth) + self.todaytaLabel.snp.makeConstraints { make in + make.centerY.equalTo(self.titleLabel) + make.right.equalToSuperview().offset(-CardLookConstants.horizontalPadding) } - self.safesumLabel.textAlignment = NSTextAlignment.center - self.safesumLabel.font = UIFont.beeminder.defaultBoldFont.withSize(13) - self.safesumLabel.numberOfLines = 0 - self.safesumLabel.snp.makeConstraints { (make) -> Void in - make.left.equalTo(self.thumbnailImageView.snp.right).offset(5) - make.centerY.equalTo(self.thumbnailImageView.snp.centerY) - make.right.equalTo(-self.margin) + self.slugLabel.snp.makeConstraints { make in + make.top.equalTo(self.titleLabel.snp.bottom).offset(2) + make.left.right.equalTo(self.titleLabel) + } + + self.countdownLabel.snp.makeConstraints { make in + make.top.equalTo(self.slugLabel.snp.bottom).offset(4) + make.left.right.equalTo(self.titleLabel) + make.bottom.lessThanOrEqualToSuperview().offset(-CardLookConstants.verticalPadding) } } - required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } + override func prepareForReuse() { super.prepareForReuse() - configure(with: nil) + self.configure(with: nil) } + func configure(with goal: Goal?) { self.thumbnailImageView.goal = goal self.titleLabel.text = goal?.title self.slugLabel.text = goal?.slug self.titleLabel.isHidden = goal?.title == goal?.slug self.todaytaLabel.text = goal?.todayta == true ? "✓" : "" - self.safesumLabel.text = goal?.capitalSafesum() - self.safesumLabel.textColor = goal?.countdownColor ?? UIColor.Beeminder.gray + self.countdownLabel.text = goal?.capitalSafesum() + self.countdownLabel.textColor = goal?.countdownColor ?? UIColor.Beeminder.SafetyBuffer.gray } } diff --git a/BeeSwift/Components/GoalImageView.swift b/BeeSwift/Components/GoalImageView.swift index 30106b4a5..85b6bcf8d 100644 --- a/BeeSwift/Components/GoalImageView.swift +++ b/BeeSwift/Components/GoalImageView.swift @@ -41,11 +41,16 @@ class GoalImageView: UIView { private func setupView() { self.addSubview(imageView) - imageView.snp.makeConstraints { (make) in make.edges.equalToSuperview() } + + self.layer.cornerRadius = CardLookConstants.cornerRadius + self.layer.borderWidth = 0 + self.clipsToBounds = true + imageView.snp.makeConstraints { $0.edges.equalToSuperview() } + self.imageView.image = UIImage(named: "GraphPlaceholder") self.addSubview(beeLemniscateView) - beeLemniscateView.snp.makeConstraints { (make) in make.edges.equalToSuperview() } + beeLemniscateView.snp.makeConstraints { $0.edges.equalToSuperview() } beeLemniscateView.isHidden = true NotificationCenter.default.addObserver( @@ -60,6 +65,7 @@ class GoalImageView: UIView { imageView.image = UIImage(named: "GraphPlaceholder") currentlyShowingGraph = false beeLemniscateView.isHidden = true + layer.borderWidth = 0 } @MainActor private func showGraphImage(image: UIImage) { @@ -73,18 +79,20 @@ class GoalImageView: UIView { duration: duration, options: .transitionCrossDissolve, animations: { [weak self] in - self?.imageView.image = image - self?.beeLemniscateView.isHidden = self?.goal == nil || self?.goal?.queued == false - - if self?.isThumbnail == true { - self?.imageView.layer.borderColor = self?.goal?.countdownColor.cgColor - self?.imageView.layer.borderWidth = self?.goal == nil ? 0 : 1 - } else { - self?.imageView.layer.borderColor = nil - self?.imageView.layer.borderWidth = 0 + guard let self else { return } + self.imageView.image = image + self.beeLemniscateView.isHidden = self.goal == nil || self.goal?.queued == false + self.imageView.contentMode = self.isThumbnail == true ? .scaleAspectFill : .scaleAspectFit + }, + completion: { [weak self] _ in + guard let self else { return } + if self.isThumbnail { + self.layer.borderColor = self.goal?.countdownColor.cgColor + self.layer.borderWidth = self.goal == nil ? 0 : 2 } + self.currentlyShowingGraph = true } - ) { [weak self] _ in self?.currentlyShowingGraph = true } + ) } @MainActor private func refresh() { @@ -104,8 +112,8 @@ class GoalImageView: UIView { return } - // - Deadbeat: Placeholder, no animation - if goal.owner.deadbeat { + // Deadbeat: Placeholder, no animation + guard !goal.owner.deadbeat else { clearGoalGraph() return } diff --git a/BeeSwift/Components/UI/CardLookConstants.swift b/BeeSwift/Components/UI/CardLookConstants.swift new file mode 100644 index 000000000..0dc62b809 --- /dev/null +++ b/BeeSwift/Components/UI/CardLookConstants.swift @@ -0,0 +1,15 @@ +// Part of BeeSwift. Copyright Beeminder + +struct CardLookConstants { + static let cornerRadius: CGFloat = 12 + static let shadowOpacity: Float = 0.08 + static let shadowRadius: CGFloat = 6 + static let shadowOffset = CGSize(width: 0, height: 2) + static let horizontalPadding: CGFloat = 16 + static let verticalPadding: CGFloat = 14 + static let spacing: CGFloat = 12 + static let primaryBackground = UIColor.secondarySystemBackground + static let secondaryBackground = UIColor.tertiarySystemBackground + static let borderColor = UIColor.separator + static let borderWidth: CGFloat = 0.5 +} diff --git a/BeeSwift/Components/UI/CardView.swift b/BeeSwift/Components/UI/CardView.swift new file mode 100644 index 000000000..cb2e50017 --- /dev/null +++ b/BeeSwift/Components/UI/CardView.swift @@ -0,0 +1,40 @@ +// Part of BeeSwift. Copyright Beeminder + +import UIKit + +class CardView: UIView { + enum Style { + case primary + case secondary + case tertiary + } + var style: Style = .primary { didSet { updateStyle() } } + var cornerRadius: CGFloat = CardLookConstants.cornerRadius { didSet { layer.cornerRadius = cornerRadius } } + var shadowOpacity: Float = CardLookConstants.shadowOpacity { didSet { layer.shadowOpacity = shadowOpacity } } + var shadowRadius: CGFloat = CardLookConstants.shadowRadius { didSet { layer.shadowRadius = shadowRadius } } + var shadowOffset: CGSize = CardLookConstants.shadowOffset { didSet { layer.shadowOffset = shadowOffset } } + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + private func setupView() { + backgroundColor = CardLookConstants.primaryBackground + layer.cornerRadius = cornerRadius + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = shadowOpacity + layer.shadowRadius = shadowRadius + layer.shadowOffset = shadowOffset + layer.masksToBounds = false + } + private func updateStyle() { + switch style { + case .primary: backgroundColor = CardLookConstants.primaryBackground + case .secondary: backgroundColor = CardLookConstants.secondaryBackground + case .tertiary: backgroundColor = .clear + } + } +} diff --git a/BeeSwift/Gallery/GalleryViewController.swift b/BeeSwift/Gallery/GalleryViewController.swift index 030a318b9..e600bfb9f 100644 --- a/BeeSwift/Gallery/GalleryViewController.swift +++ b/BeeSwift/Gallery/GalleryViewController.swift @@ -41,8 +41,8 @@ class GalleryViewController: UIViewController { }() private lazy var collectionContainer = UIView() private lazy var collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: stackView.frame, collectionViewLayout: self.collectionViewLayout) - collectionView.backgroundColor = .systemBackground + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: self.collectionViewLayout) + collectionView.backgroundColor = .systemGray6 collectionView.alwaysBounceVertical = true collectionView.register( UICollectionReusableView.self, @@ -144,7 +144,7 @@ class GalleryViewController: UIViewController { object: nil ) self.view.addSubview(self.stackView) - stackView.snp.makeConstraints { (make) -> Void in make.edges.equalToSuperview() } + self.stackView.snp.makeConstraints { (make) -> Void in make.edges.equalToSuperview() } NotificationCenter.default.addObserver( self, @@ -473,7 +473,7 @@ extension GalleryViewController: UICollectionViewDelegateFlowLayout { let minimumWidth: CGFloat = 320 let itemSpacing = self.collectionViewLayout.minimumInteritemSpacing let availableWidth = - collectionView.frame.width - collectionView.contentInset.left - collectionView.contentInset.right + collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right // Calculate how many cells could fit at the minimum width, rounding down (as we can't show a fractional cell) // We need to account for there being margin between cells, so there is 1 fewer margin than cell. We do this by // imagining there is some non-showed spacing after the final cell. For example with wo cells: diff --git a/BeeSwiftUITests/BeeSwiftUITests.swift b/BeeSwiftUITests/BeeSwiftUITests.swift deleted file mode 100644 index adb2d8031..000000000 --- a/BeeSwiftUITests/BeeSwiftUITests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// BeeSwiftUITests.swift -// BeeSwiftUITests -// -// Created by Andrew Brett on 8/14/20. -// Copyright 2020 APB. All rights reserved. -// - -import XCTest - -class BeeSwiftUITests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { XCUIApplication().launch() } - } -}