I'm making a macOS app in SwiftUI with the new NavigationSplitView. If a user resizes the sidebar, I'd like that new width to be remembered and restored when the app next loads.
A preference can be read in like so...
#State private var width = UserDefaults.standard.float(forKey: "sidebarWidth")
...
NavigationSplitView {
...
}.navigationSplitViewColumnWidth(ideal: width)
But this isn't a binding, so the width isn't updated when it changes.
Is it possible to save the current sidebar width when it changes (or when the app closes), so that it might later be restored?
Many thanks!
This is a function to retrieve the width:
extension View {
#ViewBuilder func onWidthChange(_ action: #escaping (CGFloat) -> Void) -> some View {
self
.background(
GeometryReader { reader in
Color.clear
.onChange(of: reader.frame(in: .global).width) { newValue in
action(newValue)
}
}
)
}
}
Usage:
Text("Hi")
.onWidthChange { newWidth in
//save width
}
I'm unable to show a context menu over a SpriteView in SwiftUI on a mac. I am able to show a context menu not over it -- right click on the blue works, on red doesn't, see image.
However, I also need to be able to pan the scene in the view, so mouseDragged() inside the scene must work. Placing an overlay() on top of the view blocks mouse dragging events from propagating down to the scene.
Xcode 13.2, deployment 12.2, Swift 5
import SwiftUI
import SpriteView
func MenuItem(_ text: String, _ action: #escaping ()->Void) -> some View {
Button {
action()
} label: {
Text(text)
}
}
var scene : SKScene {
let r = SKScene.init(size: CGSize(width: 500, height: 500))
r.isUserInteractionEnabled = false
r.scaleMode = .aspectFill
r.backgroundColor = .red
return r
}
struct ContentView: View {
var body: some View {
ZStack {
SpriteView.init(scene: scene)
.padding()
.contextMenu {
MenuItem("1 Preferences ...") {
}
}
}
.background(Color.blue)
.contextMenu {
MenuItem("0 Preferences ...") {
}
}
}
}
A possible solution is to add transparent overlay and attach context menu there, like
SpriteView.init(scene: scene)
.padding()
.overlay(
Color.clear
.contentShape(Rectangle())
.contextMenu { // << here !!
MenuItem("1 Preferences ...") {
}
}
)
Tested with Xcode 13.4 / macOS 12.5
In contrast to TextField which has an animated blue focus ring, the TextEditor control doesn't have it. How can this be added? It should look and behave (i.e. animations) exactly like the one from TextField, so just adding a border isn't enough.
By using something like Introspect, you could do:
struct TextEditorWithFocusRing: View {
#Binding var text: String
var body: some View {
TextEditor(text: $text)
.introspectTextView { textView in
textView.enclosingScrollView?.focusRingType = .exterior
}
}
}
}
I have a view sitting on top of a mapView (in a ZStack) and want to be able to have the green, upper view fade in and out with the .easeInOut animation modifier applied to the view's opacity. As you can see in the gif, it fades in nicely but disappears abruptly.
If I remove the mapView then all is good. If I replace the mapView with a simple Rectangle() then the problem returns so I believe it has something to do with the ZStack rather than the map. Funnily enough I'm actually using Mapbox rather than MapKit (as in the code below for simplicity) and the fade/abrupt disappear behaviour is reversed.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var show = false
var body: some View {
VStack {
ZStack {
MapView()
if show {
LabelView()
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
}
}
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
}
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.mapType = .standard
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct LabelView: View {
var body: some View {
Text("Hi there!")
.padding(10)
.font(.title)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.green).shadow(color: .gray, radius: 3))
}
}
I have tried using alternative animation code, replacing the LabelView transition with:
.transition(.opacity)
and changing the button code to:
Button("Animate") {
withAnimation(.easeInOut(duration: 1.0)) {
self.show.toggle()
}
}
but the same behaviour appears each time. I'm guessing this is a SwiftUI bug but couldn't find any previous reference.
Here is working solution. Tested with Xcode 11.4 / iOS 13.4.
As it seen in demo presence of transparent label does not affect functionality of map view.
var body: some View {
VStack {
ZStack {
MapView()
LabelView().opacity(show ? 1 : 0) // here !!
}.animation(.easeInOut(duration: 1.0))
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
Another alternate, actually with the same visual effect is to embed LabelView into container and apply transition to it (it must be left something to render view disappearing)
var body: some View {
VStack {
ZStack {
MapView()
VStack { // << here !!
if show {
LabelView()
}
}.transition(.opacity).animation(.easeInOut(duration: 1.0))
}
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
try this: -> just added zIndex ...everything else the same
struct ContentView: View {
#State private var show = false
var body: some View {
VStack {
ZStack {
MapView().zIndex(0)
if show {
LabelView()
.zIndex(1)
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
}
}
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
}
and read this:
Transition animation not working in SwiftUI
I am trying to add a toolbar inside the title bar to a macOS app using SwiftUI, something similar to what is shown below.
I am unable to figure out a way to achieve this using SwiftUI. Currently, I have my toolbar (which just has a text field) inside my view, but I want to move it into the title bar.
My current code:
struct TestView: View {
var body: some View {
VStack {
TextField("Placeholder", text: .constant("")).padding()
Spacer()
}
}
}
So, in my case, I need to have the textfield inside the toolbar.
As of macOS 11 you’ll likely want to use the new API as documented in WWDC Session 10104 as the new standard. Explicit code examples were provided in WWDC Session 10041 at the 12min mark.
NSWindowToolbarStyle.unified
or
NSWindowToolbarStyle.unifiedCompact
And in SwiftUI you can use the new .toolbar { } builder.
struct ContentView: View {
var body: some View {
List {
Text("Book List")
}
.toolbar {
Button(action: recordProgress) {
Label("Record Progress", systemImage: "book.circle")
}
}
}
private func recordProgress() {}
}
Approach 1:
This is done by adding a titlebar accessory. I was able to get this done by modifying the AppDelegate.swift file. I had to apply some weird padding to make it look right.
AppDelegate.swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the titlebar accessory
let titlebarAccessoryView = TitlebarAccessory().padding([.top, .leading, .trailing], 16.0).padding(.bottom,-8.0).edgesIgnoringSafeArea(.top)
let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize
let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
// Add the titlebar accessory
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
TitlebarAccessory.swift
import SwiftUI
struct TitlebarAccessory: View {
var body: some View {
TextField("Placeholder", text: .constant(""))
}
}
Result:
Approach 2 (Alternative method):
The idea here is to do the toolbar part using storyboard and the rest of the app using SwiftUI. This is done by creating a new app with storyboard as the user interface. Then go to the storyboard and delete the default View Controller and add a new NSHostingController. Connect the newly added Hosting Controller to the main window by setting its relationship. Add your toolbar to the window using interface builder.
Attach a custom class to your NSHostingController and load your SwiftUI view into it.
Example code below:
import Cocoa
import SwiftUI
class HostingController: NSHostingController<SwiftUIView> {
#objc required dynamic init?(coder: NSCoder) {
super.init(coder: coder, rootView: SwiftUIView())
}
}
Using this approach also gives you the ability to customize the toolbar.
Inspired by your first approach I managed to get a toolbar too. As I'm using Divider()s in it, your Paddings didn't work great for me.
This one seems to work a bit smoother with different Layout-Sizes:
let titlebarAccessoryView = TitlebarAccessory().padding([.leading, .trailing], 10).edgesIgnoringSafeArea(.top)
let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
accessoryHostingView.frame.size.height = accessoryHostingView.fittingSize.height+16
accessoryHostingView.frame.size.width = accessoryHostingView.fittingSize.width
Maybe there is an even smoother way to get rid of this +16 and the padding trailing and leading (there are several other options instead of fittingSize), but I couldn't find any that looks great without adding numerical values.
I've finally managed to do this without any fiddly padding and in a way which looks great in full screen as well. Also, The previous solutions do not allow horizontal resizing.
Wrap your title view in an HStack() and add in an invisible text view which is allowed to expand to infinity height. This seems to be what keeps everything centered. Ignore the safe area at the top to now center it in the full height of the titlebar.
struct TitleView : View {
var body: some View {
HStack {
Text("").font(.system(size: 0, weight: .light, design: .default)).frame(maxHeight: .infinity)
Text("This is my Title")
}.edgesIgnoringSafeArea(.top)
}
}
In your app delegate, when you add in the NSTitlebarAccessoryViewController() set the layoutAttribute to top. This will allow it to resize horizontally as your window size changes (leading and left fix the width to minimums and has caused me a lot of pain looking for the answer to this.
let titlebarAccessoryView = TitleView()
let accessoryHostingView = NSHostingView(rootView: titlebarAccessoryView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize
let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
titlebarAccessory.layoutAttribute = .top
In my case I also want some buttons on the very right which position independently of the rest of the title, so I chose to add them separately, making use of the ability to add multiple view controllers
let titlebarAccessoryRight = NSTitlebarAccessoryViewController()
titlebarAccessoryRight.view = accessoryHostingRightView
titlebarAccessoryRight.layoutAttribute = .trailing
window.toolbar = NSToolbar()
window.toolbar?.displayMode = .iconOnly
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.addTitlebarAccessoryViewController(titlebarAccessoryRight)
https://developer.apple.com/documentation/uikit/uititlebar
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
if let titlebar = windowScene.titlebar {
//toolbar
let identifier = NSToolbar.Identifier(toolbarIdentifier)
let toolbar = NSToolbar(identifier: identifier)
toolbar.allowsUserCustomization = true
toolbar.centeredItemIdentifier = NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier)
titlebar.toolbar = toolbar
titlebar.toolbar?.delegate = self
titlebar.titleVisibility = .hidden
titlebar.autoHidesToolbarInFullScreen = true
}
window.makeKeyAndVisible()
}
#if targetEnvironment(macCatalyst)
let toolbarIdentifier = "com.example.apple-samplecode.toolbar"
let centerToolbarIdentifier = "com.example.apple-samplecode.centerToolbar"
let addToolbarIdentifier = "com.example.apple-samplecode.add"
extension SceneDelegate: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == NSToolbarItem.Identifier(rawValue: toolbarIdentifier) {
let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: toolbarIdentifier), titles: ["Solver", "Resistance", "Settings"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
if itemIdentifier == NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier) {
let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), titles: ["Solver1", "Resistance1", "Settings1"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
if itemIdentifier == NSToolbarItem.Identifier(rawValue: addToolbarIdentifier) {
let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(self.add(sender:)))
let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem)
return button
}
return nil
}
#objc func toolbarGroupSelectionChanged(sender: NSToolbarItemGroup) {
print("selection changed to index: \(sender.selectedIndex)")
}
#objc func add(sender: UIBarButtonItem) {
print("add clicked")
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[NSToolbarItem.Identifier(rawValue: toolbarIdentifier), NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), NSToolbarItem.Identifier.flexibleSpace,
NSToolbarItem.Identifier(rawValue: addToolbarIdentifier),
NSToolbarItem.Identifier(rawValue: addToolbarIdentifier)]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
self.toolbarDefaultItemIdentifiers(toolbar)
}
}
#endif