How can I vertically center text-only TabBar buttons in SwiftUI? - swift

I'm making some TabView buttons in SwiftUI (Xcode 11.1, Swift 5.1, and iOS 13.1.3).
For my TabView, I don't want any images -- just text. This code accomplishes that nicely:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
Text("The First Tab")
.tabItem {
Text("My Projects")
}
Text("Another Tab")
.tabItem {
Text("Augmented Reality")
}
Text("The Last Tab")
.tabItem {
Text("Products")
}
}
}
}
However, in this case, the text ends up aligned to the very bottom of the tab bar items, like this:
What I want, though is for the tab bar not to reserve space for the icons, and to vertically center the text -- something like this mock-up:
I've tried sticking it in a VStack and trying to adjust the alignment, but nothing changes.
Is there some smart way to do this, or do I need to do some sort of offset by a specific number of points?
Also FYI, Apple's developer doc says, "Tab views only support tab items of type Text, Image, or an image followed by text. Passing any other type of view results in a visible but empty tab item."
I should add that I can use .offset to adjust the entire TabView, but that's obviously not what we want. .tabItem itself ignores any .offset given, as does the Text within .tabItem.
I was able to get closer, by doing this -- essentially I'm moving the content view for each tab down by 40.0 points, and then moving the entire TabView up by 40. This looks much closer, but the background behind the tabs is then messed up:
Here's the code:
struct ContentView: View {
let vOffset: CGFloat = 40.0
var body: some View {
TabView {
Text("The First Tab")
.tabItem {
Text("My Projects")
}.offset(CGSize(width: 0.0, height: vOffset))
Text("Another Tab")
.tabItem {
Text("Augmented Reality")
}.offset(CGSize(width: 0.0, height: vOffset))
Text("The Last Tab")
.tabItem {
Text("Products")
}.offset(CGSize(width: 0.0, height: vOffset))
}
.offset(CGSize(width: 0.0, height: -vOffset))
}
}
Here's what it looks like:
I assume it will be possible in some way to fix that background, though haven't quite figured out how yet.
The other thought is that I wonder if it's even a good idea to do this sort of "hacky" thing. Or if this even is a hacky thing? I know the whole idea of the declarative nature of SwiftUI is to separate the implementation from the declaration. With that in mind, it would be conceivable to expect that some future implementation could look very different, and thus be made to look stupid via the hacky offsets I'm doing here.
That aside, I still want to do it, for now anyway. 😊
So for now I'm looking for a way to fix the background color of the tab bar area, and also, of course, a less hacky way to solve the original problem.
Thanks!

Update: Xcode 13.3 / iOS 15.4
Not Text is entered (that can be considered as fix for PO)...
... but image is always shown to the top of center (independently of there is text or not):

The empty space is room for an icon. The TabView is not really customizable. You can turn the text into graphics and insert it using Image - that should push it upwards. It won’t be perfectly centered though.

try
TabView(alignment: .center ,spacing: 20))
{
Text("The First Tab")
.tabItem {
Text("My Projects")
}
}

Related

The EditButton for NavigationView is not working properly for iPad ( But correct for iPhone)

I am new in SwiftUI developing, In a SwiftUI project, I created a list of items then I followed the tips in this link to enabled edit button for this,
https://developer.apple.com/documentation/swiftui/editbutton
It works properly for iPhone interface, But in iPad, It has a very strange behaviour. Look at this video below to see how it works in iPad.
https://imgur.com/a/0CLkqiz
If you check this, the way it shows in iPad, when the Edit button text wants to turn to Done, the text steps forward and also creates a transparency with done and edit while it works properly and smooth and fixed in iPhone. I wonder if there might be any special settings for iPad interface in SwiftUI that would fix this problem.
This is my code for this:
struct QRCreator: View {
#State var showingCreateView = false
#State public var fruits = [
"Apple",
"Banana",
"Papaya",
"Mango"
]
var body: some View {
NavigationView {
List {
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
.onDelete { fruits.remove(atOffsets: $0) }
.onMove { fruits.move(fromOffsets: $0, toOffset: $1) }
}
.navigationTitle("Fruits2")
.navigationBarItems(trailing:
Button(action: {
showingCreateView = true
}){
Image(systemName: "plus.viewfinder")
.font(.largeTitle)
}
)
.toolbar {
EditButton()
}
}
}
and here is ContentView which I define three tabs in it like this
TabView
{
NavigationView{
QRScanner()
}
.tabItem
{
Image(systemName: "qrcode.viewfinder")
Text("Scanner")
}
NavigationView {
QRCreator()
}
.tabItem
{
Image(systemName: "doc.fill.badge.plus")
Text("Creator")
}
NavigationView
{
QRSetting()
}
.tabItem
{
Image(systemName: "gear")
Text("Setting")
}
}
There seem to be two layers at play:
From the screen recording, it would appear that your NavigationView is actually wrapped by another NavigationView (or NavigationStack/SplitView) somewhere further up the view hierarchy in your implementation. Besides the odd layout, this also creates a tricky situation in regards to toolbar items like your buttons, and the EditMode environment value that EditButton manipulates.
There is an iPad-specific animation bug in SwiftUI's implementation of EditButton. When clicked with a mouse/trackpad as in your screen recording, the button briefly shows both labels ("Edit" & "Done") at the same time. This doesn't happen when you tap the button directly.
It is only when issues 1 & 2 collide, that I actually run into the more problematic behavior that you've captured: the button jumps and the list also jumps.
If I keep everything as you have shown it (including the doubled-up NavigationViews), but tap the button instead of clicking it with a cursor, things seem fine (although I would expect other possible issues down the road).
If I get rid of the outer NavigationView, but click the button, the button itself still exhibits a slightly odd animation, but it is nowhere near as bad as before. And most importantly, the list animates and behaves correctly.
I tried a couple of approaches to work around the button's remaining animation bug, but nothing short of re-implementing a custom edit button worked.
PS: I know you might've already come across this, but since you said that you're just starting out with iOS 16 introduced new views and APIs for navigation (and in typical fashion for SwiftUI's documentation, older pages like the one for EditButton have not been updated). Depending on how complex your app is, switching later on can be a bit of a pain, so here's a good WWDC video introducing the new API: The SwiftUI cookbook for navigation as well as some blog posts.

Applying padding to SwiftUI Picker with menu style is causing multiline text

I have a Picker with menu style presenting a label with an icon and text. When applying a padding to any views containing the picker, the picker will behave as though there is not enough space and run to multiple lines even though there is enough space.
Sample Code
struct ContentView: View {
let options = [
"Some Long Text"
]
private var selectedOption: Binding<String> =
.constant("Some Long Text")
var body: some View {
HStack {
Text("Some Text")
Picker("Work Type", selection: selectedOption) {
ForEach(self.options, id: \.self) {
Label($0, systemImage: "pencil.tip.crop.circle")
}
}.pickerStyle(.menu)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ContentView()
ContentView()
.padding()
}
}
}
This might be a layout bug in SwiftUI. There is clearly enough space to put it all in one line, even with the padding. You may want to file a bug report to Apple.
However, you can easily fix this by applying the fixedSize() modifier to your Picker:
Picker("Work Type", selection: selectedOption) {
// ...
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: false)
This will make sure that the Picker doesn't concern itself with external sizing requirements, ignores the proposed with and just uses renders with its ideal size.
Please note that this might lead to unintended behavior. For example, when the user chooses a bigger font size in accessibility settings, the text in the Picker will never break and might be rendered off-screen. So make sure to think through all your use cases.
PS: You can also use a Menu instead of a Picker with .menu style. Of course, you won't get the selection behavior for free and have to do it manually, but it doesn't have this layout bug.
This seems like intended behavior to me. Because of the padding applied around the HStack (specifically at the horizontal edges) there is not enough space to render the form label on one line. If you were to change the size of the screen (for example using an iPad simulator or using landscape mode) the label would have enough room and be displayed on one line. I tested this code on an iPad simulator and it worked as expected.
If you want to force the label text to be rendered on one line, use fixedSize(horizontal:vertical:). Though this can lead to unexpected behavior.
Picker("Work Type", selection: selectedOption) {
ForEach(self.options, id: \.self) {
Label($0, systemImage: "pencil.tip.crop.circle")
}
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: false)

MacOS Toolbar Space makes Items disappear despite sufficient space

I created a macOS App with a NavigationView and a Toolbar.
Somehow next to my toolbarItem there is a lot of space.. So whenever I change the size of my apps window the toolbarItem disappears. Despite there is still a lot of space for my item.
I did not find out how to reduce that space..
Can you help me?
var body: some View {
NavigationView {
}
.toolbar(content: {
ToolbarItem(placement: .navigation) {
Image(systemName: "gearshape")
.frame(height: 20)
}
})
I found the problem.
The title is existing and just invisible.
You can remove the title by adding .windowToolbarStyle(.unifiedCompact(showsTitle: false))
to the WindowGroup.

Using SwiftUI on watchOS, how to align views to the navigation title?

I would like to align some content to the same inset the the navigation title uses.
On older watch devices, there is no inset, but on newer devices with the rounded corners, the title is inset, and for certain things I would like to align to the same inset.
The apple docs talk about this margin, but I can't figure out how to use it.
How can I modify this example to have the left edge of the Text and Button align with the Navigation Title?
var body: some View {
VStack(alignment: .leading) {
Text("Hello World")
Button("Test") {}
}
.navigationTitle("Title")
}
Use .scenePadding(.horizontal). That will automatically apply the padding you want on the larger watches. Docs: https://developer.apple.com/documentation/swiftui/menu/scenepadding(_:)
Leading padding is simply (on the VStack):
.padding(.leading, 9.5)
but you have to do it based on the watch model.
Here is an example of how to determine the watch model:
How to determine the Apple Watch model?
And you might want to put this in a conditional view modifier:
https://stackoverflow.com/a/62962375/14351818
You could use the current screen width:
var screenWidth = WKInterfaceDevice.current().screenBounds.width
and apply that (minus the desired margin) as a frame width to your VStack:
VStack(alignment: .leading){
Text("Hello World")
Button("Test") {}
}
.frame(width: screenWidth - 25)
.navigationTitle("Title")
This worked fine on 45mm and 40mm, but unfortunately still not quite there on the 38mm watch, so you might want to explicitly set different paddings/frames for smaller devices.

Why does SwiftUI View background extend into safe area?

Here's a view that navigates to a 2nd view:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
SecondView()
} label: {
Text("Go to 2nd view")
}
}
}
}
struct SecondView: View {
var body: some View {
ZStack {
Color.red
VStack {
Text("This is a test")
.background(.green)
//Spacer() // <--If you add this, it pushes the Text to the top, but its background no longer respects the safe area--why is this?
}
}
}
}
On the 2nd screen, the green background of the Text view only extends to its border, which makes sense because the Text is a pull-in view:
Now, if you add Spacer() after the Text(), the Text view pushes to the top of the VStack, which makes sense, but why does its green background suddenly push into the safe area?
In the Apple documentation here, it says:
By default, SwiftUI sizes and positions views to avoid system defined
safe areas to ensure that system content or the edges of the device
won’t obstruct your views. If your design calls for the background to
extend to the screen edges, use the ignoresSafeArea(_:edges:) modifier
to override the default.
However, in this case, I'm not using ignoresSafeArea, so why is it acting as if I did, rather than perform the default like Apple says, which is to avoid the safe areas?
You think that you use old background modifier version.
But actually, the above code uses a new one, introduced in iOS 15 version of background modifier which by default ignores all safe area edges:
func background<S>(_ style: S, ignoresSafeAreaEdges edges: Edge.Set = .all) -> some View where S : ShapeStyle
To fix the issue just replace .background(.green) with .background(Color.green) if your app deployment target < iOS 15.0, otherwise use a new modifier: .background { Color.green }.