macOS SwiftUI: MenuItem to open default browser to a URL? - xcode

I've been trying to make a MenuItem in my macOS SwiftUI app to open a default browser to a specific URL.
Since I already had a MenuItem open up a PDF, I tried to modify this:
#IBAction func Guide1(_ sender: Any) {
if let pdfURL = Bundle.main.url(forResource: "Guide1", withExtension: "pdf"){
if NSWorkspace.shared.open(pdfURL) {
}
}
}
Into this:
#IBAction func Google(_ sender: NSMenuItem) {
if let fileURL = Bundle.main.url(forResource: "http://google.fi") {
NSWorkspace.shared.open(fileURL as URL)
}
}
But kept being told that forResource should be replaced with forAuxiliaryExecutable. I make that change, and the code still does nothing.
I've mapped, of course the MenuItem Google to First Responder and then to the specific IBAction, but..
What am I missing?

It should be as follows
#IBAction func Google(_ sender: NSMenuItem) {
if let url = URL(string: "http://google.fi") {
NSWorkspace.shared.open(url)
}
}

Related

Create a share sheet in iOS 15 with swiftUI

I am trying to create a share sheet to share a Text, it was working fine in iOS 14 but in iOS 15 it tells me that
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead.
how can I make it work on iOS 15 with SwiftUI
Button {
let TextoCompartido = "Hola 😀 "
let AV = UIActivityViewController(activityItems: [TextoCompartido], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(AV, animated: true, completion: nil)
}
I think you would be best served using SwiftUI APIs directly. Generally, I would follow these steps.
Create SwiftUI View named ActivityView that adheres to UIViewControllerRepresentable. This will allow you to bring UIActivityViewController to SwiftUI.
Create an Identifiable struct to contain the text you'd like to display in the ActivityView. Making this type will allow you to use the SwiftUI sheet API and leverage SwiftUI state to tell the app when a new ActivityView to be shown.
Create an optional #State variable that will hold on to your Identifiable text construct. When this variable changes, the sheet API will perform the callback.
When the button is tapped, update the state of the variable set in step 3.
Use the sheet API to create an ActivityView which will be presented to your user.
The code below should help get you started.
import UIKit
import SwiftUI
// 1. Activity View
struct ActivityView: UIViewControllerRepresentable {
let text: String
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: [text], applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {}
}
// 2. Share Text
struct ShareText: Identifiable {
let id = UUID()
let text: String
}
struct ContentView: View {
// 3. Share Text State
#State var shareText: ShareText?
var body: some View {
VStack {
Button("Show Activity View") {
// 4. New Identifiable Share Text
shareText = ShareText(text: "Hola 😀")
}
.padding()
}
// 5. Sheet to display Share Text
.sheet(item: $shareText) { shareText in
ActivityView(text: shareText.text)
}
}
}
For the future, iOS 16 will have the ShareLink view which works like this:
Gallery(...)
.toolbar {
ShareLink(item: image, preview: SharePreview("Birthday Effects"))
}
Source: https://developer.apple.com/videos/play/wwdc2022/10052/
Time code offset: 25 minutes 28 seconds
To avoid warning, change the way you retrieve the window scene.
Do the following:
Button {
let TextoCompartido = "Hola 😀 "
let AV = UIActivityViewController(activityItems: [TextoCompartido], applicationActivities: nil)
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
windowScene?.keyWindow?.rootViewController?.present(AV, animated: true, completion: nil)
}
Tested in in iOS 15 with SwiftUI
func shareViaActionSheet() {
if vedioData.vedioURL != nil {
let activityVC = UIActivityViewController(activityItems: [vedioData.vedioURL as Any], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(activityVC, animated: true, completion: nil)
}
}
To avoid iOS 15 method deprecation warning use this extension
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
}
}
you could try the following using the answer 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?
Note that your code works for me, but the compiler give the deprecation warning.
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 TextoCompartido = "Hola 😀 "
var body: some View {
Button(action: {
let AV = UIActivityViewController(activityItems: [TextoCompartido], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(AV, animated: true, completion: nil)
// This works for me, but the compiler give the deprecation warning
// UIApplication.shared.windows.first?.rootViewController?.present(AV, animated: true, completion: nil)
}) {
Text("Hola click me")
}
}
}

macOS: SwiftUI: MenuItem for taking a screenshot of WKWebView and save it to ~/Pictures with a timestamp?

I'm creating a macOS SwiftUI app that opens a WKWebView to a specific URL.
Now I'm attempting to make a MenuItem mapped to a function that takes a screenshot of the WKWebView window, and saves it to ~/Pictures with a timestamp.
I tried to look for this via tutorials but only found iOS WKSnapShot type stuff.
While the "MenuItem" -> bind to -> First Responder -> #IBAction is something I'm kind of familiar with now, I'm not entirely sure how to call the WKWebView snapshotting and how to define it's timestamped name.
#IBAction func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?,
completionHandler: #escaping (NSImage?, Error?) -> Void)
{
}
This started shooting errors at me: #IBAction methods must have 1 argument
You just need to call the snapshot function on the webView. To get the webView snapshot function to be available, you need to make it available in the AppDelegate.swift
Then you can save it by using saveImage - which is linked in the handy function at this answer.
public let webView: WKWebView = WKWebView()
#IBAction func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration,
completionHandler: #escaping (NSImage *snapshotImage) -> Void)
{
self.webView.takeSnapshot(with: snapshotConfiguration) { image, error in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy_MM_dd_hh_mm_ss"
name = (formatter.string(from: Date()) as NSString) as String
if let image = image {
saveImage(name, image)
}
}
}

Swift - Failed (found nil) calling reloadData() from another class but succeeded from self class

I'm apparently designing a drag and drop dropbox which can either select files by clicking it or dragging and dropping the files on it, and I want the selected files to be visible in a table next to it. My design logic is that whenever the user selects files from an NSOpenPanel, it passes the selected file paths into the CoreData and then an array retrieves them one by one from the CoreData, and finally, update the NSTableView's content by using reloadData().
Basically, my problem is that whenever I try to call ViewController().getDroppedFiles() from DropboxButton class, I always get a Fatal error: unexpectedly found nil while unwrapping an optional value.
My ViewController.swift:
import Cocoa
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
getDroppedFiles()
}
#IBOutlet weak var DroppedFilesTableView: NSTableView!
var droppedFiles: [DroppedFiles] = [] // Core Data class definition: DroppedFiles
func numberOfRows(in tableView: NSTableView) -> Int {
return droppedFiles.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let droppedFilesCollection = droppedFiles[row]
if (tableView?.identifier)!.rawValue == "fileNameColumn" {
if let fileNameCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "fileNameCell")) as? NSTableCellView {
fileNameCell.textField?.stringValue = droppedFilesCollection.fileName!
return fileNameCell
}
} else if (tableView?.identifier)!.rawValue == "filePathColumn" {
if let filePathCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "filePathCell")) as? NSTableCellView {
filePathCell.textField?.stringValue = droppedFilesCollection.filePath!
return filePathCell
}
}
return nil
}
#IBAction func DropboxClicked(_ sender: NSButton) {
// selected file paths
for filePath in selectedFilePaths {
if let context = (NSApp.delegate as? AppDelegate)?.persistentContainer.viewContext {
let droppedFilesData = DroppedFiles(context: context)
droppedFilesData.fileName = getFileName(withPath: filePath)
droppedFilesData.filePath = filePath
do {
try context.save()
} catch {
print("Unable to save core data.")
}
}
getDroppedFiles()
}
}
func getDroppedFiles() {
if let context = (NSApp.delegate as? AppDelegate)?.persistentContainer.viewContext {
do {
try droppedFiles = context.fetch(DroppedFiles.fetchRequest())
} catch {
print("Unable to fetch core data.")
}
}
DroppedFilesTableView.reloadData() // Fatal Error: unexpectedly found nil while unwrapping an optional value (whenever I call this function in other class)
}
}
I'm using a push button (NSButton) as the dropbox (it has its own class), which can easily be clicked and also supports dragging options.
My DropboxButton.swift:
import Cocoa
class DropboxButton: NSButton {
required init?(coder: NSCoder) {
super.init(coder: coder)
registerForDraggedTypes([NSPasteboard.PasteboardType.URL, NSPasteboard.PasteboardType.fileURL])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
// some other codes
return .copy
}
override func draggingExited(_ sender: NSDraggingInfo?) {
// some other codes
}
override func draggingEnded(_ sender: NSDraggingInfo) {
// some other codes
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray,
let filePaths = pasteboard as? [String] else {
return false
}
for filePath in filePaths {
if let context = (NSApp.delegate as? AppDelegate)?.persistentContainer.viewContext {
let droppedFilesData = DroppedFiles(context: context)
droppedFilesData.fileName = getFileName(withPath: filePath)
droppedFilesData.filePath = filePath
do {
try context.save()
} catch {
print("Unable to save core data.")
}
}
ViewController().getDroppedFiles() // found nil with reloadData() in ViewController.swift
}
return true
}
}
And this is my interface and code logic:
So, how can I reloadData() for the table view in my ViewController class from another class (DropboxButton: NSButton) so that whenever the user drags and drops files into the dropbox, the table view will reload?
P.S. To get this done means a lot to me, I really need to get this fixed in a short time, is there anyone can spend some time and help me?
You need to call getDroppedFiles() on a loaded instance of ViewController.
With ViewController().getDroppedFiles() you're creating a new instance of ViewController that is not shown anywhere (so controls are not initialized resulting in the nil error).
I found this solution useful for my case.
I used observer to pass through data and call functions from other controller classes, now I understand that I was creating a new instance of ViewController which is not loaded. Here is my code:
ViewController.swift:
class ViewController: NSViewController {
// other codes
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(getDroppedFiles), name: NSNotification.Name(rawValue: "reloadTableViewData"), object: nil)
}
#objc func getDroppedFiles() {
DroppedFilesTableView.reloadData()
}
}
DropboxButton.swift:
class DropboxButton: NSButton {
// other codes
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
// other codes
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableViewData"), object: nil)
return true
}
}
And now, everything works perfectly, I can even add an userInfo: to pass data between files and classes.

Error with NSBundle.mainBundle when adding sound

I've trying to create a simple sound board for when a button is pressed a sound is made, however when I used this code
let url = NSURL(fileURLWithPath: Bundle.mainBundle().pathForResource(sound, ofType: "mp3")!)
let audioPlayer = try AVAudioPlayer(contentsOfURL: url)
I get an error with "Bundle.mainBundle" orginally it told me to change "Bundle" from "NSBundle" because it has be renamed but when I correct that I get another error saying "Cannot call value for non-function type 'Bundle' "
my entire code:
import UIKit
import AVFoundation
class ViewController: UIViewController {
let soundFilenames = ["60gs", "check", "dada", "danceforme", "eat", "gods", "irelandbaby", "ko'd", "lefthand", "littlewerp", "nocalls", "precision", "sundaymorning", "surprise", "whothefuckisthatguy", "youlldonothing"]
var audioPlayers = [AVAudioPlayer]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
for sound in soundFilenames {
do {
let url = Bundle.main.url(forResource:sound, withExtension: "mp3")!
let audioPlayer = try AVAudioPlayer(contentsOfURL: url)
audioPlayer.append(audioPlayer())
}
catch {
audioPlayers.append(AVAudioPlayer())
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonTapped(_ sender: UIButton) {
let audioPlayer = audioPlayers[sender.tag]
audioPlayer.play()
}
}
can anyone help me with this please.
First of all in Swift 3 mainBundle() became main but why not the real short way:
let url = Bundle.main.url(forResource:sound, withExtension: "mp3")!
And the init method has been renamed to
let audioPlayer = try AVAudioPlayer(contentsOf: url)
and change the next line to:
audioPlayers.append(audioPlayer)

Segues in Swift

im new to swift / xcode. I am trying to set up a facebook login.
The login works fine but when the login is successful the segue doesnt appear to work. There are no errors it just doesnt go to the next viewcontroller.
I suspect the issue lies here somehow:
self.performSegueWithIdentifier("showNew", sender: self)
any ideas?
import UIKit
import FBSDKCoreKit
import FBSDKLoginKit
class ViewController: UIViewController, FBSDKLoginButtonDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if (FBSDKAccessToken.currentAccessToken()==nil){
println("not Logged in")
}
else{
println("Logged in!")
self.performSegueWithIdentifier("showNew", sender: self)
}
var loginButton = FBSDKLoginButton()
loginButton.readPermissions = ["public_profile","email","user_friends"]
loginButton.center = self.view.center
loginButton.delegate = self
self.view.addSubview(loginButton)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!) {
if error == nil {
println("Login Complete")
}
else
{
println(error.localizedDescription)
}
}
func loginButtonDidLogOut(loginButton: FBSDKLoginButton!) {
println("User Logged Out!")
}
}
Your forgot to declare the identifier.
I assume that you want the app to move to next view controller after the FB login. Basically move on login success.
Move
self.performSegueWithIdentifier("showNew", sender: self)
to loginButton delegate.
func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!)
{
if (error == nil)
{
println("Login Complete")
self.performSegueWithIdentifier("showNew", sender: self)
}
else
{
println(error.localizedDescription)
}
}
If your segue was not defined properly, the app would have crashed. Since it is not crashing at self.performSegueWithIdentifier, you can believe that it is defined.

Resources