Color picker

TODO

-[x] tap to select/deselect
-[x] initially selected
-[x] integrate into conversation settings
-[x] colorPickerDelegate
-[x] translate strings
-[] reorder colors
-[x] SheetView: add top handle

Nice to have:

-[] SheetView: interactively swipe/unswipe to dismiss?
-[] preview color in bubbles
This commit is contained in:
Michael Kirk 2018-09-26 19:10:28 -06:00
parent 95a6df6496
commit 4765ed9a06
8 changed files with 198 additions and 113 deletions

View File

@ -4,138 +4,184 @@
import Foundation
let colorSwatchHeight: CGFloat = 40
protocol ColorViewDelegate: class {
func colorViewWasTapped(_ colorView: ColorView)
}
class ColorView: UIView {
let color: UIColor
let swatchView: UIView
public weak var delegate: ColorViewDelegate?
public let conversationColor: OWSConversationColor
required init(color: UIColor) {
self.color = color
private let swatchView: UIView
private let selectedRing: UIView
public var isSelected: Bool = false {
didSet {
self.selectedRing.isHidden = !isSelected
}
}
required init(conversationColor: OWSConversationColor) {
self.conversationColor = conversationColor
self.swatchView = UIView()
self.selectedRing = UIView()
super.init(frame: .zero)
swatchView.backgroundColor = color
self.swatchView.layer.cornerRadius = colorSwatchHeight / 2
self.addSubview(selectedRing)
self.addSubview(swatchView)
swatchView.autoVCenterInSuperview()
swatchView.autoSetDimension(.height, toSize: colorSwatchHeight)
swatchView.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual)
swatchView.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual)
swatchView.autoPinLeadingToSuperviewMargin()
swatchView.autoPinTrailingToSuperviewMargin()
let cellHeight: CGFloat = 64
selectedRing.autoSetDimensions(to: CGSize(width: cellHeight, height: cellHeight))
selectedRing.layer.cornerRadius = cellHeight / 2
selectedRing.layer.borderColor = Theme.secondaryColor.cgColor
selectedRing.layer.borderWidth = 2
selectedRing.autoPinEdgesToSuperviewEdges()
selectedRing.isHidden = true
swatchView.backgroundColor = conversationColor.primaryColor
let swatchSize: CGFloat = 48
self.swatchView.layer.cornerRadius = swatchSize / 2
swatchView.autoSetDimensions(to: CGSize(width: swatchSize, height: swatchSize))
swatchView.autoCenterInSuperview()
// gestures
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
self.addGestureRecognizer(tapGesture)
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: Actions
@objc
func didTap() {
delegate?.colorViewWasTapped(self)
}
}
@objc
protocol ColorPickerDelegate: class {
func colorPickerDidCancel(_ colorPicker: ColorPickerViewController)
func colorPicker(_ colorPicker: ColorPickerViewController, didPickColorName colorName: String)
func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor)
}
@objc
class ColorPickerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
private let pickerView: UIPickerView
private let thread: TSThread
private let colorNames: [String]
@objc public weak var delegate: ColorPickerDelegate?
@objc(OWSColorPicker)
class ColorPicker: NSObject, ColorPickerViewDelegate {
@objc
required init(thread: TSThread) {
self.thread = thread
self.pickerView = UIPickerView()
self.colorNames = OWSConversationColor.conversationColorNames
public weak var delegate: ColorPickerDelegate?
super.init(nibName: nil, bundle: nil)
@objc
let sheetViewController: SheetViewController
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(didTapSave))
private let currentConversationColor: OWSConversationColor
pickerView.dataSource = self
pickerView.delegate = self
@objc
init(currentConversationColor: OWSConversationColor) {
self.currentConversationColor = currentConversationColor
sheetViewController = SheetViewController()
super.init()
let colorPickerView = ColorPickerView()
colorPickerView.delegate = self
colorPickerView.select(conversationColor: currentConversationColor)
sheetViewController.contentView.addSubview(colorPickerView)
colorPickerView.autoPinEdgesToSuperviewEdges()
}
// MARK: ColorPickerViewDelegate
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor) {
self.delegate?.colorPicker(self, didPickConversationColor: conversationColor)
}
}
protocol ColorPickerViewDelegate: class {
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor)
}
class ColorPickerView: UIView, ColorViewDelegate {
private let colorViews: [ColorView]
weak var delegate: ColorPickerViewDelegate?
override init(frame: CGRect) {
let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) }
self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) }
super.init(frame: frame)
colorViews.forEach { $0.delegate = self }
let headerView = self.buildHeaderView()
let paletteView = self.buildPaletteView(colorViews: colorViews)
let rowsStackView = UIStackView(arrangedSubviews: [headerView, paletteView])
rowsStackView.axis = .vertical
addSubview(rowsStackView)
rowsStackView.autoPinEdgesToSuperviewEdges()
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
override func loadView() {
self.view = UIView()
view.backgroundColor = Theme.backgroundColor
view.addSubview(pickerView)
// MARK: ColorViewDelegate
pickerView.autoVCenterInSuperview()
pickerView.autoPinLeadingToSuperviewMargin()
pickerView.autoPinTrailingToSuperviewMargin()
func colorViewWasTapped(_ colorView: ColorView) {
self.select(conversationColor: colorView.conversationColor)
self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor)
}
override func viewDidLoad() {
super.viewDidLoad()
let colorName = thread.conversationColorName
if let index = colorNames.index(of: colorName) {
pickerView.selectRow(index, inComponent: 0, animated: false)
fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) {
colorViews.forEach { colorView in
colorView.isSelected = colorView.conversationColor == selectedConversationColor
}
}
// MARK: UIPickerViewDataSource
// MARK: View Building
public func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
private func buildHeaderView() -> UIView {
let headerView = UIView()
headerView.layoutMargins = UIEdgeInsets(top: 15, left: 16, bottom: 15, right: 16)
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("COLOR_PICKER_SHEET_TITLE", comment: "Modal Sheet title when picking a conversation color.")
titleLabel.textAlignment = .center
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
titleLabel.textColor = Theme.primaryColor
headerView.addSubview(titleLabel)
titleLabel.ows_autoPinToSuperviewMargins()
let bottomBorderView = UIView()
bottomBorderView.backgroundColor = Theme.hairlineColor
headerView.addSubview(bottomBorderView)
bottomBorderView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
bottomBorderView.autoSetDimension(.height, toSize: CGHairlineWidth())
return headerView
}
public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return self.colorNames.count
}
private func buildPaletteView(colorViews: [ColorView]) -> UIView {
let paletteView = UIView()
paletteView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
// MARK: UIPickerViewDelegate
public func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
let vMargin: CGFloat = 16
return colorSwatchHeight + vMargin * 2
}
public func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
guard let colorName = colorNames[safe: row] else {
owsFailDebug("color was unexpectedly nil")
return ColorView(color: .white)
let kRowLength = 4
let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in
let row = UIStackView(arrangedSubviews: colorViewsInRow)
row.distribution = UIStackViewDistribution.equalSpacing
return row
}
guard let colors = OWSConversationColor.conversationColor(colorName: colorName) else {
owsFailDebug("unknown color name")
return ColorView(color: OWSConversationColor.default().themeColor)
}
return ColorView(color: colors.themeColor)
}
let rowsStackView = UIStackView(arrangedSubviews: rows)
rowsStackView.axis = .vertical
rowsStackView.spacing = ScaleFromIPhone5To7Plus(16, 50)
// MARK: Actions
var currentColorName: String {
let index = pickerView.selectedRow(inComponent: 0)
guard let colorName = colorNames[safe: index] else {
owsFailDebug("index was unexpectedly nil")
return OWSConversationColor.defaultConversationColorName()
}
return colorName
}
@objc
public func didTapSave() {
let colorName = self.currentColorName
self.delegate?.colorPicker(self, didPickColorName: colorName)
}
@objc
public func didTapCancel() {
self.delegate?.colorPickerDidCancel(self)
paletteView.addSubview(rowsStackView)
rowsStackView.ows_autoPinToSuperviewMargins()
return paletteView
}
}

View File

@ -22,6 +22,7 @@
#import <SignalMessaging/OWSProfileManager.h>
#import <SignalMessaging/OWSSounds.h>
#import <SignalMessaging/OWSUserProfile.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalMessaging/UIUtil.h>
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
@ -41,7 +42,8 @@ const CGFloat kIconViewLength = 24;
@interface OWSConversationSettingsViewController () <ContactEditingDelegate,
ContactsViewHelperDelegate,
ColorPickerDelegate>
ColorPickerDelegate,
OWSSheetViewControllerDelegate>
@property (nonatomic) TSThread *thread;
@property (nonatomic) YapDatabaseConnection *uiDatabaseConnection;
@ -57,6 +59,7 @@ const CGFloat kIconViewLength = 24;
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
@property (nonatomic, readonly) UIImageView *avatarView;
@property (nonatomic, readonly) UILabel *disappearingMessagesDurationLabel;
@property (nonatomic) OWSColorPicker *colorPicker;
@end
@ -247,6 +250,11 @@ const CGFloat kIconViewLength = 24;
[[OWSDisappearingMessagesConfiguration alloc] initDefaultWithThreadId:self.thread.uniqueId];
}
NSString *colorName = self.thread.conversationColorName;
OWSConversationColor *currentConversationColor = [OWSConversationColor conversationColorOrDefaultForColorName:colorName];
self.colorPicker = [[OWSColorPicker alloc] initWithCurrentConversationColor:currentConversationColor];
self.colorPicker.delegate = self;
[self updateTableContents];
}
@ -1299,18 +1307,22 @@ const CGFloat kIconViewLength = 24;
- (void)showColorPicker
{
ColorPickerViewController *pickerController = [[ColorPickerViewController alloc] initWithThread:self.thread];
pickerController.delegate = self;
OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:pickerController];
OWSSheetViewController *sheetViewController = self.colorPicker.sheetViewController;
sheetViewController.delegate = self;
[self presentViewController:modal animated:YES completion:nil];
[self presentViewController:sheetViewController
animated:YES
completion:^() {
OWSLogInfo(@"presented sheet view");
}];
}
- (void)colorPicker:(ColorPickerViewController *)colorPicker didPickColorName:(NSString *)colorName
- (void)colorPicker:(OWSColorPicker *)colorPicker
didPickConversationColor:(OWSConversationColor *_Nonnull)conversationColor
{
OWSLogDebug(@"picked color: %@", colorName);
OWSLogDebug(@"picked color: %@", conversationColor.name);
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.thread updateConversationColorName:colorName transaction:transaction];
[self.thread updateConversationColorName:conversationColor.name transaction:transaction];
}];
[self.contactsManager.avatarCache removeAllImages];
@ -1323,11 +1335,11 @@ const CGFloat kIconViewLength = 24;
OWSAssertDebug(operation.isReady);
[operation start];
});
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)colorPickerDidCancel:(ColorPickerViewController *)colorPicker
#pragma mark - OWSSheetViewController
- (void)sheetViewControllerRequestedDismiss:(OWSSheetViewController *)sheetViewController
{
[self dismissViewControllerAnimated:YES completion:nil];
}

View File

@ -386,6 +386,9 @@
/* Error indicating that the app was prevented from accessing the user's CloudKit account. */
"CLOUDKIT_STATUS_RESTRICTED" = "Signal was not allowed to access your iCloud account for backups.";
/* Modal Sheet title when picking a conversation color. */
"COLOR_PICKER_SHEET_TITLE" = "Conversation Color";
/* Activity Sheet label */
"COMPARE_SAFETY_NUMBER_ACTION" = "Compare with Clipboard";

View File

@ -19,6 +19,7 @@ public class SheetViewController: UIViewController {
public let contentView: UIView = UIView()
private let sheetView: SheetView = SheetView()
private let handleView: UIView = UIView()
deinit {
Logger.verbose("")
@ -97,7 +98,6 @@ public class SheetViewController: UIViewController {
fileprivate func animateDismiss(completion: @escaping (Bool) -> Void) {
guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else {
owsFailDebug("sheetVerticalConstraint was unexpectedly nil")
// self.delegate?.sheetViewDidHide(self)
return
}

View File

@ -8,16 +8,17 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSConversationColor : NSObject
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) UIColor *primaryColor;
@property (nonatomic, readonly) UIColor *shadeColor;
@property (nonatomic, readonly) UIColor *tintColor;
@property (nonatomic, readonly) UIColor *themeColor;
+ (OWSConversationColor *)conversationColorWithPrimaryColor:(UIColor *)primaryColor
shadeColor:(UIColor *)shadeColor
tintColor:(UIColor *)tintColor;
+ (OWSConversationColor *)conversationColorWithName:(NSString *)name
primaryColor:(UIColor *)primaryColor
shadeColor:(UIColor *)shadeColor
tintColor:(UIColor *)tintColor;
#pragma mark - Conversation Colors
@property (class, readonly, nonatomic) UIColor *ows_crimsonColor;

View File

@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSConversationColor ()
@property (nonatomic) NSString *name;
@property (nonatomic) UIColor *primaryColor;
@property (nonatomic) UIColor *shadeColor;
@property (nonatomic) UIColor *tintColor;
@ -20,22 +21,36 @@ NS_ASSUME_NONNULL_BEGIN
@implementation OWSConversationColor
+ (OWSConversationColor *)conversationColorWithPrimaryColor:(UIColor *)primaryColor
shadeColor:(UIColor *)shadeColor
tintColor:(UIColor *)tintColor
+ (OWSConversationColor *)conversationColorWithName:(NSString *)name
primaryColor:(UIColor *)primaryColor
shadeColor:(UIColor *)shadeColor
tintColor:(UIColor *)tintColor
{
OWSConversationColor *instance = [OWSConversationColor new];
instance.name = name;
instance.primaryColor = primaryColor;
instance.shadeColor = shadeColor;
instance.tintColor = tintColor;
return instance;
}
#pragma mark -
- (UIColor *)themeColor
{
return Theme.isDarkThemeEnabled ? self.shadeColor : self.primaryColor;
}
- (BOOL)isEqual:(id)other
{
if (![other isKindOfClass:[OWSConversationColor class]]) {
return NO;
}
OWSConversationColor *otherColor = (OWSConversationColor *)other;
return [self.name isEqual:otherColor.name];
}
#pragma mark - Conversation Colors
+ (UIColor *)ows_crimsonColor
@ -350,8 +365,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(primaryColor);
OWSAssertDebug(shadeColor);
OWSAssertDebug(tintColor);
return
[OWSConversationColor conversationColorWithPrimaryColor:primaryColor shadeColor:shadeColor tintColor:tintColor];
return [OWSConversationColor conversationColorWithName:conversationColorName primaryColor:primaryColor shadeColor:shadeColor tintColor:tintColor];
}
+ (OWSConversationColor *)conversationColorOrDefaultForColorName:(NSString *)conversationColorName

View File

@ -9,3 +9,11 @@ public extension Collection {
return indices.contains(index) ? self[index] : nil
}
}
public extension Array {
func chunked(by chunkSize: Int) -> [[Element]] {
return stride(from: 0, to: self.count, by: chunkSize).map {
Array(self[$0..<Swift.min($0 + chunkSize, self.count)])
}
}
}

View File

@ -2,8 +2,9 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMath.h"
#import "UIColor+OWS.h"
#import "OWSMath.h"
#import "Theme.h"
#import <SignalServiceKit/Cryptography.h>
NS_ASSUME_NONNULL_BEGIN