Add action to button created using UIViewRepresentable - ios

I am using a FluentUI#button, which behind uses UIKit
I need to display that button in a SwiftUI View, and I'm trying to toggle an #State property or add a #selector to the button, but I'm not able to do it
I created a generic UIViewRepresentable structure to help me embed any UIView in my SwiftUI Views, following this tutorial:
struct Anything<Wrapper : UIView>: UIViewRepresentable {
typealias Updater = (Wrapper, Context) -> Void
var makeView: () -> Wrapper
var update: (Wrapper, Context) -> Void
init(_ makeView: #escaping #autoclosure () -> Wrapper,
updater update: #escaping (Wrapper) -> Void) {
self.makeView = makeView
self.update = { view, _ in update(view) }
}
func makeUIView(context: Context) -> Wrapper {
makeView()
}
func updateUIView(_ view: Wrapper, context: Context) {
update(view, context)
}
}
And I have the following code:
import SwiftUI
import FluentUI
struct MyView: View {
#State var isGreen = true
var body: some View {
VStack {
Text("Hello, World!")
.background(isGreen ? Color.green : Color.blue)
Spacer().frame(height: 20)
Anything(FluentUI.Button(style: .primaryFilled)) {
$0.setTitle("Try me!", for: .normal)
}
.frame(height: 30)
.padding()
}
}
}
struct Anything<Wrapper: UIView>: UIViewRepresentable {
typealias Updater = (Wrapper, Context) -> Void
var makeView: () -> Wrapper
var update: (Wrapper, Context) -> Void
var action: (() -> Void)?
init(_ makeView: #escaping #autoclosure () -> Wrapper,
updater update: #escaping (Wrapper) -> Void) {
self.makeView = makeView
self.update = { view, _ in update(view) }
}
func makeUIView(context: Context) -> Wrapper {
makeView()
}
func updateUIView(_ view: Wrapper, context: Context) {
update(view, context)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
And if I try to add this:
$0.addTarget(self, action: #selector(toggleColor), for: .touchUpInside)
With:
func toggleColor() {
isGreen = !isGreen
}
I get this error:
Argument of '#selector' refers to instance method 'toggleColor()' that is not exposed to Objective-C
And if I add #objc to the method I get this error:
#objc can only be used with members of classes, #objc protocols, and concrete extensions of classes
And as my Anything struct isn't a Button from SwiftUI, I cannot add the action parameter as normally
How can I add a target/action to my button in this way?

Here is a demo of possible solution - we need a wrapper between UIKit objective-c selectors and SwiftUI swift function.
Tested with Xcode 13.3 / iOS 15.4
Here is main part (used UIButton instead of FluentUI.Button for simplicity):
Anything(UIButton(type: .system)) {
$0.setTitle("Try me!", for: .normal)
$0.addTarget(toggleColor, action: #selector(Action.perform(sender:)), for: .touchUpInside)
toggleColor.action = {
isGreen.toggle()
}
}
Complete test module is here

Related

Integrating Persona SDK in a SwiftUI View

I'm trying it integrate the Person SDK v2 in a SwiftUI view. It's setup for UIKit to present from a specific UIViewController. Here is my code.
https://docs.withpersona.com/docs/ios-sdk-v2-integration-guide
I'm not sure how to call my present function from SwiftUI. The SDK is setup so when you create that Inquiry object it triggers it's nav to present on the view controller.
struct PersonaInquiry: UIViewControllerRepresentable {
private var viewController = UIViewController()
private var coordinator = Coordinator()
class Coordinator: NSObject, InquiryDelegate {
func inquiryComplete(inquiryId: String, status: String, fields: [String : Persona2.InquiryField]) {
}
func inquiryCanceled(inquiryId: String?, sessionToken: String?) {
}
func inquiryError(_ error: Error) {
}
}
func makeUIViewController(context: Context) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
func present(templateId: String) {
let config = InquiryConfiguration(templateId: templateId)
// Create the inquiry with the view controller
// as the delegate and presenter.
Inquiry(config: config, delegate: coordinator).start(from: viewController)
}
func makeCoordinator() -> Coordinator {
return coordinator
}
}
struct PersonaInquiry_Previews: PreviewProvider {
static var previews: some View {
PersonaInquiry()
}
}
Here's an example
ContentView:
struct ContentView: View {
#State private var isPresentingSDK = false
#State private var message = ""
var body: some View {
VStack {
Button(
action: {
isPresentingSDK.toggle()
},
label: {
Text("Launch Inquiry from SwiftUI 🚀")
.foregroundColor(Color.white)
.padding()
}
)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.capsule)
.fullScreenCover(
isPresented: $isPresentingSDK,
onDismiss: {
// Do nothing
},
content: {
InquirySDKWrapper(
inquiryComplete: { inquiryId, status, fields in
self.message = """
Inquiry Complete
Inquiry ID: \(inquiryId)
Status: \(String(describing: status))
"""
},
inquiryCanceled: { inquiryId, sessionToken in
self.message = "🤷‍♀️ Inquiry Cancelled"
},
inquiryErrored: { error in
self.message = """
💀 Inquiry Error
\(error.localizedDescription)
"""
}
)
}
)
Text(message)
.multilineTextAlignment(.center)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
InquirySDKWrapper:
import Persona2
import SwiftUI
import UIKit
struct InquirySDKWrapper: UIViewControllerRepresentable {
/// The wrapper VC presents the SDK and acts as its delegate.
/// The delegate methods in turn call the callbacks in the WrapperDelegate
private let wrapperVC: WrapperViewController
/// Pass in the callbacks for each delegate method
init(
inquiryComplete: #escaping (String, String, [String: InquiryField]) -> Void,
inquiryCanceled: #escaping (_ inquiryId: String?, _ sessionToken: String?) -> Void,
inquiryErrored: #escaping (_ error: Error) -> Void
) {
wrapperVC = WrapperViewController()
wrapperVC.inquiryComplete = inquiryComplete
wrapperVC.inquiryCanceled = inquiryCanceled
wrapperVC.inquiryErrored = inquiryErrored
}
func makeUIViewController(context: Context) -> some UIViewController {
return wrapperVC
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// Do nothing
}
}
final class WrapperViewController: UIViewController {
private var isPresenting = false
// The callbacks
var inquiryComplete: ((_ inquiryId: String, _ status: String, _ fields: [String: InquiryField]) -> Void)!
var inquiryCanceled: ((_ inquiryId: String?, _ sessionToken: String?) -> Void)!
var inquiryErrored: ((_ error: Error) -> Void)!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Otherwise this would trigger once the SDK exits too
guard !isPresenting else { return }
let inquiry = Inquiry(
config: InquiryConfiguration(
templateId: "YOUR TEMPLATE ID HERE"
),
delegate: self
)
inquiry.start(from: self)
isPresenting = true
}
}
extension WrapperViewController: InquiryDelegate {
func inquiryComplete(inquiryId: String, status: String, fields: [String: InquiryField]) {
inquiryComplete(inquiryId, status, fields)
dismiss(animated: true, completion: nil)
}
func inquiryCanceled(inquiryId: String?, sessionToken: String?) {
inquiryCanceled(inquiryId, sessionToken)
dismiss(animated: true, completion: nil)
}
func inquiryError(_ error: Error) {
inquiryErrored(error)
dismiss(animated: true, completion: nil)
}
}

SwiftUI Custom View For Context Menu

I want to implement same custom popup view when press long press gesture on a view as shown in the photo (from Tweeter App), so I can show a custom view and context menu at same time.
You need to make a custom ContextMenu using UIContextMenu from UIKit.
struct ContextMenuHelper<Content: View, Preview: View>: UIViewRepresentable {
var content: Content
var preview: Preview
var menu: UIMenu
var navigate: () -> Void
init(content: Content, preview: Preview, menu: UIMenu, navigate: #escaping () -> Void) {
self.content = content
self.preview = preview
self.menu = menu
self.navigate = navigate
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
let hostView = UIHostingController(rootView: content)
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor)
]
view.addSubview(hostView.view)
view.addConstraints(constraints)
let interaction = UIContextMenuInteraction(delegate: context.coordinator)
view.addInteraction(interaction)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
var parent: ContextMenuHelper
init(_ parent: ContextMenuHelper) {
self.parent = parent
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil) {
let previewController = UIHostingController(rootView: self.parent.preview)
return previewController
} actionProvider: { items in
return self.parent.menu
}
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
parent.navigate()
}
}
}
extension View {
func contextMenu<Preview: View>(navigate: #escaping () -> Void = {}, #ViewBuilder preview: #escaping () -> Preview, menu: #escaping () -> UIMenu) -> some View {
return CustomContextMenu(navigate: navigate, content: {self}, preview: preview, menu: menu)
}
}
struct CustomContextMenu<Content: View, Preview: View>: View {
var content: Content
var preview: Preview
var menu: UIMenu
var navigate: () -> Void
init(navigate: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content, #ViewBuilder preview: #escaping () -> Preview, menu: #escaping () -> UIMenu) {
self.content = content()
self.preview = preview()
self.menu = menu()
self.navigate = navigate
}
var body: some View {
ZStack {
content
.overlay(ContextMenuHelper(content: content, preview: preview, menu: menu, navigate: navigate))
}
}
}
Usage:
.contextMenu(navigate: {
UIApplication.shared.open(url) //User tapped the preview
}) {
LinkView(link: url.absoluteString) //Preview
.environment(\.managedObjectContext, viewContext)
.accentColor(Color(hex: "59AF97"))
.environmentObject(variables)
}menu: {
let openUrl = UIAction(title: "Open", image: UIImage(systemName: "sidebar.left")) { _ in
withAnimation() {
UIApplication.shared.open(url)
}
}
let menu = UIMenu(title: url.absoluteString, image: nil, identifier: nil, options: .displayInline, children: [openUrl]) //Menu
return menu
}
For navigation:
add isActive: $navigate to your NavigationLink:
NavigationLink(destination: SomeView(), isActive: $navigate)
along with a new property:
#State var navigate = false
.contextMenu(navigate: {
navigate.toggle() //User tapped the preview
}) {
LinkView(link: url.absoluteString) //Preview
.environment(\.managedObjectContext, viewContext)
.accentColor(Color(hex: "59AF97"))
.environmentObject(variables)
}menu: {
let openUrl = UIAction(title: "Open", image: UIImage(systemName: "sidebar.left")) { _ in
withAnimation() {
UIApplication.shared.open(url)
}
}
let menu = UIMenu(title: url.absoluteString, image: nil, identifier: nil, options: .displayInline, children: [openUrl]) //Menu
return menu
}
There is a new method in iOS 16 SDK (currently in beta) that allows for showing a preview directly from SwiftUI without the need of tapping into the UIKit.
contextMenu(menuItems:preview:)

SwiftUI with UIViewControllerRepresentable

I am trying to use a UIViewController representable in a swiftUi project. Specifically I am trying to press one button (assetOne) that allows the EU to select a video and then press another button (assetTwo) and it allows the user to select another video. Then the user will have the option to merge the videos (with a third button). I assumed that I would need to use a Coordinator to accomplish this but after seeing a SO solution without it I tried to do it without one. But when I run my project the build is successful but when I click on any of the buttons from the content view I get the error message below. What am I doing wrong? Do I need a Coordinator and how do I incorporate it with my current configuration?
Warning: Attempt to present <UIImagePickerController: 0x7fa05f827600>
on <TempTest.MergeVideoViewController: 0x7fa05ed088c0> whose view is
not in the window hierarchy!
Content View:
import SwiftUI
struct ContentView: View {
let someView = ImagePicker()
var body: some View {
VStack {
Button(action: {
print("SwiftUI: assetOne button tapped")
// Call func in SomeView()
self.someView.assetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
//...
ImagePicker: UIViewControllerRepresentable
struct ImagePicker: UIViewControllerRepresentable{
let someView = MergeVideoViewController()
func makeUIViewController(context: Context) -> MergeVideoViewController {
someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
func assetOne() {
someView.loadAssetOne()
}
//...
}
My UIViewController class:
class MergeVideoViewController: UIViewController {
var firstAsset: AVAsset?
var secondAsset: AVAsset?
var audioAsset: AVAsset?
var loadingAssetOne = false
var activityMonitor: UIActivityIndicatorView!
func exportDidFinish(_ session: AVAssetExportSession) {
// Cleanup assets
activityMonitor.stopAnimating()
firstAsset = nil
secondAsset = nil
audioAsset = nil
//...
func loadAssetOne() {
// func loadAssetOne(_ sender: AnyObject) {
if savedPhotosAvailable() {
loadingAssetOne = true
VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)
}
}
//...
The ImagePicker is-a View, it should be somewhere in body.
Here is possible approach - the idea is to get controller reference back in SwiftUI and call its actions directly when needed.
struct ImagePicker: UIViewControllerRepresentable{
let configure: (MergeVideoViewController) -> ()
func makeUIViewController(context: Context) -> MergeVideoViewController {
let someView = MergeVideoViewController()
configure(someView)
return someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
}
struct ContentView: View {
#State private var controller: MergeVideoViewController?
var body: some View {
VStack {
ImagePicker {
self.controller = $0
}
Button(action: {
print("SwiftUI: assetOne button tapped")
self.controller?.loadAssetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
}
}

Is there a SwiftUI equivalent for viewWillDisappear(_:) or detect when a view is about to be removed?

In SwiftUI, I'm trying to find a way to detect that a view is about to be removed only when using the default navigationBackButton. Then perform some action.
Using onDisappear(perform:) acts like viewDidDisappear(_:), and the action performs after another view appears.
Or, I was thinking the above problem might be solved by detecting when the default navigationBarBackButton is pressed. But I've found no way to detect that.
Is there any solution to perform some action before another view appears?
(I already know it is possible to do that by creating a custom navigation back button to dismiss a view)
Here is approach that works for me, it is not pure-SwiftUI but I assume worth posting
Usage:
SomeView()
.onDisappear {
print("x Default disappear")
}
.onWillDisappear { // << order does NOT matter
print(">>> going to disappear")
}
Code:
struct WillDisappearHandler: UIViewControllerRepresentable {
func makeCoordinator() -> WillDisappearHandler.Coordinator {
Coordinator(onWillDisappear: onWillDisappear)
}
let onWillDisappear: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
}
typealias UIViewControllerType = UIViewController
class Coordinator: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
struct WillDisappearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(WillDisappearHandler(onWillDisappear: callback))
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
self.modifier(WillDisappearModifier(callback: perform))
}
}
You can bind the visibility of the child view to some state, and monitor that state for changes.
When the child view is pushed, the onChange block is called with show == true. When the child view is popped, the same block is called with show == false:
struct ParentView: View {
#State childViewShown: Bool = false
var body: some View {
NavigationLink(destination: Text("child view"),
isActive: self.$childViewShown) {
Text("show child view")
}
.onChange(of: self.childViewShown) { show in
if show {
// child view is appearing
} else {
// child view is disappearing
}
}
}
}
Here's a slightly more succinct version of the accepted answer:
private struct WillDisappearHandler: UIViewControllerRepresentable {
let onWillDisappear: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
ViewWillDisappearViewController(onWillDisappear: onWillDisappear)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
private class ViewWillDisappearViewController: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
background(WillDisappearHandler(onWillDisappear: perform))
}
}
you have a couple of actions for each object that you want to show on the screen
func onDisappear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view disappears.
func onAppear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view appears.
you can use the like the sample ( in this sample it affects on the VStack):
import SwiftUI
struct TestView: View {
#State var textObject: String
var body: some View {
VStack {
Text(textObject)
}
.onAppear {
textObject = "Vertical stack is appeared"
}
.onDisappear {
textObject = ""
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TestView()
}
}
}
You can trigger the change of the #Environment .scenePhase like this :
struct YourView: View {
#Environment(\.scenePhase) var scenePhase
var body: Some View {
VStack {
// Your View code
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("active")
case .inactive:
print("inactive")
case .background:
print("background")
#unknown default:
print("?")
}
}
}
}

How to use ObservableObject with UIViewRepresentable

I'm building a SwiftUI app using MVVM.
I need some additional behaviors for text field, so I'm wrapping a UITextField in a UIViewRepresentable view.
If I use a simple #State in the view that contains my text fields to bind the text, the custom text field behave as expected; but since I want to store all texts of my text fields in the view model, I'm using an #ObservedObject; when using that, the text field binding doesn't work:
it looks like it's always reset to initial state (empty text) and it doesn't publish any value (and the view doesn't refresh).
This weird behavior happens only for UIViewRepresentable views.
My main view contains a form and it looks like this:
struct LoginSceneView: View {
#ObservedObject private var viewModel: LoginViewModel = LoginViewModel()
var body: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 22) {
UIKitTextField(text: $viewModel.email, isFirstResponder: $viewModel.isFirstResponder)
SecureField("Password", text: $viewModel.password)
Button(action: {}) {
Text("LOGIN")
}
.disabled(!viewModel.isButtonEnabled)
}
.padding(.vertical, 40)
}
}
}
The view model is this:
class LoginViewModel: ObservableObject {
#Published var email = ""
#Published var password = ""
#Published var isFirstResponder = false
var isButtonEnabled: Bool { !email.isEmpty && !password.isEmpty }
}
And finally, this is my custom text field:
struct UIKitTextField: UIViewRepresentable {
// MARK: - Coordinator
class Coordinator: NSObject, UITextFieldDelegate {
private let textField: UIKitTextField
fileprivate init(_ textField: UIKitTextField) {
self.textField = textField
super.init()
}
#objc fileprivate func editingChanged(_ sender: UITextField) {
let text = sender.text ?? ""
textField.text = text
textField.onEditingChanged(text)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.textField.onEditingBegin()
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.textField.onEditingEnd()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.textField.onReturnKeyPressed()
}
}
// MARK: - Properties
#Binding private var text: String
private let onEditingChanged: (String) -> Void
private let onEditingBegin: () -> Void
private let onEditingEnd: () -> Void
private let onReturnKeyPressed: () -> Bool
// MARK: - Initializers
init(text: Binding<String>,
onEditingChanged: #escaping (String) -> Void = { _ in },
onEditingBegin: #escaping () -> Void = {},
onEditingEnd: #escaping () -> Void = {},
onReturnKeyPressed: #escaping () -> Bool = { true }) {
_text = text
self.onEditingChanged = onEditingChanged
self.onEditingBegin = onEditingBegin
self.onEditingEnd = onEditingEnd
self.onReturnKeyPressed = onReturnKeyPressed
}
// MARK: - UIViewRepresentable methods
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.addTarget(context.coordinator, action: #selector(Coordinator.editingChanged(_:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
}
Your problem may be that UIViewRepresentable is created every time that some binding var is changed. Put debug code to check.
struct UIKitTextField: UIViewRepresentable {
init() {
print("UIViewRepresentable init()")
}
...
}
I've edited your code, see if that's what you are looking for.
class LoginViewModel: ObservableObject {
#Published var email = ""
#Published var password = ""
var isButtonEnabled: Bool { !email.isEmpty && !password.isEmpty }
}
struct LoginSceneView: View {
#ObservedObject private var viewModel: LoginViewModel = LoginViewModel()
var body: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 22) {
UIKitTextField(text: $viewModel.email)
SecureField("Password", text: $viewModel.password)
Button(action: {
print("email is \(self.viewModel.email)")
print("password is \(self.viewModel.password)")
UIApplication.shared.endEditing()
}) {
Text("LOGIN")
}
.disabled(!viewModel.isButtonEnabled)
}
.padding(.vertical, 40)
}
}
}
struct UIKitTextField: UIViewRepresentable {
// MARK: - Coordinator
class Coordinator: NSObject, UITextFieldDelegate {
let textField: UIKitTextField
fileprivate init(_ textField: UIKitTextField) {
self.textField = textField
super.init()
}
#objc fileprivate func editingChanged(_ sender: UITextField) {
let text = sender.text ?? ""
textField.text = text
textField.onEditingChanged(text)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.textField.onEditingBegin()
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.textField.onEditingEnd()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.textField.onReturnKeyPressed()
}
}
// MARK: - Properties
#Binding private var text: String
private let onEditingChanged: (String) -> Void
private let onEditingBegin: () -> Void
private let onEditingEnd: () -> Void
private let onReturnKeyPressed: () -> Bool
// MARK: - Initializers
init(text: Binding<String>,
onEditingChanged: #escaping (String) -> Void = { _ in },
onEditingBegin: #escaping () -> Void = {},
onEditingEnd: #escaping () -> Void = {},
onReturnKeyPressed: #escaping () -> Bool = { true }) {
_text = text
self.onEditingChanged = onEditingChanged
self.onEditingBegin = onEditingBegin
self.onEditingEnd = onEditingEnd
self.onReturnKeyPressed = onReturnKeyPressed
}
// MARK: - UIViewRepresentable methods
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.addTarget(context.coordinator, action: #selector(Coordinator.editingChanged(_:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
}
I highly recommend not making your UIViewRepresentable complex for features like using of isFirstResponder to do what's possible with alternative ways unless necessary. I'm assuming you want to use that as a parameter to dismiss the keyboard. There are some alternatives like :
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}

Resources