Use ListItemSelectionIndicatorView in All Media view.

Added functionality to show selection indicator with a white outline - to be displayed on top of media.

Rename ListItemSelectionIndicatorView to SelectionIndicatorView.
This commit is contained in:
Igor Solomennikov 2026-05-15 14:34:18 -07:00 committed by GitHub
parent 868ed6bb4b
commit b5838a1afc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 203 additions and 234 deletions

View File

@ -504,7 +504,6 @@
45069FC629D3A7C800D0DD14 /* WideMediaTileViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FC529D3A7C800D0DD14 /* WideMediaTileViewLayout.swift */; };
45069FC829D3A7E700D0DD14 /* SquareMediaTileViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FC729D3A7E700D0DD14 /* SquareMediaTileViewLayout.swift */; };
45069FCA29D4FFBB00D0DD14 /* MediaTileDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FC929D4FFBB00D0DD14 /* MediaTileDateFormatter.swift */; };
45069FCE29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45069FCD29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift */; };
450B0FC929FB301700B9A458 /* AudioMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450B0FC829FB301700B9A458 /* AudioMessageView.swift */; };
45161BA928A2E54B0055AB45 /* ThreadReplyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45161BA828A2E54B0055AB45 /* ThreadReplyInfo.swift */; };
4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; };
@ -1518,7 +1517,7 @@
768FC7B42F316CF800707F72 /* CVDimmableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768FC7B32F316CEF00707F72 /* CVDimmableView.swift */; };
768FC7B62F32A0B000707F72 /* UIImage+Blur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768FC7B52F32A0AB00707F72 /* UIImage+Blur.swift */; };
7694C9172FB51AA4006E727F /* DisappearingMessagesChatIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7694C9162FB51AA4006E727F /* DisappearingMessagesChatIndicatorView.swift */; };
76964AFB2FB4035D007130BE /* ListItemSelectionIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76964AFA2FB4034C007130BE /* ListItemSelectionIndicatorView.swift */; };
76964AFB2FB4035D007130BE /* SelectionIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76964AFA2FB4034C007130BE /* SelectionIndicatorView.swift */; };
76995F15283868BD009DD4F4 /* ImageEditorViewController+StrokeWidthSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76995F14283868BD009DD4F4 /* ImageEditorViewController+StrokeWidthSlider.swift */; };
76A066EA2EA99D1F009B3ED5 /* ConversationInputToolbar+VoiceMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A066E92EA99D0E009B3ED5 /* ConversationInputToolbar+VoiceMemo.swift */; };
76A2EB1028B578B800A29C24 /* MediaTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A2EB0F28B578B800A29C24 /* MediaTextView.swift */; };
@ -4717,7 +4716,6 @@
45069FC529D3A7C800D0DD14 /* WideMediaTileViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideMediaTileViewLayout.swift; sourceTree = "<group>"; };
45069FC729D3A7E700D0DD14 /* SquareMediaTileViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareMediaTileViewLayout.swift; sourceTree = "<group>"; };
45069FC929D4FFBB00D0DD14 /* MediaTileDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTileDateFormatter.swift; sourceTree = "<group>"; };
45069FCD29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSelectionIndicatorView.swift; sourceTree = "<group>"; };
450B0FC829FB301700B9A458 /* AudioMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageView.swift; sourceTree = "<group>"; };
45161BA828A2E54B0055AB45 /* ThreadReplyInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadReplyInfo.swift; sourceTree = "<group>"; };
451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = "<group>"; };
@ -5720,7 +5718,7 @@
768FC7B32F316CEF00707F72 /* CVDimmableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVDimmableView.swift; sourceTree = "<group>"; };
768FC7B52F32A0AB00707F72 /* UIImage+Blur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Blur.swift"; sourceTree = "<group>"; };
7694C9162FB51AA4006E727F /* DisappearingMessagesChatIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesChatIndicatorView.swift; sourceTree = "<group>"; };
76964AFA2FB4034C007130BE /* ListItemSelectionIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemSelectionIndicatorView.swift; sourceTree = "<group>"; };
76964AFA2FB4034C007130BE /* SelectionIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionIndicatorView.swift; sourceTree = "<group>"; };
76995F14283868BD009DD4F4 /* ImageEditorViewController+StrokeWidthSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageEditorViewController+StrokeWidthSlider.swift"; sourceTree = "<group>"; };
76A066E92EA99D0E009B3ED5 /* ConversationInputToolbar+VoiceMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationInputToolbar+VoiceMemo.swift"; sourceTree = "<group>"; };
76A2EB0F28B578B800A29C24 /* MediaTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTextView.swift; sourceTree = "<group>"; };
@ -9139,7 +9137,6 @@
34A95546271B510400B05242 /* GradientView.swift */,
66F98DE52DBBED68009F1A86 /* LineWrappingStackView.swift */,
34A95529271B510400B05242 /* LinkingTextView.swift */,
76964AFA2FB4034C007130BE /* ListItemSelectionIndicatorView.swift */,
34A9552F271B510400B05242 /* LoopingVideoView.swift */,
34A9551D271B510400B05242 /* ManualLayoutView.swift */,
34A9550C271B510400B05242 /* ManualStackView.swift */,
@ -9160,6 +9157,7 @@
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
763D7DDA27E155ED002EA7E6 /* RoundMediaButton.swift */,
766BCA7C29FB049400046016 /* RTLEnabledCollectionViewFlowLayout.swift */,
76964AFA2FB4034C007130BE /* SelectionIndicatorView.swift */,
885275BF27E26775003F2F9B /* TextAttachmentView.swift */,
764FE0402A2EF3A7004D2804 /* TextFieldFormatting.swift */,
34A9553E271B510400B05242 /* TextViewWithPlaceholder.swift */,
@ -11169,7 +11167,6 @@
32AC5CE6255B51E900829BD8 /* JoinGroupCallPill.swift */,
88A941982409A391000E9700 /* LottieToggleButton.swift */,
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
45069FCD29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift */,
45069FC929D4FFBB00D0DD14 /* MediaTileDateFormatter.swift */,
8829883A25B9FD6700DEE1E3 /* MockConversationView.swift */,
3236FCC32592B67B006D33B9 /* NameCollisionReviewCell.swift */,
@ -17857,7 +17854,6 @@
5003BB3F299DA0F10037159B /* LinkPreviewFetchState.swift in Sources */,
3402AA77271D9E180084CBAE /* LinkPreviewState.swift in Sources */,
3402AA6D271D9E180084CBAE /* LinkPreviewView.swift in Sources */,
76964AFB2FB4035D007130BE /* ListItemSelectionIndicatorView.swift in Sources */,
3402AA8C271D9E180084CBAE /* LoopingVideoView.swift in Sources */,
3402AA9C271D9E180084CBAE /* ManualLayoutView.swift in Sources */,
3402AA8D271D9E180084CBAE /* ManualStackView.swift in Sources */,
@ -17932,6 +17928,7 @@
B9488E752CDED27200C1294B /* ScrollOffset.swift in Sources */,
50597BBF2B97D629004681E1 /* SearchableNameFinder.swift in Sources */,
66FC638E29EDABAC00F00DAC /* SearchDisplayConfigurations.swift in Sources */,
76964AFB2FB4035D007130BE /* SelectionIndicatorView.swift in Sources */,
66FBC4E328DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift in Sources */,
504861AA2EEB4D0D00B13C49 /* SendableAttachment.swift in Sources */,
D9791BB92EAA84830016AA5A /* SheetDisplayableError.swift in Sources */,
@ -18537,7 +18534,6 @@
76BB06FA29AD84DB00978856 /* MediaItemViewController.swift in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
4CD675C722E7D393008010D2 /* MediaPresentationContext.swift in Sources */,
45069FCE29D64CB300D0DD14 /* MediaSelectionIndicatorView.swift in Sources */,
45069FCA29D4FFBB00D0DD14 /* MediaTileDateFormatter.swift in Sources */,
45D9784229F0B50000BBB3C0 /* MediaTileListModeCell.swift in Sources */,
45CADA8B298DD2B4009EBDF5 /* MediaTileScrollFlag.swift in Sources */,

View File

@ -29,7 +29,7 @@ class MediaTileListModeCell: UICollectionViewCell, MediaGalleryCollectionViewCel
return view
}()
let selectionButton = ListItemSelectionIndicatorView()
let selectionButton = SelectionIndicatorView()
private let selectedMaskView = UIView()

View File

@ -1,98 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalUI
import UIKit
/// A checkmark in a circle to indicate an item (typically in a table view or collection view) is
/// selected.
class MediaSelectionIndicatorView: UIView {
private let outlineBadgeView: UIView = {
let imageView = UIImageView(image: UIImage(imageLiteralResourceName: "circle"))
imageView.contentMode = .center
imageView.tintColor = .white
imageView.isHidden = true
return imageView
}()
private let selectedBadgeView: UIView = {
let imageView = UIImageView(image: Theme.iconImage(.checkCircleFill))
imageView.contentMode = .center
imageView.tintColor = .ows_accentBlue
// This will give checkmark it's color.
let backgroundView = CircleView(diameter: 18)
backgroundView.backgroundColor = .white
let containerView = UIView(frame: imageView.bounds)
containerView.isHidden = true
containerView.addSubview(backgroundView)
backgroundView.autoCenterInSuperview()
containerView.addSubview(imageView)
imageView.autoPinEdgesToSuperviewEdges()
return containerView
}()
var isSelected: Bool = false {
didSet {
updateAppearance()
}
}
var allowsMultipleSelection: Bool = false {
didSet {
updateAppearance()
}
}
var outlineColor: UIColor = .white {
didSet {
outlineBadgeView.tintColor = outlineColor
}
}
var hidesOutlineWhenSelected: Bool = false {
didSet {
updateAppearance()
}
}
init() {
super.init(frame: .zero)
addSubview(selectedBadgeView)
selectedBadgeView.autoCenterInSuperview()
addSubview(outlineBadgeView)
outlineBadgeView.autoCenterInSuperview()
autoSetDimensions(to: .square(24))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateAppearance() {
if isSelected {
outlineBadgeView.isHidden = hidesOutlineWhenSelected
selectedBadgeView.isHidden = false
} else if allowsMultipleSelection {
outlineBadgeView.isHidden = false
selectedBadgeView.isHidden = true
} else {
outlineBadgeView.isHidden = true
selectedBadgeView.isHidden = true
}
}
func reset() {
selectedBadgeView.isHidden = true
outlineBadgeView.isHidden = true
}
}

View File

@ -69,7 +69,7 @@ class PhotoGridViewCell: UICollectionViewCell {
private var durationLabel: UILabel?
private var durationLabelBackground: UIView?
private let selectionButton = MediaSelectionIndicatorView()
private let selectionIndicator = SelectionIndicatorView(style: .media)
private let highlightedMaskView: UIView
private let selectedMaskView: UIView
@ -117,14 +117,14 @@ class PhotoGridViewCell: UICollectionViewCell {
contentView.addSubview(imageView)
contentView.addSubview(highlightedMaskView)
contentView.addSubview(selectedMaskView)
contentView.addSubview(selectionButton)
contentView.addSubview(selectionIndicator)
imageView.autoPinEdgesToSuperviewEdges()
highlightedMaskView.autoPinEdgesToSuperviewEdges()
selectedMaskView.autoPinEdgesToSuperviewEdges()
selectionButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 5)
selectionButton.autoPinEdge(toSuperviewEdge: .top, withInset: 5)
selectionIndicator.autoPinEdge(toSuperviewEdge: .trailing, withInset: 5)
selectionIndicator.autoPinEdge(toSuperviewEdge: .top, withInset: 5)
}
@available(*, unavailable, message: "Unimplemented")
@ -134,8 +134,8 @@ class PhotoGridViewCell: UICollectionViewCell {
private func updateSelectionState() {
selectedMaskView.isHidden = !isSelected
selectionButton.isSelected = isSelected
selectionButton.allowsMultipleSelection = allowsMultipleSelection
selectionIndicator.isSelected = isSelected
selectionIndicator.isHidden = allowsMultipleSelection == false
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@ -279,7 +279,6 @@ class PhotoGridViewCell: UICollectionViewCell {
durationLabelBackground?.isHidden = true
highlightedMaskView.isHidden = true
selectedMaskView.isHidden = true
selectionButton.reset()
}
func mediaPresentationContext(collectionView: UICollectionView, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {

View File

@ -231,7 +231,7 @@ public class ContactShareViewController: OWSTableViewController2 {
let field: ContactShareField
private lazy var checkmark = ListItemSelectionIndicatorView()
private lazy var checkmark = SelectionIndicatorView()
init(field: ContactShareField) {
self.field = field
@ -275,7 +275,7 @@ public class ContactShareViewController: OWSTableViewController2 {
}
class func contactNameCell(for contactName: String) -> UITableViewCell {
let checkmark = ListItemSelectionIndicatorView()
let checkmark = SelectionIndicatorView()
checkmark.isSelected = true
checkmark.isEnabled = false

View File

@ -423,7 +423,7 @@ extension BaseMemberViewController: RecipientPickerDelegate {
let accessoryView: UIView
if isPreExistingMember {
let indicatorView = ListItemSelectionIndicatorView()
let indicatorView = SelectionIndicatorView()
indicatorView.isSelected = true
indicatorView.isEnabled = false
@ -432,13 +432,13 @@ extension BaseMemberViewController: RecipientPickerDelegate {
if let customIndicatorView = memberViewDelegate.memberViewCustomIndicatorForPickedMember(recipient) {
accessoryView = customIndicatorView
} else {
let indicatorView = ListItemSelectionIndicatorView()
let indicatorView = SelectionIndicatorView()
indicatorView.isSelected = true
accessoryView = indicatorView
}
} else {
accessoryView = ListItemSelectionIndicatorView()
accessoryView = SelectionIndicatorView()
}
let accessoryViewWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(accessoryView)
return ContactCellAccessoryView(accessoryView: accessoryViewWrapper, size: .square(24))

View File

@ -1353,7 +1353,7 @@ class ConversationPickerCell: ContactTableViewCell {
// MARK: - Subviews
private lazy var selectionView = ListItemSelectionIndicatorView()
private lazy var selectionView = SelectionIndicatorView()
func buildAccessoryView(disappearingMessagesConfig: DisappearingMessagesConfigurationRecord?) -> ContactCellAccessoryView {

View File

@ -280,7 +280,7 @@ private class MyStorySettingsDataSource: NSObject {
hStack.autoSetDimension(.height, toSize: 35, relation: .greaterThanOrEqual)
hStack.autoPinHeightToSuperview(withMargin: 6)
let selectionIndicator = ListItemSelectionIndicatorView()
let selectionIndicator = SelectionIndicatorView()
selectionIndicator.isSelected = isSelected
hStack.addArrangedSubview(selectionIndicator)

View File

@ -150,6 +150,14 @@ public class RingView: UIView {
}
}
override public var bounds: CGRect {
didSet {
if bounds.size != oldValue.size {
updatePath()
}
}
}
override public var tintColor: UIColor! {
didSet {
updateColor()

View File

@ -1,113 +0,0 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import UIKit
public class ListItemSelectionIndicatorView: UIView {
// MARK: UIView
override init(frame: CGRect = .init(origin: .zero, size: .square(ListItemSelectionIndicatorView.preferredSize))) {
super.init(frame: frame)
directionalLayoutMargins = .init(margin: 1)
// Because it is often paired with UILabels, we want to make
// this view as compact and as compression resistant as possible.
setContentHuggingPriority(.defaultHigh, for: .horizontal)
setContentHuggingPriority(.defaultHigh, for: .vertical)
setContentCompressionResistancePriority(.required - 10, for: .horizontal)
setContentCompressionResistancePriority(.required - 10, for: .vertical)
sizeToFit()
addSubview(unselectedView)
addSubview(selectedView)
updateAppearance(animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Layout
private static let preferredSize: CGFloat = 22
override public var intrinsicContentSize: CGSize {
.square(Self.preferredSize) + directionalLayoutMargins.asSize
}
override public func layoutSubviews() {
super.layoutSubviews()
let circleRadius = Self.preferredSize / 2
let origin = CGPoint(x: bounds.center.x - circleRadius, y: bounds.center.y - circleRadius)
let size = CGSize.square(Self.preferredSize)
let frame = CGRect(origin: origin, size: size)
unselectedView.frame = frame
selectedView.frame = frame
checkmarkIcon.center = selectedView.bounds.center
}
// MARK: State
private var _isSelected: Bool = false
public var isSelected: Bool {
get { _isSelected }
set { setIsSelected(newValue, animated: false) }
}
public func setIsSelected(_ isSelected: Bool, animated: Bool) {
guard isSelected != _isSelected else { return }
_isSelected = isSelected
updateAppearance(animated: animated)
}
private var _isEnabled: Bool = true
public var isEnabled: Bool {
get { _isEnabled }
set { setIsEnabled(newValue, animated: false) }
}
public func setIsEnabled(_ isEnabled: Bool, animated: Bool) {
guard isEnabled != _isEnabled else { return }
_isEnabled = isEnabled
updateAppearance(animated: animated)
}
// MARK: Appearance
private let unselectedView: UIView = {
let ringView = RingView()
ringView.lineWidth = 2
ringView.tintColor = .Signal.tertiaryLabel
return ringView
}()
private lazy var selectedView: UIView = {
let circleView = CircleView()
circleView.backgroundColor = .Signal.accent
circleView.addSubview(checkmarkIcon)
return circleView
}()
private let checkmarkIcon: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "check-compact"))
imageView.contentMode = .scaleAspectFit
imageView.tintColor = .white
return imageView
}()
private func updateAppearance(animated: Bool) {
selectedView.setIsHidden(isSelected == false, animated: animated)
unselectedView.setIsHidden(isSelected, animated: animated)
selectedView.backgroundColor = isEnabled ? .Signal.accent : .Signal.tertiaryLabel
}
}

View File

@ -0,0 +1,177 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import UIKit
public class SelectionIndicatorView: UIView {
public enum Style {
/// Use in lists over plain colored background.
case list
/// Use over media.
case media
}
// MARK: UIView
public init(style: Style = .list) {
self.style = style
super.init(frame: .init(origin: .zero, size: .square(SelectionIndicatorView.preferredSize)))
// Because it is often paired with UILabels, we want to make
// this view as compact and as compression resistant as possible.
setContentHuggingPriority(.defaultHigh, for: .horizontal)
setContentHuggingPriority(.defaultHigh, for: .vertical)
setContentCompressionResistancePriority(.required - 10, for: .horizontal)
setContentCompressionResistancePriority(.required - 10, for: .vertical)
switch style {
case .list:
addSubview(innerRing)
case .media:
addSubview(outerRing)
}
addSubview(selectedView)
updateAppearance(animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Layout
private static let preferredSize: CGFloat = 24
private static let ringStrokeWidth: CGFloat = 2
private static let innerRingInset: CGFloat = 1
override public var intrinsicContentSize: CGSize {
.square(Self.preferredSize)
}
override public func layoutSubviews() {
super.layoutSubviews()
switch style {
case .list:
innerRing.center = bounds.center
// Inner ring is inset by 1dp relative to the view's bounds.
// Filled circle (checkmark's background) has the same diameter as inner ring.
let circleDiameter = Self.preferredSize - 2 * Self.innerRingInset
innerRing.bounds.size = .square(circleDiameter)
selectedView.bounds.size = .square(circleDiameter)
case .media:
outerRing.center = bounds.center
outerRing.bounds.size = .square(Self.preferredSize)
// Filled circle (checkmark's background) fills inside of the outer ring.
selectedView.bounds.size = .square(Self.preferredSize - 2 * Self.ringStrokeWidth)
}
selectedView.center = bounds.center
// Checkmark is self-sized and only needs to be centered properly.
checkmarkIcon.center = selectedView.bounds.center
}
// MARK: State
private var _isSelected: Bool = false
public var isSelected: Bool {
get { _isSelected }
set { setIsSelected(newValue, animated: false) }
}
public func setIsSelected(_ isSelected: Bool, animated: Bool) {
guard isSelected != _isSelected else { return }
_isSelected = isSelected
updateAppearance(animated: animated)
}
private var _isEnabled: Bool = true
public var isEnabled: Bool {
get { _isEnabled }
set { setIsEnabled(newValue, animated: false) }
}
public func setIsEnabled(_ isEnabled: Bool, animated: Bool) {
guard isEnabled != _isEnabled else { return }
_isEnabled = isEnabled
updateAppearance(animated: animated)
}
// Make this a `let` to simplify layout and avoid overhead of creating unused views.
// The assumption is to only reference `innerRing` when style is `list`
// and only reference `outerRing` when style is `media`.
public let style: Style
// MARK: Appearance
/// Color that fills the selection ring and is the background for checkmark image.
public var fillColor: UIColor = .Signal.accent {
didSet {
selectedView.backgroundColor = fillColor
}
}
private var effectiveFillColor: UIColor {
isEnabled ? fillColor : .Signal.tertiaryLabel
}
/// Color for the ckeckmark image and outer ring for media-style indicators.
public var strokeColor: UIColor = .white {
didSet {
checkmarkIcon.tintColor = strokeColor
if case .media = style {
outerRing.tintColor = strokeColor
}
}
}
private lazy var innerRing: UIView = {
owsAssertDebug(style == .list, "Invalid access")
let ringView = RingView()
ringView.lineWidth = SelectionIndicatorView.ringStrokeWidth
ringView.tintColor = .Signal.tertiaryLabel
return ringView
}()
private lazy var outerRing: UIView = {
owsAssertDebug(style == .media, "Invalid access")
let ringView = RingView()
ringView.lineWidth = SelectionIndicatorView.ringStrokeWidth
ringView.tintColor = strokeColor
return ringView
}()
private lazy var selectedView: UIView = {
let circleView = CircleView()
circleView.backgroundColor = effectiveFillColor
circleView.addSubview(checkmarkIcon)
return circleView
}()
private lazy var checkmarkIcon: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "check-compact"))
imageView.contentMode = .scaleAspectFit
imageView.tintColor = strokeColor
return imageView
}()
private func updateAppearance(animated: Bool) {
if case .list = style {
innerRing.setIsHidden(isSelected, animated: animated)
}
// Outer ring is always visible.
selectedView.setIsHidden(isSelected == false, animated: animated)
selectedView.backgroundColor = effectiveFillColor
}
}