Last active
April 27, 2025 02:34
-
-
Save Koshimizu-Takehito/a48bce95143e8ef124804bc472142f33 to your computer and use it in GitHub Desktop.
ビューに枠線を引く
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
// MARK: - Demo Screen | |
/// A sample screen demonstrating three stacked stroke effects produced by | |
/// `StrokeModifier`. | |
/// Each call to `.stroke(_:width:)` adds an additional blurred outline, | |
/// resulting in a multi-layered border. | |
struct StrokeModifierDemoScreen: View { | |
var body: some View { | |
VStack { | |
Image(systemName: "swift") | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
.frame(width: 256) | |
.padding(16) | |
.foregroundStyle( | |
.linearGradient( | |
colors: [.orange, .red], | |
startPoint: .top, | |
endPoint: .bottom | |
) | |
) | |
Text("Hello, World!!") | |
.font(.system(size: 56, weight: .bold, design: .default)) | |
.foregroundStyle(.foreground) | |
.padding(16) | |
} | |
.padding(12) | |
// Three concentric strokes (white ➜ cyan ➜ blue) | |
.stroke(.background, width: 4) | |
.stroke(.cyan, width: 4) | |
.stroke(.blue, width: 4) | |
} | |
} | |
// MARK: - Convenience API | |
extension View { | |
/// Overlays the view with an outward-blurred stroke of the specified color | |
/// and width. | |
/// | |
/// Internally this applies `StrokeModifier`, which builds the outline in a | |
/// single off-screen render pass. | |
/// | |
/// - Parameters: | |
/// - style: The `ShapeStyle` used to fill the stroke. | |
/// Defaults to solid white. | |
/// - width: The stroke’s thickness, expressed in points. | |
/// Values ≤ 0 produce no effect. | |
/// - Returns: A view with the stroke applied. | |
@ViewBuilder | |
func stroke(_ style: some ShapeStyle = .white, width: Double = 4) -> some View { | |
if let stroke = StrokeModifier(style: style, width: width) { | |
modifier(stroke) | |
} else { | |
modifier(EmptyModifier()) | |
} | |
} | |
} | |
// MARK: - Stroke Modifier | |
/// A `ViewModifier` that renders a blurred, outward-growing outline | |
/// (“stroke”) surrounding the target view. | |
/// | |
/// The effect is achieved by: | |
/// 1. Capturing the view’s alpha into a `Canvas` symbol | |
/// 2. Applying an `alphaThreshold` to make the interior fully opaque | |
/// 3. Blurring the result by the requested width, which dilates the mask | |
/// 4. Filling that mask with the supplied `ShapeStyle` | |
private struct StrokeModifier<Style: ShapeStyle>: ViewModifier { | |
/// Style used to fill the generated stroke. | |
private var style: Style | |
/// Desired thickness of the stroke, in points. | |
private var width: Double | |
init?(style: Style, width: Double) { | |
guard width > 0 else { | |
return nil | |
} | |
self.style = style | |
self.width = width | |
} | |
func body(content: Content) -> some View { | |
content.background { | |
Rectangle() | |
.foregroundStyle(style) | |
.mask(alignment: .center) { | |
mask(content: content) | |
} | |
} | |
} | |
} | |
// MARK: - Internal helpers | |
extension StrokeModifier { | |
/// Symbol identifiers passed to `Canvas`. | |
fileprivate enum SymbolID: Int { | |
case content | |
} | |
/// Builds the alpha-threshold-blur mask inside a `Canvas`. | |
/// | |
/// - Parameter content: The original view whose outline should be stroked. | |
/// - Returns: A view forming the mask. | |
@ViewBuilder | |
fileprivate func mask(content: Content) -> some View { | |
Canvas { context, size in | |
let symbol = context.resolveSymbol(id: SymbolID.content)! | |
// Keep only non-transparent pixels | |
context.addFilter(.alphaThreshold(min: 0.01)) | |
// Dilate the mask by blurring | |
context.addFilter(.blur(radius: width)) | |
context.draw(symbol, at: CGPoint(x: size.width / 2, y: size.height / 2)) | |
} symbols: { | |
content.tag(SymbolID.content) | |
} | |
} | |
} | |
// MARK: - Preview | |
#Preview { | |
StrokeModifierDemoScreen() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment