session-ios/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift

311 lines
12 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SignalCoreKit
import SignalUtilitiesKit
class EmojiSkinTonePicker: UIView {
let emoji: Emoji
let preferredSkinTonePermutation: [Emoji.SkinTone]?
let completion: (EmojiWithSkinTones?) -> Void
private let referenceOverlay = UIView()
private let containerView = UIView()
class func present(
referenceView: UIView,
emoji: EmojiWithSkinTones,
completion: @escaping (EmojiWithSkinTones?) -> Void
) -> EmojiSkinTonePicker? {
guard let baseEmoji = emoji.baseEmoji, baseEmoji.hasSkinTones else { return nil }
UIImpactFeedbackGenerator(style: .light).impactOccurred()
let picker = EmojiSkinTonePicker(emoji: emoji, completion: completion)
guard let superview = referenceView.superview else {
owsFailDebug("reference is missing superview")
return nil
}
superview.addSubview(picker)
picker.referenceOverlay.autoMatch(.width, to: .width, of: referenceView)
picker.referenceOverlay.autoMatch(.height, to: .height, of: referenceView, withOffset: 30)
picker.referenceOverlay.autoPinEdge(.leading, to: .leading, of: referenceView)
let leadingConstraint = picker.autoPinEdge(toSuperviewEdge: .leading)
picker.layoutIfNeeded()
let halfWidth = picker.width() / 2
let margin: CGFloat = 8
if (halfWidth + margin) > referenceView.center.x {
leadingConstraint.constant = margin
} else if (halfWidth + margin) > (superview.width() - referenceView.center.x) {
leadingConstraint.constant = superview.width() - picker.width() - margin
} else {
leadingConstraint.constant = referenceView.center.x - halfWidth
}
let distanceFromTop = referenceView.frame.minY - superview.bounds.minY
if distanceFromTop > picker.containerView.height() {
picker.containerView.autoPinEdge(toSuperviewEdge: .top)
picker.referenceOverlay.autoPinEdge(.top, to: .bottom, of: picker.containerView, withOffset: -20)
picker.referenceOverlay.autoPinEdge(toSuperviewEdge: .bottom)
picker.autoPinEdge(.bottom, to: .bottom, of: referenceView)
} else {
picker.containerView.autoPinEdge(toSuperviewEdge: .bottom)
picker.referenceOverlay.autoPinEdge(.bottom, to: .top, of: picker.containerView, withOffset: 20)
picker.referenceOverlay.autoPinEdge(toSuperviewEdge: .top)
picker.autoPinEdge(.top, to: .top, of: referenceView)
}
picker.alpha = 0
UIView.animate(withDuration: 0.12) { picker.alpha = 1 }
return picker
}
func dismiss() {
UIView.animate(withDuration: 0.12, animations: { self.alpha = 0 }) { _ in
self.removeFromSuperview()
}
}
func didChangeLongPress(_ sender: UILongPressGestureRecognizer) {
guard let singleSelectionButtons = singleSelectionButtons else { return }
if referenceOverlay.frame.contains(sender.location(in: self)) {
singleSelectionButtons.forEach { $0.isSelected = false }
} else {
let point = sender.location(in: containerView)
let previouslySelectedButton = singleSelectionButtons.first { $0.isSelected }
singleSelectionButtons.forEach { $0.isSelected = $0.frame.insetBy(dx: -3, dy: -80).contains(point) }
let selectedButton = singleSelectionButtons.first { $0.isSelected }
if let selectedButton = selectedButton, selectedButton != previouslySelectedButton {
SelectionHapticFeedback().selectionChanged()
}
}
}
func didEndLongPress(_ sender: UILongPressGestureRecognizer) {
guard let singleSelectionButtons = singleSelectionButtons else { return }
let point = sender.location(in: containerView)
if referenceOverlay.frame.contains(sender.location(in: self)) {
// Do nothing.
} else if let selectedButton = singleSelectionButtons.first(where: {
$0.frame.insetBy(dx: -3, dy: -80).contains(point)
}) {
selectedButton.sendActions(for: .touchUpInside)
} else {
dismiss()
}
}
init(emoji: EmojiWithSkinTones, completion: @escaping (EmojiWithSkinTones?) -> Void) {
owsAssertDebug(emoji.baseEmoji!.hasSkinTones)
self.emoji = emoji.baseEmoji!
self.preferredSkinTonePermutation = emoji.skinTones
self.completion = completion
super.init(frame: .zero)
layer.shadowOffset = .zero
layer.shadowOpacity = 0.25
layer.shadowRadius = 4
referenceOverlay.themeBackgroundColor = .backgroundSecondary
referenceOverlay.layer.cornerRadius = 9
addSubview(referenceOverlay)
containerView.layoutMargins = UIEdgeInsets(top: 9, leading: 16, bottom: 9, trailing: 16)
containerView.themeBackgroundColor = .backgroundSecondary
containerView.layer.cornerRadius = 11
addSubview(containerView)
containerView.autoPinWidthToSuperview()
containerView.setCompressionResistanceHigh()
if emoji.baseEmoji!.allowsMultipleSkinTones {
prepareForMultipleSkinTones()
}
else {
prepareForSingleSkinTone()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Single Skin Tone
private lazy var yellowEmoji = EmojiWithSkinTones(baseEmoji: emoji, skinTones: nil)
private lazy var yellowButton = button(for: yellowEmoji) { [weak self] emojiWithSkinTone in
self?.completion(emojiWithSkinTone)
}
private var singleSelectionButtons: [UIButton]?
private func prepareForSingleSkinTone() {
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.spacing = 8
containerView.addSubview(hStack)
hStack.autoPinEdgesToSuperviewMargins()
hStack.addArrangedSubview(yellowButton)
hStack.addArrangedSubview(.spacer(withWidth: 2))
let divider = UIView()
divider.autoSetDimension(.width, toSize: 1)
divider.themeBackgroundColor = .borderSeparator
hStack.addArrangedSubview(divider)
hStack.addArrangedSubview(.spacer(withWidth: 2))
let skinToneButtons = self.skinToneButtons(for: emoji) { [weak self] emojiWithSkinTone in
self?.completion(emojiWithSkinTone)
}
singleSelectionButtons = skinToneButtons.map { $0.button }
singleSelectionButtons?.forEach { hStack.addArrangedSubview($0) }
singleSelectionButtons?.append(yellowButton)
}
// MARK: - Multiple Skin Tones
private lazy var skinToneComponentEmoji: [Emoji] = {
guard let skinToneComponentEmoji = emoji.skinToneComponentEmoji else {
owsFailDebug("missing skin tone component emoji \(emoji)")
return []
}
return skinToneComponentEmoji
}()
private var buttonsPerComponentEmojiIndex = [Int: [(Emoji.SkinTone, UIButton)]]()
private lazy var skinToneButton = button(for: EmojiWithSkinTones(
baseEmoji: emoji,
skinTones: .init(repeating: .medium, count: skinToneComponentEmoji.count)
)) { [weak self] _ in
guard let self = self else { return }
guard self.selectedSkinTones.count == self.skinToneComponentEmoji.count else { return }
self.completion(EmojiWithSkinTones(baseEmoji: self.emoji, skinTones: self.selectedSkinTones))
}
private var selectedSkinTones = [Emoji.SkinTone]() {
didSet {
if selectedSkinTones.count == skinToneComponentEmoji.count {
skinToneButton.setTitle(
EmojiWithSkinTones(
baseEmoji: emoji,
skinTones: selectedSkinTones
).rawValue,
for: .normal
)
skinToneButton.isEnabled = true
skinToneButton.alpha = 1
} else {
skinToneButton.setTitle(
EmojiWithSkinTones(
baseEmoji: emoji,
skinTones: [.medium]
).rawValue,
for: .normal
)
skinToneButton.isEnabled = false
skinToneButton.alpha = 0.2
}
}
}
private var skinTonePerComponentEmojiIndex = [Int: Emoji.SkinTone]() {
didSet {
var selectedSkinTones = [Emoji.SkinTone]()
for idx in skinToneComponentEmoji.indices {
for (skinTone, button) in buttonsPerComponentEmojiIndex[idx] ?? [] {
if skinTonePerComponentEmojiIndex[idx] == skinTone {
selectedSkinTones.append(skinTone)
button.isSelected = true
} else {
button.isSelected = false
}
}
}
self.selectedSkinTones = selectedSkinTones
}
}
private func prepareForMultipleSkinTones() {
let vStack = UIStackView()
vStack.axis = .vertical
vStack.spacing = 6
containerView.addSubview(vStack)
vStack.autoPinEdgesToSuperviewMargins()
for (idx, emoji) in skinToneComponentEmoji.enumerated() {
let skinToneButtons = self.skinToneButtons(for: emoji) { [weak self] emojiWithSkinTone in
self?.skinTonePerComponentEmojiIndex[idx] = emojiWithSkinTone.skinTones?.first
}
buttonsPerComponentEmojiIndex[idx] = skinToneButtons
let hStack = UIStackView(arrangedSubviews: skinToneButtons.map { $0.button })
hStack.axis = .horizontal
hStack.spacing = 6
vStack.addArrangedSubview(hStack)
skinTonePerComponentEmojiIndex[idx] = preferredSkinTonePermutation?[safe: idx]
// If there's only one preferred skin tone, all the component emoji use it.
if preferredSkinTonePermutation?.count == 1 {
skinTonePerComponentEmojiIndex[idx] = preferredSkinTonePermutation?.first
} else {
skinTonePerComponentEmojiIndex[idx] = preferredSkinTonePermutation?[safe: idx]
}
}
let divider = UIView()
divider.autoSetDimension(.height, toSize: 1)
divider.themeBackgroundColor = .borderSeparator
vStack.addArrangedSubview(divider)
let leftSpacer = UIView.hStretchingSpacer()
let middleSpacer = UIView.hStretchingSpacer()
let rightSpacer = UIView.hStretchingSpacer()
let hStack = UIStackView(arrangedSubviews: [leftSpacer, yellowButton, middleSpacer, skinToneButton, rightSpacer])
hStack.axis = .horizontal
vStack.addArrangedSubview(hStack)
leftSpacer.autoMatch(.width, to: .width, of: rightSpacer)
middleSpacer.autoMatch(.width, to: .width, of: rightSpacer)
}
// MARK: - Button Helpers
func skinToneButtons(for emoji: Emoji, handler: @escaping (EmojiWithSkinTones) -> Void) -> [(skinTone: Emoji.SkinTone, button: UIButton)] {
var buttons = [(Emoji.SkinTone, UIButton)]()
for skinTone in Emoji.SkinTone.allCases {
let emojiWithSkinTone = EmojiWithSkinTones(baseEmoji: emoji, skinTones: [skinTone])
buttons.append((skinTone, button(for: emojiWithSkinTone, handler: handler)))
}
return buttons
}
func button(for emoji: EmojiWithSkinTones, handler: @escaping (EmojiWithSkinTones) -> Void) -> UIButton {
let button = OWSButton { handler(emoji) }
button.titleLabel?.font = .boldSystemFont(ofSize: 32)
button.setTitle(emoji.rawValue, for: .normal)
button.setThemeBackgroundColor(.backgroundPrimary, for: .selected)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.autoSetDimensions(to: CGSize(width: 38, height: 38))
return button
}
}