Updating documentation

This commit is contained in:
Yannick Loriot 2016-10-07 11:45:14 +02:00
parent 379d11e657
commit 659475bc23
19 changed files with 401 additions and 124 deletions

View File

@ -17,15 +17,17 @@
CED4FCEC1D9FD95D00F54838 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CED4FCEA1D9FD95D00F54838 /* LaunchScreen.storyboard */; };
CED4FCF71DA014B100F54838 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED4FCF61DA014B100F54838 /* Reaction.swift */; };
CED4FCF91DA10D6600F54838 /* ReactionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED4FCF81DA10D6600F54838 /* ReactionButton.swift */; };
CED4FCFB1DA10E4300F54838 /* ReactionSelectControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED4FCFA1DA10E4300F54838 /* ReactionSelectControl.swift */; };
CED4FCFB1DA10E4300F54838 /* ReactionSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED4FCFA1DA10E4300F54838 /* ReactionSelector.swift */; };
CED4FCFD1DA1100900F54838 /* ReactionFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED4FCFC1DA1100900F54838 /* ReactionFeedback.swift */; };
CED9D93C1DA64FCD00A70C2D /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D93B1DA64FCD00A70C2D /* Components.swift */; };
CED9D93E1DA6566700A70C2D /* FacebookReactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D93D1DA6566700A70C2D /* FacebookReactions.swift */; };
CED9D9401DA6869500A70C2D /* ReactionSelectControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D93F1DA6869500A70C2D /* ReactionSelectControlConfig.swift */; };
CED9D9401DA6869500A70C2D /* ReactionSelectorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D93F1DA6869500A70C2D /* ReactionSelectorConfig.swift */; };
CED9D9421DA6931A00A70C2D /* ReactionAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D9411DA6931A00A70C2D /* ReactionAlignment.swift */; };
CED9D9461DA6A07D00A70C2D /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D9451DA6A07D00A70C2D /* Sequence.swift */; };
CED9D9481DA6EAFA00A70C2D /* ReactionFeedbackDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D9471DA6EAFA00A70C2D /* ReactionFeedbackDelegate.swift */; };
CED9D94A1DA78F9400A70C2D /* ReactionButtonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D9491DA78F9400A70C2D /* ReactionButtonConfig.swift */; };
CED9D94C1DA7D36700A70C2D /* ReactionSummaryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D94B1DA7D36700A70C2D /* ReactionSummaryConfig.swift */; };
CED9D94E1DA7E49500A70C2D /* Configurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED9D94D1DA7E49500A70C2D /* Configurable.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -41,15 +43,17 @@
CED4FCED1D9FD95D00F54838 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CED4FCF61DA014B100F54838 /* Reaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = "<group>"; };
CED4FCF81DA10D6600F54838 /* ReactionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionButton.swift; sourceTree = "<group>"; };
CED4FCFA1DA10E4300F54838 /* ReactionSelectControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSelectControl.swift; sourceTree = "<group>"; };
CED4FCFA1DA10E4300F54838 /* ReactionSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSelector.swift; sourceTree = "<group>"; };
CED4FCFC1DA1100900F54838 /* ReactionFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionFeedback.swift; sourceTree = "<group>"; };
CED9D93B1DA64FCD00A70C2D /* Components.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = "<group>"; };
CED9D93D1DA6566700A70C2D /* FacebookReactions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FacebookReactions.swift; sourceTree = "<group>"; };
CED9D93F1DA6869500A70C2D /* ReactionSelectControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSelectControlConfig.swift; sourceTree = "<group>"; };
CED9D93F1DA6869500A70C2D /* ReactionSelectorConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSelectorConfig.swift; sourceTree = "<group>"; };
CED9D9411DA6931A00A70C2D /* ReactionAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionAlignment.swift; sourceTree = "<group>"; };
CED9D9451DA6A07D00A70C2D /* Sequence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
CED9D9471DA6EAFA00A70C2D /* ReactionFeedbackDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionFeedbackDelegate.swift; sourceTree = "<group>"; };
CED9D9491DA78F9400A70C2D /* ReactionButtonConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionButtonConfig.swift; sourceTree = "<group>"; };
CED9D94B1DA7D36700A70C2D /* ReactionSummaryConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionSummaryConfig.swift; sourceTree = "<group>"; };
CED9D94D1DA7E49500A70C2D /* Configurable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configurable.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -98,6 +102,7 @@
children = (
CE874F021DA2DA64000D309E /* ComponentBuilder.swift */,
CED9D93B1DA64FCD00A70C2D /* Components.swift */,
CED9D94D1DA7E49500A70C2D /* Configurable.swift */,
CED9D93D1DA6566700A70C2D /* FacebookReactions.swift */,
CED4FCF61DA014B100F54838 /* Reaction.swift */,
CED9D9411DA6931A00A70C2D /* ReactionAlignment.swift */,
@ -105,9 +110,10 @@
CED9D9491DA78F9400A70C2D /* ReactionButtonConfig.swift */,
CED4FCFC1DA1100900F54838 /* ReactionFeedback.swift */,
CED9D9471DA6EAFA00A70C2D /* ReactionFeedbackDelegate.swift */,
CED4FCFA1DA10E4300F54838 /* ReactionSelectControl.swift */,
CED9D93F1DA6869500A70C2D /* ReactionSelectControlConfig.swift */,
CED4FCFA1DA10E4300F54838 /* ReactionSelector.swift */,
CED9D93F1DA6869500A70C2D /* ReactionSelectorConfig.swift */,
CE874F001DA2D916000D309E /* ReactionSummary.swift */,
CED9D94B1DA7D36700A70C2D /* ReactionSummaryConfig.swift */,
CED9D9451DA6A07D00A70C2D /* Sequence.swift */,
CEB867BB1DA41CB600031B0D /* UIReactionControl.swift */,
);
@ -194,16 +200,18 @@
CE874F011DA2D916000D309E /* ReactionSummary.swift in Sources */,
CED4FCF91DA10D6600F54838 /* ReactionButton.swift in Sources */,
CEB867BC1DA41CB600031B0D /* UIReactionControl.swift in Sources */,
CED9D94E1DA7E49500A70C2D /* Configurable.swift in Sources */,
CE874F031DA2DA64000D309E /* ComponentBuilder.swift in Sources */,
CED4FCE41D9FD95D00F54838 /* ViewController.swift in Sources */,
CED4FCE21D9FD95D00F54838 /* AppDelegate.swift in Sources */,
CED4FCFD1DA1100900F54838 /* ReactionFeedback.swift in Sources */,
CED9D9461DA6A07D00A70C2D /* Sequence.swift in Sources */,
CED9D9401DA6869500A70C2D /* ReactionSelectControlConfig.swift in Sources */,
CED9D9401DA6869500A70C2D /* ReactionSelectorConfig.swift in Sources */,
CED9D94A1DA78F9400A70C2D /* ReactionButtonConfig.swift in Sources */,
CED9D93C1DA64FCD00A70C2D /* Components.swift in Sources */,
CED4FCF71DA014B100F54838 /* Reaction.swift in Sources */,
CED4FCFB1DA10E4300F54838 /* ReactionSelectControl.swift in Sources */,
CED9D94C1DA7D36700A70C2D /* ReactionSummaryConfig.swift in Sources */,
CED4FCFB1DA10E4300F54838 /* ReactionSelector.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -18,7 +18,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yZN-Tr-YCb" customClass="ReactionSelectControl" customModule="ReactionsExample" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yZN-Tr-YCb" customClass="ReactionSelector" customModule="ReactionsExample" customModuleProvider="target">
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" constant="52" id="JKf-vL-t3z"/>

View File

@ -9,7 +9,7 @@
import UIKit
class ViewController: UIViewController, ReactionFeedbackDelegate {
@IBOutlet weak var reactionSelect: ReactionSelectControl!
@IBOutlet weak var reactionSelect: ReactionSelector!
@IBOutlet weak var reactionButton: ReactionButton! {
didSet {
reactionButton.config = ReactionButtonConfig() {
@ -22,20 +22,19 @@ class ViewController: UIViewController, ReactionFeedbackDelegate {
@IBOutlet weak var facebookReactionButton: ReactionButton! {
didSet {
facebookReactionButton.reactionSelectControl = ReactionSelectControl()
facebookReactionButton.config = ReactionButtonConfig() {
facebookReactionButton.reactionSelector = ReactionSelector()
facebookReactionButton.config = ReactionButtonConfig() {
$0.iconMarging = 8
$0.spacing = 4
$0.font = UIFont(name: "HelveticaNeue", size: 14)
}
facebookReactionButton.reactionSelectControl?.feedbackDelegate = self
facebookReactionButton.reactionSelector?.feedbackDelegate = self
}
}
@IBOutlet weak var reactionSummary: ReactionSummary! {
didSet {
reactionSummary.reactions = Reaction.facebook.all
reactionSummary.alignment = .left
reactionSummary.text = "A description"
}
}
@ -61,7 +60,7 @@ class ViewController: UIViewController, ReactionFeedbackDelegate {
}
@IBAction func summaryTouchedAction(_ sender: AnyObject) {
facebookReactionButton.presentOverlay()
facebookReactionButton.presentReactionSelector()
}
// MARK: - ReactionFeedback Methods
@ -69,7 +68,7 @@ class ViewController: UIViewController, ReactionFeedbackDelegate {
func reactionFeedbackDidChanged(_ feedback: ReactionFeedback?) {
feedbackLabel.isHidden = feedback == nil
feedbackLabel.text = feedback?.localizedString()
feedbackLabel.text = feedback?.localizedString
}
}

View File

@ -26,10 +26,11 @@
import Foundation
/// Protocol helper to build interface component in an easy and elegant way
/// Protocol helper to build interface component in an easy and elegant way.
protocol ComponentBuilder {}
extension ComponentBuilder where Self: AnyObject {
/// Calls the parameter block in order to update the receiver properties and then returns the object.
func build(_ block: (Self) -> Void) -> Self {
block(self)

View File

@ -86,12 +86,5 @@ struct Components {
$0.contents = option.icon.cgImage
}
}
static func facebookSummaryLabel() -> UILabel {
return UILabel().build {
$0.font = UIFont(name: "HelveticaNeue", size: 14)
$0.textColor = UIColor(red: 0.47, green: 0.47, blue: 0.47, alpha: 1)
}
}
}
}

View File

@ -0,0 +1,43 @@
/*
* 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 Foundation
/// Protocol to create a config object.
protocol Configurable {
/**
The builder block.
The block gives a reference of receiver you can configure.
*/
typealias ConfigurableBlock = (Self) -> Void
/**
Initialize a configurable with default values.
- Parameter block: A configurable block to configure itself.
*/
init(block: ConfigurableBlock)
}

View File

@ -26,32 +26,41 @@
import UIKit
/// Default implementation of the facebook reactions.
extension Reaction {
/// Struct which defines the standard facebook reactions.
public struct facebook {
/// The facebook's "like" reaction.
public static var like: Reaction {
return Reaction(id: "like", title: "J'aime", color: UIColor(red: 0.29, green: 0.54, blue: 0.95, alpha: 1), icon: UIImage(named: "like")!, alternativeIcon: UIImage(named: "like-template")?.withRenderingMode(.alwaysTemplate))
}
/// The facebook's "love" reaction.
public static var love: Reaction {
return Reaction(id: "love", title: "J'adore", color: UIColor(red: 0.93, green: 0.23, blue: 0.33, alpha: 1), icon: UIImage(named: "love")!)
}
/// The facebook's "haha" reaction.
public static var haha: Reaction {
return Reaction(id: "haha", title: "Haha", color: UIColor(red: 0.99, green: 0.84, blue: 0.38, alpha: 1), icon: UIImage(named: "haha")!)
}
/// The facebook's "wow" reaction.
public static var wow: Reaction {
return Reaction(id: "wow", title: "Wouah", color: UIColor(red: 0.99, green: 0.84, blue: 0.38, alpha: 1), icon: UIImage(named: "wow")!)
}
/// The facebook's "sad" reaction.
public static var sad: Reaction {
return Reaction(id: "sad", title: "Triste", color: UIColor(red: 0.99, green: 0.84, blue: 0.38, alpha: 1), icon: UIImage(named: "sad")!)
}
/// The facebook's "angry" reaction.
public static var angry: Reaction {
return Reaction(id: "angry", title: "Grrr", color: UIColor(red: 0.96, green: 0.37, blue: 0.34, alpha: 1), icon: UIImage(named: "angry")!)
}
/// The list of standard facebook reactions in this order: `.like`, `.love`, `.haha`, `.wow`, `.sad`, `.angry`.
public static let all: [Reaction] = [facebook.like, facebook.love, facebook.haha, facebook.wow, facebook.sad, facebook.angry]
}
}

View File

@ -27,17 +27,40 @@
import UIKit
/**
A reaction structure is
The `Reaction` struct implements a reaction on a `ReactionSelect` object. A tab bar operates strictly in radio mode, where one item is selected at a timetapping a tab bar item toggles the view above the tab bar. You can also specify a badge value on the tab bar item for adding additional visual informationfor example, the Messages app uses a badge on the item to show the number of new messages. This class also provides a number of system defaults for creating items.
The `Reaction` struct defines several attributes like the title, the icon or the color of the reaction.
A `Reaction` can be used with objects like `ReactionSelector`, `ReactionButton` and `ReactionSummary`.
*/
public struct Reaction {
/// The reaction's identifier.
public let id: String
/// The reaction's title.
public let title: String
/// The reaction's color.
public let color: UIColor
/// The reaction's icon image.
public let icon: UIImage
/**
The reaction's alternative icon image.
The alternative icon is only used by the `ReactionButton`. It tries to display the alternative as icon and if it fails it uses the `icon`.
*/
public let alternativeIcon: UIImage?
/**
Creates and returns a new reaction using the specified properties.
- Parameter id: The reaction's identifier.
- Parameter title: The reaction's title.
- Parameter color: The reaction's color.
- Parameter icon: The reaction's icon image.
- Parameter alternativeIcon: The reaction's alternative icon image.
- Returns: Newly initialized reaction with the specified properties.
*/
public init(id: String, title: String, color: UIColor, icon: UIImage, alternativeIcon: UIImage? = nil) {
self.id = id
self.title = title
@ -48,18 +71,21 @@ public struct Reaction {
}
extension Reaction: Equatable {
/// Returns a Boolean value indicating whether two values are equal.
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
return lhs.id == rhs.id
}
}
extension Reaction: Hashable {
/// The hash value.
public var hashValue: Int {
return id.hashValue
}
}
extension Reaction: CustomStringConvertible {
/// A textual representation of this instance.
public var description: String {
return "<Reaction id=\(id) title=\(title)>"
}

View File

@ -24,11 +24,16 @@
*
*/
import UIKit
/**
These constants specify reaction alignment.
*/
public enum ReactionAlignment {
/// Text and icon are visually left aligned. The icon is to the left of text.
case left
/// Text and icon are visually right aligned. The icon is to the right of text.
case right
/// Text and icon are visually center aligned. The icon is to the left of text.
case centerLeft
/// Text and icon are visually center aligned. The icon is to the right of text.
case centerRight
}

View File

@ -26,6 +26,13 @@
import UIKit
/**
A `ReactionButton` object is a control that executes a reaction in response to user interactions.
You can tap a reaction button in order to highlight/unhighlight a reaction. You can also make a long press to the button to display a `ReactionSelector` if you have attached one.
You can configure/skin the button using a `ReactionButtonConfig`.
*/
public final class ReactionButton: UIReactionControl {
private let iconImageView: UIImageView = Components.reactionButton.facebookLikeIcon()
private let titleLabel: UILabel = Components.reactionButton.facebookLikeLabel()
@ -34,22 +41,38 @@ public final class ReactionButton: UIReactionControl {
$0.backgroundColor = .clear
$0.alpha = 0
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ReactionButton.dismissOverlay)))
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ReactionButton.dismissReactionSelector)))
}
/**
A Boolean value indicating whether the reaction button is in the selected state.
*/
public override var isSelected: Bool {
didSet { update() }
}
/**
The reaction button configuration.
*/
public var config: ReactionButtonConfig = ReactionButtonConfig() {
didSet { update() }
}
/**
The reaction used to build the button.
The reaction `title` fills the button one, and the `alternativeIcon` is used to display the icon. If the `alternativeIcon` is nil, the `icon` is used instead.
*/
public var reaction = Reaction.facebook.like {
didSet { update() }
}
public var reactionSelectControl: ReactionSelectControl? {
/**
The attached selector that the button will use in order to choose a reaction.
There are two ways to display the selector: calling the `presentReactionSelector` method or by doing a long press to the button.
*/
public var reactionSelector: ReactionSelector? {
didSet { setupReactionSelect(old: oldValue) }
}
@ -63,19 +86,19 @@ public final class ReactionButton: UIReactionControl {
addSubview(titleLabel)
}
private func setupReactionSelect(old: ReactionSelectControl?) {
if let reactionSelect = reactionSelectControl {
overlay.addSubview(reactionSelect)
private func setupReactionSelect(old: ReactionSelector?) {
if let selector = reactionSelector {
overlay.addSubview(selector)
}
old?.removeFromSuperview()
old?.removeTarget(self, action: #selector(ReactionButton.reactionTouchedInsideAction), for: .touchUpInside)
old?.removeTarget(self, action: #selector(ReactionButton.reactionTouchedOutsideAction), for: .touchUpOutside)
reaction = reactionSelectControl?.reactions.first ?? Reaction.facebook.like
reaction = reactionSelector?.reactions.first ?? Reaction.facebook.like
reactionSelectControl?.addTarget(self, action: #selector(ReactionButton.reactionTouchedInsideAction), for: .touchUpInside)
reactionSelectControl?.addTarget(self, action: #selector(ReactionButton.reactionTouchedOutsideAction), for: .touchUpOutside)
reactionSelector?.addTarget(self, action: #selector(ReactionButton.reactionTouchedInsideAction), for: .touchUpInside)
reactionSelector?.addTarget(self, action: #selector(ReactionButton.reactionTouchedOutsideAction), for: .touchUpOutside)
}
// MARK: - Updating Object State
@ -139,34 +162,34 @@ public final class ReactionButton: UIReactionControl {
private var isLongPressMoved = false
func longPressAction(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard let reactionSelect = reactionSelectControl else { return }
guard let selector = reactionSelector else { return }
if gestureRecognizer.state == .began {
isLongPressMoved = false
displayOverlay(feedback: .slideFingerAcross)
displayReactionSelector(feedback: .slideFingerAcross)
}
if gestureRecognizer.state == .changed {
isLongPressMoved = true
reactionSelect.longPressAction(gestureRecognizer)
selector.longPressAction(gestureRecognizer)
}
else if gestureRecognizer.state == .ended {
if isLongPressMoved {
reactionSelect.longPressAction(gestureRecognizer)
selector.longPressAction(gestureRecognizer)
dismissOverlay()
dismissReactionSelector()
}
else {
reactionSelect.feedback = .tapToSelectAReaction
selector.feedback = .tapToSelectAReaction
}
}
}
// MARK: - Responding to Select Events
func reactionTouchedInsideAction(_ sender: ReactionSelectControl) {
func reactionTouchedInsideAction(_ sender: ReactionSelector) {
guard let selectedReaction = sender.selectedReaction else { return }
let isReactionChanged = reaction != selectedReaction
@ -177,28 +200,36 @@ public final class ReactionButton: UIReactionControl {
if isReactionChanged {
sendActions(for: .valueChanged)
}
dismissOverlay()
dismissReactionSelector()
}
func reactionTouchedOutsideAction(_ sender: ReactionSelectControl) {
dismissOverlay()
func reactionTouchedOutsideAction(_ sender: ReactionSelector) {
dismissReactionSelector()
}
// MARK: -
// MARK: - Presenting Reaction Selectors
public func presentOverlay() {
displayOverlay(feedback: .tapToSelectAReaction)
/**
Presents the attached reaction selector.
If no reaction selector is attached, the method does nothing.
*/
public func presentReactionSelector() {
displayReactionSelector(feedback: .tapToSelectAReaction)
}
public func dismissOverlay() {
reactionSelectControl?.feedback = nil
/**
Dismisses the attached reaction selector that was presented by the button.
*/
public func dismissReactionSelector() {
reactionSelector?.feedback = nil
animateOverlay(alpha: 0, center: CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY))
}
func displayOverlay(feedback: ReactionFeedback) {
guard let reactionSelect = reactionSelectControl, let window = UIApplication.shared.keyWindow else { return }
private func displayReactionSelector(feedback: ReactionFeedback) {
guard let selector = reactionSelector, let window = UIApplication.shared.keyWindow else { return }
if overlay.superview == nil {
UIApplication.shared.keyWindow?.addSubview(overlay)
@ -206,26 +237,26 @@ public final class ReactionButton: UIReactionControl {
overlay.frame = CGRect(x:0 , y: 0, width: window.bounds.width, height: window.bounds.height * 2)
let centerPoint = convert(CGPoint(x: bounds.midX, y: 0), to: nil)
reactionSelect.frame = reactionSelect.boundsToFit()
reactionSelect.center = centerPoint
let centerPoint = convert(CGPoint(x: bounds.midX, y: 0), to: nil)
selector.frame = selector.boundsToFit()
selector.center = centerPoint
if reactionSelect.frame.origin.x - config.spacing < 0 {
reactionSelect.center = CGPoint(x: centerPoint.x - reactionSelect.frame.origin.x + config.spacing, y: centerPoint.y)
if selector.frame.origin.x - config.spacing < 0 {
selector.center = CGPoint(x: centerPoint.x - selector.frame.origin.x + config.spacing, y: centerPoint.y)
}
else if reactionSelect.frame.origin.x + reactionSelect.frame.width + config.spacing > overlay.bounds.width {
reactionSelect.center = CGPoint(x: centerPoint.x - (reactionSelect.frame.origin.x + reactionSelect.frame.width + config.spacing - overlay.bounds.width), y: centerPoint.y)
else if selector.frame.origin.x + selector.frame.width + config.spacing > overlay.bounds.width {
selector.center = CGPoint(x: centerPoint.x - (selector.frame.origin.x + selector.frame.width + config.spacing - overlay.bounds.width), y: centerPoint.y)
}
reactionSelect.feedback = feedback
selector.feedback = feedback
animateOverlay(alpha: 1, center: CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY - reactionSelect.bounds.height))
animateOverlay(alpha: 1, center: CGPoint(x: overlay.bounds.midX, y: overlay.bounds.midY - selector.bounds.height))
}
private func animateOverlay(alpha: CGFloat, center: CGPoint) {
UIView.animate(withDuration: 0.1) { [weak self] in
guard let overlay = self?.overlay else { return }
overlay.alpha = alpha
overlay.center = center
}

View File

@ -26,14 +26,26 @@
import UIKit
public final class ReactionButtonConfig {
public typealias ReactionButtonConfigBlock = (ReactionButtonConfig) -> Void
/**
The reaction button configuration object.
*/
public final class ReactionButtonConfig: Configurable {
/**
The builder block.
The block gives a reference of receiver you can configure.
*/
public typealias ConfigurableBlock = (ReactionButtonConfig) -> Void
/// The spacing between the icon and the text.
public var spacing: CGFloat = 8
/// The marging between the icon and border.
public var iconMarging: CGFloat = 4
/// The font of the text.
public var font: UIFont! = UIFont(name: "HelveticaNeue", size: 16)
/// The color of the text (and image) when no reaction is selected.
public var neutralTintColor: UIColor = UIColor(red: 0.47, green: 0.47, blue: 0.47, alpha: 1)
/**
@ -43,9 +55,17 @@ public final class ReactionButtonConfig {
*/
public var alignment: ReactionAlignment = .left
// MARK: - Initializing a Reaction Button
// Initialize a configurable with default values.
init() {}
public init(block: ReactionButtonConfigBlock) {
/**
Initialize a configurable with default values.
- Parameter block: A configurable block to configure itself.
*/
public init(block: ConfigurableBlock) {
block(self)
}
}

View File

@ -24,12 +24,21 @@
*
*/
/**
These constants specify reaction feedback when interacting with the `ReactionSelector`.
*/
public enum ReactionFeedback {
/// Slide finger across feedback
case slideFingerAcross
/// Release to cancel feedback
case releaseToCancel
/// Tap to select a reaction feedback
case tapToSelectAReaction
public func localizedString() -> String {
// MARK: - Getting a Localized Feedback Description
/// A string containing the localized description of the feedback.
public var localizedString: String {
switch self {
case .slideFingerAcross:
return "Slide finger across"

View File

@ -24,6 +24,14 @@
*
*/
/**
The delegate of a `ReactionSelector` object should adopt the `ReactionFeedbackDelegate` protocol. It allows the delegate to know in which state the selector is.
*/
public protocol ReactionFeedbackDelegate: class {
/**
Tells the delegate that the feedback did changed.
- Parameter feedback: The reaction feedback.
*/
func reactionFeedbackDidChanged(_ feedback: ReactionFeedback?)
}

View File

@ -27,33 +27,45 @@
import UIKit
/**
A `ReactionSelectControl` object is a horizontal control made of multiple reactions, each reaction functioning as a discrete button. A select control affords a compact means to group together a number of reactions.
A `ReactionSelector` object is a horizontal control made of multiple reactions, each reaction functioning as a discrete button. A select control affords a compact means to group together a number of reactions.
The `ReactionSelectControl` object automatically resizes reactions using the `iconSize` and `spacing` values defined in the `config` property.
The `ReactionSelector` object automatically resizes reactions using the `iconSize` and `spacing` values defined in the `config` property.
You can configure/skin the button using a `ReactionSelectorConfig`.
*/
public final class ReactionSelectControl: UIReactionControl {
public final class ReactionSelector: UIReactionControl {
private var reactionIconLayers: [CALayer] = []
private var reactionLabels: [UILabel] = []
private let backgroundLayer = Components.reactionSelect.backgroundLayer()
/**
The reactions available in the selector.
*/
public var reactions: [Reaction] = Reaction.facebook.all {
didSet { setupAndUpdate() }
}
/// The feedback delegate.
public weak var feedbackDelegate: ReactionFeedbackDelegate?
/// The selector feedback state.
public internal(set) var feedback: ReactionFeedback? {
didSet {
if oldValue != feedback { feedbackDelegate?.reactionFeedbackDidChanged(feedback) }
}
}
public var config = ReactionSelectControlConfig()
private var reactionIconLayers: [CALayer] = []
private var reactionLabels: [UILabel] = []
private let backgroundLayer = Components.reactionSelect.backgroundLayer()
/**
The reaction selector configuration.
*/
public var config = ReactionSelectorConfig()
// MARK: - Managing Internal State
private var stateHighlightedReactionIndex: Int?
private var stateSelectedReaction: Reaction?
/// The selected reaction.
public var selectedReaction: Reaction? {
get { return stateSelectedReaction }
set {
@ -81,7 +93,7 @@ public final class ReactionSelectControl: UIReactionControl {
if backgroundLayer.superlayer == nil {
addGestureRecognizer(UILongPressGestureRecognizer().build {
$0.addTarget(self, action: #selector(ReactionSelectControl.longPressAction))
$0.addTarget(self, action: #selector(ReactionSelector.longPressAction))
$0.minimumPressDuration = 0
})

View File

@ -26,16 +26,36 @@
import UIKit
public final class ReactionSelectControlConfig {
public typealias ReactionSelectControlConfigBlock = (ReactionSelectControlConfig) -> Void
/**
The reaction selector configuration object.
*/
public final class ReactionSelectorConfig: Configurable {
/**
The builder block.
The block gives a reference of receiver you can configure.
*/
public typealias ConfigurableBlock = (ReactionSelectorConfig) -> Void
/// The spacing between the icons and borders.
public var spacing: CGFloat = 6
/// The icon size when the selector is inactive.
public var iconSize: CGFloat? = nil
/// Boolean value to know whether the reactions needs to be sticked when they are selected.
public var stickyReaction: Bool = false
// MARK: - Initializing a Reaction Selector
// Initialize a configurable with default values.
init() {}
public init(block: ReactionSelectControlConfigBlock) {
/**
Initialize a configurable with default values.
- Parameter block: A configurable block to configure itself.
*/
public init(block: ConfigurableBlock) {
block(self)
}

View File

@ -26,21 +26,34 @@
import UIKit
/**
A `ReactionSummary` component aims to display a list of reactions as a thumbnail associate to a text description.
You can configure/skin the summary using a `ReactionSummaryConfig`.
*/
public final class ReactionSummary: UIReactionControl {
private let textLabel: UILabel = Components.reactionSummary.facebookSummaryLabel()
private let textLabel: UILabel = UILabel()
private var reactionIconLayers: [CALayer] = []
private let spacing: CGFloat = 8
public var font: UIFont! {
get { return textLabel.font }
set {
textLabel.font = newValue
update()
}
/**
The reaction summary configuration.
*/
public var config: ReactionSummaryConfig = ReactionSummaryConfig() {
didSet { setupAndUpdate() }
}
/**
The reactions to summarize.
*/
public var reactions: [Reaction] = [] {
didSet { setupAndUpdate() }
}
/**
The text displayed by the reaction summary.
This string is nil by default.
*/
public var text: String? {
get { return textLabel.text }
set {
@ -50,19 +63,6 @@ public final class ReactionSummary: UIReactionControl {
}
}
/**
The technique to use for aligning the icon and the text.
The default value of this property is left.
*/
public var alignment: ReactionAlignment = .left {
didSet { update() }
}
public var reactions: [Reaction] = [] {
didSet { setupAndUpdate() }
}
// MARK: - Building Object
override func setup() {
@ -92,28 +92,21 @@ public final class ReactionSummary: UIReactionControl {
// MARK: - Updating Object State
override func update() {
let textSize = textLabel.sizeThatFits(CGSize(width: bounds.width, height: bounds.height))
let iconSize = min(bounds.height, textSize.height + 4)
let iconWidth = (iconSize - 3) * CGFloat(reactionIconLayers.count) + spacing
let margin = (bounds.width - iconWidth - textSize.width) / 2
textLabel.font = config.font
textLabel.textColor = config.textColor
for (index, l) in reactionIconLayers.enumerated() {
let x: CGFloat
let textSize = textLabel.sizeThatFits(CGSize(width: bounds.width, height: bounds.height))
let iconSize = min(bounds.height, textSize.height + 4)
let iconWidth = (iconSize - 3) * CGFloat(reactionIconLayers.count) + config.spacing
let margin = (bounds.width - iconWidth - textSize.width) / 2
switch alignment {
case .left: x = (iconSize - 3) * CGFloat(index)
case .right: x = bounds.width - iconSize - (iconSize - 3) * CGFloat(index)
case .centerLeft: x = margin + (iconSize - 3) * CGFloat(index)
case .centerRight: x = bounds.width - iconSize - (iconSize - 3) * CGFloat(index) - margin
}
l.frame = CGRect(x: x, y: (bounds.height - iconSize) / 2, width: iconSize, height: iconSize)
l.cornerRadius = iconSize / 2
for index in 0 ..< reactionIconLayers.count {
updateIconAtIndex(index, with: iconSize, margin: margin)
}
let textX: CGFloat
switch alignment {
switch config.alignment {
case .left: textX = iconWidth
case .right: textX = bounds.width - iconWidth - textSize.width
case .centerLeft: textX = margin + iconWidth
@ -123,6 +116,21 @@ 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

@ -0,0 +1,68 @@
/*
* 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 UIKit
/**
The reaction summary configuration object.
*/
public final class ReactionSummaryConfig: Configurable {
/**
The builder block.
The block gives a reference of receiver you can configure.
*/
public typealias ConfigurableBlock = (ReactionSummaryConfig) -> Void
/// The spacing between the icons and the text.
public var spacing: CGFloat = 8
/// The font of the text.
public var font: UIFont! = UIFont(name: "HelveticaNeue", size: 14)
/// The color of the text.
public var textColor: UIColor! = UIColor(red: 0.47, green: 0.47, blue: 0.47, alpha: 1)
/**
The technique to use for aligning the icon and the text.
The default value of this property is left.
*/
public var alignment: ReactionAlignment = .left
// MARK: - Initializing a Reaction Summary
// Initialize a configurable with default values.
init() {}
/**
Initialize a configurable with default values.
- Parameter block: A configurable block to configure itself.
*/
public init(block: ConfigurableBlock) {
block(self)
}
}

View File

@ -26,7 +26,9 @@
import Foundation
/// Convenient projet extension
extension Sequence where Iterator.Element: Hashable {
/// Returns uniq elements in the sequence by keeping the element order.
func uniq() -> [Iterator.Element] {
var alreadySeen: [Iterator.Element: Bool] = [:]

View File

@ -26,16 +26,26 @@
import UIKit
/**
The `UIReactionControl` class implements common behavior for reaction elements. It mainly defines two methods:
- `setup`: Manage the view hierarchy by adding and/or removing elements.
- `update`: Layout the view hierarchy and update state.
You should override these methods if you subclass the `UIReactionControl`.
*/
public class UIReactionControl: UIControl {
// MARK: - Initializing a ReactionSelect Object
override init(frame: CGRect) {
/// Initializes and returns a newly allocated view object with the specified frame rectangle.
public override init(frame: CGRect) {
super.init(frame: frame)
setupAndUpdate()
}
required public init?(coder aDecoder: NSCoder) {
/// Returns an object initialized from data in a given unarchiver.
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupAndUpdate()
@ -43,12 +53,14 @@ public class UIReactionControl: UIControl {
// MARK: - Laying out Subviews
/// Lays out subviews.
public override func layoutSubviews() {
super.layoutSubviews()
update()
}
/// Called when a designable object is created in Interface Builder.
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
@ -57,8 +69,10 @@ public class UIReactionControl: UIControl {
// MARK: - Building Object
/// Setup the view hierarchy
func setup() {}
/// Call the setup then the update method
final func setupAndUpdate() {
setup()
@ -69,5 +83,6 @@ public class UIReactionControl: UIControl {
// MARK: - Updating Object State
/// Update the state and layout the view hierarchy
func update() {}
}