Skip to content

Instantly share code, notes, and snippets.

@juliensagot
Created October 13, 2023 16:55
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save juliensagot/f430288b74a5b3387ab1a38c3303c250 to your computer and use it in GitHub Desktop.
Save juliensagot/f430288b74a5b3387ab1a38c3303c250 to your computer and use it in GitHub Desktop.
MobileMe button style in SwiftUI as seen here → https://twitter.com/Barbapapapps/status/1712785604906275090
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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment