Right click to show popover SwiftUI / MacOS - swiftui

When I pass a binding into the NSViewRepresentable, it does not update. Any tips? My use case is to show a popover on right click.
(Also posted on Apple Dev Forums: https://developer.apple.com/forums/thread/655056
Some View Struct
#State var showMenu = false
var body: some View {
ZStack {
RightClickableSwiftUIView(onClick: $showMenu)
Image(name)
.popover(isPresented: $showMenu, ...)
}
}
RightClick.swift
import SwiftUI
struct RightClickableSwiftUIView: NSViewRepresentable {
#Binding var onClick: Bool
func updateNSView(_ nsView: RightClickableView, context: NSViewRepresentableContext<RightClickableSwiftUIView>) {
}
func makeNSView(context: Context) -> RightClickableView {
RightClickableView(onClick: $onClick)
}
class RightClickableView : NSView {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(onClick: Binding<Bool>) {
_onClick = onClick
super.init(frame: NSRect())
}
#Binding var onClick: Bool
override func mouseDown(with theEvent: NSEvent) {
print("left mouse")
}
override func rightMouseDown(with theEvent: NSEvent) {
onClick = true
print("right mouse")
}
}
}

Oh egads that's embarrassing. The class was in the struct, so of course the Binding didn't update.

Related

SwiftUI: In SplitView, how can I detect if the master view is visible?

When SwiftUI creates a SplitView, it adds a toolbar button that hides/shows the Master view. How can I detect this change so that I can resize the font in the detail screen and use all the space optimally?
I've tried using .onChange with geometry but can't seem to get that to work.
If you're using iOS 16 you can use NavigationSplitView with NavigationSplitViewVisibility
Example:
struct MySplitView: View {
#State private var columnVisibility: NavigationSplitViewVisibility = .all
var bothAreShown: Bool { columnVisibility != .detailOnly }
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Master Column")
} detail: {
Text("Detail Column")
Text(bothAreShown ? "Both are shown" : "Just detail shown")
}
}
}
After thinkering for a while on this I got to this solution:
struct ContentView: View {
#State var isOpen = true
var body: some View {
NavigationView {
VStack{
Text("Primary")
.onUIKitAppear {
isOpen.toggle()
}
.onAppear{
print("hello")
isOpen.toggle()
}
.onDisappear{
isOpen.toggle()
print("hello: bye")
}
.navigationTitle("options")
}
Text("Secondary").font(isOpen ? .body : .title)
}.navigationViewStyle(.columns)
}
}
The onUIKitAppear is a custom extension suggested by apple to be only executed once the view has been presented to the user https://developer.apple.com/forums/thread/655338?page=2
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(action: self.action)
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
class Coordinator: ActionRepresentable {
var action: () -> Void
init(action: #escaping () -> Void) {
self.action = action
}
func remoteAction() {
action()
}
}
}
protocol ActionRepresentable: AnyObject {
func remoteAction()
}
class UIAppearViewController: UIViewController {
weak var delegate: ActionRepresentable?
var savedView: UIView?
override func viewDidLoad() {
self.savedView = UILabel()
if let _view = self.savedView {
view.addSubview(_view)
}
}
override func viewDidAppear(_ animated: Bool) {
delegate?.remoteAction()
}
override func viewDidDisappear(_ animated: Bool) {
view.removeFromSuperview()
savedView?.removeFromSuperview()
}
}
public extension View {
func onUIKitAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}

Can't drag down/dismiss UIColorPickerViewController

I am displaying a UIColorPickerViewController as a sheet using the sheet() method, everything works fine but I can't drag down/dismiss the view anymore.
import Foundation
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
private var selectedColor: UIColor!
init(selectedColor: UIColor) {
self.selectedColor = selectedColor
}
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let colorPicker = UIColorPickerViewController()
colorPicker.selectedColor = self.selectedColor
return colorPicker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
// Silent
}
}
.sheet(isPresented: self.$viewManager.showSheet, onDismiss: {
ColorPickerView()
}
Any idea how to make the drag/down dismiss gesture works?
Thanks!
Ran into the same problem when trying to build a color picker similar to above. What worked was "wrapping" the color picker in a view with a Dismiss button. And also discovered that the bar at the top of the view would allow the picker to now be dragged down and away. Below is my wrapper. (One could add more features such as a title to the bar.)
struct ColorWrapper: View {
var inputColor: UIColor
#Binding var isShowingColorPicker: Bool
#Binding var selectedColor: UIColor?
var body: some View {
VStack {
HStack {
Spacer()
Button("Dismiss", action: {
isShowingColorPicker = false
}).padding()
}
ColorPickerView(inputColor: inputColor, selectedColor: $selectedColor)
}
}
}
And for completeness, here is my version of the color picker:
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIColorPickerViewController
var inputColor: UIColor
#Binding var selectedColor: UIColor?
#Environment(\.presentationMode) var isPresented
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let picker = UIColorPickerViewController()
picker.delegate = context.coordinator
picker.supportsAlpha = false
picker.selectedColor = inputColor
return picker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
uiViewController.supportsAlpha = false
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIColorPickerViewControllerDelegate {
var parent: ColorPickerView
init(parent: ColorPickerView) {
self.parent = parent
}
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
parent.isPresented.wrappedValue.dismiss()
}
func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
parent.selectedColor = color
// parent.isPresented.wrappedValue.dismiss()
}
}
}

UISearchController how to activate from SwiftUI

I found an integration of the uiSearchController in SwiftUI, but I don't know how to let it become active?
I found this:
I want that the searchBar becomes active when changing an Bool in the SwiftUI View with a #State for example.
If I add a Binding to the view modifier and set the isActive property of the searchController in
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
then is doesn't become active.
Im not really familiar with UIKit, perhaps anybody knows how to correctly activate the searchbar that one can start typing for a search.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {
}
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
To activate a UISearchBar (which is what you're using), just do:
searchController.searchBar.becomeFirstResponder()
(from this answer)
Now all we need to do is reference searchController.searchBar from the SwiftUI view. First, add a function to your SearchBar class.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
/// add this function
func activate() {
searchController.searchBar.becomeFirstResponder()
}
}
Then, just call it. I think this is better than setting a #State, but if you require that, let me know and I'll edit my answer.
struct ContentView: View {
#StateObject var searchBar = SearchBar()
var body: some View {
NavigationView {
Button(action: {
searchBar.activate() /// activate the search bar
}) {
Text("Activate search bar")
}
.modifier(SearchBarModifier(searchBar: searchBar))
.navigationTitle("Navigation View")
}
}
}
Result:

How to dismiss PKToolPicker when view disappears in SwiftUI?

I'm trying to wrap PKCanvasView as a SwiftUI view called CanvasView. I'd like to be able to toggle the whole canvas on top of another view. When the CanvasView appears, I'd like the PKToolPicker to appear. When it disappears, I'd like the PKToolPicker to disappear.
I've found a few similar approaches on here but they only involve showing the picker or toggling the picker with a button; I'd like the picker visibility to be tied to the view visibility.
In the below example you can see that you can toggle the canvas, but once the tool picker is visible, it stays visible.
Here's my CanvasView:
import SwiftUI
import PencilKit
struct CanvasView: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var canvasView: Binding<PKCanvasView>
let onChange: () -> Void
init(canvasView: Binding<PKCanvasView>, onChange: #escaping () -> Void) {
self.canvasView = canvasView
self.onChange = onChange
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing.bounds.isEmpty == false {
onChange()
}
}
}
#Binding var canvasView: PKCanvasView
#Binding var toolPickerIsActive: Bool
private let toolPicker = PKToolPicker()
let onChange: () -> Void
func makeUIView(context: Context) -> PKCanvasView {
canvasView.backgroundColor = .clear
canvasView.isOpaque = true
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
toolPicker.setVisible(toolPickerIsActive, forFirstResponder: uiView)
}
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, onChange: onChange)
}
}
And an example ContentView:
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var toolPickerIsActive = false
#State private var canvasIsVisible = false
var body: some View {
ZStack {
if canvasIsVisible {
CanvasView(canvasView: $canvasView,
toolPickerIsActive: $toolPickerIsActive,
onChange: canvasDidChange)
.onAppear { toolPickerIsActive = true }
.onDisappear { toolPickerIsActive = false }
}
Button(action: {
canvasIsVisible.toggle()
}, label: {
Text("Toggle canvas view")
})
}
}
private func canvasDidChange() {
// Do something with updated canvas.
}
}
Any guidance would be much appreciated!
In your scenario the CanvasView is destroyed on disappear, so SwiftUI rendering engine just not update it on any state change (as it see that no needs for that).
The possible solution for this use-case is to hide picker on coordinator deinit (because it is destroyed with owner view).
Here is a demo. Tested with Xcode 12.4 / iOS 14.4
struct CanvasView: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var canvasView: Binding<PKCanvasView>
let onChange: () -> Void
private let toolPicker: PKToolPicker
deinit { // << here !!
toolPicker.setVisible(false, forFirstResponder: canvasView.wrappedValue)
toolPicker.removeObserver(canvasView.wrappedValue)
}
init(canvasView: Binding<PKCanvasView>, toolPicker: PKToolPicker, onChange: #escaping () -> Void) {
self.canvasView = canvasView
self.onChange = onChange
self.toolPicker = toolPicker
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing.bounds.isEmpty == false {
onChange()
}
}
}
#Binding var canvasView: PKCanvasView
#Binding var toolPickerIsActive: Bool
private let toolPicker = PKToolPicker()
let onChange: () -> Void
func makeUIView(context: Context) -> PKCanvasView {
canvasView.backgroundColor = .clear
canvasView.isOpaque = true
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
toolPicker.setVisible(toolPickerIsActive, forFirstResponder: uiView)
}
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, toolPicker: toolPicker, onChange: onChange)
}
}
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var toolPickerIsActive = false
#State private var canvasIsVisible = false
var body: some View {
ZStack {
if canvasIsVisible {
CanvasView(canvasView: $canvasView,
toolPickerIsActive: $toolPickerIsActive,
onChange: canvasDidChange)
.onAppear { toolPickerIsActive = true }
// .onDisappear { toolPickerIsActive = false }
}
Button(action: {
canvasIsVisible.toggle()
}, label: {
Text("Toggle canvas view")
})
}
}
private func canvasDidChange() {
// Do something with updated canvas.
}
}
Note: there might be redesign of ownership, so toolPicker will live only within coordinator, but it does not change idea, and is up to you.

How to change #State property wrapper from nested view

I'm wondering how I should change #State property wrapper showErrorAlert in view below
struct SettingsView: View {
#State private var shouldPresent = false
#State var showErrorAlert = false
var body: some View {
VStack {
Form {
Text("Settings")
.font(.title)
Button("Import source data") {
self.shouldPresent.toggle()
}
.sheet(isPresented: $shouldPresent) {
DocumentPicker()
}
Button("Show error alert") {
self.showErrorAlert.toggle()
}
.alert(isPresented: $showErrorAlert, content: {
Alert(title: Text("Error"))
})
}
}
}
}
from DocumentPicker struct code in case that reading of selected file fails.
struct DocumentPicker: UIViewControllerRepresentable {
func makeCoordinator() -> DocumentPicker.Coordinator {
return DocumentPicker.Coordinator(parent: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeJSON)], in: .import)
picker.allowsMultipleSelection = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocumentPicker>) {
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var myParent: DocumentPicker
init(parent: DocumentPicker) {
myParent = parent
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let fileURL = urls.first!
do {
let origFile = try String(contentsOf: fileURL)
//File processing will be here
} catch let error {
print(error)
}
}
}
}
I mean how to set property wrapper value to true to show the alert. Should I rather use #ObservedObject or #EnvironmentObject instead?
Thanks.
To change the wrapper value in your DocumentPicker struct you can define a #Binding variable and pass your value to it, this toggle your variable on your parent view, but before showing the alert you need to dismiss the DocumentPicker