-
-
Save j-f1/d1c22e5863a44e675fd2e2c62fa94334 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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