Skip to content

Instantly share code, notes, and snippets.

@j-f1
Created October 1, 2021 03:13
Show Gist options
  • Save j-f1/d1c22e5863a44e675fd2e2c62fa94334 to your computer and use it in GitHub Desktop.
Save j-f1/d1c22e5863a44e675fd2e2c62fa94334 to your computer and use it in GitHub Desktop.
//
// EmojiField.swift
// Binaries
//
// Created by Jed Fox on 10/24/20.
//
import SwiftUI
import Combine
// https://stackoverflow.com/a/44753740/5244995
class EmojiTextField: UITextField, UITextFieldDelegate {
override var textInputContextIdentifier: String? { "" }
override var textInputMode: UITextInputMode? {
UITextInputMode.activeInputModes.first { $0.primaryLanguage == "emoji" }
}
var binding: Binding<String?>
var focusBinding: Binding<Bool>
private var subscription: AnyCancellable?
init(binding: Binding<String?>, focusBinding: Binding<Bool>) {
self.binding = binding
self.focusBinding = focusBinding
super.init(frame: CGRect(origin: .zero, size: CGSize(width: 40, height: 40)))
self.autoresizingMask = []
delegate = self
subscription = NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
.sink { [unowned self] notif in
if self.isFirstResponder {
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textFieldDidBeginEditing(_ textField: UITextField) {
withAnimation {
focusBinding.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
withAnimation {
focusBinding.wrappedValue = false
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// https://stackoverflow.com/a/39425959/5244995
guard
string.count == 1,
let char = string.first,
let firstScalar = char.unicodeScalars.first
else { return false }
// characters like `3` can be turned into emoji (3️⃣) by adding additional modifying scalars afterward. Only higher-value characters can be emoji on their own
if firstScalar.properties.isEmoji && (firstScalar.value > 0x238C || char.unicodeScalars.count > 1) {
text = string
binding.wrappedValue = string
}
return false
}
}
struct EmojiField: UIViewRepresentable {
@Binding var value: String?
@Binding var isFocused: Bool
let fontSize: CGFloat
func makeUIView(context: Context) -> EmojiTextField {
let tf = EmojiTextField(binding: _value, focusBinding: _isFocused)
tf.textAlignment = .center
tf.tintColor = .clear
tf.autocorrectionType = .no
tf.becomeFirstResponder()
return tf
}
func updateUIView(_ tf: EmojiTextField, context: Context) {
if let value = value {
tf.text = value
}
tf.binding = _value
tf.font = .systemFont(ofSize: fontSize)
tf.attributedPlaceholder = NSAttributedString(
string: "tap to set an emoji",
attributes: [
.font: UIFont.systemFont(ofSize: min(fontSize, 40))
]
)
}
}
struct EmojiPickerView: View {
@Binding var value: String?
@State var focused = false
var size: CGFloat = 55
@ViewBuilder var focusRing: some View {
if value != nil {
let lineWidth = 5 + size / 20
Color.tertiarySystemFill
.frame(width: size, height: size)
.cornerRadius(size / 4)
.overlay(
RoundedRectangle(cornerRadius: size / 4 + lineWidth / 2)
.stroke(
(focused ? Color.accentColor : Color.secondary),
lineWidth: lineWidth
)
.padding(-lineWidth / 2)
)
.offset(x: 0.01 * size, y: 0.01 * size)
}
}
var body: some View {
VStack {
Divider().background(.bar)
EmojiField(value: _value, isFocused: $focused, fontSize: size * 0.75)
.frame(height: size * 0.75)
.background(focusRing)
.padding(.vertical, size * 0.25)
Spacer()
}
}
}
struct EmojiField_Previews: PreviewProvider {
static var previews: some View {
VStack {
EmojiPickerView(value: .constant("⭐️"), focused: true)
EmojiPickerView(value: .constant("⭐️"), focused: false)
Divider()
EmojiPickerView(value: .constant(nil), focused: true)
EmojiPickerView(value: .constant(nil), focused: false)
}
VStack {
EmojiPickerView(value: .constant("⭐️"), focused: true, size: 200)
EmojiPickerView(value: .constant("⭐️"), focused: false, size: 200)
Divider()
EmojiPickerView(value: .constant(nil), focused: true, size: 200)
EmojiPickerView(value: .constant(nil), focused: false, size: 200)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment