560 lines
25 KiB
Swift
560 lines
25 KiB
Swift
import SwiftUI
|
||
import UIKit
|
||
|
||
// MARK: - TestView
|
||
/// 高质感试验页面:暖色景深背景 + 纸质相片卡片 + 滑块与胶囊按钮
|
||
struct TestView: View {
|
||
@State private var slider: Double = 0.6
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
// 背景:暖色渐变 + 散景光斑
|
||
BackgroundBokeh()
|
||
.ignoresSafeArea()
|
||
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(spacing: 20) {
|
||
// 顶部标题区域(手写风格)
|
||
VStack(spacing: 6) {
|
||
Text("MemoryWake")
|
||
.font(Typography.font(for: .headline, family: .quicksandBold, size: 30))
|
||
.kerning(1.2)
|
||
.foregroundStyle(Color.black.opacity(0.85))
|
||
.shadow(color: .black.opacity(0.06), radius: 6, x: 0, y: 3)
|
||
}
|
||
.padding(.top, 26)
|
||
|
||
// 纸质相片卡片
|
||
PaperPhotoCard()
|
||
.padding(.horizontal, 24)
|
||
.padding(.top, 6)
|
||
|
||
// 年份滑块(带毛玻璃底)
|
||
// VStack(spacing: 12) {
|
||
// ZStack {
|
||
// RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||
// .fill(.ultraThinMaterial)
|
||
// .overlay { RoundedRectangle(cornerRadius: 16).stroke(Color.white.opacity(0.35), lineWidth: 0.5) }
|
||
// .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 6)
|
||
|
||
// VStack(spacing: 8) {
|
||
// HStack {
|
||
// Text("2014")
|
||
// .font(Typography.font(for: .caption))
|
||
// .foregroundStyle(Color.black.opacity(0.55))
|
||
// Spacer()
|
||
// Text("2024")
|
||
// .font(Typography.font(for: .caption))
|
||
// .foregroundStyle(Color.black.opacity(0.55))
|
||
// }
|
||
// .padding(.horizontal, 14)
|
||
|
||
// Slider(value: $slider, in: 0...1)
|
||
// .tint(Theme.Colors.primary)
|
||
// .padding(.horizontal, 14)
|
||
// .padding(.bottom, 8)
|
||
// }
|
||
// .padding(.vertical, 8)
|
||
// }
|
||
// .frame(height: 76)
|
||
// .padding(.horizontal, 24)
|
||
|
||
// // 胶囊按钮
|
||
// Button {
|
||
// // Action placeholder
|
||
// } label: {
|
||
// Text("Oc cinor rnand")
|
||
// .font(Typography.font(for: .body, family: .quicksandBold))
|
||
// .foregroundStyle(Color.white)
|
||
// .padding(.vertical, 14)
|
||
// .frame(maxWidth: .infinity)
|
||
// .background(
|
||
// Capsule(style: .continuous)
|
||
// .fill(LinearGradient(colors: [Theme.Colors.primaryDark, Theme.Colors.primary], startPoint: .leading, endPoint: .trailing))
|
||
// )
|
||
// .overlay { Capsule().stroke(Color.white.opacity(0.3), lineWidth: 0.6) }
|
||
// .shadow(color: Theme.Shadows.medium, radius: 10, x: 0, y: 6)
|
||
// }
|
||
// .padding(.horizontal, 48)
|
||
// .padding(.bottom, 24)
|
||
// }
|
||
}
|
||
.padding(.bottom, 40)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 背景散景
|
||
private struct BackgroundBokeh: View {
|
||
var body: some View {
|
||
ZStack {
|
||
Theme.Gradients.backgroundGradient
|
||
.ignoresSafeArea()
|
||
.overlay(Color.white.opacity(0.35).blendMode(.softLight))
|
||
|
||
// 大散景圆(Canvas 生成,带模糊 & 叠加)
|
||
Canvas { context, size in
|
||
func circle(_ center: CGPoint, _ radius: CGFloat, _ color: Color) {
|
||
let rect = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
|
||
context.fill(Path(ellipseIn: rect), with: .color(color.opacity(0.45)))
|
||
}
|
||
circle(CGPoint(x: size.width * 0.2, y: size.height * 0.2), 90, .white)
|
||
circle(CGPoint(x: size.width * 0.85, y: size.height * 0.18), 70, .white)
|
||
circle(CGPoint(x: size.width * 0.75, y: size.height * 0.65), 110, .white)
|
||
circle(CGPoint(x: size.width * 0.15, y: size.height * 0.75), 80, .white)
|
||
}
|
||
.blur(radius: 22)
|
||
.blendMode(.screen)
|
||
.opacity(0.8)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 纸质相片卡片
|
||
private struct PaperPhotoCard: View {
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
ZStack {
|
||
// 纸质底(多层次质感 + 不规则撕边遮罩)
|
||
let radius: CGFloat = 14
|
||
let deckle = DeckleShape(cornerRadius: radius, amplitude: 2.0, frequency: 48, seed: 1337)
|
||
deckle
|
||
.fill(
|
||
LinearGradient(colors: [Color.white, Color(hex: "F5EEE4")], startPoint: .top, endPoint: .bottom)
|
||
)
|
||
// 轻微纸张噪点
|
||
.overlay {
|
||
NoiseOverlay(opacity: 0.09)
|
||
.clipShape(deckle)
|
||
}
|
||
// 边缘高光(上左)
|
||
.overlay {
|
||
deckle
|
||
.stroke(LinearGradient(colors: [Color.white.opacity(0.65), .clear], startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 1)
|
||
}
|
||
// 边缘阴影(下右)
|
||
.overlay {
|
||
deckle
|
||
.stroke(LinearGradient(colors: [.clear, Color.black.opacity(0.12)], startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 1)
|
||
}
|
||
// 纤维毛边/撕边的错乱感(叠加一些短纤维线条)
|
||
.overlay { EdgeFibers().clipShape(deckle) }
|
||
// 更明显的毛边凹凸(微小颗粒沿轮廓散布)
|
||
.overlay { DeckleOverlay(cornerRadius: radius, amount: 1.6, amplitude: 2.0, frequency: 48, seed: 1337) }
|
||
// 内阴影,形成纸张厚度
|
||
.modifier(InnerShadow(cornerRadius: radius, shadow: .black.opacity(0.15), radius: 10, x: 0, y: 4))
|
||
// 外投影与微浮雕(参考图:光源偏右上)
|
||
.shadow(color: .black.opacity(0.16), radius: 12, x: 0, y: 10) // 基础下方投影(范围更窄)
|
||
// 加重左下与右下两侧的阴影(左下略重,右下略轻)
|
||
.shadow(color: .black.opacity(0.22), radius: 8, x: -4, y: 7) // 左下更重(范围更窄)
|
||
.shadow(color: .black.opacity(0.18), radius: 8, x: 3, y: 8) // 右下略轻(范围更窄)
|
||
// 左侧轻微遮蔽(环境遮挡)
|
||
.shadow(color: .black.opacity(0.12), radius: 2, x: -2, y: 2)
|
||
.shadow(color: .white.opacity(0.7), radius: 1, x: -1, y: -1)
|
||
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
// 相片
|
||
ImageAtPath(path: "/Users/fairclip/Documents/Projects/wake-ios/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/screenshot-20250821-154302.png")
|
||
.clipShape(DeckleShape(cornerRadius: 8, amplitude: 0.8, frequency: 32, seed: 1338))
|
||
.overlay { DeckleShape(cornerRadius: 8, amplitude: 0.8, frequency: 32, seed: 1338).stroke(Color.black.opacity(0.12), lineWidth: 0.6) }
|
||
.shadow(color: .black.opacity(0.12), radius: 8, x: 0, y: 4)
|
||
.padding(.top, 14)
|
||
.clipped()
|
||
|
||
HStack(alignment: .bottom) {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("You uploaded your portrait, and chose to remember the moments you almost let slip away.")
|
||
.font(Typography.font(for: .title, family: .lavishlyYours))
|
||
.foregroundStyle(Color.black.opacity(0.45))
|
||
}
|
||
}
|
||
.padding(.horizontal, 14)
|
||
.padding(.bottom, 26)
|
||
}
|
||
.padding(.horizontal, 12)
|
||
|
||
|
||
// 底部轻微缺口(撕口提示)
|
||
// VStack { Spacer(minLength: 0)
|
||
// Capsule(style: .continuous)
|
||
// .fill(Color.black.opacity(0.08))
|
||
// .frame(width: 36, height: 8)
|
||
// .blur(radius: 0.2)
|
||
// .offset(y: 10)
|
||
// }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 相片内容(木箱 + 景深效果,用渐变和阴影模拟)
|
||
private struct ImageCard: View {
|
||
var body: some View {
|
||
ZStack {
|
||
// 地面与远景
|
||
LinearGradient(colors: [Color(hex: "E6E0D8"), Color(hex: "C9BBAA")], startPoint: .top, endPoint: .bottom)
|
||
.overlay(
|
||
// 右上角光束
|
||
RadialGradient(colors: [Color.white.opacity(0.75), .clear], center: .topTrailing, startRadius: 4, endRadius: 220)
|
||
.blendMode(.screen)
|
||
)
|
||
|
||
// 木箱(简化几何)
|
||
VStack(spacing: 0) {
|
||
RoundedRectangle(cornerRadius: 3).fill(Color(hex: "7B5134"))
|
||
.frame(height: 54)
|
||
.overlay {
|
||
LinearGradient(colors: [Color(hex: "9C6A46"), Color(hex: "5E3D28")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||
}
|
||
.shadow(color: .black.opacity(0.25), radius: 10, x: 0, y: 8)
|
||
RoundedRectangle(cornerRadius: 3).fill(Color(hex: "6A462F"))
|
||
.frame(height: 20)
|
||
}
|
||
.padding(.horizontal, 22)
|
||
.padding(.vertical, 18)
|
||
|
||
// 小单车(符号占位)
|
||
Image(systemName: "bicycle")
|
||
.resizable()
|
||
.scaledToFit()
|
||
.frame(width: 36, height: 36)
|
||
.foregroundStyle(Color.white)
|
||
.shadow(color: .black.opacity(0.35), radius: 6, x: 0, y: 4)
|
||
}
|
||
.frame(height: 400)
|
||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||
}
|
||
}
|
||
|
||
// MARK: - 噪点叠加(轻微纸张颗粒)
|
||
private struct NoiseOverlay: View {
|
||
let opacity: CGFloat
|
||
var body: some View {
|
||
Canvas { context, size in
|
||
let rect = CGRect(origin: .zero, size: size)
|
||
// 绘制随机小点
|
||
let count = Int(size.width * size.height / 800)
|
||
for _ in 0..<count {
|
||
let x = CGFloat.random(in: 0...size.width)
|
||
let y = CGFloat.random(in: 0...size.height)
|
||
let r = CGFloat.random(in: 0.3...0.8)
|
||
let alpha = Double.random(in: 0.15...0.55)
|
||
let color = Color.black.opacity(alpha)
|
||
context.fill(Path(ellipseIn: CGRect(x: x, y: y, width: r, height: r)), with: .color(color))
|
||
}
|
||
// 轻微暗角
|
||
context.stroke(Path(rect), with: .color(.black.opacity(0.08)), lineWidth: 0.5)
|
||
}
|
||
.opacity(opacity)
|
||
.blendMode(.multiply)
|
||
}
|
||
}
|
||
|
||
// MARK: - 边缘纤维与毛边质感
|
||
private struct EdgeFibers: View {
|
||
var body: some View {
|
||
GeometryReader { geo in
|
||
let w = geo.size.width
|
||
let h = geo.size.height
|
||
Canvas { context, _ in
|
||
// 在四周绘制短纤维线条与点,颜色极淡,模拟纸纤维
|
||
let fibers = Int((w + h) * 1.6) // 再提升密度
|
||
for i in 0..<fibers {
|
||
let side = i % 4
|
||
let len: CGFloat = .random(in: 5...14)
|
||
let offset: CGFloat = .random(in: 0...((side % 2 == 0) ? w : h))
|
||
let inset: CGFloat = .random(in: 0...10)
|
||
var p = Path()
|
||
var start = CGPoint.zero
|
||
var end = CGPoint.zero
|
||
switch side {
|
||
case 0: // top
|
||
start = CGPoint(x: offset, y: inset)
|
||
end = CGPoint(x: offset + .random(in: -1...1), y: inset + len)
|
||
case 1: // right
|
||
start = CGPoint(x: w - inset, y: offset)
|
||
end = CGPoint(x: w - inset - len, y: offset + .random(in: -1...1))
|
||
case 2: // bottom
|
||
start = CGPoint(x: offset, y: h - inset)
|
||
end = CGPoint(x: offset + .random(in: -1...1), y: h - inset - len)
|
||
default: // left
|
||
start = CGPoint(x: inset, y: offset)
|
||
end = CGPoint(x: inset + len, y: offset + .random(in: -1...1))
|
||
}
|
||
p.move(to: start)
|
||
p.addLine(to: end)
|
||
let tone = Color(hex: "A68E72").opacity(Double.random(in: 0.08...0.18))
|
||
context.stroke(p, with: .color(tone), lineWidth: CGFloat.random(in: 0.4...1.2))
|
||
}
|
||
// 边缘不均匀阴影带
|
||
let insetBand: CGFloat = 2
|
||
let rr = RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||
let outer = Path(rr.path(in: CGRect(x: 0, y: 0, width: w, height: h)).cgPath)
|
||
let inner = Path(rr.path(in: CGRect(x: insetBand, y: insetBand, width: w - insetBand*2, height: h - insetBand*2)).cgPath)
|
||
var ring = outer
|
||
ring.addPath(inner)
|
||
context.fill(ring, with: .color(Color.black.opacity(0.04)))
|
||
}
|
||
.blendMode(.multiply)
|
||
.opacity(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 不规则撕边形状(带种子)
|
||
private struct DeckleShape: Shape {
|
||
var cornerRadius: CGFloat
|
||
var amplitude: CGFloat // 抖动幅度(像素)
|
||
var frequency: Int // 每条边的采样点数
|
||
var seed: UInt64 = 0
|
||
|
||
func path(in rect: CGRect) -> Path {
|
||
let r = min(cornerRadius, min(rect.width, rect.height) / 2)
|
||
let steps = max(8, frequency)
|
||
var points: [CGPoint] = []
|
||
|
||
// 简单线性同余随机(可复现)
|
||
var state = seed == 0 ? 0x9E3779B97F4A7C15 : seed
|
||
func rand() -> CGFloat {
|
||
state &+= 0xBF58476D1CE4E5B9
|
||
var z = state
|
||
z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9
|
||
z = (z ^ (z >> 27)) &* 0x94D049BB133111EB
|
||
let v = Double((z ^ (z >> 31)) & 0x1FFFFFFFFFFFFF) / Double(0x1FFFFFFFFFFFFF)
|
||
return CGFloat(v)
|
||
}
|
||
func jitter(_ normal: CGPoint) -> CGPoint {
|
||
let a = (rand() * 2 - 1) * amplitude
|
||
return CGPoint(x: normal.x * a, y: normal.y * a)
|
||
}
|
||
|
||
// 边与角的基础点生成
|
||
let left = rect.minX
|
||
let right = rect.maxX
|
||
let top = rect.minY
|
||
let bottom = rect.maxY
|
||
|
||
// 四条边: top -> right -> bottom -> left
|
||
// 直边部分(去掉角落 r)
|
||
// Top straight
|
||
if rect.width > 2*r {
|
||
for i in 0...steps {
|
||
let t = CGFloat(i) / CGFloat(steps)
|
||
let x = left + r + t * (rect.width - 2*r)
|
||
let base = CGPoint(x: x, y: top)
|
||
let n = CGPoint(x: 0, y: -1)
|
||
points.append(base + jitter(n))
|
||
}
|
||
}
|
||
// Top-right corner arc
|
||
let arcSteps = max(6, steps/3)
|
||
for i in 0...arcSteps {
|
||
let ang = CGFloat.pi * 1.5 + CGFloat(i) / CGFloat(arcSteps) * (.pi/2)
|
||
let center = CGPoint(x: right - r, y: top + r)
|
||
let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r)
|
||
let normal = CGPoint(x: cos(ang), y: sin(ang))
|
||
points.append(base + jitter(normal))
|
||
}
|
||
// Right straight
|
||
if rect.height > 2*r {
|
||
for i in 0...steps {
|
||
let t = CGFloat(i) / CGFloat(steps)
|
||
let y = top + r + t * (rect.height - 2*r)
|
||
let base = CGPoint(x: right, y: y)
|
||
let n = CGPoint(x: 1, y: 0)
|
||
points.append(base + jitter(n))
|
||
}
|
||
}
|
||
// Bottom-right arc
|
||
for i in 0...arcSteps {
|
||
let ang = 0 + CGFloat(i) / CGFloat(arcSteps) * (.pi/2)
|
||
let center = CGPoint(x: right - r, y: bottom - r)
|
||
let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r)
|
||
let normal = CGPoint(x: cos(ang), y: sin(ang))
|
||
points.append(base + jitter(normal))
|
||
}
|
||
// Bottom straight
|
||
if rect.width > 2*r {
|
||
for i in 0...steps {
|
||
let t = CGFloat(i) / CGFloat(steps)
|
||
let x = right - r - t * (rect.width - 2*r)
|
||
let base = CGPoint(x: x, y: bottom)
|
||
let n = CGPoint(x: 0, y: 1)
|
||
points.append(base + jitter(n))
|
||
}
|
||
}
|
||
// Bottom-left arc
|
||
for i in 0...arcSteps {
|
||
let ang = .pi/2 + CGFloat(i) / CGFloat(arcSteps) * (.pi/2)
|
||
let center = CGPoint(x: left + r, y: bottom - r)
|
||
let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r)
|
||
let normal = CGPoint(x: cos(ang), y: sin(ang))
|
||
points.append(base + jitter(normal))
|
||
}
|
||
// Left straight
|
||
if rect.height > 2*r {
|
||
for i in 0...steps {
|
||
let t = CGFloat(i) / CGFloat(steps)
|
||
let y = bottom - r - t * (rect.height - 2*r)
|
||
let base = CGPoint(x: left, y: y)
|
||
let n = CGPoint(x: -1, y: 0)
|
||
points.append(base + jitter(n))
|
||
}
|
||
}
|
||
// Top-left arc
|
||
for i in 0...arcSteps {
|
||
let ang = .pi + CGFloat(i) / CGFloat(arcSteps) * (.pi/2)
|
||
let center = CGPoint(x: left + r, y: top + r)
|
||
let base = CGPoint(x: center.x + cos(ang) * r, y: center.y + sin(ang) * r)
|
||
let normal = CGPoint(x: cos(ang), y: sin(ang))
|
||
points.append(base + jitter(normal))
|
||
}
|
||
|
||
// 连接为路径
|
||
var p = Path()
|
||
guard let first = points.first else { return p }
|
||
p.move(to: first)
|
||
for pt in points.dropFirst() { p.addLine(to: pt) }
|
||
p.closeSubpath()
|
||
return p
|
||
}
|
||
}
|
||
|
||
// CGPoint 运算便捷
|
||
private extension CGPoint {
|
||
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) }
|
||
}
|
||
|
||
// MARK: - 毛边凹凸/颗粒覆盖
|
||
private struct DeckleOverlay: View {
|
||
var cornerRadius: CGFloat
|
||
var amount: CGFloat = 1.0 // 0.5~2.0 建议范围
|
||
var amplitude: CGFloat = 2.0
|
||
var frequency: Int = 48
|
||
var seed: UInt64 = 0
|
||
var body: some View {
|
||
GeometryReader { geo in
|
||
let w = geo.size.width
|
||
let h = geo.size.height
|
||
Canvas { context, _ in
|
||
// 沿周边撒布很多小圆点和短椭圆,部分向外偏移、部分向内侵蚀
|
||
let perimeter = 2 * (w + h)
|
||
let count = Int(CGFloat(perimeter) / 4 * max(0.4, amount))
|
||
for i in 0..<count {
|
||
let side = i % 4
|
||
let offset = CGFloat.random(in: 0...((side % 2 == 0) ? w : h))
|
||
let jitter: CGFloat = .random(in: -2...2)
|
||
let dist: CGFloat = .random(in: -1.8...1.8) // 内外轻微偏移
|
||
var center = CGPoint.zero
|
||
switch side {
|
||
case 0: center = CGPoint(x: offset, y: 0 + jitter + dist) // top
|
||
case 1: center = CGPoint(x: w + jitter - dist, y: offset) // right
|
||
case 2: center = CGPoint(x: offset, y: h + jitter - dist) // bottom
|
||
default: center = CGPoint(x: 0 + jitter + dist, y: offset) // left
|
||
}
|
||
let rw = CGFloat.random(in: 0.6...1.6)
|
||
let rh = CGFloat.random(in: 0.6...1.6)
|
||
let rect = CGRect(x: center.x - rw/2, y: center.y - rh/2, width: rw, height: rh)
|
||
let alpha = Double.random(in: 0.12...0.35)
|
||
// 同时绘制一深一浅两层,增强凹凸感
|
||
context.fill(Path(ellipseIn: rect), with: .color(Color.black.opacity(alpha * 0.6)))
|
||
context.fill(Path(ellipseIn: rect.insetBy(dx: -0.4, dy: -0.4)), with: .color(Color.white.opacity(alpha * 0.35)))
|
||
}
|
||
}
|
||
// 两种混合让亮/暗颗粒与纸面融合
|
||
.blendMode(.overlay)
|
||
.overlay(
|
||
Canvas { context, _ in
|
||
// 轻微随机噪点再叠一次(只沿边缘)
|
||
let steps = 220
|
||
for i in 0..<steps {
|
||
let t = CGFloat(i) / CGFloat(steps)
|
||
// 取一圈 roundedRect 的近似点:用四条边线性插值即可
|
||
var pt = CGPoint.zero
|
||
let per = t * 4
|
||
let edge = Int(per)
|
||
let u = per - CGFloat(edge)
|
||
switch edge {
|
||
case 0: pt = CGPoint(x: u * w, y: 0)
|
||
case 1: pt = CGPoint(x: w, y: u * h)
|
||
case 2: pt = CGPoint(x: (1 - u) * w, y: h)
|
||
default: pt = CGPoint(x: 0, y: (1 - u) * h)
|
||
}
|
||
let offset = CGPoint(x: pt.x + .random(in: -2...2), y: pt.y + .random(in: -2...2))
|
||
let s = CGFloat.random(in: 0.3...1.0)
|
||
context.fill(Path(ellipseIn: CGRect(x: offset.x, y: offset.y, width: s, height: s)), with: .color(Color.black.opacity(0.08)))
|
||
}
|
||
}
|
||
.blendMode(.multiply)
|
||
)
|
||
}
|
||
.allowsHitTesting(false)
|
||
.clipShape(DeckleShape(cornerRadius: cornerRadius, amplitude: amplitude, frequency: frequency, seed: seed))
|
||
}
|
||
}
|
||
|
||
// MARK: - 内阴影辅助
|
||
private struct InnerShadow: ViewModifier {
|
||
var cornerRadius: CGFloat
|
||
var shadow: Color
|
||
var radius: CGFloat
|
||
var x: CGFloat
|
||
var y: CGFloat
|
||
func body(content: Content) -> some View {
|
||
content
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||
.stroke(Color.clear, lineWidth: 0)
|
||
.shadow(color: shadow, radius: radius, x: x, y: y)
|
||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||
.blendMode(.multiply)
|
||
)
|
||
}
|
||
}
|
||
|
||
#Preview("TestView - Light") {
|
||
TestView()
|
||
}
|
||
|
||
#Preview("TestView - Dark") {
|
||
TestView()
|
||
.preferredColorScheme(.dark)
|
||
}
|
||
|
||
// MARK: - 从绝对路径加载图片(仅用于开发/预览;真机请放入 Assets)
|
||
private struct ImageAtPath: View {
|
||
let path: String
|
||
var body: some View {
|
||
Group {
|
||
if let ui = UIImage(contentsOfFile: path) {
|
||
Image(uiImage: ui)
|
||
.resizable()
|
||
.scaledToFit()
|
||
} else {
|
||
ZStack {
|
||
Color.black.opacity(0.04)
|
||
VStack(spacing: 6) {
|
||
Image(systemName: "photo")
|
||
.font(.system(size: 28))
|
||
.foregroundStyle(Color.black.opacity(0.4))
|
||
Text("未找到图片")
|
||
.font(.caption)
|
||
.foregroundStyle(Color.black.opacity(0.5))
|
||
Text(path)
|
||
.font(.caption2)
|
||
.foregroundStyle(Color.black.opacity(0.4))
|
||
.multilineTextAlignment(.center)
|
||
.lineLimit(2)
|
||
}
|
||
.padding()
|
||
}
|
||
.clipped()
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.clipped()
|
||
}
|
||
}
|