I have a document-based SwiftUI app. I'd like to make a inspector sidebar like the one in Xcode.
Starting with Xcode's Document App template, I tried the following:
struct ContentView: View {
#Binding var document: DocumentTestDocument
#State var showInspector = true
var body: some View {
HSplitView {
TextEditor(text: $document.text)
if showInspector {
Text("Inspector")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.toolbar {
Button(action: { showInspector.toggle() }) {
Label("Toggle Inspector", systemImage: "sidebar.right")
}
}
}
}
Which yielded:
How can I extend the right sidebar to full height like in Xcode?
NavigationView works for left-side sidebars, but I'm not sure how to do it for right-side sidebars.
Here is some stripped down code that I have used in the past. It has the look and feel that you want.
It uses a NavigationView with .navigationViewStyle(.columns) with essentially three panes. Also, the HiddenTitleBarWindowStyle() is important.
The first (navigation) pane is never given any width because the second (Detail) pane is always given all of the width when there is no Inspector, or all of the width less the Inspector's width when it's present. The ToolBar needs to be broken up and have its contents placed differently depending on whether the Inspector is present or not.
#main
struct DocumentTestDocumentApp: App {
var body: some Scene {
DocumentGroup(newDocument: DocumentTestDocument()) { file in
ContentView(document: file.$document)
}
.windowStyle(HiddenTitleBarWindowStyle())
}
}
struct ContentView: View {
#Binding var document: DocumentTestDocument
#State var showInspector = true
var body: some View {
GeometryReader { window in
if showInspector {
NavigationView {
TextEditor(text: $document.text)
.frame(minWidth: showInspector ? window.size.width - 200.0 : window.size.width)
.toolbar {
LeftToolBarItems(showInspector: $showInspector)
}
Inspector()
.toolbar {
RightToolBarItems(showInspector: $showInspector)
}
}
.navigationViewStyle(.columns)
} else {
NavigationView {
TextEditor(text: $document.text)
.frame(width: window.size.width)
.toolbar {
LeftToolBarItems(showInspector: $showInspector)
RightToolBarItems(showInspector: $showInspector)
}
}
.navigationViewStyle(.columns)
}
}
}
}
struct LeftToolBarItems: ToolbarContent {
#Binding var showInspector: Bool
var body: some ToolbarContent {
ToolbarItem(content: { Text("test left toolbar stuff") } )
}
}
struct RightToolBarItems: ToolbarContent {
#Binding var showInspector: Bool
var body: some ToolbarContent {
ToolbarItem(content: { Spacer() } )
ToolbarItem(placement: .primaryAction) {
Button(action: { showInspector.toggle() }) {
Label("Toggle Inspector", systemImage: "sidebar.right")
}
}
}
}
struct Inspector: View {
var body: some View {
VStack {
Text("Inspector Top")
Spacer()
Text("Bottom")
}
}
}
Related
In my app I add/remove a subview to/from a TabView based on some condition. I'd like to animate tab item addition/removal in tab bar. My experiment (see code below) shows it's not working. I read on the net that TabView support for animation is quite limited and some people rolled their own implementation. But just in case, is it possible to implement it?
import SwiftUI
struct ContentView: View {
#State var showBoth: Bool = false
var body: some View {
TabView {
Button("Test") {
withAnimation {
showBoth.toggle()
}
}
.tabItem {
Label("1", systemImage: "1.circle")
}
if showBoth {
Text("2")
.tabItem {
Label("2", systemImage: "2.circle")
}
.transition(.slide)
}
}
}
}
Note: moving transition() call to the Label passed to tabItem() doesn't work either.
As commented Apple wants the TabBar to stay unchanged throughout the App.
But you can simply implement your own Tabbar with full control:
struct ContentView: View {
#State private var currentTab = "One"
#State var showBoth: Bool = false
var body: some View {
VStack {
TabView(selection: $currentTab) {
// Tab 1.
VStack {
Button("Toggle 2. Tab") {
withAnimation {
showBoth.toggle()
}
}
} .tag("One")
// Tab 2.
VStack {
Text("Two")
} .tag("Two")
}
// custom Tabbar buttons
Divider()
HStack {
OwnTabBarButton("One", imageName: "1.circle")
if showBoth {
OwnTabBarButton("Two", imageName: "2.circle")
.transition(.scale)
}
}
}
}
func OwnTabBarButton(_ label: String, imageName: String) -> some View {
Button {
currentTab = label
} label: {
VStack {
Image(systemName: imageName)
Text(label)
}
}
.padding([.horizontal,.top])
}
}
In a multiplatform app I'm showing a sheet to collect a small amount of user input. On iOS, when the sheet is dismissed, the relevant .onDismiss method is called but not on macOS.
I've read that having the .onDismiss in the List can cause problems so I've attached it to the button itself with no improvement. I've also tried passing the isPresented binding through and toggling that within the sheet itself to dismiss, but again with no success.
I am employing a NavigationView but removing that makes no difference. The following simplified example demonstrates my problem. Any ideas? Should I even be using a sheet for this purpose on macOS?
I just want to make clear that I have no problem closing the sheet. The other questions I found were regarding problems closing the sheet - I can do that fine.
import SwiftUI
#main
struct SheetTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
ListView()
}
}
}
The List view.
struct ListView: View {
#State private var isPresented: Bool = false
var body: some View {
VStack {
Text("Patterns").font(.title)
Button(action: {
isPresented = true
}, label: {
Text("Add")
})
.sheet(isPresented: $isPresented, onDismiss: {
doSomethingAfter()
}) {
TestSheetView()
}
List {
Text("Bingo")
Text("Bongo")
Text("Banjo")
}
.onAppear(perform: {
doSomethingBefore()
})
}
}
func doSomethingBefore() {
print("Johnny")
}
func doSomethingAfter() {
print("Cash")
}
}
This is the sheet view.
struct TestSheetView: View {
#Environment(\.presentationMode) var presentationMode
#State private var name = ""
var body: some View {
Form {
TextField("Enter name", text: $name)
.padding()
HStack {
Spacer()
Button("Save") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
}
}
.frame(minWidth: 300, minHeight: 300)
.navigationTitle("Jedward")
}
}
Bad issue.. you are right. OnDismiss is not called. Here is a workaround with Proxybinding
var body: some View {
VStack {
Text("Patterns").font(.title)
Button(action: {
isPresented = true
}, label: {
Text("Add")
})
List {
Text("Bingo")
Text("Bongo")
Text("Banjo")
}
.onAppear(perform: {
doSomethingBefore()
})
}
.sheet(isPresented: Binding<Bool>(
get: {
isPresented
}, set: {
isPresented = $0
if !$0 {
doSomethingAfter()
}
})) {
TestSheetView()
}
}
I have an image background, which should stay in place when the keyboard shows, but instead it moves up together with everything on the screen. I saw someone recommend using ignoresSafeArea(.keyboard), and this question Simple SwiftUI Background Image keeps moving when keyboard appears, but neither works for me. Here is my super simplified code sample. Please keep in mind that while the background should remain unchanged, the content itself should still avoid the keyboard as usual.
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
}
}
Here a possible salvation:
import SwiftUI
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass
#State var valueOfTextField: String = String()
var body: some View {
GeometryReader { proxy in
Image("Your Image name here").resizable().scaledToFill().ignoresSafeArea()
ZStack {
if verticalSizeClass == UserInterfaceSizeClass.regular { TextFieldSomeView.ignoresSafeArea(.keyboard) }
else { TextFieldSomeView }
VStack {
Spacer()
Button(action: { print("OK!") }, label: { Text("OK").padding(.horizontal, 80.0).padding(.vertical, 5.0).background(Color.yellow).cornerRadius(5.0) }).padding()
}
}
.position(x: proxy.size.width/2, y: proxy.size.height/2)
}
}
var TextFieldSomeView: some View {
return VStack {
Spacer()
TextField("write something", text: $valueOfTextField).padding(5.0).background(Color.yellow).cornerRadius(5.0).padding()
Spacer()
}
}
}
u can use GeometryReader
get parent View size
import SwiftUI
import Combine
struct KeyboardAdaptive: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) {
self.keyboardHeight = $0
}
}
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
.keyboardAdaptive()
}
}
I want to replace a View with another when a Button is pressed in SwiftUI on MacOS (AppKit not Catalyst).
I tried with a NavigationLink like this
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink( destination: Text("DESTINATIONVIEW").frame(maxWidth: .infinity, maxHeight: .infinity) ,
label: {Text("Next")} )
}.frame(width: 500.0, height: 500.0)
}
}
The Destination View is presented like in iPadOS, like a Master-Detail Presentation right of the Source View.
What I want, is to replace the SourceView with the Destination (almost like in iOS)
Another approach was to switch the presented view with a bool variable
struct MasterList: View
{
private let names = ["Frank", "John", "Tom", "Lisa"]
#State var showDetail: Bool = false
#State var selectedName: String = ""
var body: some View
{
VStack
{
if (showDetail)
{ DetailViewReplace(showDetail:$showDetail, text: "\(selectedName)" )
} else
{
List()
{
ForEach(names, id: \.self)
{ name in
Button(action:
{
self.showDetail.toggle()
self.selectedName = name
})
{ Text("\(name)")}
.buttonStyle( LinkButtonStyle() )
}
}.listStyle( SidebarListStyle())
}
}
}
}
struct DetailViewReplace: View {
#Binding var showDetail: Bool
let text: String
var body: some View
{
VStack
{
Text(text)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(action:
{ self.showDetail.toggle()
})
{ Text("Return")
}
}
}
}
But that seems quite cumbersome for me if dealing with many views.
Is there a best practice to do that?
Updated to Xcode beta-3, Popover was deprecated... having one hell of a time trying to figure out how to make it work again!?!?
It no longer "pops up" it slides up from the bottom.
It's no longer positioned or sized correctly, takes up the whole screen.
Once dismissed, it never wants to appear again.
This was the old code, that worked perfectly...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
#State private var showPositions = false
var body: some View {
HStack {
Spacer()
Button(action: { self.showPositions = true } ) {
Text("Position")
}
.presentation(showPositions ? Popover(content: MultiPicker(items: Exercise.Position.allCases, selected:$filter.positions),
dismissHandler: { self.showPositions = false })
: nil)
}
.padding()
}
}
And this is the new code...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
#State private var showPositions = false
var body: some View {
HStack {
Spacer()
Button(action: { self.showPositions = true } ) {
Text("Position")
}
.popover(isPresented: $showPositions) {
MultiPicker(items: Exercise.Position.allCases, selected:self.$filter.positions)
.onDisappear { self.showPositions = false }
}
}
.padding()
}
}
I ended up using PresentationLink just so I can move forward with everything else...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
var body: some View {
HStack {
Spacer()
PresentationLink(destination: MultiPicker(items: Exercise.Position.allCases, selected:$filter.positions)) {
Text("Position")
}
}
.padding()
}
}
It works, as far as testing is concerned, but it's not a popover.
Thanks for any suggestions!
BTW, this code is being in the iPad simulator.
On OSX the code below works fine
struct ContentView : View {
#State var poSelAbove = false
#State var poSelBelow = false
#State var pick : Int = 1
var body: some View {
let picker = Picker(selection: $pick, label: Text("Pick option"), content:
{
Text("Option 0").tag(0)
Text("Option 1").tag(1)
Text("Option 2").tag(2)
})
let popoverWithButtons =
VStack {
Button("Not Dismiss") {
}
Divider()
Button("Dismiss") {
self.poSelAbove = false
}
}
.padding()
return VStack {
Group {
Button("Show button popover above") {
self.poSelAbove = true
}.popover(isPresented: $poSelAbove, arrowEdge: .bottom) {
popoverWithButtons
}
Divider()
Button("Show picker popover below") {
self.poSelBelow = true
}.popover(isPresented: $poSelBelow, arrowEdge: .top) {
Group {
picker
}
}
}
Divider()
picker
.frame(width: 300, alignment: .center)
Text("Picked option: \(self.pick)")
.font(.subheadline)
}
// comment the line below for iOS
.frame(width: 800, height: 600)
}
On iOS (iPad) the popover will appear in a strange transparent full screen mode. I don't think this is intended. I have added the problem to my existing bug report.