如何在 SwiftIUI 中制作上方弯曲且居中的自定义 TabBar?

问题描述 投票:0回答:1

我是 SwiftUI 新手,已经完成了 TabBar UI 的一半。但是,我坚持在中心按钮的两侧添加一条小曲线,类似于示例图像。

Expected TabBar UI

我不确定如何在中心按钮的两侧添加小曲线?这是我迄今为止所取得的成就:

What I have done

下面是我的 TabBar 代码:

import SwiftUI

struct ContentView: View {
    // MARK: - HIDING NATIVE TAB BAR
    init(){
        UITabBar.appearance().isHidden = true
    }
    
    var body: some View {
        VStack {
            Spacer()
            TabBarShape()
                .fill(Color.white)
                .frame(height: 80)
                .shadow(color: Color.black.opacity(0.4), radius: 2, x: 0, y: -1)
                .overlay(
                    ZStack {
                        Button(action: {
                            print("Create Button Action")
                        }, label: {
                            Image("plus_icon")
                                .frame(width: 60, height: 60, alignment: .center)
                                .background(Color.custom64B054Color)
                                .cornerRadius(30)
                        }).offset(x: 0, y: -36)
                        Text("Create")
                            .padding(.top, 32)
                        HStack(spacing: 0) {
                            TabBarItem(iconName: "house.fill", action: {})
                            TabBarItem(iconName: "person.fill", action: {})
                        }
                            .frame(height: 80)
                    }
                )
        }.ignoresSafeArea()
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 // MARK: - TabBar Shape
struct TabBarShape: Shape {
    // Constants used for the shape
    private enum Constants {
        static let cornerRadius: CGFloat = 20
        static let buttonRadius: CGFloat = 30
        static let buttonPadding: CGFloat = 9
    }
    
    // Function to define the shape's path
    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath()
        
        // Move to the starting point at the bottom-left corner
        path.move(to: .init(x: 0, y: rect.height))
        
        // Add a line to the upper-left corner, leaving space for the corner radius
        path.addLine(to: .init(x: 0, y: rect.height - Constants.cornerRadius))
        
        // Add a quarter-circle in the upper-left corner
        path.addArc(withCenter: .init(x: Constants.cornerRadius, y: Constants.cornerRadius),
                    radius: Constants.cornerRadius,
                    startAngle: CGFloat.pi,
                    endAngle: -CGFloat.pi/2,
                    clockwise: true)
        
        // Calculate the end point for the line before the first button
        let lineEnd = rect.width/2 - 2 * Constants.buttonPadding - Constants.buttonRadius
        
        // Add a line to the calculated end point
        path.addLine(to: .init(x: lineEnd, y: 0))
        
        // Add a quarter-circle for the first button
        path.addArc(withCenter: .init(x: lineEnd, y: Constants.buttonPadding),
                    radius: Constants.buttonPadding,
                    startAngle: 0,
                    endAngle: -CGFloat.pi/2,
                    clockwise: true)
        
        // Add a half-circle for the first button
        path.addArc(withCenter: .init(x: rect.width/2, y: 0),
                    radius: Constants.buttonPadding + Constants.buttonRadius,
                    startAngle: 0,
                    endAngle: CGFloat.pi,
                    clockwise: false)
                
        // Calculate the start point for the line after the first button
        let lineStart = rect.width/2 + 2 * Constants.buttonPadding - Constants.buttonRadius
        
        // Add a quarter-circle for the second button
        path.addArc(withCenter: .init(x: lineStart, y: Constants.buttonPadding),
                    radius: Constants.buttonPadding,
                    startAngle: -CGFloat.pi/2,
                    endAngle: -CGFloat.pi/2,
                    clockwise: true)
        
        // Add a line to the calculated start point for the second button
        path.addLine(to: .init(x: rect.width - Constants.cornerRadius, y: 0))
        
        // Add a quarter-circle in the upper-right corner
        path.addArc(withCenter: .init(x: rect.width - Constants.cornerRadius, y: Constants.cornerRadius),
                    radius: Constants.cornerRadius,
                    startAngle: -CGFloat.pi/2,
                    endAngle: 0,
                    clockwise: true)
        
        // Add a line to the bottom-right corner
        path.addLine(to: .init(x: rect.width, y: rect.height))
        
        // Close the path to complete the shape
        path.close()
        
        // Convert the UIBezierPath to a SwiftUI Path
        return Path(path.cgPath)
    }
}

 // MARK: - TabBar Item
struct TabBarItem: View {
    let iconName: String
    let action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Image(systemName: iconName)
                .font(.system(size: 24))
                .foregroundColor(.blue)
                .padding(20)
        }
        .frame(maxWidth: .infinity)
    }
}

如果有人能提供帮助,那就太好了。如果这是我预期的 TabBar UI 的正确方法,有人可以指导我吗?

谢谢您!

swift swiftui uibezierpath tabbar swiftui-tabview
1个回答
0
投票

我尝试使用与 TabBar Customization in SwiftUI 的答案中使用的构建路径相同的技术来实现此形状(这是我的答案)。

我不熟悉

UIBezierPath
,所以我坚持使用
Path

这是修改后的

TabBarShape

struct TabBarShape: Shape {

    // Constants used for the shape
    private enum Constants {
        static let cornerRadius: CGFloat = 20
        static let smallCornerRadius: CGFloat = 15
        static let buttonRadius: CGFloat = 30
        static let buttonPadding: CGFloat = 9
    }

    // Function to define the shape's path
    func path(in rect: CGRect) -> Path {
        var path = Path()

        // Move to the starting point at the bottom-left corner
        var x = rect.minX
        var y = rect.maxY
        path.move(to: CGPoint(x: x, y: y))

        // Add the rounded corner on the top-left corner
        x += Constants.cornerRadius
        y = Constants.buttonRadius + Constants.buttonPadding + Constants.cornerRadius
        path.addArc(
            center: CGPoint(x: x, y: y),
            radius: Constants.cornerRadius,
            startAngle: .degrees(180),
            endAngle: .degrees(270),
            clockwise: false
        )
        // Add a small corner leading to the main half-circle
        x = rect.midX - Constants.buttonRadius - Constants.buttonPadding - Constants.smallCornerRadius
        y = Constants.buttonRadius + Constants.buttonPadding - Constants.smallCornerRadius
        path.addArc(
            center: CGPoint(x: x, y: y),
            radius: Constants.smallCornerRadius,
            startAngle: .degrees(90),
            endAngle: .degrees(30), // 0
            clockwise: true
        )
        // Add the main half-circle
        x = rect.midX
        y += Constants.smallCornerRadius
        path.addArc(
            center: CGPoint(x: x, y: y),
            radius: Constants.buttonRadius + Constants.buttonPadding,
            startAngle: .degrees(215), // 180
            endAngle: .degrees(325), // 0
            clockwise: false
        )
        // Add a trailing small corner
        x += Constants.buttonRadius + Constants.buttonPadding + Constants.smallCornerRadius
        y -= Constants.smallCornerRadius
        path.addArc(
            center: CGPoint(x: x, y: y),
            radius: Constants.smallCornerRadius,
            startAngle: .degrees(150), // 180
            endAngle: .degrees(90),
            clockwise: true
        )
        // Add the rounded corner on the top-right corner
        x = rect.maxX - Constants.cornerRadius
        y = Constants.buttonRadius + Constants.buttonPadding + Constants.cornerRadius
        path.addArc(
            center: CGPoint(x: x, y: y),
            radius: Constants.cornerRadius,
            startAngle: .degrees(270),
            endAngle: .degrees(0),
            clockwise: false
        )
        // Connect the bottom corner
        x = rect.maxX
        y = rect.maxY
        path.addLine(to: CGPoint(x: x, y: y))

        // Close the path to complete the shape
        path.closeSubpath()
        return path
    }
}

您会注意到,无需在圆弧之间绘制线条,因为它们是自动添加的。进出主半圆的小角被绘制为单独的圆弧。为了平滑连接,这些弧和主半圆不会一直达到 90 度。这些部分角度是通过一些尝试和错误找到的。

以下是如何在主

body
的改编版本中使用该形状。一些注意事项:

  • 底部对齐的
    HStack
    用于包含常规选项卡栏项目。
  • + 按钮显示为覆盖层。通过向
    HStack
    添加顶部填充来为按钮保留空间。
  • HStack
    下面添加底部填充,以允许插入安全区域。插图使用
    GeometryReader
    进行测量。
  • 形状被应用为背景。这样,它会自动采用其所应用的框架的大小。
var body: some View {
    GeometryReader { proxy in
        VStack {
            Spacer()
            HStack(alignment: .bottom) {
                TabBarItem(label: "Schedule", iconName: "house.fill") {}
                Text("Create")
                TabBarItem(label: "Profile", iconName: "person.fill") {}
            }
            .font(.footnote)
            .padding(.top, 50)
            .overlay(alignment: .top) {
                Button {
                    print("Create Button Action")
                } label: {
                    Image(systemName: "plus") // "plus_icon"
                        .resizable()
                        .scaledToFit()
                        .padding()
                        .frame(width: 60, height: 60, alignment: .center)
                        .foregroundColor(.white)
                        .background(.green) // custom64B054Color
                        .cornerRadius(30)
                        .shadow(radius: 3)
                }
                .padding(9)
            }
            .padding(.bottom, max(8, proxy.safeAreaInsets.bottom))
            .background {
                TabBarShape()
                    .fill(Color.white)
                    .shadow(radius: 3)
            }
        }
        .ignoresSafeArea(edges: .bottom)
    }
}

我还对

TabBarItem
进行了一些小更改,以包含标签并省略填充:

struct TabBarItem: View {
    let label: String
    let iconName: String
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            VStack(spacing: 4) {
                Image(systemName: iconName)
                    .font(.system(size: 24))
                    .foregroundColor(.blue)
                Text(label)
            }
        }
        .frame(maxWidth: .infinity)
    }
}

这就是一切的样子:

Screenshot

希望有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.