I am making a SwiftUI Button from a custom UIButton class called UIPillButton. Here is the code for creating the SwiftUI button:
Button(action: {
print("Button tapped")
}) {
PillButton()
.padding(.top)
}
Here is my class for creating a SwiftUI view called PillButton which converts my custom UIButton over:
struct PillButton: UIViewRepresentable {
var ntPillButton = NTPillButton(type: .filled, title: "Start Test")
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject {
var parent: PillButton
init(_ pillButton: PillButton) {
self.parent = pillButton
super.init()
}
}
func makeUIView(context: UIViewRepresentableContext<PillButton>) -> UIView {
let view = UIView()
view.addSubview(ntPillButton)
NSLayoutConstraint.activate([
ntPillButton.leadingAnchor.constraint(equalTo: view.leadingAnchor),
ntPillButton.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PillButton>) {}
}
The issue is that the Button is not running the action if I tap on the PillButton itself. It only works if I select the buffer (padding) space above the PillButton. How can I make it so I can use the custom PillButton class as a normal SwiftUI button?
It is not clear what is NTPillButton, but if it is subclass of UIButton then the following demo of generic approach (using base UIButton) should clear and applicable.
Tested with Xcode 11.4 / iOS 13.4
The below gives this simple usage
PillButton(title: "Start Test") {
print("Button tapped")
}
.frame(maxWidth: .infinity) // << screen-wide
.padding(.top)
so now PillButton demo itself:
struct PillButton: UIViewRepresentable {
let title: String
let action: () -> ()
var ntPillButton = UIButton()//NTPillButton(type: .filled, title: "Start Test")
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject {
var parent: PillButton
init(_ pillButton: PillButton) {
self.parent = pillButton
super.init()
}
#objc func doAction(_ sender: Any) {
self.parent.action()
}
}
func makeUIView(context: Context) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(self.title, for: .normal)
button.addTarget(context.coordinator, action: #selector(Coordinator.doAction(_ :)), for: .touchDown)
return button
}
func updateUIView(_ uiView: UIButton, context: Context) {}
}
Related
In SwiftUI I have a VStack with tap gesture enabled. And this VStack Contains two buttons. One button is from UIKit and other one is from SwiftUI itself.
If I click SwiftUI button, it is working as expected. But when I click UIKit button it is not working instead tap gesture is triggering, reason the tap gesture is enabled for VStack.
How to make UIKit button action works, if the tap gesture is enabled?
I have to support my project from iOS 14 and tap gesture should be enabled for VStack.
SwiftUI Code
var body: some View {
NavigationView {
VStack {
Bridge()
.frame(height: 100)
Button("SwiftUI Button") {
debugPrint("SwiftUI Button Clicked")
}
}
.onTapGesture {
debugPrint("User tapped")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
Bridge Code
struct Bridge: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
return UIKitButtonView()
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func updateUIView(_ uiView: UIViewType, context: Context) { }
class Coordinator: NSObject {}
}
UIView Code
class UIKitButtonView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet var uikitButton: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
Bundle.main.loadNibNamed("UIKitButtonView", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.frame = self.bounds
}
#IBAction func buttonAction(_ sender: Any) {
debugPrint("UIKit button clicked")
}
}
I am a SwiftUI newbie struggling to add SwiftUI functionality to my existing UIKit/Storyboard code. I would like to call a UIKit function from SwiftUI button. Greatly appreciate your help. Here is the relevant code simplified for this discussion.
From the code below, I'd like to call the functions startAction() and stopAction() from the If statement in SwiftUI...
if (startStop_flag) {
//******** call StartAction()
} else {
//******* call StopAction()
}
The entire code below.
Some context: when the app is run, the bottom half of the screen will show "UIkit Storyboard View" and show the button "Open Swift Container View". When the user clicks this button, the SwiftUI container view will open up. This view will display "This is a swiftUI view" and display a button "Start/Stop". When the user clicks this button, StartAction() or StopAction() needs to be called. These two functions reside in the UIViewController. I hope I am clear with the problem and the request.
ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var nativeView: UIView!
#IBOutlet weak var nativeView_Label: UILabel!
#IBOutlet weak var nativeView_openSwiftViewBtn: UIButton!
#IBOutlet weak var containerView_forSwiftUI: UIView!
#IBSegueAction func embedSwiftUIView(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(coder: coder, rootView: SwiftUIView2(text: "Container View"))
}
var toggleOpenCloseContainerView : Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
containerView_forSwiftUI.isHidden = true
}
#IBAction func openContainerView_touchInside(_ sender: Any) {
if (toggleOpenCloseContainerView) {
containerView_forSwiftUI.isHidden = false
toggleOpenCloseContainerView = false
nativeView_openSwiftViewBtn.setTitle("Close Swift Container View", for: .normal)
} else {
containerView_forSwiftUI.isHidden = true
toggleOpenCloseContainerView = true
nativeView_openSwiftViewBtn.setTitle("Open Swift Container View", for: .normal)
}
}
// These two functions need to be called from the SwiftUI's button.
func startAction() {
print("Start Action called from SwiftUI's button")
}
func stopAction() {
print("Stop Action called from SwiftUI's button")
}
}
The swiftUI functions are in this file
struct SwiftUIView2: View {
var text: String
#State var startStop_flag: Bool = true
var body: some View {
VStack {
Text(text)
HStack {
Image(systemName: "smiley")
Text("This is a SwiftUI View")
Spacer()
Button("\(startStop_flag ? "Start": "Stop")") {
startStop_flag = !startStop_flag
if (startStop_flag) {
//******** call StartAction()
} else {
//******* call StopAction()
}
} .padding()
.background(Color.red)
.cornerRadius(40)
.foregroundColor(.white)
.padding(5)
.overlay(
RoundedRectangle(cornerRadius: 40)
.stroke(Color.red, lineWidth: 1)
)
}
}
.font(.largeTitle)
.background(Color.blue)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView2(text: "Sample Text")
}
}
You can use closures for this. First, define and call them inside your SwiftUI view.
struct SwiftUIView2: View {
var text: String
var startAction: (() -> Void) /// define closure 1
var stopAction: (() -> Void) /// define closure 2
...
...
Button("\(startStop_flag ? "Start": "Stop")") {
startStop_flag = !startStop_flag
if (startStop_flag) {
//******** call StartAction()
startAction()
} else {
//******* call StopAction()
stopAction()
}
}
}
Then, just assign the closure's contents inside ViewController.swift.
#IBSegueAction func embedSwiftUIView(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(
coder: coder,
rootView: SwiftUIView2(
text: "Container View",
startAction: { [weak self] in
self?.startAction()
},
stopAction: { [weak self] in
self?.stopAction()
}
)
)
}
Here is the easiest way to open UIkit ViewController from SwiftUI Button Press.
Button("Next Page") {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootViewController = storyboard.instantiateViewController(withIdentifier: "ProfileVC")
if let window = UIApplication.shared.windows.first {
window.rootViewController!.present(rootViewController, animated: true)
}
}
}
So there seems to be a retain cycle when injecting a Binding that is a published property from an ObservableObject into UIViewControllerRepresentable.
It seems if you create a view inside another view in and that second view has an ObservableObject and injects it's published property into the UIViewControllerRepresentable and is then used in the coordinator, the ObservableObject is never released when the original view is refreshed.
Also it looks like the Binding gets completely broken and the UIViewControllerRepresentable no longer works
When looking at it, it makes sense that Coordinator(self) is bad, but Apple's own documentation says to do it this way. Am I missing something?
Here is a quick example:
struct ContentView: View {
#State var resetView: Bool = true
var body: some View {
VStack {
OtherView()
Text("\(resetView ? 1 : 0)")
// This button just changes the state to refresh the view
// Also after this is pressed the UIViewControllerRepresentable no longer works
Button(action: {resetView.toggle()}, label: {
Text("Refresh")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct OtherView: View {
#ObservedObject var viewModel = OtherViewModel()
var body: some View {
VStack {
Text("Value: \(viewModel.value)")
Wrapper(value: $viewModel.value).frame(width: 100, height: 50, alignment: .center)
}
}
}
class OtherViewModel: ObservableObject {
#Published var value: Int
deinit {
print("OtherViewModel deinit") // this never gets called
}
init() {
self.value = 0
}
}
struct Wrapper: UIViewControllerRepresentable {
#Binding var value: Int
class Coordinator: NSObject, ViewControllerDelegate {
var parent: Wrapper
init(_ parent: Wrapper) {
self.parent = parent
}
func buttonTapped() {
// After the original view is refreshed this will no longer work
parent.value += 1
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> ViewController {
let vc = ViewController()
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}
protocol ViewControllerDelegate: class {
func buttonTapped()
}
class ViewController: UIViewController {
weak var delegate: ViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
button.setTitle("increment", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
self.view.addSubview(button)
}
#objc func buttonTapped(sender : UIButton) {
delegate?.buttonTapped()
}
}
I am trying to use PKToolPicker from PencilKit (iOS/Swift) from a custom view (which is NOT PKCanvasView). The custom view conforms to PKToolPickerObserver. Everything works fine during compile time but I never get to see the PKToolPicker! If I replace my custom view with PKCanvasView, everything works fine!
I am doing this in SwiftUI with UIViewRepresentable (thus First Responder seems a mystery!).
Here is the SwiftUI view in question:
struct PencilKitView: UIViewRepresentable {
typealias UIViewType = myView
let coordinator = Coordinator()
class Coordinator: NSObject, PKToolPickerObserver {
func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
// some code
}
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
// some code
}
}
func makeCoordinator() -> PencilKitView.Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> myView {
let canvasView = myView()
canvasView.isOpaque = false
canvasView.backgroundColor = UIColor.clear
canvasView.becomeFirstResponder()
if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first,
let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.addObserver(canvasView)
toolPicker.addObserver(coordinator)
toolPicker.setVisible(true, forFirstResponder: canvasView)
}
return canvasView
}
func updateUIView(_ uiView: myView, context: Context) {
}
}
If I replace myView with PKCanvasView above, the PKToolPicker will be seen.
For the sake of completeness, here is the MyView stub:
class myView: UIScrollView, PKToolPickerObserver {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
public func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
public func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
public func toolPickerIsRulerActiveDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
public func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
}
Anyone has succeeded in doing this? Is there some undocumented requirement for adopting PKToolPicker?
Here is simplest demo to show PKToolPicker for any custom UIView in SwiftUI.
Tested with Xcode 11.4 / iOS 13.4
struct ToolPickerDemo: View {
#State private var showPicker = false
var body: some View {
Button("Picker") { self.showPicker.toggle() }
.background(ToolPickerHelper(isActive: $showPicker))
}
}
class PickerHelperView: UIView {
override var canBecomeFirstResponder: Bool { true }
}
struct ToolPickerHelper: UIViewRepresentable {
#Binding var isActive: Bool
func makeUIView(context: Context) -> PickerHelperView {
PickerHelperView()
}
func updateUIView(_ uiView: PickerHelperView, context: Context) {
guard let window = uiView.window else { return }
let picker = PKToolPicker.shared(for: window)
picker?.setVisible(isActive, forFirstResponder: uiView)
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
I want to disable the option of copy/paste from my Textfield in SwiftUI. How to achieve that?
This works for me:
Using UITextField conforms to UIViewRepresentable.
import SwiftUI
struct ContentView: View {
#State private var text = ""
var body: some View {
VStack {
Text(text)
UITextFieldViewRepresentable(text: $text) // Using it
.frame(height: 44)
.border(.red)
}
}
}
// MARK: UITextFieldViewRepresentable
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var text: String
typealias UIViewType = ProtectedTextField
func makeUIView(context: Context) -> ProtectedTextField {
let textField = ProtectedTextField()
textField.delegate = context.coordinator
return textField
}
// From SwiftUI to UIKit
func updateUIView(_ uiView: ProtectedTextField, context: Context) {
uiView.text = text
}
// From UIKit to SwiftUI
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
}
// Custom TextField with disabling paste action
class ProtectedTextField: UITextField {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(paste(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
Use UIViewRepresentable class make wrapper class like this.
import SwiftUI
struct CustomeTextField: View {
#State var textStr = ""
var body: some View {
VStack(spacing: 10) {
Text("This is textfield:")
.font(.body)
.foregroundColor(.gray)
TextFieldWrapperView(text: self.$textStr)
.background(Color.gray)
.frame(width: 200, height: 50)
}
.frame(height: 40)
}
}
struct TextFieldWrapperView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> TFCoordinator {
TFCoordinator(self)
}
}
extension TextFieldWrapperView {
func makeUIView(context: UIViewRepresentableContext<TextFieldWrapperView>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
class TFCoordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldWrapperView
init(_ textField: TextFieldWrapperView) {
self.parent = textField
}
func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == #selector(UIResponderStandardEditActions.paste(_:)) {
return false
}
return canPerformAction(action: action, withSender: sender)
}
}