Preparing the summary aggregation
This commit is contained in:
parent
e109989be3
commit
46af25c7ed
|
@ -28,6 +28,9 @@
|
|||
CE0386081DAA8C7B00D7F482 /* ReactionFeedbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0386071DAA8C7B00D7F482 /* ReactionFeedbackTests.swift */; };
|
||||
CE03860A1DAAA58D00D7F482 /* ReactionSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0386091DAAA58D00D7F482 /* ReactionSelectorTests.swift */; };
|
||||
CE03860C1DAAA63F00D7F482 /* ReactionSelectorConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE03860B1DAAA63F00D7F482 /* ReactionSelectorConfigTests.swift */; };
|
||||
CE225E511DB1611700C21D8D /* CAReactionSummaryLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE225E501DB1611700C21D8D /* CAReactionSummaryLayer.swift */; };
|
||||
CE225E521DB1611700C21D8D /* CAReactionSummaryLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE225E501DB1611700C21D8D /* CAReactionSummaryLayer.swift */; };
|
||||
CE225E531DB1611700C21D8D /* CAReactionSummaryLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE225E501DB1611700C21D8D /* CAReactionSummaryLayer.swift */; };
|
||||
CE83F88E1DA8F033000943AB /* Reactions.h in Headers */ = {isa = PBXBuildFile; fileRef = CE83F88C1DA8F033000943AB /* Reactions.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
CE83F8911DA8F033000943AB /* Reactions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE83F88A1DA8F033000943AB /* Reactions.framework */; };
|
||||
CE83F8921DA8F033000943AB /* Reactions.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE83F88A1DA8F033000943AB /* Reactions.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -119,6 +122,7 @@
|
|||
CE0386071DAA8C7B00D7F482 /* ReactionFeedbackTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionFeedbackTests.swift; sourceTree = "<group>"; };
|
||||
CE0386091DAAA58D00D7F482 /* ReactionSelectorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSelectorTests.swift; sourceTree = "<group>"; };
|
||||
CE03860B1DAAA63F00D7F482 /* ReactionSelectorConfigTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSelectorConfigTests.swift; sourceTree = "<group>"; };
|
||||
CE225E501DB1611700C21D8D /* CAReactionSummaryLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAReactionSummaryLayer.swift; sourceTree = "<group>"; };
|
||||
CE83F88A1DA8F033000943AB /* Reactions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Reactions.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE83F88C1DA8F033000943AB /* Reactions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Reactions.h; sourceTree = "<group>"; };
|
||||
CE83F88D1DA8F033000943AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -263,6 +267,7 @@
|
|||
CED4FCF51DA014B100F54838 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE225E501DB1611700C21D8D /* CAReactionSummaryLayer.swift */,
|
||||
CE874F021DA2DA64000D309E /* ComponentBuilder.swift */,
|
||||
CED9D93B1DA64FCD00A70C2D /* Components.swift */,
|
||||
CED9D94D1DA7E49500A70C2D /* Configurable.swift */,
|
||||
|
@ -459,6 +464,7 @@
|
|||
CE83F8A41DA8F05A000943AB /* ReactionFeedbackDelegate.swift in Sources */,
|
||||
CE83F8A21DA8F05A000943AB /* ReactionButtonConfig.swift in Sources */,
|
||||
CE83F8A81DA8F05A000943AB /* ReactionSummaryConfig.swift in Sources */,
|
||||
CE225E521DB1611700C21D8D /* CAReactionSummaryLayer.swift in Sources */,
|
||||
CE83F8A61DA8F05A000943AB /* ReactionSelectorConfig.swift in Sources */,
|
||||
CE83F8A91DA8F05A000943AB /* UIReactionControl.swift in Sources */,
|
||||
CE83F89C1DA8F05A000943AB /* Configurable.swift in Sources */,
|
||||
|
@ -486,6 +492,7 @@
|
|||
CE0385EA1DAA51FC00D7F482 /* UIReactionControlTests.swift in Sources */,
|
||||
CE0386001DAA87C100D7F482 /* ReactionSelector.swift in Sources */,
|
||||
CE0386031DAA87C100D7F482 /* ReactionSummaryConfig.swift in Sources */,
|
||||
CE225E531DB1611700C21D8D /* CAReactionSummaryLayer.swift in Sources */,
|
||||
CEDBA1301DACEC0D0031AB42 /* ReactionSummaryTests.swift in Sources */,
|
||||
CE0385FD1DAA87C100D7F482 /* ReactionButtonConfig.swift in Sources */,
|
||||
CE83F8BA1DA95D41000943AB /* ReactionTests.swift in Sources */,
|
||||
|
@ -521,6 +528,7 @@
|
|||
CED4FCFD1DA1100900F54838 /* ReactionFeedback.swift in Sources */,
|
||||
CED9D9401DA6869500A70C2D /* ReactionSelectorConfig.swift in Sources */,
|
||||
CED9D94A1DA78F9400A70C2D /* ReactionButtonConfig.swift in Sources */,
|
||||
CE225E511DB1611700C21D8D /* CAReactionSummaryLayer.swift in Sources */,
|
||||
CED9D93C1DA64FCD00A70C2D /* Components.swift in Sources */,
|
||||
CED4FCF71DA014B100F54838 /* Reaction.swift in Sources */,
|
||||
CED9D94C1DA7D36700A70C2D /* ReactionSummaryConfig.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Reactions
|
||||
*
|
||||
* Copyright 2016-present Yannick Loriot.
|
||||
* http://yannickloriot.com
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
import CoreText
|
||||
import UIKit
|
||||
|
||||
final class CAReactionSummaryLayer: CALayer {
|
||||
var indicatorIcons: [CGImage] = [] {
|
||||
didSet {
|
||||
indicatorLayers = indicatorIcons.map({
|
||||
let l = CALayer()
|
||||
l.contents = $0
|
||||
l.masksToBounds = true
|
||||
l.borderColor = UIColor.white.cgColor
|
||||
l.borderWidth = 2
|
||||
|
||||
return l
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private var indicatorLayers: [CALayer] = [] {
|
||||
didSet {
|
||||
for l in oldValue {
|
||||
l.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
for index in 0 ..< indicatorIcons.count {
|
||||
let l = indicatorLayers[indicatorLayers.count - 1 - index]
|
||||
|
||||
addSublayer(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var config: ReactionSummaryConfig = ReactionSummaryConfig()
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
super.draw(in: ctx)
|
||||
|
||||
/*var b = bounds
|
||||
|
||||
for i in 0 ..< indicatorIcons.count {
|
||||
b.origin.x = b.height + 50 * CGFloat(i)
|
||||
|
||||
let path = CGPath(rect: b, transform: nil)
|
||||
let str = NSMutableAttributedString(string: "0")
|
||||
|
||||
str.addAttribute(kCTForegroundColorAttributeName as String, value: UIColor.black, range: NSMakeRange(0,str.length))
|
||||
|
||||
let fontRef = UIFont.systemFont(ofSize: 20)
|
||||
str.addAttribute(kCTFontAttributeName as String, value: fontRef, range:NSMakeRange(0, str.length))
|
||||
|
||||
let frameSetter = CTFramesetterCreateWithAttributedString(str)
|
||||
let ctFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,str.length), path, nil)
|
||||
|
||||
CTFrameDraw(ctFrame, ctx)
|
||||
}*/
|
||||
|
||||
ctx.translateBy(x: 0, y: bounds.height)
|
||||
ctx.scaleBy(x: 1, y: -1)
|
||||
|
||||
for index in 0 ..< indicatorIcons.count {
|
||||
updateIconAtIndex(index, with: bounds.height - config.iconMarging * 2, margin: 8, in: ctx)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIconAtIndex(_ index: Int, with size: CGFloat, margin: CGFloat, in ctx: CGContext) {
|
||||
let x: CGFloat
|
||||
let layer = indicatorLayers[index]
|
||||
|
||||
switch config.alignment {
|
||||
case .left: x = (size - 3) * CGFloat(index)
|
||||
case .right: x = bounds.width - size - (size - 3) * CGFloat(index)
|
||||
case .centerLeft: x = margin + (size - 3) * CGFloat(index)
|
||||
case .centerRight: x = bounds.width - size - (size - 3) * CGFloat(index) - margin
|
||||
}
|
||||
|
||||
let iconFrame = CGRect(x: x, y: (bounds.height - size) / 2, width: size, height: size)
|
||||
|
||||
layer.frame = iconFrame
|
||||
layer.cornerRadius = iconFrame.height / 2
|
||||
layer.draw(in: ctx)
|
||||
//ctx.interpolationQuality = .high
|
||||
|
||||
//ctx.draw(icon, in: iconFrame)
|
||||
}
|
||||
}
|
|
@ -32,8 +32,8 @@ import UIKit
|
|||
You can configure/skin the summary using a `ReactionSummaryConfig`.
|
||||
*/
|
||||
public final class ReactionSummary: UIReactionControl {
|
||||
private let textLabel: UILabel = UILabel()
|
||||
private var reactionIconLayers: [CALayer] = []
|
||||
private let textLabel = UILabel()
|
||||
private var summaryLayer = CAReactionSummaryLayer()
|
||||
|
||||
/**
|
||||
The reaction summary configuration.
|
||||
|
@ -73,25 +73,20 @@ public final class ReactionSummary: UIReactionControl {
|
|||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ReactionSummary.tapAction)))
|
||||
|
||||
textLabel.removeFromSuperview()
|
||||
reactionIconLayers.forEach { $0.removeFromSuperlayer() }
|
||||
summaryLayer.removeFromSuperlayer()
|
||||
|
||||
reactionIconLayers = reactions.uniq().map { Components.reactionSummary.reactionIcon(option: $0) }
|
||||
|
||||
for index in 0 ..< reactionIconLayers.count {
|
||||
let iconLayer = reactionIconLayers[reactionIconLayers.count - 1 - index]
|
||||
iconLayer.masksToBounds = true
|
||||
iconLayer.borderWidth = 1
|
||||
iconLayer.borderColor = UIColor.white.cgColor
|
||||
|
||||
layer.addSublayer(iconLayer)
|
||||
}
|
||||
summaryLayer.indicatorIcons = reactions.uniq().flatMap { $0.icon.cgImage }
|
||||
|
||||
layer.addSublayer(summaryLayer)
|
||||
addSubview(textLabel)
|
||||
}
|
||||
|
||||
// MARK: - Updating Object State
|
||||
|
||||
override func update() {
|
||||
summaryLayer.frame = bounds
|
||||
summaryLayer.setNeedsDisplay()
|
||||
|
||||
textLabel.font = config.font
|
||||
textLabel.textColor = config.textColor
|
||||
|
||||
|
@ -101,14 +96,10 @@ public final class ReactionSummary: UIReactionControl {
|
|||
textSize.height = bounds.height
|
||||
}
|
||||
|
||||
let iconSize = min(bounds.height, textSize.height + 4)
|
||||
let iconWidth = (iconSize - 3) * CGFloat(reactionIconLayers.count) + config.spacing
|
||||
let iconSize = bounds.height - config.iconMarging * 2
|
||||
let iconWidth = (iconSize - 3) * CGFloat(summaryLayer.indicatorIcons.count) + config.spacing
|
||||
let margin = (bounds.width - iconWidth - textSize.width) / 2
|
||||
|
||||
for index in 0 ..< reactionIconLayers.count {
|
||||
updateIconAtIndex(index, with: iconSize, margin: margin)
|
||||
}
|
||||
|
||||
let textX: CGFloat
|
||||
|
||||
switch config.alignment {
|
||||
|
@ -121,21 +112,6 @@ public final class ReactionSummary: UIReactionControl {
|
|||
textLabel.frame = CGRect(x: textX, y: 0, width: textSize.width, height: bounds.height)
|
||||
}
|
||||
|
||||
private func updateIconAtIndex(_ index: Int, with size: CGFloat, margin: CGFloat) {
|
||||
let x: CGFloat
|
||||
let layer = reactionIconLayers[index]
|
||||
|
||||
switch config.alignment {
|
||||
case .left: x = (size - 3) * CGFloat(index)
|
||||
case .right: x = bounds.width - size - (size - 3) * CGFloat(index)
|
||||
case .centerLeft: x = margin + (size - 3) * CGFloat(index)
|
||||
case .centerRight: x = bounds.width - size - (size - 3) * CGFloat(index) - margin
|
||||
}
|
||||
|
||||
layer.frame = CGRect(x: x, y: (bounds.height - size) / 2, width: size, height: size)
|
||||
layer.cornerRadius = size / 2
|
||||
}
|
||||
|
||||
// MARK: - Responding to Gesture Events
|
||||
|
||||
func tapAction(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
|
|
|
@ -39,6 +39,9 @@ public final class ReactionSummaryConfig: Configurable {
|
|||
/// The spacing between the icons and the text.
|
||||
public var spacing: CGFloat = 8
|
||||
|
||||
/// The marging between the icon and border.
|
||||
public var iconMarging: CGFloat = 2
|
||||
|
||||
/// The font of the text.
|
||||
public var font: UIFont! = UIFont(name: "HelveticaNeue", size: 12)
|
||||
|
||||
|
@ -52,6 +55,13 @@ public final class ReactionSummaryConfig: Configurable {
|
|||
*/
|
||||
public var alignment: ReactionAlignment = .left
|
||||
|
||||
/**
|
||||
A Boolean value that indicates whether the summary should aggregate the reactions into one total indicator.
|
||||
|
||||
The default value is true.
|
||||
*/
|
||||
public var isAggregated: Bool = true
|
||||
|
||||
// MARK: - Initializing a Reaction Summary
|
||||
|
||||
// Initialize a configurable with default values.
|
||||
|
|
Loading…
Reference in New Issue