From 30aaf9583b5072ab37c927def152717bd3452a7c Mon Sep 17 00:00:00 2001 From: Ivan Izyumkin Date: Mon, 29 May 2023 12:19:17 +0300 Subject: [PATCH 1/4] Implemented a wrapper for MCEmojiPickerViewController --- .../NSNotification.Name+Extension.swift | 27 ++++++ .../Common/Extensions/View+Extension.swift | 33 ++++++++ ...MCEmojiPickerRepresentableController.swift | 83 +++++++++++++++++++ .../View/MCEmojiPickerViewController.swift | 6 ++ 4 files changed, 149 insertions(+) create mode 100644 Sources/MCEmojiPicker/Common/Extensions/NSNotification.Name+Extension.swift create mode 100644 Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift create mode 100644 Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift diff --git a/Sources/MCEmojiPicker/Common/Extensions/NSNotification.Name+Extension.swift b/Sources/MCEmojiPicker/Common/Extensions/NSNotification.Name+Extension.swift new file mode 100644 index 0000000..0bbf786 --- /dev/null +++ b/Sources/MCEmojiPicker/Common/Extensions/NSNotification.Name+Extension.swift @@ -0,0 +1,27 @@ +// The MIT License (MIT) +// +// Copyright © 2023 Ivan Izyumkin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +extension NSNotification.Name { + static let MCEmojiPickerDidDisappear = NSNotification.Name("MCEmojiPickerDidDisappear") +} diff --git a/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift b/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift new file mode 100644 index 0000000..7d15e9d --- /dev/null +++ b/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright © 2023 Ivan Izyumkin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +@available(iOS 13, *) +extension View { + @ViewBuilder public func emojiPicker(isPresented: Binding) -> some View { + self.overlay( + MCEmojiPickerRepresentableController(isPresented: isPresented) + .allowsHitTesting(false) + ) + } +} diff --git a/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift b/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift new file mode 100644 index 0000000..222dd18 --- /dev/null +++ b/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift @@ -0,0 +1,83 @@ +// The MIT License (MIT) +// +// Copyright © 2023 Ivan Izyumkin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +@available(iOS 13.0, *) +struct MCEmojiPickerRepresentableController: UIViewControllerRepresentable { + + // MARK: - Public Properties + + @Binding var isPresented: Bool + + // FIXME: - Add settings for the picker + + // MARK: - Public Methods + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> UIViewController { + UIViewController() + } + + func updateUIViewController(_ representableController: UIViewController, context: Context) { + switch isPresented { + case true: + let controller = MCEmojiPickerViewController() + controller.delegate = context.coordinator + controller.sourceView = representableController.view + context.coordinator.addPickerDismissingObserver() + representableController.present(controller, animated: true) + case false: + representableController.presentedViewController?.dismiss(animated: true) + } + } +} + +// MARK: - Coordinator + +@available(iOS 13.0, *) +extension MCEmojiPickerRepresentableController { + public class Coordinator: NSObject, MCEmojiPickerDelegate { + + private let sourceController: MCEmojiPickerRepresentableController + + init(_ sourceController: MCEmojiPickerRepresentableController) { + self.sourceController = sourceController + } + + public func addPickerDismissingObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(pickerDismissingAction), name: .MCEmojiPickerDidDisappear, object: nil) + } + + public func didGetEmoji(emoji: String) { + + } + + @objc public func pickerDismissingAction() { + NotificationCenter.default.removeObserver(self, name: .MCEmojiPickerDidDisappear, object: nil) + sourceController.isPresented = false + } + } +} diff --git a/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift b/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift index 12819ca..40d2374 100644 --- a/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift +++ b/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift @@ -125,6 +125,12 @@ public final class MCEmojiPickerViewController: UIViewController { setupHorizontalInset() } + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + NotificationCenter.default.post(name: .MCEmojiPickerDidDisappear, object: nil) + } + + // MARK: - Private Methods private func bindViewModel() { From ba309bbde2c7f0453dbcfbf74dd91e57a1dee1c5 Mon Sep 17 00:00:00 2001 From: Ivan Izyumkin Date: Mon, 29 May 2023 22:18:11 +0300 Subject: [PATCH 2/4] Added customization of the picker and emoji selection processing --- .../Common/Extensions/View+Extension.swift | 33 +++++- ...MCEmojiPickerRepresentableController.swift | 104 +++++++++++++++--- 2 files changed, 121 insertions(+), 16 deletions(-) diff --git a/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift b/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift index 7d15e9d..253cf76 100644 --- a/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift +++ b/Sources/MCEmojiPicker/Common/Extensions/View+Extension.swift @@ -24,9 +24,38 @@ import SwiftUI @available(iOS 13, *) extension View { - @ViewBuilder public func emojiPicker(isPresented: Binding) -> some View { + /// The method adds a macOS style emoji picker. + /// + /// - Parameters: + /// - isPresented: Observed value which is responsible for the state of the picker. + /// - selectedEmoji: Observed value which is updated by the selected emoji. + /// - arrowDirection: The direction of the arrow for EmojiPicker. + /// - customHeight: Custom height for EmojiPicker. + /// - horizontalInset: Inset from the sourceView border. + /// - isDismissAfterChoosing: A boolean value that determines whether the screen will be hidden after the emoji is selected. + /// - selectedEmojiCategoryTintColor: Color for the selected emoji category. + /// - feedBackGeneratorStyle: Feedback generator style. To turn off, set `nil` to this parameter. + @ViewBuilder public func emojiPicker( + isPresented: Binding, + selectedEmoji: Binding, + arrowDirection: MCPickerArrowDirection? = nil, + customHeight: CGFloat? = nil, + horizontalInset: CGFloat? = nil, + isDismissAfterChoosing: Bool? = nil, + selectedEmojiCategoryTintColor: UIColor? = nil, + feedBackGeneratorStyle: UIImpactFeedbackGenerator.FeedbackStyle? = nil + ) -> some View { self.overlay( - MCEmojiPickerRepresentableController(isPresented: isPresented) + MCEmojiPickerRepresentableController( + isPresented: isPresented, + selectedEmoji: selectedEmoji, + arrowDirection: arrowDirection, + customHeight: customHeight, + horizontalInset: horizontalInset, + isDismissAfterChoosing: isDismissAfterChoosing, + selectedEmojiCategoryTintColor: selectedEmojiCategoryTintColor, + feedBackGeneratorStyle: feedBackGeneratorStyle + ) .allowsHitTesting(false) ) } diff --git a/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift b/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift index 222dd18..aee3f8a 100644 --- a/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift +++ b/Sources/MCEmojiPicker/View/MCEmojiPickerRepresentableController.swift @@ -23,32 +23,105 @@ import SwiftUI @available(iOS 13.0, *) -struct MCEmojiPickerRepresentableController: UIViewControllerRepresentable { +public struct MCEmojiPickerRepresentableController: UIViewControllerRepresentable { // MARK: - Public Properties + /// Observed value which is responsible for the state of the picker. + /// + /// If the value of this property is `true`, the EmojiPicker will be presented. + /// If the value of this property is `false`, the EmojiPicker will be hidden. @Binding var isPresented: Bool - // FIXME: - Add settings for the picker + /// Observed value which is updated by the selected emoji. + @Binding var selectedEmoji: String + + /// The direction of the arrow for EmojiPicker. + /// + /// The default value of this property is `.up`. + public var arrowDirection: MCPickerArrowDirection? + + /// Custom height for EmojiPicker. + /// But it will be limited by the distance from sourceView.origin.y to the upper or lower bound(depends on permittedArrowDirections). + /// + /// The default value of this property is `nil`. + public var customHeight: CGFloat? + + /// Inset from the sourceView border. + /// + /// The default value of this property is `0`. + public var horizontalInset: CGFloat? + + /// A boolean value that determines whether the screen will be hidden after the emoji is selected. + /// + /// If this property’s value is `true`, the EmojiPicker will be dismissed after the emoji is selected. + /// If you want EmojiPicker not to dismissed after emoji selection, you must set this property to `false`. + /// The default value of this property is `true`. + public var isDismissAfterChoosing: Bool? + + /// Color for the selected emoji category. + /// + /// The default value of this property is `.systemBlue`. + public var selectedEmojiCategoryTintColor: UIColor? + + /// Feedback generator style. To turn off, set `nil` to this parameter. + /// + /// The default value of this property is `.light`. + public var feedBackGeneratorStyle: UIImpactFeedbackGenerator.FeedbackStyle? + + // MARK: - Initializers + + public init( + isPresented: Binding, + selectedEmoji: Binding, + arrowDirection: MCPickerArrowDirection? = nil, + customHeight: CGFloat? = nil, + horizontalInset: CGFloat? = nil, + isDismissAfterChoosing: Bool? = nil, + selectedEmojiCategoryTintColor: UIColor? = nil, + feedBackGeneratorStyle: UIImpactFeedbackGenerator.FeedbackStyle? = nil + ) { + self._isPresented = isPresented + self._selectedEmoji = selectedEmoji + self.arrowDirection = arrowDirection + self.customHeight = customHeight + self.horizontalInset = horizontalInset + self.isDismissAfterChoosing = isDismissAfterChoosing + self.selectedEmojiCategoryTintColor = selectedEmojiCategoryTintColor + self.feedBackGeneratorStyle = feedBackGeneratorStyle + } // MARK: - Public Methods - func makeCoordinator() -> Coordinator { + public func makeCoordinator() -> Coordinator { Coordinator(self) } - func makeUIViewController(context: Context) -> UIViewController { + public func makeUIViewController(context: Context) -> UIViewController { UIViewController() } - func updateUIViewController(_ representableController: UIViewController, context: Context) { + public func updateUIViewController(_ representableController: UIViewController, context: Context) { + guard !context.coordinator.isNewEmojiSet else { + context.coordinator.isNewEmojiSet.toggle() + return + } switch isPresented { case true: - let controller = MCEmojiPickerViewController() - controller.delegate = context.coordinator - controller.sourceView = representableController.view + guard representableController.presentedViewController == nil else { return } + let emojiPicker = MCEmojiPickerViewController() + emojiPicker.delegate = context.coordinator + emojiPicker.sourceView = representableController.view + if let arrowDirection { emojiPicker.arrowDirection = arrowDirection } + if let customHeight { emojiPicker.customHeight = customHeight } + if let horizontalInset { emojiPicker.horizontalInset = horizontalInset } + if let isDismissAfterChoosing { emojiPicker.isDismissAfterChoosing = isDismissAfterChoosing } + if let selectedEmojiCategoryTintColor { + emojiPicker.selectedEmojiCategoryTintColor = selectedEmojiCategoryTintColor + } + if let feedBackGeneratorStyle { emojiPicker.feedBackGeneratorStyle = feedBackGeneratorStyle } context.coordinator.addPickerDismissingObserver() - representableController.present(controller, animated: true) + representableController.present(emojiPicker, animated: true) case false: representableController.presentedViewController?.dismiss(animated: true) } @@ -61,10 +134,12 @@ struct MCEmojiPickerRepresentableController: UIViewControllerRepresentable { extension MCEmojiPickerRepresentableController { public class Coordinator: NSObject, MCEmojiPickerDelegate { - private let sourceController: MCEmojiPickerRepresentableController + public var isNewEmojiSet = false + + private var representableController: MCEmojiPickerRepresentableController - init(_ sourceController: MCEmojiPickerRepresentableController) { - self.sourceController = sourceController + init(_ representableController: MCEmojiPickerRepresentableController) { + self.representableController = representableController } public func addPickerDismissingObserver() { @@ -72,12 +147,13 @@ extension MCEmojiPickerRepresentableController { } public func didGetEmoji(emoji: String) { - + isNewEmojiSet.toggle() + representableController.selectedEmoji = emoji } @objc public func pickerDismissingAction() { NotificationCenter.default.removeObserver(self, name: .MCEmojiPickerDidDisappear, object: nil) - sourceController.isPresented = false + representableController.isPresented = false } } } From 996a18c2505bd8dfa5e1c147e0b11e707339f02f Mon Sep 17 00:00:00 2001 From: Ivan Izyumkin Date: Mon, 29 May 2023 22:27:11 +0300 Subject: [PATCH 3/4] Remove extra space --- Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift b/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift index 40d2374..7f2d061 100644 --- a/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift +++ b/Sources/MCEmojiPicker/View/MCEmojiPickerViewController.swift @@ -129,7 +129,6 @@ public final class MCEmojiPickerViewController: UIViewController { super.viewDidDisappear(animated) NotificationCenter.default.post(name: .MCEmojiPickerDidDisappear, object: nil) } - // MARK: - Private Methods From 85b31a1c170fbfd0f9875948be11ffc7ade451e5 Mon Sep 17 00:00:00 2001 From: Ivan Izyumkin Date: Tue, 30 May 2023 07:56:53 +0300 Subject: [PATCH 4/4] Added information about SwiftUI support to the README --- README.md | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2d1a1f1..8b7d6b7 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,15 @@ If you use a `MCEmojiPicker`, add your application via Pull Request. Fore more i - [Is dismiss after choosing](#is-dismiss-after-choosing) - [Custom height](#custom-height) - [Feedback generator style](#feedback-generator-style) +- [SwiftUI](#swiftui) - [Localization](#localization) - [TODO](#todo) ## Requirements -Swift `4.2` & `5.0`. Ready for use on iOS 11.1+ +- Swift `4.2` & `5.0` +- Ready for use on iOS 11.1+ +- SwiftUI is supported from iOS 13.0 ## Installation @@ -140,6 +143,34 @@ Feedback generator style. To turn off, set `nil` to this parameter. The default viewController.feedBackGeneratorStyle = .soft ``` +## SwiftUI + +Use like system popover. All settings are available in the method initializer. + +```swift +Button(selectedEmoji) { + isPresented.toggle() +}.emojiPicker( + isPresented: $isPresented, + selectedEmoji: $selectedEmoji +) +``` + +or interact directly with the SwiftUI wrapper for the MCEmojiPickerViewController: + +```swift +MCEmojiPickerRepresentableController( + isPresented: $isPresented, + selectedEmoji: $selectedEmoji, + arrowDirection: .up, + customHeight: 380.0, + horizontalInset: .zero, + isDismissAfterChoosing: true, + selectedEmojiCategoryTintColor: .systemBlue, + feedBackGeneratorStyle: .light +) +``` + ## Localization 🌍 This library supports all existing localizations @@ -152,11 +183,3 @@ viewController.feedBackGeneratorStyle = .soft - [x] Select skin tones from popup - [x] Frequently used - [ ] Search bar and search results - - - - - - - -