├── .gitignore ├── EmojiPickerUI ├── EmojiPickerUI.h ├── Info.plist ├── Resources │ └── emoji.csv └── Source │ ├── Controllers │ ├── EmojiCollectionViewController.swift │ ├── EmojiPickerFlowController.swift │ └── EmojiPickerHeaderViewController.swift │ ├── EmojiPickerManager.swift │ ├── Models │ ├── Emoji.swift │ ├── EmojiDatabase.swift │ └── EmojiPickerViewModel.swift │ ├── Util │ ├── BuiltinEmojiDatabase.swift │ ├── Caret.swift │ ├── FluidTimingCurve.swift │ ├── KeyCommandSwizzler.swift │ └── UIViewController+Child.swift │ └── Views │ ├── EmojiCollectionViewCell.swift │ └── EmojiPickerWindow.swift ├── EmojiPickerUIExample.xcodeproj └── project.pbxproj ├── EmojiPickerUIExample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── LICENSE ├── README.md └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __MACOSX 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | DerivedData 17 | .idea/ 18 | Crashlytics.sh 19 | generatechangelog.sh 20 | Pods/ 21 | Carthage 22 | Provisioning 23 | Crashlytics.sh -------------------------------------------------------------------------------- /EmojiPickerUI/EmojiPickerUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiPickerUI.h 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for EmojiPickerUI. 12 | FOUNDATION_EXPORT double EmojiPickerUIVersionNumber; 13 | 14 | //! Project version string for EmojiPickerUI. 15 | FOUNDATION_EXPORT const unsigned char EmojiPickerUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /EmojiPickerUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Controllers/EmojiCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiCollectionViewController.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | final class EmojiCollectionViewController: UICollectionViewController { 13 | 14 | private static func makeLayout() -> UICollectionViewFlowLayout { 15 | let l = UICollectionViewFlowLayout() 16 | 17 | l.itemSize = CGSize(width: 48, height: 48) 18 | l.scrollDirection = .horizontal 19 | l.minimumLineSpacing = 8 20 | l.minimumInteritemSpacing = 8 21 | l.sectionInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 16) 22 | 23 | return l 24 | } 25 | 26 | let viewModel: EmojiPickerViewModel 27 | 28 | private var filteredEmoji: [Emoji]? { 29 | didSet { 30 | guard filteredEmoji != oldValue else { return } 31 | 32 | collectionView?.reloadData() 33 | } 34 | } 35 | 36 | func filteringChanged() { 37 | filteredEmoji = viewModel.filteredEmoji 38 | } 39 | 40 | init(viewModel: EmojiPickerViewModel) { 41 | self.viewModel = viewModel 42 | 43 | super.init(collectionViewLayout: Self.makeLayout()) 44 | } 45 | 46 | required init?(coder: NSCoder) { 47 | fatalError() 48 | } 49 | 50 | private static let cellIdentifier = "emojicell" 51 | 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | 55 | collectionView?.register(EmojiCollectionViewCell.self, forCellWithReuseIdentifier: Self.cellIdentifier) 56 | 57 | collectionView?.isOpaque = false 58 | collectionView?.backgroundColor = .clear 59 | } 60 | 61 | // MARK: UICollectionViewDataSource 62 | 63 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 64 | if filteredEmoji != nil { 65 | return 1 66 | } else { 67 | return viewModel.categories.count 68 | } 69 | } 70 | 71 | private func emoji(at indexPath: IndexPath) -> Emoji { 72 | if let filteredEmoji = filteredEmoji { 73 | return filteredEmoji[indexPath.item] 74 | } else { 75 | return viewModel.allEmoji(in: viewModel.categories[indexPath.section])[indexPath.item] 76 | } 77 | } 78 | 79 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 80 | if let filteredEmoji = filteredEmoji { 81 | return filteredEmoji.count 82 | } else { 83 | return viewModel.allEmoji(in: viewModel.categories[section]).count 84 | } 85 | } 86 | 87 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 88 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Self.cellIdentifier, for: indexPath) as! EmojiCollectionViewCell 89 | 90 | cell.emoji = emoji(at: indexPath) 91 | 92 | return cell 93 | } 94 | 95 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 96 | if let cell = collectionView.cellForItem(at: indexPath), let window = cell.window { 97 | postSnapshot(contentView: cell.contentView, cell: cell, window: window) 98 | } 99 | 100 | NotificationCenter.default.post(name: .FloatingEmojiPickerDidPickEmoji, object: emoji(at: indexPath).string) 101 | } 102 | 103 | private func postSnapshot(contentView: UIView, cell: UIView, window: UIWindow) { 104 | let renderer = UIGraphicsImageRenderer(bounds: contentView.bounds, format: .init(for: traitCollection)) 105 | 106 | let image = renderer.image { ctx in 107 | contentView.layer.render(in: ctx.cgContext) 108 | } 109 | 110 | let frame = cell.convert(contentView.frame, to: window) 111 | let snap = EmojiSnapshot(image: image, frame: frame) 112 | 113 | NotificationCenter.default.post(name: .FloatingEmojiPickerDidSnapshotSelectedEmoji, object: snap) 114 | } 115 | 116 | } 117 | 118 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Controllers/EmojiPickerFlowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiPickerFlowController.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | extension Notification.Name { 13 | static let FloatingEmojiPickerDidPickEmoji = Notification.Name("codes.rambo.EmojiPickerDidPickEmoji") 14 | static let FloatingEmojiPickerDidSnapshotSelectedEmoji = Notification.Name("codes.rambo.FloatingEmojiPickerDidSnapshotSelectedEmoji") 15 | } 16 | 17 | protocol EmojiPickerTransitionParticipant: AnyObject { 18 | func performEmojiPickerTransition() 19 | } 20 | 21 | final class EmojiPickerFlowController: UIViewController, EmojiPickerTransitionParticipant { 22 | 23 | private var cancellables: [Cancellable] = [] 24 | 25 | let database: EmojiDatabase 26 | 27 | init(database: EmojiDatabase) { 28 | self.database = database 29 | 30 | super.init(nibName: nil, bundle: nil) 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError() 35 | } 36 | 37 | override var keyCommands: [UIKeyCommand]? { 38 | [UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(close))] 39 | } 40 | 41 | @objc private func close() { 42 | EmojiPickerManager.shared.dismiss() 43 | } 44 | 45 | lazy var viewModel: EmojiPickerViewModel = { 46 | EmojiPickerViewModel(database: database) 47 | }() 48 | 49 | private lazy var headerController: EmojiPickerHeaderViewController = { 50 | let c = EmojiPickerHeaderViewController() 51 | 52 | c.textDidChange = { [weak self] term in 53 | self?.viewModel.searchTerm = term 54 | } 55 | 56 | return c 57 | }() 58 | 59 | private lazy var collectionController: EmojiCollectionViewController = { 60 | EmojiCollectionViewController(viewModel: viewModel) 61 | }() 62 | 63 | override func loadView() { 64 | view = UIView() 65 | view.backgroundColor = .systemBackground 66 | view.layer.cornerRadius = EmojiPickerMetrics.cornerRadius 67 | view.layer.cornerCurve = .continuous 68 | view.clipsToBounds = true 69 | 70 | addChild(collectionController) 71 | collectionController.view.translatesAutoresizingMaskIntoConstraints = false 72 | view.addSubview(collectionController.view) 73 | collectionController.didMove(toParent: self) 74 | 75 | addChild(headerController) 76 | view.addSubview(headerController.view) 77 | headerController.didMove(toParent: self) 78 | 79 | NSLayoutConstraint.activate([ 80 | headerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 81 | headerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 82 | headerController.view.topAnchor.constraint(equalTo: view.topAnchor), 83 | collectionController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 84 | collectionController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 85 | collectionController.view.topAnchor.constraint(equalTo: headerController.view.bottomAnchor), 86 | collectionController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 87 | ]) 88 | 89 | let filterBinding = viewModel.$filteredEmoji.sink { [weak self] _ in 90 | self?.collectionController.filteringChanged() 91 | } 92 | cancellables.append(filterBinding) 93 | 94 | headerController.view.alpha = 0 95 | collectionController.view.alpha = 0 96 | } 97 | 98 | func performEmojiPickerTransition() { 99 | headerController.view.alpha = 1 100 | collectionController.view.alpha = 1 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Controllers/EmojiPickerHeaderViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiPickerHeaderViewController.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Notification.Name { 12 | static let emojiPickerDidResignSearchField = Notification.Name("codes.rambo.emojiPickerDidResignSearchField") 13 | } 14 | 15 | final class EmojiPickerHeaderViewController: UIViewController { 16 | 17 | var textDidChange: (String?) -> Void = { _ in } 18 | 19 | private(set) lazy var grabberView: UIView = { 20 | let v = UIView() 21 | 22 | v.backgroundColor = .separator 23 | v.heightAnchor.constraint(equalToConstant: 6).isActive = true 24 | v.widthAnchor.constraint(equalToConstant: 36).isActive = true 25 | v.layer.cornerRadius = 3 26 | v.translatesAutoresizingMaskIntoConstraints = false 27 | if #available(iOS 13.4, *) { 28 | v.addInteraction(UIPointerInteraction()) 29 | } 30 | 31 | let pan = UIPanGestureRecognizer(target: self, action: #selector(drag(using:))) 32 | v.addGestureRecognizer(pan) 33 | 34 | return v 35 | }() 36 | 37 | private lazy var searchField: UISearchTextField = { 38 | let v = UISearchTextField() 39 | 40 | v.translatesAutoresizingMaskIntoConstraints = false 41 | v.delegate = self 42 | 43 | return v 44 | }() 45 | 46 | override func loadView() { 47 | view = UIView() 48 | view.translatesAutoresizingMaskIntoConstraints = false 49 | 50 | view.addSubview(grabberView) 51 | view.addSubview(searchField) 52 | 53 | NSLayoutConstraint.activate([ 54 | view.heightAnchor.constraint(equalToConstant: EmojiPickerMetrics.headerHeight), 55 | grabberView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), 56 | grabberView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 57 | searchField.topAnchor.constraint(equalTo: grabberView.bottomAnchor, constant: 8), 58 | searchField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), 59 | searchField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), 60 | ]) 61 | 62 | searchField.addTarget(self, action: #selector(textChanged), for: .editingChanged) 63 | } 64 | 65 | @objc private func textChanged(_ field: UITextField) { 66 | textDidChange(field.text) 67 | } 68 | 69 | override func viewDidAppear(_ animated: Bool) { 70 | super.viewDidAppear(animated) 71 | 72 | view.window?.makeKey() 73 | 74 | perform(#selector(focusSearchField), with: nil, afterDelay: 0.1) 75 | } 76 | 77 | @objc private func focusSearchField() { 78 | _ = searchField.becomeFirstResponder() 79 | } 80 | 81 | @objc private func drag(using recognizer: UIPanGestureRecognizer) { 82 | guard let target = view.window as? DraggableTarget else { return } 83 | target.handleDrag(with: recognizer) 84 | } 85 | 86 | } 87 | 88 | extension EmojiPickerHeaderViewController: UITextFieldDelegate { 89 | 90 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 91 | _ = textField.resignFirstResponder() 92 | 93 | NotificationCenter.default.post(name: .emojiPickerDidResignSearchField, object: textField) 94 | 95 | textDidChange(textField.text) 96 | 97 | return true 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/EmojiPickerManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiPickerManager.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct EmojiPickerMetrics { 12 | static let headerHeight: CGFloat = 54 13 | static let windowSize = CGSize(width: 300, height: 352) 14 | static let cornerRadius: CGFloat = 16 15 | static let verticalMarginFromCaret: CGFloat = 48 16 | static let marginFromScreenEdges: CGFloat = 22 17 | } 18 | 19 | @objc(EPUIEmojiPickerManager) 20 | @objcMembers public final class EmojiPickerManager: NSObject { 21 | 22 | public typealias TargetView = UIView & UITextInput 23 | 24 | public static let shared = EmojiPickerManager() 25 | 26 | public static func install() { 27 | UIKeyCommand.swizzleKeyCommandsForEmojiPicker() 28 | } 29 | 30 | private var window: EmojiPickerWindow? 31 | private weak var targetView: TargetView? 32 | 33 | public override init() { 34 | super.init() 35 | 36 | NotificationCenter.default.addObserver(self, selector: #selector(didPickEmoji), name: .FloatingEmojiPickerDidPickEmoji, object: nil) 37 | NotificationCenter.default.addObserver(self, selector: #selector(didResignSearchField), name: .emojiPickerDidResignSearchField, object: nil) 38 | } 39 | 40 | public lazy var database: EmojiDatabase = { 41 | guard let url = Bundle(for: EmojiPickerViewModel.self).url(forResource: "emoji", withExtension: "csv") else { 42 | fatalError("Missing emoji.csv in EmojiPickerUI bundle") 43 | } 44 | return BuiltinEmojiDatabase(url: url) 45 | }() 46 | 47 | @objc(showForView:) 48 | public func show(for view: TargetView) { 49 | self.targetView = view 50 | 51 | guard let refWindow = view.window, let scene = refWindow.windowScene else { return } 52 | 53 | window = EmojiPickerWindow(windowScene: scene) 54 | 55 | window?.rootViewController = EmojiPickerFlowController(database: database) 56 | 57 | window?.animateIn(from: view) 58 | 59 | _ = targetView?.resignFirstResponder() 60 | } 61 | 62 | @objc private func didPickEmoji(_ note: Notification) { 63 | guard let str = note.object as? String else { return } 64 | 65 | guard let target = targetView else { return } 66 | 67 | window?.animateOut(into: target, insertEmoji: { [weak target] in 68 | target?.insertText(str) 69 | }, completion: { [weak self] in 70 | self?.focusInput() 71 | }) 72 | } 73 | 74 | @objc private func didResignSearchField() { 75 | focusInput() 76 | } 77 | 78 | func dismiss() { 79 | window?.dismiss { [weak self] in 80 | self?.focusInput() 81 | } 82 | } 83 | 84 | private func focusInput() { 85 | _ = targetView?.becomeFirstResponder() 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Models/Emoji.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emoji.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Emoji: Hashable, Codable { 12 | 13 | public let string: String 14 | public var skinToneVariants: [Emoji] 15 | public var metadata: String = "" 16 | 17 | public init(string: String, skinToneVariants: [Emoji] = []) { 18 | self.string = string 19 | self.skinToneVariants = skinToneVariants 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Models/EmojiDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiDatabase.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol EmojiDatabase: AnyObject { 12 | var categories: [String] { get } 13 | func allEmoji(in category: String) -> [Emoji] 14 | func emoji(matching searchTerm: String) -> [Emoji] 15 | } 16 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Models/EmojiPickerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiPickerViewModel.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | final class EmojiPickerViewModel: ObservableObject { 13 | 14 | @Published var searchTerm: String? 15 | @Published var filteredEmoji: [Emoji]? 16 | 17 | var categories: [String] { database.categories } 18 | func allEmoji(in category: String) -> [Emoji] { database.allEmoji(in: category) } 19 | 20 | private var cancellables: [Cancellable] = [] 21 | 22 | let database: EmojiDatabase 23 | 24 | init(database: EmojiDatabase) { 25 | self.database = database 26 | 27 | let searchTermBinding = $searchTerm.sink { [unowned self] term in 28 | guard let term = term, !term.isEmpty else { 29 | self.filteredEmoji = nil 30 | return 31 | } 32 | 33 | self.filteredEmoji = self.database.emoji(matching: term) 34 | } 35 | 36 | cancellables.append(searchTermBinding) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Util/BuiltinEmojiDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuiltinEmojiDatabase.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Provided as a sample emoji database, not meant to be used in production. 12 | /// emoji.csv taken from https://www.kaggle.com/eliasdabbas/emoji-data-descriptions-codepoints/data 13 | final class BuiltinEmojiDatabase: EmojiDatabase { 14 | 15 | var categories: [String] { Array(groups) } 16 | 17 | func allEmoji(in category: String) -> [Emoji] { 18 | return categoryToEmojiMap[category] ?? [] 19 | } 20 | 21 | func emoji(matching searchTerm: String) -> [Emoji] { 22 | emojiData.filter({ $0.metadata.lowercased().contains(searchTerm.lowercased()) }) 23 | } 24 | 25 | private let url: URL 26 | 27 | init(url: URL) { 28 | self.url = url 29 | 30 | readDatabase() 31 | } 32 | 33 | private var emojiData: [Emoji] = [] 34 | private var categoryToEmojiMap: [String: [Emoji]] = [:] 35 | 36 | private var groups = Set() 37 | 38 | private func readDatabase() { 39 | do { 40 | let data = try Data(contentsOf: url) 41 | let contents = String(decoding: data, as: UTF8.self) 42 | 43 | // NOTE: This parsing is not great, but good enough for what we're doing here. 44 | contents.components(separatedBy: "\n").forEach { line in 45 | processDatabaseLine(line.components(separatedBy: ",")) 46 | } 47 | } catch { 48 | fatalError("Giving up: \(error)") 49 | } 50 | } 51 | 52 | private func processDatabaseLine(_ item: [String]) { 53 | guard item.count > 2 else { return } 54 | 55 | var emoji = Emoji(string: item[0], skinToneVariants: []) 56 | emoji.metadata = item[1] 57 | 58 | let category = item[2] 59 | 60 | if categoryToEmojiMap[category] != nil { 61 | categoryToEmojiMap[category]?.append(emoji) 62 | } else { 63 | categoryToEmojiMap[category] = [emoji] 64 | } 65 | 66 | groups.insert(category) 67 | emojiData.append(emoji) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Util/Caret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Caret.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | fileprivate let caretViewClass: AnyClass? = NSClassFromString("UITextSelectionView") 12 | 13 | extension UITextInput where Self: UIView { 14 | 15 | func findCaret() -> UIView? { 16 | recursivelyLookForCaret(startingAt: self) 17 | } 18 | 19 | private func recursivelyLookForCaret(startingAt referenceView: UIView) -> UIView? { 20 | guard let cls = caretViewClass else { return nil } 21 | 22 | if referenceView.isKind(of: cls) { return referenceView } 23 | 24 | for subview in referenceView.subviews { 25 | if let caret = recursivelyLookForCaret(startingAt: subview) { 26 | return caret 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Util/FluidTimingCurve.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FluidTimingCurve.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 05/08/19. 6 | // Copyright © 2019 Peixe Urbano. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FluidTimingCurve: NSObject, UITimingCurveProvider { 12 | 13 | public let initialVelocity: CGVector 14 | let mass: CGFloat 15 | let stiffness: CGFloat 16 | let damping: CGFloat 17 | 18 | public init(velocity: CGVector, stiffness: CGFloat = 400, damping: CGFloat = 30, mass: CGFloat = 1.0) { 19 | self.initialVelocity = velocity 20 | self.stiffness = stiffness 21 | self.damping = damping 22 | self.mass = mass 23 | 24 | super.init() 25 | } 26 | 27 | public func encode(with aCoder: NSCoder) { 28 | fatalError("Not supported") 29 | } 30 | 31 | public init?(coder aDecoder: NSCoder) { 32 | fatalError("Not supported") 33 | } 34 | 35 | public func copy(with zone: NSZone? = nil) -> Any { 36 | return FluidTimingCurve(velocity: initialVelocity) 37 | } 38 | 39 | public var timingCurveType: UITimingCurveType { 40 | return .composed 41 | } 42 | 43 | public var cubicTimingParameters: UICubicTimingParameters? { 44 | return .init(animationCurve: .easeIn) 45 | } 46 | 47 | public var springTimingParameters: UISpringTimingParameters? { 48 | return UISpringTimingParameters(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: initialVelocity) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Util/KeyCommandSwizzler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyCommandSwizzler.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ObjectiveC 11 | 12 | @objc extension UIKeyCommand { 13 | 14 | static func swizzleKeyCommandsForEmojiPicker() { 15 | guard let m1 = class_getInstanceMethod(UIResponder.self, #selector(getter: UIResponder.keyCommands)) else { return } 16 | guard let m2 = class_getInstanceMethod(Self.self, #selector(emojiPickerUI_keyCommands)) else { return } 17 | 18 | guard let originalImpl = class_getInstanceMethod(Self.self, #selector(originalKeyCommands)) else { return } 19 | 20 | class_addMethod(UIResponder.self, #selector(originalKeyCommands), method_getImplementation(originalImpl), method_getTypeEncoding(originalImpl)!) 21 | method_exchangeImplementations(m1, m2) 22 | } 23 | 24 | @objc func originalKeyCommands() -> [UIKeyCommand]? { 25 | // implementation added at runtime 26 | return nil 27 | } 28 | 29 | @objc func emojiPickerUI_keyCommands() -> [UIKeyCommand]? { 30 | guard isKind(of: UITextView.self) || isKind(of: UITextField.self) else { return originalKeyCommands() } 31 | 32 | let comm = UIKeyCommand(input: " ", modifierFlags: [.command, .control], action: #selector(UIResponder.showEmojiPicker)) 33 | 34 | guard let original = originalKeyCommands() else { 35 | return [comm] 36 | } 37 | 38 | return original + [comm] 39 | } 40 | 41 | } 42 | 43 | extension UIResponder { 44 | 45 | @objc func showEmojiPicker() { 46 | guard let target = self as? (UIView & UITextInput) else { return } 47 | 48 | EmojiPickerManager.shared.show(for: target) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Util/UIViewController+Child.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Child.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 10/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | func install(_ child: UIViewController) { 14 | addChild(child) 15 | 16 | child.view.translatesAutoresizingMaskIntoConstraints = false 17 | view.addSubview(child.view) 18 | 19 | NSLayoutConstraint.activate([ 20 | child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 21 | child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 22 | child.view.topAnchor.constraint(equalTo: view.topAnchor), 23 | child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 24 | ]) 25 | 26 | child.didMove(toParent: self) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Views/EmojiCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiCollectionViewCell.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class EmojiCollectionViewCell: UICollectionViewCell { 12 | 13 | var emoji: Emoji? { 14 | didSet { 15 | label.text = emoji?.string 16 | } 17 | } 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | 22 | setup() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError() 27 | } 28 | 29 | private lazy var label: UILabel = { 30 | let l = UILabel() 31 | 32 | l.textAlignment = .center 33 | l.font = UIFont.systemFont(ofSize: 32) 34 | l.translatesAutoresizingMaskIntoConstraints = false 35 | 36 | return l 37 | }() 38 | 39 | private func setup() { 40 | contentView.addSubview(label) 41 | 42 | NSLayoutConstraint.activate([ 43 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 44 | label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) 45 | ]) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /EmojiPickerUI/Source/Views/EmojiPickerWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiPickerWindow.swift 3 | // EmojiPickerUI 4 | // 5 | // Created by Guilherme Rambo on 25/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct EmojiSnapshot { 12 | let image: UIImage 13 | let frame: CGRect 14 | } 15 | 16 | @objc protocol DraggableTarget { 17 | func handleDrag(with recognizer: UIPanGestureRecognizer) 18 | } 19 | 20 | final class EmojiPickerWindow: UIWindow, DraggableTarget { 21 | 22 | override var isHidden: Bool { 23 | didSet { 24 | guard isHidden != oldValue, !isHidden else { return } 25 | 26 | setupLook() 27 | } 28 | } 29 | 30 | private func setupLook() { 31 | layer.cornerRadius = EmojiPickerMetrics.cornerRadius 32 | layer.cornerCurve = .continuous 33 | layer.masksToBounds = false 34 | backgroundColor = .systemBackground 35 | 36 | NotificationCenter.default.addObserver(self, selector: #selector(receiveEmojiSnapshot), name: .FloatingEmojiPickerDidSnapshotSelectedEmoji, object: nil) 37 | } 38 | 39 | private func addShadow() { 40 | layer.shadowColor = UIColor.black.cgColor 41 | layer.shadowOpacity = 0.1 42 | layer.shadowRadius = 22 43 | layer.shadowOffset = CGSize(width: 1, height: 1) 44 | } 45 | 46 | private var currentAnimator: UIViewPropertyAnimator? 47 | 48 | private var caretPosition: CGPoint = .zero 49 | private var hiddenFrame: CGRect = .zero 50 | 51 | private lazy var rootSnapshotView: UIView = { 52 | let v = UIView() 53 | v.autoresizingMask = [.flexibleWidth, .flexibleHeight] 54 | return v 55 | }() 56 | 57 | private lazy var selectedEmojiSnapshotView: UIView = { 58 | let v = UIView() 59 | v.autoresizingMask = [] 60 | return v 61 | }() 62 | 63 | @objc private func receiveEmojiSnapshot(_ note: Notification) { 64 | guard let snap = note.object as? EmojiSnapshot else { return } 65 | 66 | selectedEmojiSnapshotView.layer.contents = snap.image.cgImage 67 | selectedEmojiSnapshotView.frame = snap.frame 68 | } 69 | 70 | private func cancelPendingAnimation() { 71 | guard let anim = currentAnimator, anim.state != .stopped else { return } 72 | 73 | currentAnimator?.stopAnimation(true) 74 | currentAnimator = nil 75 | } 76 | 77 | func animateIn(from input: UITextInput & UIView) { 78 | cancelPendingAnimation() 79 | 80 | guard let caret = input.findCaret()?.subviews.first else { 81 | isHidden = false 82 | return 83 | } 84 | 85 | guard let refWindow = input.window else { return } 86 | 87 | caretPosition = input.convert(caret.frame.origin, to: refWindow) 88 | 89 | hiddenFrame = CGRect( 90 | x: caretPosition.x - caret.frame.width/2, 91 | y: caretPosition.y - caret.frame.height/2, 92 | width: caret.frame.width, 93 | height: caret.frame.height 94 | ) 95 | 96 | var finalFrame = CGRect( 97 | x: caretPosition.x - EmojiPickerMetrics.windowSize.width/2, 98 | y: caretPosition.y - EmojiPickerMetrics.windowSize.height - EmojiPickerMetrics.verticalMarginFromCaret, 99 | width: EmojiPickerMetrics.windowSize.width, 100 | height: EmojiPickerMetrics.windowSize.height 101 | ) 102 | 103 | finalFrame = validateFrame(finalFrame, adjustY: { [weak self] validFrame in 104 | guard let self = self else { return } 105 | 106 | validFrame.origin.y = self.caretPosition.y + EmojiPickerMetrics.verticalMarginFromCaret 107 | }) 108 | 109 | guard !UIAccessibility.isReduceMotionEnabled else { 110 | frame = finalFrame 111 | isHidden = false 112 | return 113 | } 114 | 115 | let finalBackgroundColor = backgroundColor 116 | 117 | frame = hiddenFrame 118 | 119 | backgroundColor = caret.backgroundColor 120 | 121 | isHidden = false 122 | 123 | let curve = FluidTimingCurve(velocity: CGVector(dx: 0.7, dy: 0.7)) 124 | 125 | currentAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: curve) 126 | 127 | currentAnimator?.addAnimations { 128 | self.addShadow() 129 | self.frame = finalFrame 130 | self.backgroundColor = finalBackgroundColor 131 | } 132 | 133 | currentAnimator?.addAnimations({ 134 | if let participant = self.rootViewController as? EmojiPickerTransitionParticipant { 135 | participant.performEmojiPickerTransition() 136 | } 137 | }, delayFactor: 0.3) 138 | 139 | currentAnimator?.startAnimation() 140 | } 141 | 142 | func animateOut(into input: UITextInput & UIView, insertEmoji: @escaping () -> Void, completion: @escaping () -> Void) { 143 | guard !UIAccessibility.isReduceMotionEnabled else { 144 | insertEmoji() 145 | isHidden = true 146 | completion() 147 | return 148 | } 149 | 150 | cancelPendingAnimation() 151 | 152 | guard let contentView = rootViewController?.view else { return } 153 | 154 | let renderer = UIGraphicsImageRenderer(bounds: contentView.bounds, format: .init(for: contentView.traitCollection)) 155 | 156 | let contentImage = renderer.image { ctx in 157 | contentView.layer.render(in: ctx.cgContext) 158 | } 159 | 160 | CATransaction.begin() 161 | CATransaction.setDisableActions(true) 162 | CATransaction.setAnimationDuration(0) 163 | 164 | rootSnapshotView.layer.cornerRadius = EmojiPickerMetrics.cornerRadius 165 | rootSnapshotView.layer.cornerCurve = .continuous 166 | rootSnapshotView.layer.masksToBounds = true 167 | rootSnapshotView.layer.contents = contentImage.cgImage 168 | rootSnapshotView.frame = bounds 169 | 170 | addSubview(rootSnapshotView) 171 | addSubview(selectedEmojiSnapshotView) 172 | contentView.removeFromSuperview() 173 | 174 | CATransaction.commit() 175 | 176 | let curve = FluidTimingCurve(velocity: CGVector(dx: 0.7, dy: 0.7)) 177 | // let curve = UICubicTimingParameters(animationCurve: .easeInOut) 178 | 179 | currentAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: curve) 180 | 181 | currentAnimator?.addAnimations { 182 | self.layer.shadowOpacity = 0 183 | self.frame = self.hiddenFrame 184 | 185 | self.rootSnapshotView.alpha = 0 186 | self.rootSnapshotView.frame = CGRect(origin: .zero, size: self.hiddenFrame.size) 187 | 188 | self.selectedEmojiSnapshotView.frame = CGRect( 189 | x: 0, 190 | y: 0, 191 | width: self.selectedEmojiSnapshotView.frame.width*0.6, 192 | height: self.selectedEmojiSnapshotView.frame.height*0.6 193 | ) 194 | self.selectedEmojiSnapshotView.alpha = 0 195 | } 196 | 197 | currentAnimator?.addAnimations({ 198 | insertEmoji() 199 | }, delayFactor: 0.8) 200 | 201 | currentAnimator?.addCompletion { _ in 202 | self.isHidden = true 203 | completion() 204 | } 205 | 206 | currentAnimator?.startAnimation() 207 | } 208 | 209 | func dismiss(completion: @escaping () -> Void) { 210 | guard !UIAccessibility.isReduceMotionEnabled else { 211 | isHidden = true 212 | return 213 | } 214 | 215 | cancelPendingAnimation() 216 | 217 | let curve = UICubicTimingParameters(animationCurve: .easeInOut) 218 | currentAnimator = UIViewPropertyAnimator(duration: 0.3, timingParameters: curve) 219 | 220 | currentAnimator?.addAnimations { 221 | self.layer.transform = CATransform3DMakeScale(1.1, 1.1, 1) 222 | self.alpha = 0 223 | } 224 | 225 | currentAnimator?.addCompletion { _ in 226 | self.isHidden = true 227 | completion() 228 | } 229 | 230 | currentAnimator?.startAnimation() 231 | } 232 | 233 | private func rubberBandValue(for position: CGFloat, limit: CGFloat) -> CGFloat { 234 | limit * (1 + log10(position/limit)) 235 | } 236 | 237 | private lazy var extrapolatedFrame: CGRect = { frame }() 238 | 239 | private var effectiveScreenHeight: CGFloat { 240 | let margin: CGFloat = 36 241 | 242 | guard let screen = windowScene?.screen else { return UIScreen.main.bounds.height - margin } 243 | 244 | return screen.bounds.height - margin 245 | } 246 | 247 | func handleDrag(with recognizer: UIPanGestureRecognizer) { 248 | let translation = recognizer.translation(in: nil) 249 | 250 | switch recognizer.state { 251 | case .began: 252 | extrapolatedFrame = frame 253 | case .changed: 254 | var f = frame 255 | 256 | f.origin.x += translation.x 257 | f.origin.y += translation.y 258 | 259 | extrapolatedFrame.origin.x += translation.x 260 | extrapolatedFrame.origin.y += translation.y 261 | 262 | // Rubber band when going beyond allowed screen edges 263 | 264 | if extrapolatedFrame.origin.x + extrapolatedFrame.width > screen.bounds.width { 265 | f.origin.x = rubberBandValue(for: (extrapolatedFrame.origin.x + extrapolatedFrame.width), limit: screen.bounds.width) - extrapolatedFrame.width 266 | } 267 | if extrapolatedFrame.origin.y + extrapolatedFrame.height > effectiveScreenHeight { 268 | f.origin.y = rubberBandValue(for: (extrapolatedFrame.origin.y + extrapolatedFrame.height), limit: effectiveScreenHeight) - extrapolatedFrame.height 269 | } 270 | if extrapolatedFrame.origin.y < 0 { 271 | f.origin.y = -rubberBandValue(for: abs(extrapolatedFrame.origin.y), limit: EmojiPickerMetrics.marginFromScreenEdges) 272 | } 273 | if extrapolatedFrame.origin.x < 0 { 274 | f.origin.x = -rubberBandValue(for: abs(extrapolatedFrame.origin.x), limit: EmojiPickerMetrics.marginFromScreenEdges) 275 | } 276 | 277 | frame = f 278 | case .ended, .cancelled, .failed: 279 | snapToValidFrame() 280 | default: 281 | break 282 | } 283 | 284 | recognizer.setTranslation(.zero, in: nil) 285 | } 286 | 287 | private func validateFrame(_ subject: CGRect, adjustY: ((inout CGRect) -> Void)? = nil) -> CGRect { 288 | guard let screen = windowScene?.screen else { return subject } 289 | 290 | var f = subject 291 | 292 | if f.origin.x - EmojiPickerMetrics.marginFromScreenEdges < 0 { 293 | f.origin.x = EmojiPickerMetrics.marginFromScreenEdges 294 | } 295 | if f.origin.x + f.width > screen.bounds.width { 296 | f.origin.x = screen.bounds.width - f.width - EmojiPickerMetrics.marginFromScreenEdges 297 | } 298 | if f.origin.y - EmojiPickerMetrics.marginFromScreenEdges < 0 { 299 | if let adjustY = adjustY { 300 | adjustY(&f) 301 | } else { 302 | f.origin.y = EmojiPickerMetrics.marginFromScreenEdges 303 | } 304 | } 305 | if f.origin.y + f.height > effectiveScreenHeight { 306 | f.origin.y = effectiveScreenHeight - f.height - EmojiPickerMetrics.marginFromScreenEdges 307 | } 308 | 309 | return f 310 | } 311 | 312 | private func snapToValidFrame() { 313 | cancelPendingAnimation() 314 | 315 | let validFrame = validateFrame(frame) 316 | 317 | let curve = FluidTimingCurve(velocity: CGVector(dx: 0.7, dy: 0.7)) 318 | 319 | currentAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: curve) 320 | 321 | currentAnimator?.addAnimations { 322 | self.frame = validFrame 323 | } 324 | 325 | currentAnimator?.startAnimation() 326 | } 327 | 328 | } 329 | -------------------------------------------------------------------------------- /EmojiPickerUIExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DD97DCFC247DCDDE00356CE6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DCFB247DCDDE00356CE6 /* AppDelegate.swift */; }; 11 | DD97DCFE247DCDDE00356CE6 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DCFD247DCDDE00356CE6 /* SceneDelegate.swift */; }; 12 | DD97DD00247DCDDE00356CE6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DCFF247DCDDE00356CE6 /* ViewController.swift */; }; 13 | DD97DD03247DCDDE00356CE6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD97DD01247DCDDE00356CE6 /* Main.storyboard */; }; 14 | DD97DD05247DCDDF00356CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD97DD04247DCDDF00356CE6 /* Assets.xcassets */; }; 15 | DD97DD08247DCDDF00356CE6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD97DD06247DCDDF00356CE6 /* LaunchScreen.storyboard */; }; 16 | DD97DD18247DCEED00356CE6 /* EmojiPickerUI.h in Headers */ = {isa = PBXBuildFile; fileRef = DD97DD16247DCEED00356CE6 /* EmojiPickerUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | DD97DD1B247DCEED00356CE6 /* EmojiPickerUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD97DD14247DCEED00356CE6 /* EmojiPickerUI.framework */; }; 18 | DD97DD1C247DCEED00356CE6 /* EmojiPickerUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD97DD14247DCEED00356CE6 /* EmojiPickerUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 19 | DD97DD36247DCF0100356CE6 /* emoji.csv in Resources */ = {isa = PBXBuildFile; fileRef = DD97DD22247DCF0100356CE6 /* emoji.csv */; }; 20 | DD97DD37247DCF0100356CE6 /* UIViewController+Child.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD25247DCF0100356CE6 /* UIViewController+Child.swift */; }; 21 | DD97DD38247DCF0100356CE6 /* Caret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD26247DCF0100356CE6 /* Caret.swift */; }; 22 | DD97DD39247DCF0100356CE6 /* FluidTimingCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD27247DCF0100356CE6 /* FluidTimingCurve.swift */; }; 23 | DD97DD3A247DCF0100356CE6 /* EmojiPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD29247DCF0100356CE6 /* EmojiPickerViewModel.swift */; }; 24 | DD97DD3B247DCF0100356CE6 /* EmojiDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD2A247DCF0100356CE6 /* EmojiDatabase.swift */; }; 25 | DD97DD3C247DCF0100356CE6 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD2B247DCF0100356CE6 /* Emoji.swift */; }; 26 | DD97DD3D247DCF0100356CE6 /* BuiltinEmojiDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD2C247DCF0100356CE6 /* BuiltinEmojiDatabase.swift */; }; 27 | DD97DD3E247DCF0100356CE6 /* EmojiPickerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD2D247DCF0100356CE6 /* EmojiPickerManager.swift */; }; 28 | DD97DD3F247DCF0100356CE6 /* EmojiPickerHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD2F247DCF0100356CE6 /* EmojiPickerHeaderViewController.swift */; }; 29 | DD97DD40247DCF0100356CE6 /* EmojiPickerFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD30247DCF0100356CE6 /* EmojiPickerFlowController.swift */; }; 30 | DD97DD41247DCF0100356CE6 /* EmojiCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD31247DCF0100356CE6 /* EmojiCollectionViewController.swift */; }; 31 | DD97DD42247DCF0100356CE6 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD33247DCF0100356CE6 /* EmojiCollectionViewCell.swift */; }; 32 | DD97DD43247DCF0100356CE6 /* EmojiPickerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD34247DCF0100356CE6 /* EmojiPickerWindow.swift */; }; 33 | DD97DD46247DCF9100356CE6 /* KeyCommandSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97DD45247DCF9100356CE6 /* KeyCommandSwizzler.swift */; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | DD97DD19247DCEED00356CE6 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = DD97DCF0247DCDDE00356CE6 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = DD97DD13247DCEED00356CE6; 42 | remoteInfo = EmojiPickerUI; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXCopyFilesBuildPhase section */ 47 | DD97DD20247DCEED00356CE6 /* Embed Frameworks */ = { 48 | isa = PBXCopyFilesBuildPhase; 49 | buildActionMask = 2147483647; 50 | dstPath = ""; 51 | dstSubfolderSpec = 10; 52 | files = ( 53 | DD97DD1C247DCEED00356CE6 /* EmojiPickerUI.framework in Embed Frameworks */, 54 | ); 55 | name = "Embed Frameworks"; 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXCopyFilesBuildPhase section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | DD97DCF8247DCDDE00356CE6 /* EmojiPickerUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmojiPickerUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | DD97DCFB247DCDDE00356CE6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 63 | DD97DCFD247DCDDE00356CE6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 64 | DD97DCFF247DCDDE00356CE6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 65 | DD97DD02247DCDDE00356CE6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 66 | DD97DD04247DCDDF00356CE6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 67 | DD97DD07247DCDDF00356CE6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 68 | DD97DD09247DCDDF00356CE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | DD97DD14247DCEED00356CE6 /* EmojiPickerUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EmojiPickerUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | DD97DD16247DCEED00356CE6 /* EmojiPickerUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EmojiPickerUI.h; sourceTree = ""; }; 71 | DD97DD17247DCEED00356CE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 72 | DD97DD22247DCF0100356CE6 /* emoji.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = emoji.csv; sourceTree = ""; }; 73 | DD97DD25247DCF0100356CE6 /* UIViewController+Child.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Child.swift"; sourceTree = ""; }; 74 | DD97DD26247DCF0100356CE6 /* Caret.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Caret.swift; sourceTree = ""; }; 75 | DD97DD27247DCF0100356CE6 /* FluidTimingCurve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FluidTimingCurve.swift; sourceTree = ""; }; 76 | DD97DD29247DCF0100356CE6 /* EmojiPickerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewModel.swift; sourceTree = ""; }; 77 | DD97DD2A247DCF0100356CE6 /* EmojiDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiDatabase.swift; sourceTree = ""; }; 78 | DD97DD2B247DCF0100356CE6 /* Emoji.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; 79 | DD97DD2C247DCF0100356CE6 /* BuiltinEmojiDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuiltinEmojiDatabase.swift; sourceTree = ""; }; 80 | DD97DD2D247DCF0100356CE6 /* EmojiPickerManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerManager.swift; sourceTree = ""; }; 81 | DD97DD2F247DCF0100356CE6 /* EmojiPickerHeaderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerHeaderViewController.swift; sourceTree = ""; }; 82 | DD97DD30247DCF0100356CE6 /* EmojiPickerFlowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerFlowController.swift; sourceTree = ""; }; 83 | DD97DD31247DCF0100356CE6 /* EmojiCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewController.swift; sourceTree = ""; }; 84 | DD97DD33247DCF0100356CE6 /* EmojiCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewCell.swift; sourceTree = ""; }; 85 | DD97DD34247DCF0100356CE6 /* EmojiPickerWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerWindow.swift; sourceTree = ""; }; 86 | DD97DD45247DCF9100356CE6 /* KeyCommandSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCommandSwizzler.swift; sourceTree = ""; }; 87 | /* End PBXFileReference section */ 88 | 89 | /* Begin PBXFrameworksBuildPhase section */ 90 | DD97DCF5247DCDDE00356CE6 /* Frameworks */ = { 91 | isa = PBXFrameworksBuildPhase; 92 | buildActionMask = 2147483647; 93 | files = ( 94 | DD97DD1B247DCEED00356CE6 /* EmojiPickerUI.framework in Frameworks */, 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | DD97DD11247DCEED00356CE6 /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | ); 103 | runOnlyForDeploymentPostprocessing = 0; 104 | }; 105 | /* End PBXFrameworksBuildPhase section */ 106 | 107 | /* Begin PBXGroup section */ 108 | DD97DCEF247DCDDE00356CE6 = { 109 | isa = PBXGroup; 110 | children = ( 111 | DD97DCFA247DCDDE00356CE6 /* EmojiPickerUIExample */, 112 | DD97DD15247DCEED00356CE6 /* EmojiPickerUI */, 113 | DD97DCF9247DCDDE00356CE6 /* Products */, 114 | ); 115 | sourceTree = ""; 116 | }; 117 | DD97DCF9247DCDDE00356CE6 /* Products */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | DD97DCF8247DCDDE00356CE6 /* EmojiPickerUIExample.app */, 121 | DD97DD14247DCEED00356CE6 /* EmojiPickerUI.framework */, 122 | ); 123 | name = Products; 124 | sourceTree = ""; 125 | }; 126 | DD97DCFA247DCDDE00356CE6 /* EmojiPickerUIExample */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | DD97DCFB247DCDDE00356CE6 /* AppDelegate.swift */, 130 | DD97DCFD247DCDDE00356CE6 /* SceneDelegate.swift */, 131 | DD97DCFF247DCDDE00356CE6 /* ViewController.swift */, 132 | DD97DD01247DCDDE00356CE6 /* Main.storyboard */, 133 | DD97DD04247DCDDF00356CE6 /* Assets.xcassets */, 134 | DD97DD06247DCDDF00356CE6 /* LaunchScreen.storyboard */, 135 | DD97DD09247DCDDF00356CE6 /* Info.plist */, 136 | ); 137 | path = EmojiPickerUIExample; 138 | sourceTree = ""; 139 | }; 140 | DD97DD15247DCEED00356CE6 /* EmojiPickerUI */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | DD97DD21247DCF0100356CE6 /* Resources */, 144 | DD97DD23247DCF0100356CE6 /* Source */, 145 | DD97DD16247DCEED00356CE6 /* EmojiPickerUI.h */, 146 | DD97DD17247DCEED00356CE6 /* Info.plist */, 147 | ); 148 | path = EmojiPickerUI; 149 | sourceTree = ""; 150 | }; 151 | DD97DD21247DCF0100356CE6 /* Resources */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | DD97DD22247DCF0100356CE6 /* emoji.csv */, 155 | ); 156 | path = Resources; 157 | sourceTree = ""; 158 | }; 159 | DD97DD23247DCF0100356CE6 /* Source */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | DD97DD24247DCF0100356CE6 /* Util */, 163 | DD97DD28247DCF0100356CE6 /* Models */, 164 | DD97DD2E247DCF0100356CE6 /* Controllers */, 165 | DD97DD32247DCF0100356CE6 /* Views */, 166 | DD97DD2D247DCF0100356CE6 /* EmojiPickerManager.swift */, 167 | ); 168 | path = Source; 169 | sourceTree = ""; 170 | }; 171 | DD97DD24247DCF0100356CE6 /* Util */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | DD97DD45247DCF9100356CE6 /* KeyCommandSwizzler.swift */, 175 | DD97DD2C247DCF0100356CE6 /* BuiltinEmojiDatabase.swift */, 176 | DD97DD25247DCF0100356CE6 /* UIViewController+Child.swift */, 177 | DD97DD26247DCF0100356CE6 /* Caret.swift */, 178 | DD97DD27247DCF0100356CE6 /* FluidTimingCurve.swift */, 179 | ); 180 | path = Util; 181 | sourceTree = ""; 182 | }; 183 | DD97DD28247DCF0100356CE6 /* Models */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | DD97DD29247DCF0100356CE6 /* EmojiPickerViewModel.swift */, 187 | DD97DD2A247DCF0100356CE6 /* EmojiDatabase.swift */, 188 | DD97DD2B247DCF0100356CE6 /* Emoji.swift */, 189 | ); 190 | path = Models; 191 | sourceTree = ""; 192 | }; 193 | DD97DD2E247DCF0100356CE6 /* Controllers */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | DD97DD2F247DCF0100356CE6 /* EmojiPickerHeaderViewController.swift */, 197 | DD97DD30247DCF0100356CE6 /* EmojiPickerFlowController.swift */, 198 | DD97DD31247DCF0100356CE6 /* EmojiCollectionViewController.swift */, 199 | ); 200 | path = Controllers; 201 | sourceTree = ""; 202 | }; 203 | DD97DD32247DCF0100356CE6 /* Views */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | DD97DD33247DCF0100356CE6 /* EmojiCollectionViewCell.swift */, 207 | DD97DD34247DCF0100356CE6 /* EmojiPickerWindow.swift */, 208 | ); 209 | path = Views; 210 | sourceTree = ""; 211 | }; 212 | /* End PBXGroup section */ 213 | 214 | /* Begin PBXHeadersBuildPhase section */ 215 | DD97DD0F247DCEED00356CE6 /* Headers */ = { 216 | isa = PBXHeadersBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | DD97DD18247DCEED00356CE6 /* EmojiPickerUI.h in Headers */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | /* End PBXHeadersBuildPhase section */ 224 | 225 | /* Begin PBXNativeTarget section */ 226 | DD97DCF7247DCDDE00356CE6 /* EmojiPickerUIExample */ = { 227 | isa = PBXNativeTarget; 228 | buildConfigurationList = DD97DD0C247DCDDF00356CE6 /* Build configuration list for PBXNativeTarget "EmojiPickerUIExample" */; 229 | buildPhases = ( 230 | DD97DCF4247DCDDE00356CE6 /* Sources */, 231 | DD97DCF5247DCDDE00356CE6 /* Frameworks */, 232 | DD97DCF6247DCDDE00356CE6 /* Resources */, 233 | DD97DD20247DCEED00356CE6 /* Embed Frameworks */, 234 | ); 235 | buildRules = ( 236 | ); 237 | dependencies = ( 238 | DD97DD1A247DCEED00356CE6 /* PBXTargetDependency */, 239 | ); 240 | name = EmojiPickerUIExample; 241 | productName = EmojiPickerUIExample; 242 | productReference = DD97DCF8247DCDDE00356CE6 /* EmojiPickerUIExample.app */; 243 | productType = "com.apple.product-type.application"; 244 | }; 245 | DD97DD13247DCEED00356CE6 /* EmojiPickerUI */ = { 246 | isa = PBXNativeTarget; 247 | buildConfigurationList = DD97DD1D247DCEED00356CE6 /* Build configuration list for PBXNativeTarget "EmojiPickerUI" */; 248 | buildPhases = ( 249 | DD97DD0F247DCEED00356CE6 /* Headers */, 250 | DD97DD10247DCEED00356CE6 /* Sources */, 251 | DD97DD11247DCEED00356CE6 /* Frameworks */, 252 | DD97DD12247DCEED00356CE6 /* Resources */, 253 | ); 254 | buildRules = ( 255 | ); 256 | dependencies = ( 257 | ); 258 | name = EmojiPickerUI; 259 | productName = EmojiPickerUI; 260 | productReference = DD97DD14247DCEED00356CE6 /* EmojiPickerUI.framework */; 261 | productType = "com.apple.product-type.framework"; 262 | }; 263 | /* End PBXNativeTarget section */ 264 | 265 | /* Begin PBXProject section */ 266 | DD97DCF0247DCDDE00356CE6 /* Project object */ = { 267 | isa = PBXProject; 268 | attributes = { 269 | LastSwiftUpdateCheck = 1150; 270 | LastUpgradeCheck = 1150; 271 | ORGANIZATIONNAME = "Guilherme Rambo"; 272 | TargetAttributes = { 273 | DD97DCF7247DCDDE00356CE6 = { 274 | CreatedOnToolsVersion = 11.5; 275 | }; 276 | DD97DD13247DCEED00356CE6 = { 277 | CreatedOnToolsVersion = 11.5; 278 | }; 279 | }; 280 | }; 281 | buildConfigurationList = DD97DCF3247DCDDE00356CE6 /* Build configuration list for PBXProject "EmojiPickerUIExample" */; 282 | compatibilityVersion = "Xcode 9.3"; 283 | developmentRegion = en; 284 | hasScannedForEncodings = 0; 285 | knownRegions = ( 286 | en, 287 | Base, 288 | ); 289 | mainGroup = DD97DCEF247DCDDE00356CE6; 290 | productRefGroup = DD97DCF9247DCDDE00356CE6 /* Products */; 291 | projectDirPath = ""; 292 | projectRoot = ""; 293 | targets = ( 294 | DD97DCF7247DCDDE00356CE6 /* EmojiPickerUIExample */, 295 | DD97DD13247DCEED00356CE6 /* EmojiPickerUI */, 296 | ); 297 | }; 298 | /* End PBXProject section */ 299 | 300 | /* Begin PBXResourcesBuildPhase section */ 301 | DD97DCF6247DCDDE00356CE6 /* Resources */ = { 302 | isa = PBXResourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | DD97DD08247DCDDF00356CE6 /* LaunchScreen.storyboard in Resources */, 306 | DD97DD05247DCDDF00356CE6 /* Assets.xcassets in Resources */, 307 | DD97DD03247DCDDE00356CE6 /* Main.storyboard in Resources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | DD97DD12247DCEED00356CE6 /* Resources */ = { 312 | isa = PBXResourcesBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | DD97DD36247DCF0100356CE6 /* emoji.csv in Resources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | /* End PBXResourcesBuildPhase section */ 320 | 321 | /* Begin PBXSourcesBuildPhase section */ 322 | DD97DCF4247DCDDE00356CE6 /* Sources */ = { 323 | isa = PBXSourcesBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | DD97DD00247DCDDE00356CE6 /* ViewController.swift in Sources */, 327 | DD97DCFC247DCDDE00356CE6 /* AppDelegate.swift in Sources */, 328 | DD97DCFE247DCDDE00356CE6 /* SceneDelegate.swift in Sources */, 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | DD97DD10247DCEED00356CE6 /* Sources */ = { 333 | isa = PBXSourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | DD97DD40247DCF0100356CE6 /* EmojiPickerFlowController.swift in Sources */, 337 | DD97DD43247DCF0100356CE6 /* EmojiPickerWindow.swift in Sources */, 338 | DD97DD3E247DCF0100356CE6 /* EmojiPickerManager.swift in Sources */, 339 | DD97DD3C247DCF0100356CE6 /* Emoji.swift in Sources */, 340 | DD97DD37247DCF0100356CE6 /* UIViewController+Child.swift in Sources */, 341 | DD97DD46247DCF9100356CE6 /* KeyCommandSwizzler.swift in Sources */, 342 | DD97DD39247DCF0100356CE6 /* FluidTimingCurve.swift in Sources */, 343 | DD97DD38247DCF0100356CE6 /* Caret.swift in Sources */, 344 | DD97DD3B247DCF0100356CE6 /* EmojiDatabase.swift in Sources */, 345 | DD97DD42247DCF0100356CE6 /* EmojiCollectionViewCell.swift in Sources */, 346 | DD97DD41247DCF0100356CE6 /* EmojiCollectionViewController.swift in Sources */, 347 | DD97DD3F247DCF0100356CE6 /* EmojiPickerHeaderViewController.swift in Sources */, 348 | DD97DD3D247DCF0100356CE6 /* BuiltinEmojiDatabase.swift in Sources */, 349 | DD97DD3A247DCF0100356CE6 /* EmojiPickerViewModel.swift in Sources */, 350 | ); 351 | runOnlyForDeploymentPostprocessing = 0; 352 | }; 353 | /* End PBXSourcesBuildPhase section */ 354 | 355 | /* Begin PBXTargetDependency section */ 356 | DD97DD1A247DCEED00356CE6 /* PBXTargetDependency */ = { 357 | isa = PBXTargetDependency; 358 | target = DD97DD13247DCEED00356CE6 /* EmojiPickerUI */; 359 | targetProxy = DD97DD19247DCEED00356CE6 /* PBXContainerItemProxy */; 360 | }; 361 | /* End PBXTargetDependency section */ 362 | 363 | /* Begin PBXVariantGroup section */ 364 | DD97DD01247DCDDE00356CE6 /* Main.storyboard */ = { 365 | isa = PBXVariantGroup; 366 | children = ( 367 | DD97DD02247DCDDE00356CE6 /* Base */, 368 | ); 369 | name = Main.storyboard; 370 | sourceTree = ""; 371 | }; 372 | DD97DD06247DCDDF00356CE6 /* LaunchScreen.storyboard */ = { 373 | isa = PBXVariantGroup; 374 | children = ( 375 | DD97DD07247DCDDF00356CE6 /* Base */, 376 | ); 377 | name = LaunchScreen.storyboard; 378 | sourceTree = ""; 379 | }; 380 | /* End PBXVariantGroup section */ 381 | 382 | /* Begin XCBuildConfiguration section */ 383 | DD97DD0A247DCDDF00356CE6 /* Debug */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ALWAYS_SEARCH_USER_PATHS = NO; 387 | CLANG_ANALYZER_NONNULL = YES; 388 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 390 | CLANG_CXX_LIBRARY = "libc++"; 391 | CLANG_ENABLE_MODULES = YES; 392 | CLANG_ENABLE_OBJC_ARC = YES; 393 | CLANG_ENABLE_OBJC_WEAK = YES; 394 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 395 | CLANG_WARN_BOOL_CONVERSION = YES; 396 | CLANG_WARN_COMMA = YES; 397 | CLANG_WARN_CONSTANT_CONVERSION = YES; 398 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 399 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 400 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 401 | CLANG_WARN_EMPTY_BODY = YES; 402 | CLANG_WARN_ENUM_CONVERSION = YES; 403 | CLANG_WARN_INFINITE_RECURSION = YES; 404 | CLANG_WARN_INT_CONVERSION = YES; 405 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 406 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 407 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 408 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 409 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 410 | CLANG_WARN_STRICT_PROTOTYPES = YES; 411 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 412 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 413 | CLANG_WARN_UNREACHABLE_CODE = YES; 414 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 415 | COPY_PHASE_STRIP = NO; 416 | DEBUG_INFORMATION_FORMAT = dwarf; 417 | ENABLE_STRICT_OBJC_MSGSEND = YES; 418 | ENABLE_TESTABILITY = YES; 419 | GCC_C_LANGUAGE_STANDARD = gnu11; 420 | GCC_DYNAMIC_NO_PIC = NO; 421 | GCC_NO_COMMON_BLOCKS = YES; 422 | GCC_OPTIMIZATION_LEVEL = 0; 423 | GCC_PREPROCESSOR_DEFINITIONS = ( 424 | "DEBUG=1", 425 | "$(inherited)", 426 | ); 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 434 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 435 | MTL_FAST_MATH = YES; 436 | ONLY_ACTIVE_ARCH = YES; 437 | SDKROOT = iphoneos; 438 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 439 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 440 | }; 441 | name = Debug; 442 | }; 443 | DD97DD0B247DCDDF00356CE6 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ALWAYS_SEARCH_USER_PATHS = NO; 447 | CLANG_ANALYZER_NONNULL = YES; 448 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 449 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 450 | CLANG_CXX_LIBRARY = "libc++"; 451 | CLANG_ENABLE_MODULES = YES; 452 | CLANG_ENABLE_OBJC_ARC = YES; 453 | CLANG_ENABLE_OBJC_WEAK = YES; 454 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 455 | CLANG_WARN_BOOL_CONVERSION = YES; 456 | CLANG_WARN_COMMA = YES; 457 | CLANG_WARN_CONSTANT_CONVERSION = YES; 458 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 459 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 460 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 461 | CLANG_WARN_EMPTY_BODY = YES; 462 | CLANG_WARN_ENUM_CONVERSION = YES; 463 | CLANG_WARN_INFINITE_RECURSION = YES; 464 | CLANG_WARN_INT_CONVERSION = YES; 465 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 466 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 467 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 468 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 469 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 470 | CLANG_WARN_STRICT_PROTOTYPES = YES; 471 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 472 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 473 | CLANG_WARN_UNREACHABLE_CODE = YES; 474 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 475 | COPY_PHASE_STRIP = NO; 476 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 477 | ENABLE_NS_ASSERTIONS = NO; 478 | ENABLE_STRICT_OBJC_MSGSEND = YES; 479 | GCC_C_LANGUAGE_STANDARD = gnu11; 480 | GCC_NO_COMMON_BLOCKS = YES; 481 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 482 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 483 | GCC_WARN_UNDECLARED_SELECTOR = YES; 484 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 485 | GCC_WARN_UNUSED_FUNCTION = YES; 486 | GCC_WARN_UNUSED_VARIABLE = YES; 487 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 488 | MTL_ENABLE_DEBUG_INFO = NO; 489 | MTL_FAST_MATH = YES; 490 | SDKROOT = iphoneos; 491 | SWIFT_COMPILATION_MODE = wholemodule; 492 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 493 | VALIDATE_PRODUCT = YES; 494 | }; 495 | name = Release; 496 | }; 497 | DD97DD0D247DCDDF00356CE6 /* Debug */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 501 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 502 | CODE_SIGN_STYLE = Automatic; 503 | DEVELOPMENT_TEAM = 8C7439RJLG; 504 | INFOPLIST_FILE = EmojiPickerUIExample/Info.plist; 505 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 506 | LD_RUNPATH_SEARCH_PATHS = ( 507 | "$(inherited)", 508 | "@executable_path/Frameworks", 509 | ); 510 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.EmojiPickerUIExample; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SWIFT_VERSION = 5.0; 513 | TARGETED_DEVICE_FAMILY = 2; 514 | }; 515 | name = Debug; 516 | }; 517 | DD97DD0E247DCDDF00356CE6 /* Release */ = { 518 | isa = XCBuildConfiguration; 519 | buildSettings = { 520 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 521 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 522 | CODE_SIGN_STYLE = Automatic; 523 | DEVELOPMENT_TEAM = 8C7439RJLG; 524 | INFOPLIST_FILE = EmojiPickerUIExample/Info.plist; 525 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 526 | LD_RUNPATH_SEARCH_PATHS = ( 527 | "$(inherited)", 528 | "@executable_path/Frameworks", 529 | ); 530 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.EmojiPickerUIExample; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_VERSION = 5.0; 533 | TARGETED_DEVICE_FAMILY = 2; 534 | }; 535 | name = Release; 536 | }; 537 | DD97DD1E247DCEED00356CE6 /* Debug */ = { 538 | isa = XCBuildConfiguration; 539 | buildSettings = { 540 | CODE_SIGN_STYLE = Automatic; 541 | CURRENT_PROJECT_VERSION = 1; 542 | DEFINES_MODULE = YES; 543 | DEVELOPMENT_TEAM = 8C7439RJLG; 544 | DYLIB_COMPATIBILITY_VERSION = 1; 545 | DYLIB_CURRENT_VERSION = 1; 546 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 547 | INFOPLIST_FILE = EmojiPickerUI/Info.plist; 548 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 549 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 550 | LD_RUNPATH_SEARCH_PATHS = ( 551 | "$(inherited)", 552 | "@executable_path/Frameworks", 553 | "@loader_path/Frameworks", 554 | ); 555 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.EmojiPickerUI; 556 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 557 | SKIP_INSTALL = YES; 558 | SWIFT_VERSION = 5.0; 559 | TARGETED_DEVICE_FAMILY = 2; 560 | VERSIONING_SYSTEM = "apple-generic"; 561 | VERSION_INFO_PREFIX = ""; 562 | }; 563 | name = Debug; 564 | }; 565 | DD97DD1F247DCEED00356CE6 /* Release */ = { 566 | isa = XCBuildConfiguration; 567 | buildSettings = { 568 | CODE_SIGN_STYLE = Automatic; 569 | CURRENT_PROJECT_VERSION = 1; 570 | DEFINES_MODULE = YES; 571 | DEVELOPMENT_TEAM = 8C7439RJLG; 572 | DYLIB_COMPATIBILITY_VERSION = 1; 573 | DYLIB_CURRENT_VERSION = 1; 574 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 575 | INFOPLIST_FILE = EmojiPickerUI/Info.plist; 576 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 577 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 578 | LD_RUNPATH_SEARCH_PATHS = ( 579 | "$(inherited)", 580 | "@executable_path/Frameworks", 581 | "@loader_path/Frameworks", 582 | ); 583 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.EmojiPickerUI; 584 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 585 | SKIP_INSTALL = YES; 586 | SWIFT_VERSION = 5.0; 587 | TARGETED_DEVICE_FAMILY = 2; 588 | VERSIONING_SYSTEM = "apple-generic"; 589 | VERSION_INFO_PREFIX = ""; 590 | }; 591 | name = Release; 592 | }; 593 | /* End XCBuildConfiguration section */ 594 | 595 | /* Begin XCConfigurationList section */ 596 | DD97DCF3247DCDDE00356CE6 /* Build configuration list for PBXProject "EmojiPickerUIExample" */ = { 597 | isa = XCConfigurationList; 598 | buildConfigurations = ( 599 | DD97DD0A247DCDDF00356CE6 /* Debug */, 600 | DD97DD0B247DCDDF00356CE6 /* Release */, 601 | ); 602 | defaultConfigurationIsVisible = 0; 603 | defaultConfigurationName = Release; 604 | }; 605 | DD97DD0C247DCDDF00356CE6 /* Build configuration list for PBXNativeTarget "EmojiPickerUIExample" */ = { 606 | isa = XCConfigurationList; 607 | buildConfigurations = ( 608 | DD97DD0D247DCDDF00356CE6 /* Debug */, 609 | DD97DD0E247DCDDF00356CE6 /* Release */, 610 | ); 611 | defaultConfigurationIsVisible = 0; 612 | defaultConfigurationName = Release; 613 | }; 614 | DD97DD1D247DCEED00356CE6 /* Build configuration list for PBXNativeTarget "EmojiPickerUI" */ = { 615 | isa = XCConfigurationList; 616 | buildConfigurations = ( 617 | DD97DD1E247DCEED00356CE6 /* Debug */, 618 | DD97DD1F247DCEED00356CE6 /* Release */, 619 | ); 620 | defaultConfigurationIsVisible = 0; 621 | defaultConfigurationName = Release; 622 | }; 623 | /* End XCConfigurationList section */ 624 | }; 625 | rootObject = DD97DCF0247DCDDE00356CE6 /* Project object */; 626 | } 627 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // EmojiPickerUIExample 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import EmojiPickerUI 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | EmojiPickerManager.install() 17 | 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // EmojiPickerUIExample 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /EmojiPickerUIExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // EmojiPickerUIExample 4 | // 5 | // Created by Guilherme Rambo on 26/05/20. 6 | // Copyright © 2020 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Guilherme Rambo 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EmojiPickerUI 2 | 3 | A framework implementing a macOS-style emoji picker for iPadOS. 4 | 5 | **This is an experiment I've done to see what an emoji picker window would feel like to use on iPad. It is not intended for use in production apps. In the demos I've shown, I was using the system emoji database, which can be gathered from a private framework, this sample code includes a basic, builtin database of emoji.** 6 | 7 | ![demo](./demo.gif) 8 | 9 | [Demo on YouTube](https://www.youtube.com/watch?v=iwfF5nwqEuw) -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/EmojiPickerUI/70859e0adbdb3bd4516a9ff039f6bcc4432a0755/demo.gif --------------------------------------------------------------------------------