SwiftUI: How to center the tabItems vertically in the TabView?

83 Views Asked by At

I added 5 tab items in TabView. The middle one is an Image view, while the others are Text views. Why are they not vertically centered? The Image view floats on top of the TabView.

Preview

This is the code,

struct ContentView: View {

    var body: some View {
        TabView {
            Text("")
                .tabItem {
                    Text("One")
                }
            Text("")
                .tabItem {
                    Text("Two")
                }
            Text("")
                .tabItem {
                    Image(systemName: "plus.app")
                }
            Text("")
                .tabItem {
                    Text("Three")
                }
            Text("")
                .tabItem {
                    Text("Four")
                }
        }
        .onAppear {
            let tabBarItemAppearence = UITabBarItemAppearance()

            let tabBarAppearance = UITabBarAppearance()
            tabBarAppearance.shadowColor = UIColor(.gray)
            tabBarAppearance.stackedLayoutAppearance = tabBarItemAppearence

            UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
        }
    }
}

#Preview {
    ContentView()
}

After replacing the Text view with the Label view that sets the systemImage property, it appears that the image in the Label view aligns with the Image view.

Preview

Are there ways to vertically center general views within tabItems?

2

There are 2 best solutions below

0
son On

Actually, you can't, TabBar item contains an image and text by default. It was designed for this alignment. You need to customize a View yourself, something like:

struct ImageTabItem: View {
    let imageName: String
    let imageSize: CGSize

    var body: some View {
        Image(systemName: imageName)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: imageSize.width, height: imageSize.height)
    }
}

struct TextTabItem: View {
    let tabName: String

    var body: some View {
        Text(tabName)
            .font(.footnote)
    }
}

struct CustomTabBar: View {
    var body: some View {
        GeometryReader { geo in
            ZStack {
                VStack {
                    Spacer()
                    HStack(spacing: 0) {
                        let tabWidth = geo.size.width / 5
                        TextTabItem(tabName: "One")
                            .frame(width: tabWidth)

                        TextTabItem(tabName: "Two")
                            .frame(width: tabWidth)

                        ImageTabItem(imageName: "plus.app", imageSize: .init(width: 30, height: 30))
                            .frame(width: tabWidth)

                        TextTabItem(tabName: "Three")
                            .frame(width: tabWidth)

                        TextTabItem(tabName: "Four")
                            .frame(width: tabWidth)
                    }
                }
            }
        }
    }
}

However, there are many drawbacks, such as: handling action gesture, safeArea padding, etc. You must cover it by yourself.

1
J W On

To solve the issue of the Image view not being vertically centered in the TabView compared to Text views, use the Label view which combines text and an icon. This ensures consistent alignment within the TabView.

So firstly we can write a simple example using image alignment principles:

TabView {
  Text("page 1 content")
    .tabItem {
      Label("One", systemImage: "2.circle")
    }
  Text("plus content")
    .tabItem {
      Label("", systemImage: "plus.app")
    }
}

enter image description here

Now, You can see that all the images are aligned. Of course, it is obvious that this is not enough, because we need to align Text and Image, so we only need to change Text into Image.

As for how to convert txt to image, it's very simple. Just take a photo of the text you want to display and remove the background.

Text("page file content")
  .tabItem {
    Image("test")
      .resizable()
      .aspectRatio(contentMode: .fit)
      .frame(width: 30, height: 30)
    Text("file")
  }

enter image description here

Now the picture is added, but the size is not satisfactory, very bad, and the method of taking pictures of Text at the same time is actually not elegant.

The method of using .resizable().frame(width: 30, height: 30) in tabItem of TabView to adjust the image size does not work for the Image object used directly in the label of TabView . This is because the icon size of the TabView label is controlled by the system and cannot be modified directly through .frame.

So, we need to change the size before we show the images.

So we can write a function to convert Text to Image and resize it:

func textToImage(text: String, size: CGSize) -> UIImage {
  let renderer = UIGraphicsImageRenderer(size: size)
  return renderer.image { ctx in
    // Set the text to be centered
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.alignment = .center
    
    // Calculate font size to fit size
    let attributes: [NSAttributedString.Key: Any] = [
      .font: UIFont.systemFont(ofSize: 12),
      .paragraphStyle: paragraphStyle
    ]
    
    // Calculate the rectangle size required for text drawing
    let attributedText = NSAttributedString(string: text, attributes: attributes)
    let textRect = attributedText.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil)
    
    // Draw text centered in the context of the renderer
    attributedText.draw(in: CGRect(x: (size.width - textRect.width) / 2.0,
                                   y: (size.height - textRect.height) / 2.0,
                                   width: textRect.width,
                                   height: textRect.height))
  }
}
Text("page 4 content")
  .tabItem {
    Image(uiImage: textToImage(text: "Four", size: CGSize(width: 30, height: 30)))
    Text("Four")
  }

enter image description here

SwiftUI is kind enough to help us highlight the Image so that we don’t have to change the color ourselves. Now everything is aligned. We just need to change some font sizes and bold fonts, and they won’t be displayed.

Below is the complete code:

import SwiftUI

struct ContentView: View {
  
  var body: some View {
    TabView {
      Text("page 1 content")
        .tabItem {
          Image(uiImage: textToImage(text: "One", size: CGSize(width: 32, height: 32)))
        }
      Text("page 2 content")
        .tabItem {
          Image(uiImage: textToImage(text: "Two", size: CGSize(width: 32, height: 32)))
        }
      Text("page plus content")
        .tabItem {
          Label("", systemImage: "plus.app")
        }
      Text("page 3 content")
        .tabItem {
          Image(uiImage: textToImage(text: "Three", size: CGSize(width: 32, height: 32)))
        }
      Text("page 4 content")
        .tabItem {
          Image(uiImage: textToImage(text: "Four", size: CGSize(width: 32, height: 32)))
        }
    }
    
  }
  
  func textToImage(text: String, size: CGSize) -> UIImage {
    let renderer = UIGraphicsImageRenderer(size: size)
    return renderer.image { ctx in
      let paragraphStyle = NSMutableParagraphStyle()
      paragraphStyle.alignment = .center
      
      let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 10),
        .paragraphStyle: paragraphStyle
      ]
      
      let attributedText = NSAttributedString(string: text, attributes: attributes)
      let textRect = attributedText.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil)
      
      attributedText.draw(in: CGRect(x: (size.width - textRect.width) / 2.0,
                                     y: (size.height - textRect.height) / 2.0,
                                     width: textRect.width,
                                     height: textRect.height))
    }
  }
}

#Preview {
  ContentView()
}

enter image description here