How to center NSPopover when using SwiftUI? - swift

If the NSPopover has its contentViewController set to some NSViewController that does not use SwiftUI directly on its view. For instance
final class ViewController: NSViewController {
private lazy var contentView = NSView()
override func loadView() {
view = contentView
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
}
let controller = ViewController()
popover.contentViewController = controller
It will be displayed centered.
However, if we simply change the line of the corresponding view:
private lazy var contentView = NSHostingView(rootView: Blah())
Where Blah is
struct Blah: View {
var body: some View {
ZStack {
Text("blah")
}
.frame(width: 400, height: 600, alignment: .center)
.background(Color.green)
}
}
You can see that the view is not centralized anymore. So, how can we let the NSViewController centralized in relation to the NSStatusItem item? (In the images those are one check icon)
If you give a close look at the images, you can see that the image with the white view has the popover arrow in the middle, while the other has not.

Related

SwiftUI: Changing values in a Class

This is a menubar macOS app. I'm trying to change the dropdown window size with a button from the content view. This is my attempt so far.
struct ContentView: View {
#ObservedObject var app = AppDelegate()
var body: some View {
VStack{
Text("Hello World")
.padding(.top, 10)
Button {
biggerwindow()
} label: {
Image("tv")
}
}
}
func biggerwindow(){
app.popover.contentSize = NSSize(width: 800, height: 800 )
}
}
Nothing happens when clicking on the button.
This is the AppDelegate class that contains the default values
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the contents
let contentView = ContentView()
// Set the SwiftUI's ContentView to the Popover's ContentViewController
popover.contentViewController = MainViewController()
//default size of the menu bar window
popover.contentSize = NSSize(width: 360, height: 360 )
popover.contentViewController?.view = NSHostingView(rootView: contentView)
// Create the Status Bar Item with the Popover
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
The problem is here:
#ObservedObject var app = AppDelegate()
You're creating a new instance of AppDelegate here, with its own instance of popover. You need to use the existing instance of AppDelegate that was created at app launch. You can access the existing instance through the global NSApplication.shared object.
struct ContentView: View {
var app: AppDelegate { NSApplication.shared.delegate as! AppDelegate }

SwiftUI: Passing a variable from Content View to a Class

In my menu bar macOS app, i'm trying to access a toggle that's in my Content View from my StatusBarController class. This switch is meant to keep the popover shown on screen even when a user clicks outside the app.
Content View
struct ContentView: View {
#State private var lock = false
var body: some View {
VStack{
Text("Hello World")
Toggle( isOn: $lock, label: {
})
.toggleStyle(SwitchToggleStyle())
}
.frame(width: 360.0, height: 160.0, alignment: .top)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
StatusBarController
class StatusBarController {
private var eventMonitor: EventMonitor?
private var content: ContentView?
func unlockpopover() {
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler)
}
if lock {
// Popover window is locked if user clicks outside the app
}else {
unlockpopover() // Popover window is unlocked if user clicks outside the app
}
}
}
}
How do i access the 'lock' variable from Content View?
'Cannot find 'lock' in scope'
is the error that's currently showing.
MenuBar project template i'm using.
EDIT
#lorem ipsum's suggestion of using EnvironmentObject and observable object.
StatusBarController
class StatusBarController:ObservableObject {
private var eventMonitor: EventMonitor?
private var content: ContentView?
#Published var lock: Bool = false
func unlockpopover() {
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler)
}
if lock {
// Popover window is locked if user clicks outside the app
}else {
unlockpopover() // Popover window is unlocked if user clicks outside the app
}
}
}
}
Content View
struct ContentView: View {
#EnvironmentObject var test: StatusBarController
var body: some View {
VStack{
Text("Hello World")
Toggle( isOn: $test.lock, label: {
})
.toggleStyle(SwitchToggleStyle())
}
.environmentObject(self.test)
.frame(width: 360.0, height: 160.0, alignment: .top)
}
}
App crashes with this error highlighted on $test.lock
Thread 1: Fatal error: No ObservableObject of type StatusBarController
found. A View.environmentObject(_:) for StatusBarController may be
missing as an ancestor of this view.
AppDelegate
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the contents
let contentView = ContentView()
// Set the SwiftUI's ContentView to the Popover's ContentViewController
popover.contentViewController = MainViewController()
popover.contentSize = NSSize(width: 360, height: 360)
popover.contentViewController?.view = NSHostingView(rootView: contentView)
// Create the Status Bar Item with the Popover
statusBar = StatusBarController.init(popover)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}

Use XCode-like icon buttons to control tab view in sidebar [duplicate]

I am trying to create a tabbed panel similar to the Xcode properties panel but the standard tabbed panel appears to have a different look and feel with no way to change it. What controls should be used to create a similar looking tabbed panel?
EDIT:
I was not using a NSTabViewController - just had the TabView !!
I just created a new project with storyboard and with the provided layout, added to the View Controllers view a custom view at top. To the custom view added buttons, style = square, type = toggle, and used provided icons of type template. Assigned tag to buttons 0-4 and did put them to a horizontal stack view. Then added a horizontal line and a container view. Then I add a Tab View Controller to the storyboard and embed it to container view. All buttons are connected to same action.
import Cocoa
class ViewController: NSViewController {
#IBOutlet var myStackView: NSStackView!
var oldSelection: Int = 0
var newSelection: Int = 0
var buttons: [NSButton]?
var tabViewDelegate: NSTabViewController?
#IBAction func selectedButton(_ sender: NSButton) {
newSelection = sender.tag
tabViewDelegate?.selectedTabViewItemIndex = newSelection
buttons![oldSelection].state = .off
sender.state = .on
oldSelection = newSelection
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
buttons = (myStackView.arrangedSubviews as! [NSButton])
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
// Once on load
tabViewDelegate = segue.destinationController as? NSTabViewController
}
}
If you use storyboards, just drag tab view controller to the surface and connect to the window.
Then go to IB settings of tab view controller and change style to toolbar as shown below:
Then add tabs and add image to each tab as shown below:
Run your app and enjoy the look as XCode settings view:
SwiftUI
Implementation
struct SystemSegmentControl : View {
// MARK: - Internal -
#Binding var selection : Int
let systemImages: [String]
var body : some View {
HStack(spacing: 5) {
ForEach (0..<systemImages.count) { i in
SystemSegmentButton(selection: self.$selection, selectionIndex: i, systemImage: systemImages[i])
}
}
}
}
struct SystemSegmentButton : View {
// MARK: - Internal -
#Binding var selection : Int
let selectionIndex: Int
let systemImage : String
var body : some View {
Button(action: { self.selection = self.selectionIndex }) {
Image(systemName: systemImage)
.padding(8)
.foregroundColor(selectionIndex == selection ? .controlAccentColor : .controlColor)
}
.buttonStyle(BorderlessButtonStyle())
}
}
Usage
struct SettingsView: View {
// MARK: - Internal -
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
SystemSegmentControl(selection: $selection, systemImages: ["slider.horizontal.3", "eye"])
Divider()
switch selection {
case 0:
Text("Tab 1")
default:
Text("Tab 2")
}
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
.frame(width: 250)
}
// MARK: - Private -
#State private var selection = 0
}
Result

Change background color of TextEditor in SwiftUI

TextEditor seems to have a default white background. So the following is not working and it displayed as white instead of defined red:
var body: some View {
TextEditor(text: .constant("Placeholder"))
.background(Color.red)
}
Is it possible to change the color to a custom one?
iOS 16
You should hide the default background to see your desired one:
TextEditor(text: .constant("Placeholder"))
.scrollContentBackground(.hidden) // <- Hide it
.background(.red) // To see this
iOS 15 and below
TextEditor is backed by UITextView. So you need to get rid of the UITextView's backgroundColor first and then you can set any View to the background.
struct ContentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
List {
TextEditor(text: .constant("Placeholder"))
.background(.red)
}
}
}
Demo
You can find my simple trick for growing TextEditor here in this answer
Pure SwiftUI solution on iOS and macOS
colorMultiply is your friend.
struct ContentView: View {
#State private var editingText: String = ""
var body: some View {
TextEditor(text: $editingText)
.frame(width: 400, height: 100, alignment: .center)
.cornerRadius(3.0)
.colorMultiply(.gray)
}
}
Update iOS 16 / SwiftUI 4.0
You need to use .scrollContentBackground(.hidden) instead of UITextView.appearance().backgroundColor = .clear
https://twitter.com/StuFFmc/status/1556561422431174656
Warning: This is an iOS 16 only so you'll probably need some if #available and potentially two different TextEditor component.
extension View {
/// Layers the given views behind this ``TextEditor``.
func textEditorBackground<V>(#ViewBuilder _ content: () -> V) -> some View where V : View {
self
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
.background(content())
}
}
Custom Background color with SwiftUI on macOS
On macOS, unfortunately, you have to fallback to AppKit and wrap NSTextView.
You need to declare a view that conforms to NSViewRepresentable
This should give you pretty much the same behaviour as SwiftUI's TextEditor-View and since the wrapped NSTextView does not draw its background, you can use the .background-ViewModifier to change the background
struct CustomizableTextEditor: View {
#Binding var text: String
var body: some View {
GeometryReader { geometry in
NSScrollableTextViewRepresentable(text: $text, size: geometry.size)
}
}
}
struct NSScrollableTextViewRepresentable: NSViewRepresentable {
typealias Representable = Self
// Hook this binding up with the parent View
#Binding var text: String
var size: CGSize
// Get the UndoManager
#Environment(\.undoManager) var undoManger
// create an NSTextView
func makeNSView(context: Context) -> NSScrollView {
// create NSTextView inside NSScrollView
let scrollView = NSTextView.scrollableTextView()
let nsTextView = scrollView.documentView as! NSTextView
// use SwiftUI Coordinator as the delegate
nsTextView.delegate = context.coordinator
// set drawsBackground to false (=> clear Background)
// use .background-modifier later with SwiftUI-View
nsTextView.drawsBackground = false
// allow undo/redo
nsTextView.allowsUndo = true
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
// get wrapped nsTextView
guard let nsTextView = scrollView.documentView as? NSTextView else {
return
}
// fill entire given size
nsTextView.minSize = size
// set NSTextView string from SwiftUI-Binding
nsTextView.string = text
}
// Create Coordinator for this View
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// Declare nested Coordinator class which conforms to NSTextViewDelegate
class Coordinator: NSObject, NSTextViewDelegate {
var parent: Representable // store reference to parent
init(_ textEditor: Representable) {
self.parent = textEditor
}
// delegate method to retrieve changed text
func textDidChange(_ notification: Notification) {
// check that Notification.name is of expected notification
// cast Notification.object as NSTextView
guard notification.name == NSText.didChangeNotification,
let nsTextView = notification.object as? NSTextView else {
return
}
// set SwiftUI-Binding
parent.text = nsTextView.string
}
// Pass SwiftUI UndoManager to NSTextView
func undoManager(for view: NSTextView) -> UndoManager? {
parent.undoManger
}
// feel free to implement more delegate methods...
}
}
Usage
ContenView: View {
#State private var text: String
var body: some View {
VStack {
Text("Enter your text here:")
CustomizableTextEditor(text: $text)
.background(Color.red)
}
.frame(minWidth: 600, minHeight: 400)
}
}
Edit:
Pass reference to SwiftUI UndoManager so that default undo/redo actions are available.
Wrap NSTextView in NSScrollView so that it is scrollable. Set minSize property of NSTextView to enclosing SwiftUIView-Size so that it fills the entire allowed space.
Caveat: Only first line of this custom TextEditor is clickable to enable text editing.
This works for me on macOS
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
struct ContentView: View {
#State var text = ""
var body: some View {
TextEditor(text: $text)
.background(Color.red)
}
Reference this answer
To achieve this visual design here is the code I used.
iOS 16
TextField(
"free_form",
text: $comment,
prompt: Text("Type your feedback..."),
axis: .vertical
)
.lineSpacing(10.0)
.lineLimit(10...)
.padding(16)
.background(Color.themeSeashell)
.cornerRadius(16)
iOS 15
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(.gray)
TextEditor(text: $comment)
.padding()
.focused($isFocused)
if !isFocused {
Text("Type your feedback...")
.padding()
}
}
.frame(height: 132)
.onAppear() {
UITextView.appearance().backgroundColor = .clear
}
You can use Mojtaba's answer (the approved answer). It works in most cases. However, if you run into this error:
"Return from initializer without initializing all stored properties"
when trying to use the init{ ... } method, try adding UITextView.appearance().backgroundColor = .clear to .onAppear{ ... } instead.
Example:
var body: some View {
VStack(alignment: .leading) {
...
}
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
Using the Introspect library, you can use .introspectTextView for changing the background color.
TextEditor(text: .constant("Placeholder"))
.cornerRadius(8)
.frame(height: 100)
.introspectTextView { textView in
textView.backgroundColor = UIColor(Color.red)
}
Result
import SwiftUI
struct AddCommentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
VStack {
if #available(iOS 16.0, *) {
TextEditor(text: $viewModel.commentText)
.scrollContentBackground(.hidden)
} else {
TextEditor(text: $viewModel.commentText)
}
}
.background(Color.blue)
.frame(height: UIScreen.main.bounds.width / 2)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.red, lineWidth: 1)
)
}
}
It appears the UITextView.appearance().backgroundColor = .clear trick in IOS 16,
only works for the first time you open the view and the effect disappear when the second time it loads.
So we need to provide both ways in the app. Answer from StuFF mc works.
var body: some View {
if #available(iOS 16.0, *) {
mainView.scrollContentBackground(.hidden)
} else {
mainView.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
}
// rename body to mainView
var mainView: some View {
TextEditor(text: $notes).background(Color.red)
}

What is the correct subclass to link storyboard and swiftUI?

I have created the subclass below to link my swiftui code to my storyboard. The goal is to have a vstack with text containers in it display inside a ContainerView. I am not sure if I am using the right class: NSViewController? I do not get any errors, but the code does not display how I want it to. Mostly, The swiftui does not display inside the window that shows up when I run the app.
import SwiftUI
class termu: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
#IBSegueAction func waka(_ coder: NSCoder) -> NSViewController? {
return NSHostingController(coder: coder, rootView: ContentView())
}
}
Here is the simplest MyViewController which you can specify for new controller scene in IB for your storyboard as custom class in Identity Inspector. (The controller scene and segue creation in IB as usual).
I selected Sheet segue for demo
import Cocoa
import SwiftUI
class MyViewController: NSHostingController<ContentView> {
#objc required dynamic init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
}
struct ContentView: View { // This SwiftUI view is just for Demo
var body: some View {
VStack {
Text("I'm SwiftUI")
.font(.largeTitle)
.padding()
.background(Color.yellow)
}
.frame(width: 400, height: 200)
}
}