NSPopover in Mac Catalyst for Mac menu bar - ios

I am trying to add a MenuBar Item to my Mac Catalyst app.
I have successfully made the iPad app work on Mac, but adding the MenuBar item is turning out to be quite difficult.
I have tried solving the issue using the following two links, but they don't have a GitHub repo which I can look at, and the explanations do some jumps.
https://www.highcaffeinecontent.com/blog/20190607-Beyond-the-Checkbox-with-Catalyst-and-AppKit
https://developer.apple.com/documentation/xcode/creating_a_mac_version_of_your_ipad_app
Right now I have the following code in my AppDelegate:
#if targetEnvironment(macCatalyst)
import AppKit
import Cocoa
#endif
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
#if targetEnvironment(macCatalyst)
var popover: NSPopover!
var statusBarItem: NSStatusItem!
#endif
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
#if targetEnvironment(macCatalyst)
// Code to include from Mac.
#endif
#if targetEnvironment(macCatalyst)
let contentView = MenuBarView()
// Create the popover
let popover = NSPopover()
popover.contentSize = NSSize(width: 400, height: 500)
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
self.popover = popover
// Create the status item
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
button.image = NSImage(named: "MenuBar")
button.action = #selector(togglePopover(_:))
}
#endif
return true
}
#if targetEnvironment(macCatalyst)
#objc func togglePopover(_ sender: AnyObject?) {
if let button = self.statusBarItem.button {
if self.popover.isShown {
self.popover.performClose(sender)
} else {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.popover.contentViewController?.view.window?.becomeKey()
}
}
}
#endif
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(macCatalyst)
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
...more code from CoreData
The Errors that is is showing
I am really new to iOS development and I am getting really lost, so any and every help is really appreciated.
Edit
Yes, the screenshot is from the AppDelegate.
I am trying to implement something like this:
https://github.com/AnaghSharma/Ambar-SwiftUI
every implementation that I have seen thus far put this into the AppDelegate, which is why I am trying to do the same thing as well.

From your screenshot, it looks like that’s the AppDelegate for your Mac bundle. If that’s the case, then you need to remove all the UIKit and #if TARGET_MAC_CATALYAT stuff and do everything in AppKit. That is, use NSResponder instead of UIResponder, etc.
As for sample code, this is a great start: https://github.com/noahsark769/CatalystPlayground
It includes an AppKit bundle and the code for loading it.

If I'm not mistaken, it's not possible to use NSPopover in Mac Catalyst yet. From apple documentation:
Mac apps built with Mac Catalyst can only use AppKit APIs marked as
available in Mac Catalyst, such as NSToolbar and NSTouchBar. Mac
Catalyst does not support accessing unavailable AppKit APIs.
And Mac Catalyst is not listed in the availability platforms list:
https://developer.apple.com/documentation/appkit/nspopover

Related

How to pass the presenting view controller and client ID for your app to the Google Sign In sign-in method

Hello I have been following (https://firebase.google.com/docs/auth/ios/google-signin) to setup a simple Google SignIn with Firebase in an iOS app. I use Xcode 12.5.1. I created a new Storyboard (UIKit) app. I did pod init and added Firebase and GoogleSignIn and did pod install.
I'm also total noob in iOS.
I'm trying to follow the steps and for non of the steps it really explains where to put the code. For some I think I succeeded, but for the step 4. I have no idea where to put that code. I tried looking for other posts/tutorials for setting up GoogleSignIn with Firebase and SwiftUI and non of them work. Steps seem pretty simple, but they probably omit something very simple that a noob in iOS doesn't know.
Can some1 please explain a bit this step 4 at least.
Some of you commented to put it in the AppDelegate. I have created AppDelegate, but not sure where to put it there. Creating a signIn function in AppDelegate for dumping the code from the step 4 does not work. I also tried to wrap it with UIViewControllerRepresentable like here with the same problem.
It complains that this presenting: self from GIDSignIn.sharedInstance.signIn(with: config, presenting: self) { [unowned self] user, error in (code from step 4 that I don't know where to put) is AppDelegate that can't be converted to UIViewController that is expected.
import SwiftUI
import Firebase
import GoogleSignIn
#main
struct FolderlesApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
#available(iOS 9.0, *)
func application(_ application: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any])
-> Bool {
return GIDSignIn.sharedInstance.handle(url)
}
}
I think you are using SwfitUI not UIkit in the code you provided. If this is true. I can answer the SwiftUI solution.
Assume you have already set up the Step 1 (URL Types) and import the GoogleService-Info.plist. If you don’t, make sure to do it first.
Then, we have to create AppDelegate by ourselves in SwiftUI. You can add this code into your “App” type struct file (usually, the file name is your Project_NameApp.swift). Just like the example down below. This is step 2, step 3 in Authenticate Using Google Sign-In on iOS  |  Firebase
// Project_NameApp.swift
struct Project_NameApp: App {
// add this var define line
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TestLoginView()
}
}
}
// add this class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// add this function for init FirebassApp
FirebaseApp.configure()
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// add this function for Google Sign in to handle the url
return GIDSignIn.sharedInstance.handle(url)
}
}
The next one is your main question - Where the step 4’s code should we be put? Following code is the example SwiftUI view that you want to login with google. You can see it just turn the step 4 code into the function, and bind to the Button you want the user to tap and Sign in with a Google account.
import SwiftUI
import GoogleSignIn
import Firebase
struct TestLoginView: View {
var body: some View {
VStack {
Button(action: {
// Just add the action function, it will works.
googleSignIn()
}, label: {
Text("Google Sign In")
})
}
}
func googleSignIn(){
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: (UIApplication.shared.windows.first?.rootViewController)!) { user, error in
if let error = error {
print(error.localizedDescription)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
// Authenticate with Firebase using the credential object
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("authentication error \(error.localizedDescription)")
return
}
print(authResult ?? "none")
}
}
}
}
struct TestLoginView_Previews: PreviewProvider {
static var previews: some View {
TestLoginView()
}
}
I have tested it on my Xcode and it works for me. You can try it and hope you can solve the problem!
Tips 1: This is just a simple code. If you want a better implementation. I recommend you implement with MVVM structure. In this case, you should put the function into your ViewModel which you create by yourself. The button should call the Google Sign in function in your ViewModel.
Tips 2: This code doesn’t implement the multi-factor login with phone verify, check out this article for more detail. Most use case won’t use this less frequently used multi-factor login.

Getting currently focused UIScene when multiple connected scenes are active in foreground

I have a problem getting the current UIScene when multiple scenes are connected.
Specifically, my app (iOS 13) supports multiple windows on the iPad. When I have two windows side by side and I access the apps connected scenes (with UIApplication.shared.connectedScenes), all of them have the activationState set to .foregroundActive. But I am actually only interacting with one of them.
I found some code here on stackoverflow to retrieve the current key window (can't remember who it was that posted it) but as you can see it will always return one of the two windows.
let keyWindow: UIWindow? = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
Am I missing some method to determine the last window that was interacted with or are the multiple .foregroundActive states a bug?
Thanks!
Although UIApplication.shared.keyWindow is deprecated after iOS 13.0 by Apple, it seems this attribute can help you find the active one when multiple scenes in the foreground by using:
UIApplication.shared.keyWindow?.windowScene
I found the solution to my problem by refactoring my code so that now I access the current UIScenes window through the window attribute of any view or view controller. This way I don't need the keyWindow anymore.
I think it's probably correct that both windows open next to each other have the activationState .foregroundActive
In case anyone needs it, these are the two extensions that helped me get to to current window from anywhere in the app:
extension UIViewController {
var window: UIWindow? {
return view.window
}
}
extension UIScene {
var window: UIWindow? {
return (delegate as? SceneDelegate)?.window
}
}
But the problem still stands if you don't have access to a UIView, UIViewController or UIScene. For example inside a data model with no reference to a view or scene. Then I don't see a way to consistently accessing the correct window the user is interacting with.
I use this extension in my project
// MARK: - Scene Delegate helpers
#available(iOS 13.0, *)
extension UIApplication {
static var firstScene: UIScene? {
UIApplication.shared.connectedScenes.first
}
static var firstSceneDelegate: SceneDelegate? {
UIApplication.firstScene?.delegate as? SceneDelegate
}
static var firstWindowScene: UIWindowScene? {
UIApplication.firstSceneDelegate?.windowScene
}
}
// MARK: - Window Scene sugar
#available(iOS 13.0, *)
protocol WindowSceneDependable {
var windowScene: UIWindowScene? { get }
var window: UIWindow? { get }
}
#available(iOS 13.0, *)
extension WindowSceneDependable {
var windowScene: UIWindowScene? {
window?.windowScene
}
}
#available(iOS 13.0, *)
extension UIScene: WindowSceneDependable { }
#available(iOS 13.0, *)
extension SceneDelegate: WindowSceneDependable { }
#available(iOS 13.0, *)
extension UIViewController: WindowSceneDependable { }
// MARK: - Window sugar
#available(iOS 13.0, *)
extension UIScene {
var window: UIWindow? {
(delegate as? SceneDelegate)?.window
}
}
extension UIViewController {
var window: UIWindow? {
view.window
}
}
And then I can do this when I have to present something:
func present(from navigation: UINavigationController) {
self.navigation = navigation
if #available(iOS 13.0, *) {
guard let currentWindowScene = navigation.windowScene else { return }
myPresenter.present(in: currentWindowScene)
} else {
myPresenter.present()
}
}
Using this I can get a window from almost anywhere. But the thing is that it's not possible if I have no access to view-related objects at all. For example, in my auth service I have to use this bullshit:
func authorize() {
if #available(iOS 13.0, *) {
guard let currentWindowScene = UIApplication.firstWindowScene else { return }
presenter.present(in: currentWindowScene)
} else {
presenter.present()
}
}
This does not look correct for me, and it works only for the first windowScene.
It's better not to do this just like I did, you have to elaborate on this solution.
This solution gives you the last interacted-with UIScene without relying on anything but using a UIWindow subclass for all your scenes.
class Window: UIWindow
{
static var lastActive: Window?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
Self.lastActive = self
return super.hitTest(point, with: event)
}
}
Now you can access Window.lastActive and its associated windowScene (UIWindowScene/UIScene).

Error when running Xcode simulator - '[framework] CUIThemeStore: No theme registered with id=0'

I am developing an app with a single view and some labels. The app fails at the viewdidload() func and does not proceed further. The code is listed below.
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
The error is '[framework] CUIThemeStore: No theme registered with id=0'
I am not using any themes or external frameworks in my code. I am running Xcode 10 on MacOS Mojave. I checked the setting in Xcode to see if it is referring to any external frameworks and I could not find any. Any help is very much appreciated.
I also got the same issue.
Simply move your image(s) to Assets.xcassets folder.
I had the same issue. It took place because I messed up with window property in the AppDelegate class.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let mainScreen = TaskNumber1()
mainScreen.title = "Task nunber 1"
let navigationController = UINavigationController(rootViewController: mainScreen)
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = navigationController
window.makeKeyAndVisible()
return true
}
I've just created new window property and there was not any warnings from Xcode that I tried to create the constant with the same name as one was created already. But I had just black screen and the same error as you. So in my case solution was simple. To replace that window code snippet with the code below:
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
Now I am simply not creating new window variable.
In my case, there was nothing to do with themes. So the reason of you getting this error is could be in AppDelegate class as well. Hope it will help somebody

Integrate React-Native app to Swift 3

I have a working app in react native. iOS project files are made by react native which is based in Objective-C. I found it harder to convert Objective-C to Swift. So I decided to make a plain Project in swift 3 and then integrate the React Native to it. I followed the instruction here :
http://facebook.github.io/react-native/docs/integration-with-existing-apps.html
I did first steps, but when it comes to :
The Magic: RCTRootView Now that your React Native component is
created via index.ios.js, you need to add that component to a new or
existing ViewController. The easiest path to take is to optionally
create an event path to your component and then add that component to
an existing ViewController.
We will tie our React Native component with a new native view in the
ViewController that will actually host it called RCTRootView .
There is no example or proper explanation.
Is there any ready to go boilerplate or any complete tutorial on how to integrate react native to swift 3 project?
UPDATE:
According to this tutorial : https://gist.github.com/boopathi/27d21956fefcb5b168fe
I updated my project to look like this :
AppDelegate.swift :
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// initialize the rootView to fetch JS from the dev server
let rootView = RCTRootView()
rootView.scriptURL = NSURL(string: "http://localhost:8081/index.ios.js.includeRequire.runModule.bundle")
rootView.moduleName = "OpenCampus"
// Initialize a Controller to use view as React View
let rootViewController = ViewController()
rootViewController.view = rootView
// Set window to use rootViewController
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
if #available(iOS 10.0, *) {
self.saveContext()
} else {
// Fallback on earlier versions
}
}
// MARK: - Core Data stack
#available(iOS 10.0, *)
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "OpenCampus")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
#available(iOS 10.0, *)
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
ViewController.swift :
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 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.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Project-name-bridging-header :
#ifndef OpenCampus_Briding_Header_h
#define OpenCampus_Briding_Header_h
#endif /* OpenCampus_Briding_Header_h */
find ../../../node_modules/react-native/React -name "*.h" | awk -F'/' '{print "#import \""$NF"\""}'
and this is the result :
Well, surprisingly code push works without editing the AppDelegate.swift. I just imported codepush at Bridging header.
So make a new swift file, select to make a bridging header for Objective-C,
Then delete AppDelegate.m AppDelegate.h and main.h files,
Import following modules in Bridging header (ProjectName-Bridging-Header.h) :
#import "RCTBridgeModule.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
#import "RCTRootView.h"
#import "RCTUtils.h"
#import "RCTConvert.h"
#import "CodePush.h"
And AppDelegate.swift should look like this :
import Foundation
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var bridge: RCTBridge!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
let jsCodeLocation = NSURL(string: "http://localhost:8081/index.ios.bundle?platform=ios&dev=true")
// jsCodeLocation = NSBundle.mainBundle().URLForResource("main", withExtension: "jsbundle")
let rootView = RCTRootView(bundleURL:jsCodeLocation, moduleName: "****ModuleName****", initialProperties: nil, launchOptions:launchOptions)
self.bridge = rootView.bridge
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let rootViewController = UIViewController()
rootViewController.view = rootView
self.window!.rootViewController = rootViewController;
self.window!.makeKeyAndVisible()
return true
}
And Replace module name with the module you register in your index.ios.js or index.android.js (App Registery)

Swift Code in App Delegate to Load Storyboard Causes Crash

I have an universal app I am writing and I initially wrote it using two storyboard files. I had code in the App Delegate didFinishLaunchingWithOptions routine to decide which storyboard to load and away it would go. I have since realised that that was a silly thing to do because I had duplicate code, so I have deleted one storyboard and made the other universal. I have fixed up the VCs to point at the right class and everything. However, my app now refuses to launch. When I run it in the sim, it gives me the error
The app delegate must implement the window property if it wants to use a main storyboard file.
.
Here is my code in App Delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
var deviceIdiom = UIDevice.currentDevice().userInterfaceIdiom
if deviceIdiom == UIUserInterfaceIdiom.Phone {
strDevice = "iPhone"
} else if deviceIdiom == UIUserInterfaceIdiom.Pad {
strDevice = "iPad"
} else {
strDevice = "Unknown"
}
return true
}
What am I doing wrong?
(Questions I've already looked at but didn't help):
Unique Issue displaying first storyboard scene in Xcode
The app delegate must implement the window property if it wants to use a main storyboard file
Managing two storyboards in App Delegate
How to manually set which storyboard view to show in app delegate
Swift - Load storyboard programatically
Your app delegate needs to start like such:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
//...
}

Resources