标签栏中的 SwiftUI 动画

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

我想要在选项卡之间切换动画,但动画非常困难。我尝试使用匹配的几何体,但动画很古怪。可能缺乏对匹配几何形状的理解。

期望:

点击选项卡将显示图像图标和胶囊后面的选项卡标题。 未点击的选项卡不会显示标题,也不会在其后面显示胶囊。一旦选择了新选项卡,胶囊将导航到所选选项卡。并且先前选择的选项卡的标题将隐藏。

import SwiftUI

struct Tabbar: View {
    @Namespace private var animation
    @State var selectedTab: RootTab = .home
    var body: some View {
        ZStack {
            HStack {
                ForEach(RootTab.allCases, id: \.self) { tab in
                    TabButton(tab: tab)
                }
            }
            .padding(.horizontal, 30)
            .padding(.vertical, 10)
            .background(Color.blue.opacity(0.4))
            .cornerRadius(30, corners: .allCorners)
        }
        .frame(maxWidth: .infinity)
        .background(Color(uiColor: .systemBackground))
    }
    
    private func TabButton(tab: RootTab) -> some View {
        HStack {
            Image(systemName: tab.systemName)
                .font(.title2)
                .foregroundStyle(.white)
                .bold()
            
            if selectedTab == tab {
                Text(tab.title)
                    .font(.callout)
                    .bold()
                    .foregroundColor(.white)
                    .animation(.default, value: selectedTab)
            }
        }
        .onTapGesture {
            withAnimation {
                selectedTab = tab
            }
        }
        .if(selectedTab == tab) { view in
            view
                .padding(.vertical, 8)
                .padding(.horizontal, 8)
                .background { Color.blue }
                .cornerRadius(15, corners: .allCorners)
                .animation(.default, value: selectedTab)
        }
    }
}

enum RootTab: String, CaseIterable {
    case home
    case profile
    
    var systemName: String {
        switch self {
            case .home: return "house"
            case .profile: return "person.crop.circle"
        }
    }
    
    var title: String {
        switch self {
            case .home: return "Home"
            case .profile: return "Profile"
        }
    }
}

extension View {
    @ViewBuilder
    func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> some View) -> some View {
        if condition() {
            transform(self)
        } else {
            self
        }
    }

    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape(RoundedCorner(radius: radius, corners: corners))
    }
}

swiftui swiftui-animation swiftui-matchedgeometryeffect
1个回答
0
投票

正如您所建议的,

.matchedGeometryEffect
可用于实现此动画:

  • 移动背景应显示为

    ZStack
    的第一层(换句话说,在按钮后面)。

  • 使用

    .matchedGeometryEffect
    isSource: false
    将移动背景的位置和大小与所选选项卡按钮相匹配。标签按钮的id用作效果的id。

  • 由于背景标记的大小由所选按钮决定,因此应将填充应用于按钮本身。

  • 选项卡按钮使用

    withAnimation
    执行更改,因此不需要任何
    .animation
    修饰符。

  • 不需要视图扩展

    .if

struct Tabbar: View {
    @Namespace private var animation
    @State var selectedTab: RootTab = .home

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 15)
                .fill(.blue)
                .matchedGeometryEffect(id: selectedTab, in: animation, isSource: false)

            HStack {
                ForEach(RootTab.allCases, id: \.self) { tab in
                    TabButton(tab: tab)
                        .padding(.vertical, 8)
                        .padding(.horizontal, 8)
                        .matchedGeometryEffect(id: tab, in: animation)
                }
            }
            .padding(.horizontal, 30)
            .padding(.vertical, 10)
            .background(Color.blue.opacity(0.4))
            .cornerRadius(30, corners: .allCorners)
        }
        .frame(maxWidth: .infinity)
        .background(Color(uiColor: .systemBackground))
    }

    private func TabButton(tab: RootTab) -> some View {
        HStack {
            Image(systemName: tab.systemName)
                .font(.title2)
                .foregroundStyle(.white)
                .bold()

            if selectedTab == tab {
                Text(tab.title)
                    .font(.callout)
                    .bold()
                    .foregroundColor(.white)
            }
        }
        .onTapGesture {
            withAnimation {
                selectedTab = tab
            }
        }
    }
}

Animation

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