How do I invoke an #IBAction on an NSViewController wrapped in NSViewControllerRepresentable? - swiftui

How do I invoke an #IBAction on an NSViewController subclass wrapped in NSViewControllerRepresentable?

One proposed solution I found worked in this case (where it was enough to send the action through the NSResponder chain). Iā€™d still be curious to know how to target a specific NSResponder (for example, if I want to send a command to my embedded view controller from the toolbar).
(in my Swift app commands):
CommandMenu("View") {
Button {
NSApp.sendAction(#selector(AppCommands.zoomIn), to: nil, from: nil)
} label: {
Label("Zoom In", systemImage: "plus")
}
.keyboardShortcut("+", modifiers: [.command])
#objc
protocol
AppCommands
{
func zoomIn(_ : Any?)
func zoomOut(_ : Any?)
}

Related

UIButton in SwiftUI Catalyst Mac app doesn't work when clicked second time

Here's the code I have:
private struct ShareButton: UIViewRepresentable {
func makeUIView(context: Context) -> UIButton {
let activityViewController = UIActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!], applicationActivities: nil)
let action = UIAction(title: "Share") { _ in UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController?.present(activityViewController, animated: false) }
let button = UIButton(primaryAction: action)
activityViewController.popoverPresentationController?.sourceView = button
return button
}
func updateUIView(_ uiView: UIButton, context: Context) { }
}
Basically it's creating a UIButton with a UIAction, inside which there's a UIActivityViewController that set sourceView for the share menu to be the UIButton.
Here's a demo of the issue:
The UIButton is created when the SwiftUI view is created, and set as the sourceView. My guess is that the issue occur because the UIButton is somehow destroyed and recreated due to some SwiftUI mechanism? I can be entirely wrong though. Anyway to solve this?
Or any other way to do share button in a SwiftUI Catalyst Mac app?
"Or any other way to do share button in a SwiftUI Catalyst Mac app?"
You could try this approach, using the extension from:
How to get rid of message " 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" with AdMob banner?
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter({
$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
struct ContentView: View {
let holaTxt = "Hola šŸ˜€ "
var body: some View {
Button(action: {
let AV = UIActivityViewController(activityItems: [holaTxt], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(AV, animated: true, completion: nil)
}) {
Text("Share")
}
}
}
Found an elegant solution for creating a share button in a SwiftUI Catalyst Mac app (in fact, all SwiftUI app), see https://github.com/SwiftUI-Plus/ActivityView

Display Share Sheet (UIActivityViewController) in a Modal .sheet() in SwiftUI [duplicate]

I'm trying to present a UIActivityViewController (share sheet) from a SwiftUI View. I created a view called ShareSheet conformed to UIViewControllerRepresentable to configure the UIActivityViewController, but it's turning out to be not as trivial to actually present this.
struct ShareSheet: UIViewControllerRepresentable {
typealias UIViewControllerType = UIActivityViewController
var sharing: [Any]
func makeUIViewController(context: UIViewControllerRepresentableContext<ShareSheet>) -> UIActivityViewController {
UIActivityViewController(activityItems: sharing, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ShareSheet>) {
}
}
Doing so naively via .sheet leads to the following.
.sheet(isPresented: $showShareSheet) {
ShareSheet(sharing: [URL(string: "https://example.com")!])
}
Is there a way to present this like it's usually presented? As in covering half the screen?
Hope this will help you,
struct ShareSheetView: View {
var body: some View {
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36)
}
}
func actionSheet() {
guard let data = URL(string: "https://www.apple.com") else { return }
let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
}
}
In iOS 14, Swift 5, Xcode 12.5 at least, I was able to accomplish this fairly easily by simply wrapping the UIActivityViewController in another view controller. It doesn't require inspecting the view hierarchy or using any 3rd party libraries. The only hackish part is asynchronously presenting the view controller, which might not even be necessary. Someone with more SwiftUI experience might be able to offer suggestions for improvement.
import Foundation
import SwiftUI
import UIKit
struct ActivityViewController: UIViewControllerRepresentable {
#Binding var shareURL: URL?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let containerViewController = UIViewController()
return containerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
guard let shareURL = shareURL, context.coordinator.presented == false else { return }
context.coordinator.presented = true
let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil)
activityViewController.completionWithItemsHandler = { activity, completed, returnedItems, activityError in
self.shareURL = nil
context.coordinator.presented = false
if completed {
// ...
} else {
// ...
}
}
// Executing this asynchronously might not be necessary but some of my tests
// failed because the view wasn't yet in the view hierarchy on the first pass of updateUIViewController
//
// There might be a better way to test for that condition in the guard statement and execute this
// synchronously if we can be be sure updateUIViewController is invoked at least once after the view is added
DispatchQueue.main.asyncAfter(deadline: .now()) {
uiViewController.present(activityViewController, animated: true)
}
}
class Coordinator: NSObject {
let parent: ActivityViewController
var presented: Bool = false
init(_ parent: ActivityViewController) {
self.parent = parent
}
}
}
struct ContentView: View {
#State var shareURL: URL? = nil
var body: some View {
ZStack {
Button(action: { shareURL = URL(string: "https://apple.com") }) {
Text("Share")
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
if shareURL != nil {
ActivityViewController(shareURL: $shareURL)
}
}
.frame(width: 375, height: 812)
}
}
iOS 15 / Swift 5 / Xcode 13
Extension to get the top presented UIViewController:
import UIKit
extension UIApplication {
// MARK: No shame!
static func TopPresentedViewController() -> UIViewController? {
guard let rootViewController = UIApplication.shared
.connectedScenes.lazy
.compactMap({ $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil })
.first(where: { $0.keyWindow != nil })?
.keyWindow?
.rootViewController
else {
return nil
}
var topController = rootViewController
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
}
Then use it to present your UIActivityViewController:
UIApplication.TopPresentedViewController?.present(activityViewController, animated: true, completion: nil)
Original Answer (deprecated code):
It's not pretty but you can call it directly like this (considering your app has only 1 window):
UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true, completion: nil)
And if you get some warning blablabla:
Warning: Attempt to present ... which is already presenting ...
you can do something like this to get the top most view controller and call present on it.
There's a UIModalPresentationStyle which can be used to display certain presentations:
case pageSheet
A presentation style that partially covers the underlying content.
The way you apply the presentation style:
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
let v = UIActivityViewController(activityItems: sharing, applicationActivities: nil)
v.modalPresentationStyle = .pageSheet
return v
}
A list of the Presentations can be found here:
https://developer.apple.com/documentation/uikit/uimodalpresentationstyle
I haven't yet tested them all myself so I apologise in advance if this didn't end up working like you expected it to.
Alternatively you can have a look at this answer where they mention a third-party library, which will allow you to create a half modal in the way that it's usually presented.

Wrapped SwiftUI View popping itself

I have a SwiftUI View wrapped in a UIHostingController that's then pushed onto the nav stack from a UIViewController.
It's pushed in this way:
let controller = TransactionDetailsHostingController(transactionRecord: record)
navigationController?.pushViewController(controller, animated: true)
I want a custom back button that will pop the view from the navstack and I tried the technique used here and elsewhere: SwiftUI - Is there a popViewController equivalent in SwiftUI?
Unfortunately, it doesn't work. The wrapped view doesn't pop from the navstack. I need a solution that works on iOS 13.
There are two approaches for pop to view the controller.
Use closure. Create closure inside the SwiftUI view and call from the controller.
Here is the demo.
struct SwiftUIView: View {
var onBack: () -> Void
var body: some View {
Button(action: {
onBack()
}, label: {
Text("Go to back")
})
}
}
class ViewController: UIViewController {
#IBAction func onNavigation(_ sender: UIButton) {
let controller = UIHostingController(rootView: SwiftUIView(onBack: {
self.navigationController?.popViewController(animated: true)
}))
navigationController?.pushViewController(controller, animated: true)
}
}
Find the top most controller and pop. You can find top controller from here
And then use like this
UIApplication.shared.topMostViewController()?.navigationController?.popViewController(animated: true)

How to load a webpage in an ovally inside a SwiftUI App vs loading in safari

The functionality I'm looking to create is what the ESPN app does when you click on one of its alerts... it loads the app but instead of formatting the view it loads a Safari view over the app that can be tapped away (honestly I hate it in that instance but in ones like these it would work great.)
current code for reference
Button(action: {
openURL(URL(string: "linkhere")!)
}) {
Image("LISTENMENU")
}
Am I going to need to setup another view and build the webkitview myself or can this functionality be specified another way? (perhaps by tinkering with the openURL string
You need to wrap the SFSafariViewController (which is from UIKit) into SwiftUI, since it isn't possible to do this natively right now. You should use UIViewControllerRepresentable to do this.
import SwiftUI
import SafariServices
struct MyView: View {
#State var showStackoverflow:Bool = false
var body: some View {
Button(action: { self.showStackoverflow = true }) {
Text("Open stackoverflow")
}
.sheet(isPresented: self.$showStackoverflow) {
SFSafariViewWrapper(url: URL(string: "https://stackoverflow.com")!)
}
}
}
struct SFSafariViewWrapper: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SFSafariViewWrapper>) {
return
}
}

popover close from another view controller swift 3

i work with Xcode 8 and swift 3.
i have a viewcontroller (Class User:NSViewcontroller) with a button.
This Button has an action:
#IBAction func btnAction(_ sender: Any) {
let popover = NSPopover()
popover.contentViewController = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "Popover") as! NSViewController
popover.show(relativeTo: button.bunds , of: button, preferredEdge: .maxX)
}
This button open another view controller as popover.
this popover view controller (Class Popover:NSViewController) do some actions.
now my question is: how can I close the popover viewcontroller from the popover class?
Not sure about the above code but in order to create your PopoverView and dismiss it. Here are the steps:-
1) Create one new View Controller names as PopOverViewController.swift
2) Implement the below method inside that:-
func updatePopOverViewController(_ button: UIButton?, with delegate: AnyObject?) {
guard let button = button else { return }
modalPresentationStyle = .popover
popoverPresentationController?.permittedArrowDirections = [.up]
popoverPresentationController?.backgroundColor = view.backgroundColor
popoverPresentationController?.sourceView = button
popoverPresentationController?.sourceRect = button.bounds
popoverPresentationController?.delegate = OtherViewControllerClass
}
3) Inside your OtherViewControllerClass implement the following code:-
extension OtherViewControllerClass: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
//your code for eg. if you want to change the tint color of our button
}
//When you tap on button it will show the popover and while tapping on yourViewcontroller view it will dismiss the popover accordingly.
#IBAction func buttonClicked(_ sender: UIButton) {
let viewController = PopOverViewController()
viewController.updatePopOverViewController(sender, with: self)
present(viewController, animated: true, completion: nil)
}
}
Well I might be totally guessing here as I have no idea about how your ViewController is written but this should fairly work
self.presentedViewController?.dismiss(animated: true, completion: nil)
The basic thing here is you can access the VC which presented the popover and then call dismiss over it.