SwiftUI macOS Xcode Style Toolbar - swift

I like to recreate a toolbar similar to Apples Notes App using SwiftUI in a macOS app (I am using Xcode 12.3 and macOS 11.1):
My attempt was to use a Navigation View to get the Master/Detail setup (for now I do not need a third panel like the original Notes App has). I am interested in how to get the appearance right, e.g. background color and behavior of the buttons in the toolbar. I tried out some approaches, the best I came up with for the moment is this for the main file:
import SwiftUI
#main
struct App_Without_Name_in_Window_Top_AreaApp: App {
var body: some Scene {
WindowGroup("") { // <-- The ("") will remove the app name in the toolbar
ContentView()
}
.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
}
}
And for the content view:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
Text("Master")
.frame(minWidth: 200, maxWidth: 300, minHeight: 300, alignment: .leading)
.padding()
.toolbar {
ToolbarItem(placement: .status) {
Button(action: {
myToggleSidebar()
}) {
Image(systemName: "sidebar.left")
}
}
}
.presentedWindowToolbarStyle(ExpandedWindowToolbarStyle())
Text("Detail")
.frame(minWidth: 200, alignment: .center)
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
print("Button pressed")
}) {
Image(systemName: "bold.italic.underline")
}
}
ToolbarItem(placement: .navigation) {
Button(action: {
print("Button pressed")
}) {
Image(systemName: "lock")
}
}
}
}
.frame(minWidth: 500, minHeight: 300)
}
}
func myToggleSidebar() {
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
which yields a result like this:
Now my question is: How can I alter the color of the left and right parts of the toolbar? I also have problems with the behavior of the toolbar. When the master panel's size is increased, the buttons of the right part of the toolbar are disappearing very early although there is a lot of space left:
What do I have to do to prevent it?

Okay, I found a trick that works:
Set the scene's windowStyle to HiddenTitleBarWindowStyle, which both hides the title and removes the white background:
WindowGroup {
ContentView()
}
.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
.windowStyle(HiddenTitleBarWindowStyle())
(Note that I don't set the scene name to an empty string, as that's no longer needed and it messed up the window name in the "Window" menu too)
To force a divider between the toolbar and the detail view content, stretch the detail content to fill the whole space and put a Divider behind it:
Text("Detail")
.frame(minWidth: 200, maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(VStack {
Divider()
Spacer()
})
.toolbar { ...
That seems to do it!

What you want is to use
.windowToolbarStyle(UnifiedWindowToolbarStyle(showsTitle: false))
because it preserves the correct behavior when the user tabs the application
Using
.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
.windowStyle(HiddenTitleBarWindowStyle())
Causes funky behavior when the user opens a new tab due to the coloring of the toolbar.

Related

How to create a view that functions like Alert in SwiftUI on watchOS?

I want to have an alert style view that can include an icon image at the top along with the title.
alert(isPresented:content:) used with Alert does not seem to support adding images in any way. However, other than that limitation, it functions as I want my alert to function. It covers the status bar and blurs the main view as the background for the alert.
I attempted to use fullScreenCover(isPresented:onDismiss:content:) instead, but this approach does not behave like an alert as far as covering up EVERYTHING including the status bar, and blurring the main view as the background.
I have tried this so far, with the following result. One of the reasons I want it to behave like a normal Alert is so that the content can scroll without overlapping the clock time.
struct AlertView: View {
#EnvironmentObject var dataProvider: DataProvider
var alert: Watch.AlertMessage
var body: some View {
ScrollView {
Image("IconAlert")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.accentColor)
Text("Test Title")
.bold()
Text("Test Description")
Button("DISMISS") { print("dismiss") }
}
.padding()
.ignoresSafeArea()
.navigationBarHidden(true)
}
}
Try this this sample custom alert. It may give you some great approach(Code is below the image):
struct CustomAlert: View {
#State var isClicked = false
var body: some View {
ZStack {
Color.orange.edgesIgnoringSafeArea(.all)
Button("Show Alert") {
withAnimation {
isClicked.toggle()
}
}
ZStack {
if isClicked {
RoundedRectangle(cornerRadius: 20)
.fill(.thickMaterial)
.frame(width: 250, height: 250)
.transition(.scale)
VStack {
Image("Swift")
.resizable()
.frame(width: 150, height: 150)
.mask(RoundedRectangle(cornerRadius: 15))
Button("Dismiss") {
withAnimation {
isClicked.toggle()
}
}
.foregroundColor(.red)
}
.transition(.scale)
}
}
}
}
}

How to disable resizability of Sidebar NavigationView in SwiftUI (MacOS)?

I'm new to Swift and I've been trying to build an app and learn as I go. I have a NavigationView that holds a List as a sidebar that renders the content as the user clicks. The bar between these two panes can be grabbed to allow the user to resize. This seems to be the default behavior of a NavigationView. I'm trying to find a way to disable it because I don't want the user to resize the sidebar.
struct Sidebar: View {
var body: some view {
List {
NavigationLink("First section", destination: FirstSection)
}
.frame(minWidth: 150, maxWidth: 150)
}
}
I also couldn't find a way to tell Swift that I want my List view to have a dynamic width that just fits the content. Just like it's done with CSS width: fit-content;
In the picture below, you can see that I was able to resize the sidebar to be almost half the screen. How to disable this behavior?
I do found a solution for that all you have to do is to set the destination width so that the sidebar can't be resized to the destination view for example like that ## consider the firstSection() as a View ##
Here the app main start
import SwiftUI
#main
struct macosTestApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
SideBar()
}.toolbar {
// add the open/close sidebar navigation here
ToolbarItem(placement: .navigation) {
Button(action: toggleSidebar, label: { // 1
Image(systemName: "sidebar.leading")
})
}
}.frame(minWidth: 800, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity, alignment: .center)
}
}
private func toggleSidebar() { // 2
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
}
Here the sidebar and the navigationView
import SwiftUI
struct SideBar: View {
#State var isActiveView:Bool = true
var body: some View {
List {
NavigationLink("First section", destination: FirstSection().frame(minWidth: 750, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity, alignment: .center),isActive: $isActiveView)
}
}
}
struct FirstSection: View {
var body: some View {
Text("Hello")
}
}
struct text_Previews: PreviewProvider {
static var previews: some View {
SideBar()
}
}
i was able to do it, in a weird way,
the first thing was to create an NSViewControllerRepresentable
so i was able to wrap an NSViewController in which i generate a 1X1 square view
because the NSViewController has the complete life cycle
i was able to write the code inside viewDidDisappear()
and toggle back the sidebar

How to create macOS Big Sur-style Search Box with exterior focus ring in SwiftUI?

In macOS Big Sur, all the built-in search box controls have a glow around their exterior to show focus. How can I achieve this effect in my SwiftUI app?
I built a search bar that looks similar, but it still has a rectangular focus ring which looks really bad. I can turn the focus ring off entirely, but that's not what I want either. I want the exterior glow.
This is my current SwiftUI code. Notice the NSTextField focusRingType override. With that, I am able to turn off the focus ring, but that's not what I want. Default gives me the rectangular, bad focus. It seems like exterior would be the ticket, but it produces the same result as default. I'm wondering if that might just be a bug in the current Big Sur/Xcode/SwiftUI developer beta.
import SwiftUI
struct SearchBarView: View {
#Binding var searchText: String
var body: some View {
HStack {
TextField("Search ...", text: self.$searchText)
.textFieldStyle(PlainTextFieldStyle())
.padding(7)
.padding(.horizontal, 25)
.background(Color("systemGray6"))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 10)
}
)
.padding(.horizontal, 10)
}
.padding()
}
}
struct SearchBarView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SearchBarView(searchText: .constant(""))
.previewLayout(.fixed(width: 35, height: 50))
.environment(\.colorScheme, .dark)
SearchBarView(searchText: .constant(""))
.previewLayout(.fixed(width: 35, height: 50))
.environment(\.colorScheme, .light)
}
}
}
extension NSTextField {
open override var focusRingType: NSFocusRingType {
//get { .none }
get { .default }
// get { .exterior } // same as .default, could just be bug in Big Sur Dev Beta 1&2?
set { }
}
}
I was able to figure this out! This style of search bar is provided by NSToolbar using NSSearchToolbarItem!

SwiftUI: Two buttons with the same width/height

I have 2 buttons in an H/VStack. Both of them contain some text, in my example "Play" and "Pause". I would like to have that both buttons have the same width (and height) determined by the largest button. I have found some answers right here at SO but I can't get this code working unfortunately.
The following code illustrates the question:
import SwiftUI
struct ButtonsView: View {
var body: some View {
VStack {
Button(action: { print("PLAY tapped") }){
Text("Play")
}
Button(action: { print("PAUSE tapped") }) {
Text("Pause")
}
}
}
}
struct ButtonsView_Previews: PreviewProvider {
static var previews: some View {
ButtonsView()
}
}
The tvOS preview from Xcode shows the problem:
I would be thankful for an explanation for newbies 🙂
Here is run-time based approach without hard-coding. The idea is to detect max width of available buttons during drawing and apply it to other buttons on next update cycle (anyway it appears fluently and invisible for user).
Tested with Xcode 11.4 / tvOS 13.4
Required: Simulator or Device for testing, due to used run-time dispatched update
struct ButtonsView: View {
#State private var maxWidth: CGFloat = .zero
var body: some View {
VStack {
Button(action: { print("PLAY tapped") }){
Text("Play")
.background(rectReader($maxWidth))
.frame(minWidth: maxWidth)
}.id(maxWidth) // !! to rebuild button (tvOS specific)
Button(action: { print("PAUSE tapped") }) {
Text("Pause Long Demo")
.background(rectReader($maxWidth))
.frame(minWidth: maxWidth)
}.id(maxWidth) // !! to rebuild button (tvOS specific)
}
}
// helper reader of view intrinsic width
private func rectReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { gp -> Color in
DispatchQueue.main.async {
binding.wrappedValue = max(binding.wrappedValue, gp.frame(in: .local).width)
}
return Color.clear
}
}
}
You can implement the second custom layout example in the WWDC 2022 talk https://developer.apple.com/videos/play/wwdc2022/10056/ titled "Compose custom layouts with SwiftUI" which, if I understand the question, specifically solves it, for an arbitrary number of buttons/subviews. The example starts at the 7:50 mark.
after reading hit and trial implementing SO solns etc finally resolved this issue posting so that newbies as well as intermediate can benefit
paste it and obtain equal size(square) views
VStack(alignment: .center){
HStack(alignment:.center,spacing:0)
{
Button(action: {}, label: {
Text("Button one")
.padding(35)
.foregroundColor(.white)
.font(.system(size: 12))
.background(Color.green)
.frame(maxWidth:.infinity,maxHeight: .infinity)
.multilineTextAlignment(.center)
.cornerRadius(6)
}).background(Color.green)
.cornerRadius(6)
.padding()
Button(action: {}, label: {
Text("Button two")
.padding(35)
.foregroundColor(.white)
.font(.system(size: 12))
.frame(maxWidth:.infinity,maxHeight: .infinity)
.background(Color.green)
.multilineTextAlignment(.center)
}) .background(Color.green)
.buttonBorderShape(.roundedRectangle(radius: 8))
.cornerRadius(6)
.padding()
}.fixedSize(horizontal: false, vertical: true)
Add as many as buttons inside it. You can adjust it for VStack by adding only one button in hstack and add another button in another Hstack. I gave a general soln for both VStack and Hstack. You can also adjust padding of button as .padding(.leading,5) .padding(.top,5) .padding(.bottom,5) .padding(.trailing,5) to adjust the gaps between buttons
I think the best solution is to use GeometryReader, which resizes the width of the content of the Button. However, you need to check that you set a width of the Wrapper around the GeometryReader, because otherwise it would try to use the full screen width. (depends where you use that view, or if it is your primary view)
VStack
{
GeometryReader { geo in
VStack
{
Button(action: { print("PLAY tapped") }){
Text("Play")
.frame(width: geo.size.width)
}
.border(Color.blue)
Button(action: { print("Pause tapped") }){
Text("PAUSE")
.frame(width: geo.size.width)
}
.border(Color.blue)
}
}
}
.frame(width: 100)
.border(Color.yellow)
... which will look like that.
What happens if you put a Spacer() right after the Text("Play")? I think that might stretch out the 'Play' button.
Or maybe before and after Text("Play").

Encountering unexpected behavior when List rotates with SwiftUI

I have setup a relatively basic List embedded in a NavigationView, but when I perform a rotation, things do not work as expected.
The following screenshots show the sequence of events.
When the view is initially presented on an iPhone (running iOS 13.1 17A844)
When the view is rotated from portrait to landscape
My main interest here is that the back button disappears.
Finally, when it is rotated back to portrait
Notice that the Bar Title has shrunk and is now aligned with the back button.
This is a simplified version of the code I am using to generate these screens:
import SwiftUI
struct ViewA: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ViewB()) {
Text("ViewB")
}
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center)
Divider()
NavigationLink(destination: ViewB()) {
Text("ViewB")
}
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center)
}
.padding(.all, 10)
.multilineTextAlignment(.center)
.navigationBarTitle(Text("Main blah"))
}
}
}
struct ViewB : View {
private let items = ["A", "B", "C"]
var body: some View {
List () { [items] in
ForEach(items.indices, id: \.self) { index in
NavigationLink(destination: DetailView(detail:
Detail(title: items[index]))) {
Text("Blah blah")
}
}
}
.navigationBarTitle(Text("Bar Title"))
.listStyle(GroupedListStyle())
}
}
Ideally, I would like to have the UI retain the same appearance after a rotation.
For comparison, when I run this on a physical iPad using iOS 13.1 (17A844), it behaves as expected.