Preparing the summary aggregation

This commit is contained in:
Yannick Loriot 2016-10-15 22:17:05 +02:00
parent e109989be3
commit 46af25c7ed
4 changed files with 140 additions and 35 deletions

View File

@ -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 */,

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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.