SwiftUI Wrapped SearchBar Can't Dismiss Keyboard - xcode

I am using a UIViewRepresentable to add a search bar to a SwiftUI project. The search bar
is for searching the main List - I have setup the search logic and it works fine,
however I have not been able to code the keyboard to disappear when the search is
cancelled. The Cancel button does not respond. If I click the textfield clearButton
the search is ended and the full list appears but the keyboard does not disappear.
If I uncomment the resignFirstResponder line in textDidChange, the behavior is as
expected, except that the keyboard disappears after every character.
Here's the search bar:
import Foundation
import SwiftUI
struct MySearchBar: UIViewRepresentable {
#Binding var sText: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var sText: String
init(sText: Binding<String>) {
_sText = sText
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
sText = searchText
//this works for EVERY character
//searchBar.resignFirstResponder()
}
}
func makeCoordinator() -> MySearchBar.Coordinator {
return Coordinator(sText: $sText)
}
func makeUIView(context: UIViewRepresentableContext<MySearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.showsCancelButton = true
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<MySearchBar>) {
uiView.text = sText
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
//this does not work
searchBar.text = ""
//none of these work
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchBar.endEditing(true)
}
}
And I load a MySearchBar in SwiftUI in the List.
var body: some View {
NavigationView {
List {
MySearchBar(sText: $searchTerm)
if !searchTerm.isEmpty {
ForEach(Patient.filterForSearchPatients(searchText: searchTerm)) { patient in
NavigationLink(destination: EditPatient(patient: patient, photoStore: self.photoStore, myTextViews: MyTextViews())) {
HStack(spacing: 30) {
//and the rest of the application
Xcode Version 11.2 beta 2 (11B44). SwiftUI. I have tested in the simulator and on a
device. Any guidance would be appreciated.

The reason searchBarCancelButtonClicked is not being called is because it is in MySearchBar but you have set the Coordinator as the search bars delegate. If you move the searchBarCancelButtonClicked func to the Coordinator, it will be called.
Here is what the coordinator should look like:
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var sText: String
init(sText: Binding<String>) {
_sText = sText
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
sText = searchText
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
searchBar.text = ""
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchBar.endEditing(true)
}
}

The searchBarCancelButtonClicked call will be passed to the UISearchBars delegate, which is set to Coordinator, so put func searchBarCancelButtonClicked there instead.
Also, when you are clearing the text you shouldnt set searchBar.text = "", which is setting the instance of UISearchBars text property directly, but instead clear your Coordinators property text. In that way your SwiftUI View will notice the change because of the #Binding property wrapper, and know that it´s time to update.
Here is the complete code for `MySearchBar´:
import UIKit
import SwiftUI
struct MySearchBar : UIViewRepresentable {
#Binding var text : String
class Coordinator : NSObject, UISearchBarDelegate {
#Binding var text : String
init(text : Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
searchBar.showsCancelButton = true
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
text = ""
searchBar.showsCancelButton = false
searchBar.endEditing(true)
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}

Related

SwiftUI & macOS : How to detect last window being closed and show alert that app will quit

I have a SwiftUI app that I am creating. Upon the user closing the last window, I would like to prompt the user and inform them that the app will also quit.
I have taken a look at both the solutions for creating an alert upon app quiting here and have also looked at the solution for closing the application when the last window closes here.
Both of which I have gotten to work however, not together. What I am looking for is a way to detect when a user closes the last window in the application, then prompt the user with an alert letting them know it will quit the application and asking if they would like to continue or cancel.
Using .onDisappear does not seem to work. I have implemented a appDelegate and it's applicationShouldTerminateAfterLastWindowClosed method, but when the last window closes, it does not seem to prompt the .alert behavior in my application.
Application class
class Application: NSObject, NSApplicationDelegate, ObservableObject {
#Published var willTerminate = false
override init() {
super.init()
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
if NSApplication.shared.windows.count == 0 {
return .terminateNow
}
self.willTerminate = true
return .terminateLater
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func resume() {
NSApplication.shared.reply(toApplicationShouldTerminate: false)
}
func close() {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
ContentView
struct ContentView: View {
#State private var window: NSWindow?
#EnvironmentObject private var appDelegate: Application
var body: some View {
ZStack {
MyView()
// ...
.onDisappear(
// Code in here does not run when WindowAccessor is set to background
})
.background(WindowAccessor(window: self.$window))
.alert(isPresented: Binding<Bool>(get: { self.appDelegate.willTerminate && self.window?.isKeyWindow ?? false }, set: { self.appDelegate.willTerminate = $0 }), content: {
SoloLogger(for: .window).coreLog(message: "ApplicationClosedEvent", level: .info)
return Alert(title: Text("Quit Application?"),
message: Text("Do you really want to quit the application?"),
primaryButton: .default(Text("Cancel"), action: {self.appDelegate.resume() }),
secondaryButton: .destructive(Text("Quit"), action: {self.appDelegate.close()}))
})
}
}
}
I've been working on something similar.
You can pick up the #AppDelegate from the environment and don't need to create a WindowAccessor.
I created a view which can be added into your content view's ZStack:
struct MacOSQuitCheckView: View {
// MARK: - PROPERTIES
#EnvironmentObject private var appDelegate: AppDelegate
// MARK: - VIEW BODY
var body: some View {
EmptyView()
.alert("App wants to quit?"), isPresented: isPresented) {
Button("Do not quit", role: .cancel, action: appDelegate.resume)
Button("Quit", action: appDelegate.close)
}
}
// MARK: - PRIVATE COMPUTED PROPERTIES
private var isPresented: Binding<Bool> {
Binding<Bool>(get: { self.appDelegate.willTerminate }, set: { self.appDelegate.willTerminate = $0 })
}
}

Search for places/ locations using MapKit and Search Bar (SwiftUI, Xcode 12.4)

I have a question about how one can connect a Search Bar with MapKit, so that it is able to search for places/ locations (not using StoryBoard). I have already written the code for the Search Bar and for the MapView in separate files, but even after trying literally every code and tutorial on the internet, I couldn't find a way to connect the Search Bar to search for locations. Below one can see respectively the used SearchBar.swift file, the MapViewController.swift and a snippet of the ContentView.swift.
SearchBar.swift
import UIKit
import Foundation
import SwiftUI
import MapKit
struct SearchBar: UIViewRepresentable {
// Binding: A property wrapper type that can read and write a value owned by a source of truth.
#Binding var text: String
// NSObject: The root class of most Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects.
// UISearchBarDelegate: A collection of optional methods that you implement to make a search bar control functional.
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
let Map = MapViewController()
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
text = ""
searchBar.showsCancelButton = true
searchBar.endEditing(true)
searchBar.resignFirstResponder()
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.showsCancelButton = true
searchBar.searchBarStyle = .minimal
//searchBar.backgroundColor = .opaqueSeparator
searchBar.showsCancelButton = true
return searchBar
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.text = text
}
}
MapViewController.swift
class MapViewController: UIViewController, CLLocationManagerDelegate {
let mapView = MKMapView()
let locationManager = CLLocationManager()
#Published var permissionDenied = false
override func viewDidLoad() {
super.viewDidLoad()
setupMapView()
checkLocationServices()
}
func setupMapView() {
view.addSubview(mapView)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
mapView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
mapView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
guard let location = locations.last else { return }
let region = MKCoordinateRegion(center: location.coordinate, span: span)
mapView.setRegion(region, animated: true)
let categories:[MKPointOfInterestCategory] = [.cafe, .restaurant]
let filters = MKPointOfInterestFilter(including: categories)
mapView.pointOfInterestFilter = .some(filters)
// Enables the scrolling around the user location without hopping back
locationManager.stopUpdatingLocation()
}
func checkLocalAuthorization() {
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
mapView.showsUserLocation = true
followUserLocation()
locationManager.startUpdatingLocation()
break
case .denied:
permissionDenied.toggle()
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
// Show alert
break
case .authorizedAlways:
break
#unknown default:
fatalError()
}
}
func checkLocationServices() {
if CLLocationManager.locationServicesEnabled() {
setupLocationManager()
checkLocalAuthorization()
} else {
// user did not turn it on
}
}
func followUserLocation() {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: 4000, longitudinalMeters: 4000)
mapView.setRegion(region, animated: true)
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
checkLocalAuthorization()
}
func setupLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
The methods are then called in the ContentView.swift, using these methods:
struct MapViewRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
return MapViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
struct ContentView: View {
#State private var searchText : String = ""
var body: some View {
ZStack(alignment: .top) {
MapViewRepresentable()
.edgesIgnoringSafeArea(.all)
.onTapGesture {
self.endTextEditing()
}
SearchBar(text: $searchText)
}
}
}
Is it possible to connect both like I explained, or is there another method you advice? I really hope you guys can help me! Thanks in advance :)

How do I debug SwiftUI AttributeGraph cycle warnings?

I'm getting a lot of AttributeGraph cycle warnings in my app that uses SwiftUI. Is there any way to debug what's causing it?
This is what shows up in the console:
=== AttributeGraph: cycle detected through attribute 11640 ===
=== AttributeGraph: cycle detected through attribute 14168 ===
=== AttributeGraph: cycle detected through attribute 14168 ===
=== AttributeGraph: cycle detected through attribute 44568 ===
=== AttributeGraph: cycle detected through attribute 3608 ===
The log is generated by (from private AttributeGraph.framework)
AG::Graph::print_cycle(unsigned int) const ()
so you can set symbolic breakpoint for print_cycle
and, well, how much it could be helpful depends on your scenario, but definitely you'll get error generated stack in Xcode.
For me this issue was caused by me disabling a text field while the user was still editing it.
To fix this, you must first resign the text field as the first responder (thus stopping editing), and then disable the text field.
I explain this more in this Stack Overflow answer.
For me, this issue was caused by trying to focus a TextField right before changing to the tab of a TabView containing the TextField.
It was fixed by simply focusing the TextField after changing the TabView tab.
This seems similar to what #wristbands was experiencing.
For me the issue was resolved by not using UIActivityIndicator... not sure why though. The component below was causing problems.
public struct UIActivityIndicator: UIViewRepresentable {
private let style: UIActivityIndicatorView.Style
/// Default iOS 11 Activity Indicator.
public init(
style: UIActivityIndicatorView.Style = .large
) {
self.style = style
}
public func makeUIView(
context: UIViewRepresentableContext<UIActivityIndicator>
) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
public func updateUIView(
_ uiView: UIActivityIndicatorView,
context: UIViewRepresentableContext<UIActivityIndicator>
) {}
}
#Asperi Here is a minimal example to reproduce AttributeGraph cycle:
import SwiftUI
struct BoomView: View {
var body: some View {
VStack {
Text("Go back to see \"AttributeGraph: cycle detected through attribute\"")
.font(.title)
Spacer()
}
}
}
struct TestView: View {
#State var text: String = ""
#State private var isSearchFieldFocused: Bool = false
var placeholderText = NSLocalizedString("Search", comment: "")
var body: some View {
NavigationView {
VStack {
FocusableTextField(text: $text, isFirstResponder: $isSearchFieldFocused, placeholder: placeholderText)
.foregroundColor(.primary)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
NavigationLink(destination: BoomView()) {
Text("Boom")
}
Spacer()
}
.onAppear {
self.isSearchFieldFocused = true
}
.onDisappear {
isSearchFieldFocused = false
}
}
}
}
FocusableTextField.swift based on https://stackoverflow.com/a/59059359/659389
import SwiftUI
struct FocusableTextField: UIViewRepresentable {
#Binding public var isFirstResponder: Bool
#Binding public var text: String
var placeholder: String = ""
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, placeholder: String = "", configuration: #escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
self.placeholder = placeholder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.placeholder = placeholder
view.autocapitalizationType = .none
view.autocorrectionType = .no
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
#objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
For me the issue was that I was dynamically loading the AppIcon asset from the main bundle. See this Stack Overflow answer for in-depth details.
I was using enum cases as tag values in a TabView on MacOS. The last case (of four) triggered three attributeGraph cycle warnings. (The others were fine).
I am now using an Int variable (InspectorType.book.typeInt instead of InspectorType.book) as my selection variable and the cycle warnings have vanished.
(I can demonstrate this by commenting out the offending line respectively by changing the type of my selection; I cannot repeat it in another app, so there's obviously something else involved; I just haven't been able to identify the other culprit yet.)

OSX SwiftUI integrating NSComboBox Not refreshing current sélected

SwiftUI Picker is looking very bad on OSX especially when dealing with long item lists
Swiftui Picker on OSX with a long item list
And since did find any solution to limit the number of item displayed in by Picker on Osx , I decided to interface NSComboBox to SwiftUI
Everythings looks fine until the selection index is modified programmatically using the #Published index of the Observable Comboselection class instance (see code below) :
the updateNSView function of the NSViewRepresentable instance is called correctly then (print message visible on the log)
combo.selectItem(at: selected.index)
combo.selectItem(at: selected.index)
combo.objectValue = combo.objectValueOfSelectedItem
print("populating index change \(selected.index) to Combo : (String(describing: combo.objectValue))")
is executed correctly and the printed log shows up the correct information
But the NSComboBox textfield is not refreshed with the accurate object value
Does somebody here have an explanation ?? ; is there something wrong in code ??
here the all code :
import SwiftUI
class ComboSelection : ObservableObject {
#Published var index : Int
init( index: Int ) {
self.index = index
}
func newSelection( newIndex : Int ) {
index = newIndex
}
}
//
// SwiftUI NSComboBox component interface
//
struct SwiftUIComboBox : NSViewRepresentable {
typealias NSViewType = NSComboBox
var content : [String]
var nbLines : Int
var selected : ComboSelection
final class Coordinator : NSObject ,
NSComboBoxDelegate {
var control : SwiftUIComboBox
var selected : ComboSelection
init( _ control: SwiftUIComboBox , selected : ComboSelection ) {
self.selected = selected
self.control = control
}
func comboBoxSelectionDidChange(_ notification: Notification) {
print ("entering coordinator selection did change")
let combo = notification.object as! NSComboBox
selected.newSelection( newIndex: combo.indexOfSelectedItem )
}
}
func makeCoordinator() -> SwiftUIComboBox.Coordinator {
return Coordinator(self, selected:selected)
}
func makeNSView(context: NSViewRepresentableContext<SwiftUIComboBox>) -> NSComboBox {
let returned = NSComboBox()
returned.numberOfVisibleItems = nbLines
returned.hasVerticalScroller = true
returned.usesDataSource = false
returned.delegate = context.coordinator // Important : not forget to define delegate
for key in content{
returned.addItem(withObjectValue: key)
}
return returned
}
func updateNSView(_ combo: NSComboBox, context: NSViewRepresentableContext<SwiftUIComboBox>) {
combo.selectItem(at: selected.index)
combo.objectValue = combo.objectValueOfSelectedItem
print("populating index change \(selected.index) to Combo : \(String(describing: combo.objectValue))")
}
}
Please see updated & simplified your code with added some working demo. The main reason of issue was absent update of SwiftUI view hierarchy, so to have such update I've used Binding, which transfers changes to UIViewRepresentable and back. Hope this approach will be helpful.
Here is demo
Below is one-module full demo code (just set
window.contentView = NSHostingView(rootView:TestComboBox()) in app delegate
struct SwiftUIComboBox : NSViewRepresentable {
typealias NSViewType = NSComboBox
var content : [String]
var nbLines : Int
#Binding var selected : Int
final class Coordinator : NSObject, NSComboBoxDelegate {
var selected : Binding<Int>
init(selected : Binding<Int>) {
self.selected = selected
}
func comboBoxSelectionDidChange(_ notification: Notification) {
print ("entering coordinator selection did change")
if let combo = notification.object as? NSComboBox, selected.wrappedValue != combo.indexOfSelectedItem {
selected.wrappedValue = combo.indexOfSelectedItem
}
}
}
func makeCoordinator() -> SwiftUIComboBox.Coordinator {
return Coordinator(selected: $selected)
}
func makeNSView(context: NSViewRepresentableContext<SwiftUIComboBox>) -> NSComboBox {
let returned = NSComboBox()
returned.numberOfVisibleItems = nbLines
returned.hasVerticalScroller = true
returned.usesDataSource = false
returned.delegate = context.coordinator // Important : not forget to define delegate
for key in content {
returned.addItem(withObjectValue: key)
}
return returned
}
func updateNSView(_ combo: NSComboBox, context: NSViewRepresentableContext<SwiftUIComboBox>) {
if selected != combo.indexOfSelectedItem {
DispatchQueue.main.async {
combo.selectItem(at: self.selected)
print("populating index change \(self.selected) to Combo : \(String(describing: combo.objectValue))")
}
}
}
}
struct TestComboBox: View {
#State var selection = 0
let content = ["Alpha", "Beta", "Gamma", "Delta", "Epselon", "Zetta", "Eta"]
var body: some View {
VStack {
Button(action: {
if self.selection + 1 < self.content.count {
self.selection += 1
} else {
self.selection = 0
}
}) {
Text("Select next")
}
Divider()
SwiftUIComboBox(content: content, nbLines: 3, selected: $selection)
Divider()
Text("Current selection: \(selection), value: \(content[selection])")
}
.frame(width: 300, height: 300)
}
}

Disabling macOS focus ring in SwiftUI

Is it possible to disable the focus ring around a TextField in swiftUI for Mac?
I had that question as well, and after a couple hours of fiddling around, it seems like the answer is no. However, it is possible to wrap an NSTextField and get rid of the focus ring.
The following code has been tested in the latest release.
struct CustomTextField: NSViewRepresentable {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField(string: text)
textField.delegate = context.coordinator
textField.isBordered = false
textField.backgroundColor = nil
textField.focusRingType = .none
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}
func makeCoordinator() -> Coordinator {
Coordinator { self.text = $0 }
}
final class Coordinator: NSObject, NSTextFieldDelegate {
var setter: (String) -> Void
init(_ setter: #escaping (String) -> Void) {
self.setter = setter
}
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
setter(textField.stringValue)
}
}
}
}
As stated in an answer by Asperi to a similar question here, it's not possible (yet) to turn off the focus ring for a specific field using SwiftUI; however, the following workaround will disable the focus ring for all NSTextField instances in the app:
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}
Not ideal, but it does provide one option that doesn't require stepping too far outside of SwiftUI.

Resources