How to add buttons when NSVisualEffectView is used - swift

I have created a window using NSVisualEffectView to get blur and rounded corners. Like here
The problem is I don't see my button in the window when I have NSVisualEffectView code. If I remove the code, the button is displayed. What is going wrong?
NSVisualEffectView code in AppDelegate.swift:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
guard let window = NSApplication.shared().windows.first else { return }
let effect = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
effect.blendingMode = .behindWindow
effect.state = .active
effect.material = .dark
effect.wantsLayer = true
effect.layer?.cornerRadius = 15.0
effect.layer?.masksToBounds = true
window.isOpaque = false
window.backgroundColor = .clear
window.contentView = effect
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
}
I have added some buttons in storyboard. When I run the project I don't see any buttons.
When I remove everything from applicationDidFinishLaunching(_ aNotification: Notification) i.e., NSVisualEffectView code, I can see the buttons.
Can anyone tell me what is happening?

I think I should have corrected you in your previous question only but I didn't.
You are using Storyboard so why are you creating NSVisualViewEffect variable in your code?
Search for nsvisualeffectview in the right panel(Utilities panel) where you search for buttons etc. (object library).
Drag it and resize it according to your main view controller.
To add the blur effect and mode, go to "Attribites Inspector" in the "Utilities panel"
and set window.backgroundColor = .clear and window.isOpaque = false
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
guard let window = NSApplication.shared.windows.first else { return }
window.isOpaque = false
window.backgroundColor = .clear
}
Now you can add your buttons, text fields and run the project. You can see all your added elements.
I hope it helps!

window is above the view you are adding buttons to, so the buttons are below the blurred-out window, and are therefore impossible to see. Why not add the visualEffectView to the same view as the buttons? You'd need to insert it below the buttons to make the buttons visible.

Related

button reversed when pressed

Would like to create a button "scan" that is reversed and outlined when pressed (see image below)
Was using a button configurator (with storyboard) but am having trouble getting background color to update with isHighlighted. Happy to switch to SwiftUI from Storyboard if necessary.
#IBOutlet var imageView: UIImageView!
let scanButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
configButton()
scanButton.configurationUpdateHandler = { button in
//this transform is for testing and works
button.transform = button.isHighlighted ? CGAffineTransform(scaleX: 0.95, y: 0.95) : .identity
var config = button.configuration
//changing the title works
config?.title = button.isHighlighted ? "pressed" : "scan"
//config?.background = button.isHighlighted ? //need some help
button.configuration = config
}
}
func configButton(){
scanButton.configuration = .filled()
scanButton.configuration?.baseBackgroundColor = .green
scanButton.configuration?.title = "scan"
scanButton.configuration?.baseForegroundColor = .black
addButtonConstraints()
}
func addButtonConstraints(){
view.addSubview(scanButton)
scanButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scanButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
scanButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
scanButton.widthAnchor.constraint(equalToConstant: 125),
scanButton.heightAnchor.constraint(equalToConstant: 30)
])
}
Does this work for you?
The button in this example was placed using Interface Builder as a Filled Button in Main.storyboard, then connected to the View Controller code with an #IBOutlet. The code example is ViewController.swift.
The button inverts its color scheme and gains a contrasting outline when it is pressed down, then reverts to its original appearance when it is released.
import UIKit
class ViewController: UIViewController {
// Set up this outlet to allow accessing the button's layer and configuration properties
#IBOutlet weak var swapColorsButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Set up the button's outline. Since the button has a white background when not pressed down, the outline does not become visible until the button is pressed.
swapColorsButton.layer.borderWidth = 3
swapColorsButton.layer.borderColor = CGColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
swapColorsButton.layer.cornerRadius = 5
}
// This is a Touch Down action -- it inverts the button's colors when it is pressed
#IBAction func pressDown(_ sender: UIButton) {
swapColorsButton.configuration?.baseBackgroundColor = .black
swapColorsButton.configuration?.baseForegroundColor = .white
}
// This is a Touch Up Inside action -- releasing the button resets it to its original colors
#IBAction func buttonReleased(_ sender: UIButton) {
swapColorsButton.configuration?.baseBackgroundColor = .white
swapColorsButton.configuration?.baseForegroundColor = .black
}
}
In order to detect being pressed down and released without using the isHighlighted property, the example code has two separate #IBActions controlling the color changes. Pressing the button down is a Touch Down action, and releasing the button is Touch Up Inside. To get the full menu of #IBAction options, instead of just the default one, right click on the button in Main.storyboard. You can then drag and drop from the menu options to ViewController.swift.

My NSWindow's shadow is getting cut off when switching screens?

I wanted to have an NSWindow with a blurred background so I created a wrapper for NSVisualEffectView to be used in my ContentView() with some help from How do you blur the background in a SwiftUI macOS application?. I also tried doing it with just the NSWindow using https://github.com/lukakerr/NSWindowStyles#:~:text=true-,6.%20Vibrant%20background,A,-vibrant.
struct VisualEffectView: NSViewRepresentable
{
let material: NSVisualEffectView.Material
let blendingMode: NSVisualEffectView.BlendingMode
func makeNSView(context: Context) -> NSVisualEffectView
{
let visualEffectView = NSVisualEffectView()
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
visualEffectView.state = NSVisualEffectView.State.active
return visualEffectView
}
func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context)
{
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
}
}
It works and looks great, however, when I move the window to a different screen -and pause with the window between both screens, then move it to the next window- it chops off a part of the NSWindow's shadow.
This is what it looks like when moving screens ⤵︎
Is there a way to prevent this shadow chop from happening?
Interface: SwiftUI
LifeCycle: Appkit AppDelegate
Figured it out! Without any hacks too thankfully lol
Rules
In order to achieve this look without the nasty artifacts in the question you have to do a few things the way macOS wants them.
1. Don't set your NSWindow.backgroundColor = .clear!
This is the cause for the nasty artifacts above in the first place! Leaving your window's color as is will make sure the window functions properly when changing screens. NSVisualEffectView captures the image behind the window and uses that for the background so there's no need to make anything transparent.
2. Make sure to include .titled in the window's styleMask!
Failure to do so will render the window without rounded corners. If you attempt to add rounded corners (like I did) to the SwiftUI view you will still have an opaque background on the NSWindow itself. If you then set your window's background color to .clear (like I did again) the shadow chop issues will ensue! However, this does not mean that the title bar will get in the way, it won't, we'll get to that in a bit.
3. Add your NSVisualEffectView to your SwiftUI view!
I found this to be easier than adding the visual effect to the NSWindow.contentView as a subview.
Solution
1. So start off by setting up your NSWindow and AppDelegate! ⤵︎
All you're doing is making sure the titlebar is present but hidden.
import Cocoa
import SwiftUI
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
// Note: You can add any styleMasks you want, just don't remove the ones below.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 300, height: 200),
styleMask: [.titled, .fullSizeContentView],
backing: .buffered, defer: false)
// Hide the titlebar
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
// Hide all Titlebar Controls
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
// Set the contentView to the SwiftUI ContentView()
window.contentView = NSHostingView(rootView: contentView)
// Make sure the window is movable when grabbing it anywhere
window.isMovableByWindowBackground = true
// Saves frame position between opening / closing
window.setFrameAutosaveName("Main Window")
// Display the window
window.makeKeyAndOrderFront(nil)
window.center()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
Your window will probably look something like this at this point (if starting with a blank project). You can see the 'Hello world!' isn't exactly centred due to the title bar. ⤵︎
2. Once your NSWindow is setup, time to do the ContentView() ⤵︎
In here you just want to create a wrapper for NSVisualEffectView and add it as a background. AND THEN make sure you remove the safe areas from the view! This makes sure to get rid of any space the title bar was eating up in the view.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(VisualEffectView(material: .popover, blendingMode: .behindWindow))
// Very important! (You could technically just ignore the top so you do you)
.edgesIgnoringSafeArea(.all)
}
}
/// Takes the image directly behind the window and uses that to create a blurred material. It can technically be added anywhere but most often it's used as a backing material for sidebars and full windows.
struct VisualEffectView: NSViewRepresentable {
let material: NSVisualEffectView.Material
let blendingMode: NSVisualEffectView.BlendingMode
func makeNSView(context: Context) -> NSVisualEffectView {
let visualEffectView = NSVisualEffectView()
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
visualEffectView.state = NSVisualEffectView.State.active
return visualEffectView
}
func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) {
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
}
}
At this point your view should look how you want it without any negative effects! Enjoy <3 (If you're having any problems with this solution leave a comment!)
Resources
Thank you to #eonil for this clever way to keep the rounded corners on. Couldn't have figured this out without this answer ⤵︎
https://stackoverflow.com/a/27613308/13142325
Thank you to lukakerr for making this list of NSWindow styles!
https://github.com/lukakerr/NSWindowStyles

How to make a window with rounded corners

I could find a good solution to make a window with rounded corners
https://github.com/lukakerr/NSWindowStyles
Section: 6. Vibrant background with border radius and no titlebar
let visualEffect = NSVisualEffectView()
visualEffect.translatesAutoresizingMaskIntoConstraints = false
visualEffect.material = .dark
visualEffect.state = .active
visualEffect.wantsLayer = true
visualEffect.layer?.cornerRadius = 16.0
window?.titleVisibility = .hidden
window?.styleMask.remove(.titled)
window?.backgroundColor = .clear
window?.isMovableByWindowBackground = true
window?.contentView?.addSubview(visualEffect)
guard let constraints = window?.contentView else {
return
}
visualEffect.leadingAnchor.constraint(equalTo: constraints.leadingAnchor).isActive = true
visualEffect.trailingAnchor.constraint(equalTo: constraints.trailingAnchor).isActive = true
visualEffect.topAnchor.constraint(equalTo: constraints.topAnchor).isActive = true
visualEffect.bottomAnchor.constraint(equalTo: constraints.bottomAnchor).isActive = true
I find that it only works if I put it in the viewWillAppear or in the viewDidLoad with a delay. In any case, when I get the border-radius and the vibrant background I cannot see anything that is in the window, for instance, a simple label with the text test. (I tried to put that text on the storyboard or by code)
#IBOutlet weak var label1: NSTextField!
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0){
self.label1.stringValue = "test text"
}
How to make a label visible with this code?
(or as second option, how to make a window with round corners, no bar, background semi-transparent or vibrant and a label non-transparent on top or inside?)
(I know how to make the title bar transparent and remove the buttons and title. But that is not a good solution for what I need because the bar is still there and creates a space that makes problems)
The problem with the label is you're adding NSVisualEffectView above it. You could instead try adding it below:
view.addSubview(visualEffect, positioned: .below, relativeTo: label1)
But be careful you add it only once: viewWillAppear can be called multiple times.

UITapGestureRecognizer works first time, but not second time

[Greatly simplified]
I'm following a "Let's Build That App" YouTube series on Firebase 3. It's from 2016, so I've had to rework some of the code since Swift has evolved since then, but mostly it's a useful tutorial.
But, I'm stuck on something.
The red box is intended to be a custom titleView with an Image and Name, but I've simplified it to try to find the problem.
viewWillAppear calls setupNavbar which sets up the navbar.titleView:
func setupNavbar() {
let titleView = UIView()
titleView.frame = CGRect(x: 0, y: 0, width: 300, height: 40)
titleView.backgroundColor = .red
let containerView = UIView() // for the Image and Label, later
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .green
// left, top, width, height anchors equal to same for titleView
containerView.leftAnchor.constraint(equalTo: titleView.leftAnchor)
// top, width, height are similar
titleView.addSubview(containerView)
self.navigationItem.titleView = titleView
let recognizer = UITapGestureRecognizer(target: self, action: #selector(showChatController))
titleView.addGestureRecognizer(recognizer)
}
The selector's function is:
#objc func showChatController() {
let chatController = ChatLogTableViewController()
navigationController?.pushViewController(chatController, animated: true)
}
The class ChatLogTableViewController has just the default viewDidLoad().
First, I'm surprised the box is red, and not green. Why is that?
Second, if I click the red box, the ChatController is loaded, as expected. But, when I click "Back" and return to this screen, the red box is not visible, thus I can no longer tap on it. BUT....If I sign out and sign in/up again, the box is red and I can click it again.
What am I missing?
Update 1: The "+" creates a new controller and presents it.
present(UINavigationController(
rootViewController: NewMessageTableViewController()),
animated: true,
completion: nil)
That controller is currently empty, except for a leftBarButtonItem which is just a barButtonSystemItem (.cancel). Just like "Sign Out", this also "resets" the gesture and it works.
Update 2: Code added upon request.
Update 3: question greatly simplified. Also, if I change the showChatController code to just print ("show Chat Controller"), I can click to my heart's content; the red box and its tap gesture both remain.
So, there is something happening when I ...pushViewController and then come back.
Have you tried to set isUserInteractionEnabled property of the label which you set as the navBar's titleView? I know it's silly but I've missed this a lot of times :)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(YourViewController.titleWasTapped))
titleView.isUserInteractionEnabled = true
titleView.addGestureRecognizer(recognizer)
you can try following things:
1. "User Interaction Enabled" in the UILabel Attributes Inspector in the Storyboard.
2. override func viewDidLoad(animated: Bool) {
super.viewDidAppear(animated)
self.titleView.userInteractionEnabled = true
var tapGesture = titleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showChatController)))
self.titleView.addGestureRecognizer(tapGesture)
}
func showChatController() {
println("Tap Gesture recognized")
}
Hi #Zonker I've tested your setupNavBar(). It's working fine nothing worng with your code when I press and go back clicking titleView. Reason why you're not getting .green color is you have to give height and width for containerView and big reason is you've put addSubView code below the anchor and there is no .isActive = true in your containerView.leftAnchor.constraint... Please check setupNavBar() code below:
func setupNavbar() {
let titleView = UIView()
titleView.frame = CGRect(x: 0, y: 0, width: 300, height: 40)
titleView.backgroundColor = .red
let containerView = UIView() // for the Image and Label, later
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .green
titleView.addSubview(containerView)
// left, top, width, height anchors equal to same for titleView
containerView.widthAnchor.constraint(equalToConstant: 300).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 40).isActive = true
containerView.leftAnchor.constraint(equalTo: titleView.leftAnchor).isActive = true
// top, width, height are similar
self.navigationItem.titleView = titleView
let recognizer = UITapGestureRecognizer(target: self, action: #selector(showChatController))
titleView.addGestureRecognizer(recognizer)
}
It would better if you push your code in github so we can test your code

Custom View in Status Bar is Not Appearing Disabled on Secondary Screen

I have an app that uses a custom view in the menu bar of macOS. macOS has a feature that items in the menu bar will appear disabled on a secondary screen (I think an alpha value will be added to the view).
When I remove the custom view from the Button, everything works fine. But when I use my custom view, the view always looks the same, no matter if it is the primary or the secondary monitor.
Even setting the property "appearsDisabled" does not change the look of the view.
This is the code that I am using:
private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
private var view: HostView?
func applicationDidFinishLaunching(_ aNotification: Notification)
{
self.createMainView()
self.createMenuBarView()
}
fileprivate func createMenuBarView()
{
// Remove all sub views from the view and create new ones.
self.view?.subviews.removeAll()
var width: CGFloat = 0
for device in self.controller.model.devices
{
if let newView = self.createView(for: device.value, x: width)
{
self.view?.addSubview(newView.view)
width += newView.width
}
}
self.view?.frame = NSRect(x: 0, y: 0, width: width, height: MenuBar.height)
self.statusItem.image = nil
self.statusItem.length = width
if let view = self.view
{
// Do I have to set some properties here?
self.statusItem.button?.addSubview(view)
}
}
fileprivate func createMainView()
{
let view = HostView(frame: NSRect(x: 0, y: 0, width: 32.0, height: MenuBar.height))
view.menu = self.menu
self.view = view
}
The problem seems to be that I am adding a NSView to the button of the NSStatusItem as a subview.
self.statusBarItem.button?.addSubview(myView)
When I set my custom view to the view-Property of the NSStatusItem, the view is grayed out on a secondary screen (note that this is deprecated since macOS 10.14).