Swift macOS popover detect change dark mode - swift

I have a popover as seen from the image.
I have to make sure that when the screen mode changes, dark mode or light mode, the color of the popover changes.
The color is taken from the asset, like this:
NSColor(named: "backgroundTheme")?.withAlphaComponent(1)
As you can see from the code when starting the popover in the init function I assign the color accordingly.
How can I intercept the change of mode?
Can you give me a hand?
AppDelegate:
import Cocoa
import SwiftUI
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
popover.contentSize = NSSize(width: 560, height: 360)
popover.contentViewController = NSHostingController(rootView: contentView)
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
StatusBarController:
import AppKit
import SwiftUI
extension NSPopover {
private struct Keys {
static var backgroundViewKey = "backgroundKey"
}
private var backgroundView: NSView {
let bgView = objc_getAssociatedObject(self, &Keys.backgroundViewKey) as? NSView
if let view = bgView {
return view
}
let view = NSView()
objc_setAssociatedObject(self, &Keys.backgroundViewKey, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
NotificationCenter.default.addObserver(self, selector: #selector(popoverWillOpen(_:)), name: NSPopover.willShowNotification, object: nil)
return view
}
#objc private func popoverWillOpen(_ notification: Notification) {
if backgroundView.superview == nil {
if let contentView = contentViewController?.view, let frameView = contentView.superview {
frameView.wantsLayer = true
backgroundView.frame = NSInsetRect(frameView.frame, 1, 1)
backgroundView.autoresizingMask = [.width, .height]
frameView.addSubview(backgroundView, positioned: .below, relativeTo: contentView)
}
}
}
var backgroundColor: NSColor? {
get {
if let bgColor = backgroundView.layer?.backgroundColor {
return NSColor(cgColor: bgColor)
}
return nil
}
set {
backgroundView.wantsLayer = true
backgroundView.layer?.backgroundColor = newValue?.cgColor
}
}
}
class StatusBarController {
private var popover: NSPopover
private var statusBar: NSStatusBar
var statusItem: NSStatusItem
init(_ popover: NSPopover) {
self.popover = popover
self.popover.backgroundColor = NSColor(named: "backgroundTheme")?.withAlphaComponent(1)
statusBar = NSStatusBar.init()
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let statusBarButton = statusItem.button {
statusBarButton.image = #imageLiteral(resourceName: "Fork")
statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
statusBarButton.image?.isTemplate = true
statusBarButton.action = #selector(togglePopover(sender:))
statusBarButton.target = self
statusBarButton.imagePosition = NSControl.ImagePosition.imageLeft
}
}
#objc func togglePopover(sender: AnyObject) {
if(popover.isShown) {
hidePopover(sender)
}else {
showPopover(sender)
}
}
func showPopover(_ sender: AnyObject) {
if let statusBarButton = statusItem.button {
popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
}
func hidePopover(_ sender: AnyObject) {
popover.performClose(sender)
}
}

Hi I would skip setting the color on the popover and instead set the background in your ContentView.swift
Then set the background to a VStack/HStack/ZStack wrapping the rest of the UI.
var body: some View {
VStack{
Text("Hello, world!").padding()
Button("Ok", action: {}).padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color("backgroundTheme").opacity(0.3))
.padding(.top, -16)
}

There are something to keep in mind:
Methods like .withAlphaComponent(_:) that transform a existing NSColor to a new color does not return a dynamic color.
CGColor is not dynamic-capable. When converting a NSColor to a CGColor using .cgColor, you are converting from the "current" color of a NSColor.
So your hacky way is not really a good approach to what you want.
Base on what you had said, if I understand correctly, you want to add a color overlay to the popover's background, including the arrow portion.
You can actually do all that in a view controller:
class PopoverViewController: NSViewController {
/// for color overlay
lazy var backgroundView: NSBox = {
// 1. This extend the frame to cover arrow potion.
let box = NSBox(frame: view.bounds.insetBy(dx: -13, dy: -13))
box.autoresizingMask = [.width, .height]
box.boxType = .custom
box.titlePosition = .noTitle
box.fillColor = NSColor(named: "backgroundTheme")
return box
}()
/// for mounting SwiftUI views
lazy var contentView: NSView = {
let view = NSView(frame: view.bounds)
view.autoresizingMask = [.width, .height]
return view
}()
override func loadView() {
view = NSView()
// 2. This avoid clipping.
view.wantsLayer = true
view.layer?.masksToBounds = false
view.addSubview(backgroundView)
view.addSubview(contentView)
}
}
Pay attention to 1 and 2, this allow backgroundView to draw beyond view's bounds, covering the arrow portion. backgroundView is a NSBox object that accept a dynamic NSColor object to style its background.
Notice that if you want to change the opacity of the color, instead of .withAlphaComponent(_:), change the opacity on your assets, right below the RGB sliders.
contentView is here as a mounting point for your SwiftUI views. To mount content from a NSHostingController, you can do:
let popoverViewController = PopoverViewController()
_ = popoverViewController.view // this trigger `loadView()`, you don't need this for auto layout
let hostingController = NSHostingController(rootView: ContentView())
hostingController.view.frame = popoverViewController.contentView.bounds
hostingController.view.autoresizingMask = [.width, .height]
popoverViewController.contentView.addSubview(hostingController.view)
popoverViewController.addChild(hostingController)
This add hostingController's view as a subview of popoverViewController's contentView.
That's it.
Do note that I use autoresizingMask instead of auto layout and extract the mounting part out of the PopoverViewController to simply my answer.

Related

ScrollView scrollToPosition programmaticaly

i have a ViewController that containts View and One more View inside of it
Scroll works fine when i do it by my mouse, but if i use method scrollView.setContentOffset nothing happens.
I tried to check if scroll available using scrollView.delegate = works fine
UiViewController
class WishListViewController: UIViewController {
private lazy var wishListHeaderView: WishListHeaderView = {
let view = WishListHeaderView()
view.delegate = self
return view
}()
}
UiView
class WishListHeaderView: UICollectionReusableView {
private lazy var wishListNavigationView: WishListNavigationView = {
let view = WishListNavigationView()
view.delegate = self
return view
}()
}
Current view with scroll, that is not working
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
private lazy var tabsStackView: UIStackView = {
let stackView = UIStackView()
stackView.distribution = .fill
stackView.axis = .horizontal
stackView.spacing = 8
stackView.backgroundColor = PaletteApp.grayBackgroundButton
return stackView
}()
private func commonInit() {
addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.left.right.top.bottom.equalToSuperview().inset(16)
make.height.equalTo(38)
}
scrollView.addSubview(tabsStackView)
tabsStackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
.........
Here is method in this view, debagger shows that i am in this method. And after this if i use print i see scrollviewOffset (100, 0)
func scrollToFirstTab() {
self.scrollView.setContentOffset(CGPoint(x: 100, y: 0), animated: true)
}
Where is a problem? Thank you
Content offset is not the correct method, offset is the gap between content and scroll view itself. For more about content offset, check this answer.
You need to use func scrollRectToVisible(CGRect, animated: Bool).
CGRect.zero to scroll to top.

How to customize a NSPopUpButton and its NSMenu?

I want to style a NSPopUpButton with my own colors. I've gotten pretty much everything else to work except for the caps at the top and bottom of the menu and I can't get the NSPopUpButton to show an image. Here are a few screenshots of the problem:
Why is the drawn background bigger on my custom view compared to the system NSPopUpButton?
Here is an image of the caps problem:
I can't figure out where those caps are drawn and how I can change their color to match the menu items?
View controller
import Cocoa
let textColor = NSColor(calibratedWhite: 0.9, alpha: 1)
let surfacePrimaryColor = NSColor(calibratedWhite: 0.1, alpha: 1)
let surfaceSecondaryColor = NSColor(calibratedWhite: 0.3, alpha: 1)
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
let stackView = NSStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let cell = PopUpButtonCell()
cell.imagePosition = .imageLeading
let icon = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)
cell.image = icon
print("cell.image: \(cell.image)")
let popUpButton = NSPopUpButton()
popUpButton.cell = cell
for title in (Array(1...100).map { "Folder \($0)" }) {
let menuItem = NSMenuItem()
menuItem.title = title
let menuItemView = MenuItemView()
menuItemView.translatesAutoresizingMaskIntoConstraints = false
menuItemView.onAction {
cell.title = title
menuItem.menu?.cancelTracking()
}
menuItem.view = menuItemView
let titleLabel = NSTextField(string: title)
titleLabel.drawsBackground = false
titleLabel.isBezeled = false
titleLabel.isSelectable = false
titleLabel.isEditable = false
titleLabel.maximumNumberOfLines = 1
titleLabel.textColor = textColor
let deleteButton = Button(systemSymbolName: "xmark")
deleteButton.font = NSFont.systemFont(ofSize: 14)
deleteButton.isBordered = false
deleteButton.contentTintColor = textColor
deleteButton.onAction {
popUpButton.removeItem(withTitle: title)
}
let menuItemStackView = NSStackView()
menuItemView.addSubview(menuItemStackView)
menuItemStackView.orientation = .horizontal
menuItemStackView.edgeInsets = NSEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
menuItemStackView.translatesAutoresizingMaskIntoConstraints = false
menuItemStackView.leadingAnchor.constraint(equalTo: menuItemView.leadingAnchor).isActive = true
menuItemStackView.trailingAnchor.constraint(equalTo: menuItemView.trailingAnchor).isActive = true
menuItemStackView.topAnchor.constraint(equalTo: menuItemView.topAnchor).isActive = true
menuItemStackView.bottomAnchor.constraint(equalTo: menuItemView.bottomAnchor).isActive = true
menuItemStackView.addView(titleLabel, in: .leading)
menuItemStackView.addView(deleteButton, in: .trailing)
popUpButton.menu?.addItem(menuItem)
}
let popUpButton2 = NSPopUpButton()
popUpButton2.addItems(withTitles: Array(1...100).map { "File \($0)" })
stackView.addArrangedSubview(popUpButton)
stackView.addArrangedSubview(popUpButton2)
}
}
Custom button with onAction closure
import AppKit
typealias Listener = () -> Void
class Button: NSButton {
private var listener: Listener?
init(systemSymbolName: String) {
super.init(frame: .zero)
image = NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)
target = self
action = #selector(actionPerformed(_:))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func actionPerformed(_ sender: AnyObject) {
listener?()
}
func onAction(_ closure: #escaping Listener) {
listener = closure
}
}
Custom popup button cell
import Cocoa
class PopUpButtonCell: NSPopUpButtonCell {
override var controlView: NSView? {
didSet {
controlView?.wantsLayer = true
controlView?.layer?.backgroundColor = surfaceSecondaryColor.cgColor
controlView?.layer?.cornerRadius = 4
}
}
// Prevent system background drawing
override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {
}
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
let attributedTitle = NSMutableAttributedString(attributedString: title)
let range = NSMakeRange(0, attributedTitle.length)
attributedTitle.addAttributes([NSAttributedString.Key.foregroundColor : textColor], range: range)
return super.drawTitle(attributedTitle, withFrame: frame, in: controlView)
}
}
Why is image nil after setting it on the NSPopUpButton?
How can I change the color of the menu caps?
Why is image nil after setting it on the NSPopUpButton?
See setImage:
This method has no effect. The image displayed in a pop up button is taken from the selected menu item (in the case of a pop up menu) or from the first menu item (in the case of a pull-down menu).

How do I create a Multi Line Text Field in SwiftUI for MacOS?

I am writing my first MacOS app using Swift. I need to create a simple editable text box which allows multiple paragraphs. In HTML, this would be a textarea.
I gather that iOS 14 will include TextEditor, but that’s not now, and I don’t know whether that will be in MacOS anyway.
Many solutions I have seen presume iOS and UIKit.
How do I do this with SwiftUI and MacOS?
Update
Someone has suggested that the question is similar to this one: How do I create a multiline TextField in SwiftUI?
I have already looked at that question, but:
The linked question is specifically for iOS, but mine is for MacOS
The answers, as far as I can tell, are specifically for iOS using UIKit. If someone cares to explain how this answers my MacOS question I would be very interested …
Here is some initial demo of component like iOS14 TextEditor.
Demo prepared & tested with Xcode 11.7 / macOS 10.15.6
struct TestTextArea: View {
#State private var text = "Placeholder: Enter some text"
var body: some View {
VStack {
TextArea(text: $text)
.border(Color.black)
// Text(text) // uncomment to see mirror of enterred text
}.padding()
}
}
struct TextArea: NSViewRepresentable {
#Binding var text: String
func makeNSView(context: Context) -> NSScrollView {
context.coordinator.createTextViewStack()
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
if let textArea = nsView.documentView as? NSTextView, textArea.string != self.text {
textArea.string = self.text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<String>
init(text: Binding<String>) {
self.text = text
}
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString text: String?) -> Bool {
defer {
self.text.wrappedValue = (textView.string as NSString).replacingCharacters(in: range, with: text!)
}
return true
}
fileprivate lazy var textStorage = NSTextStorage()
fileprivate lazy var layoutManager = NSLayoutManager()
fileprivate lazy var textContainer = NSTextContainer()
fileprivate lazy var textView: NSTextView = NSTextView(frame: CGRect(), textContainer: textContainer)
fileprivate lazy var scrollview = NSScrollView()
func createTextViewStack() -> NSScrollView {
let contentSize = scrollview.contentSize
textContainer.containerSize = CGSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
textContainer.widthTracksTextView = true
textView.minSize = CGSize(width: 0, height: 0)
textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.isVerticallyResizable = true
textView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
textView.autoresizingMask = [.width]
textView.delegate = self
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.documentView = textView
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
return scrollview
}
}
}

After a sheet is presented in SwiftUI, the text color of interactive UI elements changes to systemBlue?

I have a sheet that is presented when a button is pressed:
struct Button: View {
#State isPresented = false
var body: some View {
Button(action: { self.isPresented.toggle() },
label: { Text("Text Label") }
).sheet(isPresented: $isPresented, content: {
//sheet contents in here
})
}
}
The class seen below is essentially the view in UIKit (I removed some of the code from the functions because it doesn't really have anything to do with the problem, but I kept the function names in there with descriptions so you can interpret what it's doing)
class CustomCalloutView: UIView, MGLCalloutView {
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
weak var delegate: MGLCalloutViewDelegate?
//MARK: Subviews -
let personImg: UIImageView = {
let img = UIImage(systemName: "person.fill")
var imgView = UIImageView(image: img)
//imgView.tintColor = UIColor(ciColor: .black)
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
let clockImg: UIImageView = {
let img = UIImage(systemName: "clock")
var imgView = UIImageView(image: img)
//imgView.tintColor = UIColor(ciColor: .black)
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
//Initialization of the view
required init() {
super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: UIScreen.main.bounds.width * 0.75, height: 130)))
setup()
}
//other initializer
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//essentially just positioning the view
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
//setting it up
func setup() {
// setup this view's properties
self.backgroundColor = UIColor.clear
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(CustomCalloutView.calloutTapped)))
// And the subviews
self.addSubview(personImg)
self.addSubview(clockImg)
// Add Constraints to subviews
//Positioning the clock image
clockImg.topAnchor.constraint(equalTo: self.separatorLine.bottomAnchor, constant: spacing).isActive = true
clockImg.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
clockImg.rightAnchor.constraint(equalTo: self.timeLabel.leftAnchor, constant: -spacing / 2).isActive = true
clockImg.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
//Positioning the person image
personImg.topAnchor.constraint(equalTo: self.timeLabel.bottomAnchor, constant: spacing / 2).isActive = true
personImg.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
personImg.rightAnchor.constraint(equalTo: self.peopleLabel.leftAnchor, constant: -spacing / 2).isActive = true
personImg.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
}
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool)
//presenting the view
}
func dismissCallout(animated: Bool) {
//dismissing the view
}
#objc func calloutTapped() {
//respond to the view being tapped
}
}
When the sheet is presented and then dismissed, it causes other UI elements that are coded in UIKit (like text buttons, for example) to have their text turn color to the systemBlue UIColor...any idea how to fix this?
There is not enough code provided to test, but the reason might be in
struct Button: View { // << this custom view named as standard Button !!!
#State isPresented = false
it is named the same as standard SwiftUI component Button so this can confuse rendering engine.
Try to rename this (and others if you practice this) to something unique to your app, like MyButton or CustomButton, SheetButton, etc.

NSButton action not working from custom NSView

I have my Push button in subview called AddContactViewController
And I'm showing it in ScrollView in another ViewController called AddContactScrollViewController.
I'm setting action for my button in AddContactViewController
But, when I click my button nothing happens.
Here are the screenshots my IB. And below is the code.
Main view controller
SubView
import Cocoa
class AddContactScrollViewController: NSViewController {
#IBOutlet weak var scrollView: NSScrollView!
var contentView: NSView?
override func viewDidLoad() {
super.viewDidLoad()
contentView = NSView(frame: NSRect(x: 0, y: 0, width: self.view.frame.width, height: 1123))
contentView!.wantsLayer = true
contentView!.layer?.backgroundColor = NSColor.clear.cgColor
let tempVC = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "addAddress")) as! AddAddressViewController
vc.view.setFrameOrigin(NSPoint(x: 0, y: 0))
vc.view.setFrameSize(NSSize(width: 1200, height: 1123))
vc.view.wantsLayer = true
contentView!.addSubview(vc.view)
scrollView.documentView = contentView
// scroll to the top
if let documentView = scrollView.documentView {
documentView.scroll(NSPoint(x: 0, y: documentView.bounds.size.height))
}
}
override func viewDidAppear() {
}
}
import Cocoa
class AddContactVeiwController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addPhoneButtonClicked(_ sender: Any) {
print("phone button clicked")
}