import Foundation import SwiftUI // MARK: - Custom Button Style struct MobileMeButtonStyle: ButtonStyle { // MARK: Metrics @ScaledMetric private var cornerRadius = 12 @ScaledMetric private var horizontalLabelPadding = 12 @ScaledMetric private var verticalLabelPadding = 8 @ScaledMetric private var shadowRadius = 2 @ScaledMetric private var shadowVerticalOffset = 1 // MARK: Immutable Properties private let strokeLineWidth = 1.0 func makeBody(configuration: Configuration) -> some View { configuration.label .labelStyle(.mobileMe) .padding(.horizontal, horizontalLabelPadding) .padding(.vertical, verticalLabelPadding) .background( RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .foregroundStyle( LinearGradient( colors: [ Color(red: 58/255, green: 63/255, blue: 66/255), Color(red: 58/255, green: 63/255, blue: 66/255), Color(red: 73/255, green: 76/255, blue: 80/255) ], startPoint: .top, endPoint: .bottom ) ) .overlay( // Top reflection ReflectionContainer { UnevenRoundedRectangle( cornerRadii: .init( topLeading: cornerRadius, bottomLeading: (cornerRadius * 0.43).rounded(.down), bottomTrailing: (cornerRadius * 0.43).rounded(.down), topTrailing: cornerRadius ), style: .continuous ) .foregroundStyle( LinearGradient( colors: [ Color.white, Color.white.opacity(0.24) ], startPoint: .top, endPoint: .bottom ) ) .blendMode(.plusLighter) .opacity(0.24) } ) .overlay( // Inner light stroke RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .strokeBorder( LinearGradient( colors: [ Color.white, Color.white.opacity(0.2), Color.white.opacity(0.24) ], startPoint: .top, endPoint: .bottom ), lineWidth: strokeLineWidth ) .blendMode(.plusLighter) .opacity(0.3) ) .overlay( // Outer shadow stroke RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .inset(by: -strokeLineWidth) .strokeBorder( LinearGradient( colors: [ Color.black.opacity(0.8), Color.black ], startPoint: .top, endPoint: .bottom ), lineWidth: strokeLineWidth ) .opacity(0.34) ) ) .shadow( color: .black.opacity(0.2), radius: shadowRadius, x: 0, y: shadowVerticalOffset ) .padding(.leading, 2) .environment(\.buttonRole, configuration.role) } } // MARK: - Custom Layout private struct ReflectionContainer: Layout { func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout () ) -> CGSize { let safeProposal = proposal.replacingUnspecifiedDimensions() return CGSize(width: safeProposal.width, height: safeProposal.height) } func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () ) { subviews.first!.place( at: CGPoint(x: bounds.minX, y: bounds.minY), proposal: .init( width: proposal.width ?? 0, height: (proposal.height ?? 0) / 1.85 ) ) } } // MARK: - Custom Label Style private struct MobileMeLabelStyle: LabelStyle { // MARK: Environment @Environment(\.buttonRole) private var role // MARK: Metrics @ScaledMetric private var shadowRadius = 2 @ScaledMetric private var shadowVerticalOffset = 1 func makeBody(configuration: Configuration) -> some View { HStack { configuration.icon configuration.title } .font(.callout.weight(.medium)) .foregroundStyle( role == .destructive ? AnyShapeStyle(Color.red.gradient) : AnyShapeStyle(Color.white.gradient.opacity(0.9)) ) .shadow( color: .black.opacity(role == .destructive ? 0.3 : 0.6), radius: shadowRadius, x: 0, y: shadowVerticalOffset ) } } // MARK: - Quality Of Life extension ButtonStyle where Self == MobileMeButtonStyle { static var mobileMe: Self { MobileMeButtonStyle() } } extension LabelStyle where Self == MobileMeLabelStyle { static var mobileMe: Self { MobileMeLabelStyle() } } // MARK: - Environment Extensions private enum ButtonRoleEnvironmentKey: EnvironmentKey { static let defaultValue: ButtonRole? = nil } extension EnvironmentValues { var buttonRole: ButtonRole? { get { self[ButtonRoleEnvironmentKey.self] } set { self[ButtonRoleEnvironmentKey.self] = newValue } } } // MARK: - Preview #Preview { HStack { Button(action: {}) { Label( title: { Text("Today") }, icon: { EmptyView() } ) } Button(role: .destructive, action: {}) { Label( title: { EmptyView() }, icon: { Image(systemName: "trash.fill") } ) } } .padding(.horizontal, 16) .padding(.vertical, 16) .background( RoundedRectangle(cornerRadius: 30, style: .continuous) .foregroundStyle( LinearGradient( colors: [ Color(red: 72/255, green: 77/255, blue: 81/255), Color(red: 46/255, green: 48/255, blue: 54/255) ], startPoint: .top, endPoint: .bottom ) ) ) .buttonStyle(.mobileMe) }