I want to make an Image 100% transparent through a small rectangle and 50% transparent from all others. As if making a small hole to see-through the small rectangle. Here is my code...
struct ImageScope: View {
var body: some View {
ZStack {
Image("test_pic")
Rectangle()
.foregroundColor(Color.black.opacity(0.5))
Rectangle()
.frame(width: 200, height: 150)
.foregroundColor(Color.orange.opacity(0.0))
.overlay(RoundedRectangle(cornerRadius: 3).stroke(Color.white, lineWidth: 3))
}
}
}
For easier understanding...
Here is working approach. It is used custom shape and even-odd fill style.
Tested with Xcode 11.4 / iOS 13.4
Below demo with more transparency contrast for better visibility.
struct Window: Shape {
let size: CGSize
func path(in rect: CGRect) -> Path {
var path = Rectangle().path(in: rect)
let origin = CGPoint(x: rect.midX - size.width / 2, y: rect.midY - size.height / 2)
path.addRect(CGRect(origin: origin, size: size))
return path
}
}
struct ImageScope: View {
var body: some View {
ZStack {
Image("test_pic")
Rectangle()
.foregroundColor(Color.black.opacity(0.5))
.mask(Window(size: CGSize(width: 200, height: 150)).fill(style: FillStyle(eoFill: true)))
RoundedRectangle(cornerRadius: 3).stroke(Color.white, lineWidth: 3)
.frame(width: 200, height: 150)
}
}
}
Using blendMode(.destinationOut) you don't have to draw custom shape and it is only one line of code. Sometimes adding .compositingGroup() modifier is neccessary.
ZStack {
Color.black.opacity(0.5)
Rectangle()
.frame(width: 200, height: 200)
.blendMode(.destinationOut) // << here
}
.compositingGroup()
For clarity, this solution is based on eja08's answer but fully flushed out using an image. The blend mode .destinationOut creates the cutout in the black rectangle. The nested ZStack places the image in the background without being impacted by the blend mode.
struct ImageScope: View {
var body: some View {
ZStack {
Image("test_pic")
ZStack {
Rectangle()
.foregroundColor(.black.opacity(0.5))
Rectangle()
.frame(width: 200, height: 150)
.blendMode(.destinationOut)
.overlay(RoundedRectangle(cornerRadius: 3).stroke(.white, lineWidth: 3))
}
.compositingGroup()
}
}
}
The result:
Related
Spending a few days playing around with different modifications and stacks I got stuck in achieve transparent part of the View and text. Goal is made views from code example with .purple color is transparent and they should be background gradient color like in expected result.
Code example:
struct CircleView: View {
var body: some View {
ZStack {
LinearGradient(colors: [.mint, .cyan], startPoint: .topLeading, endPoint: .bottomTrailing)
HStack(spacing: -20) {
ZStack() {
Circle()
.frame(width: 120, height: 120)
.foregroundColor(.white)
Text("🥑")
.font(.system(size: 60))
}
ZStack() {
Circle()
.strokeBorder(.purple, lineWidth: 5)
.background(Circle().fill(.white))
.frame(width: 75, height: 75)
VStack() {
Text("108")
Text("Days")
}
.foregroundColor(.purple)
}
}
}
}
}
Expected result:
I personally would do this with the following extension:
public extension View {
#inlinable
func reverseMask<Mask: View>(
alignment: Alignment = .center,
#ViewBuilder _ mask: () -> Mask
) -> some View {
self.mask {
Rectangle()
.overlay(alignment: alignment) {
mask()
.blendMode(.destinationOut)
}
}
}
}
You can use this on any view to subtract from it. In your example the right circle would look like this:
Circle()
.fill(Color.white)
.frame(width: 75, height: 75)
.reverseMask {
VStack() {
Text("108")
Text("Days")
}
}
Cameron has a great answer and it should be accepted. Based on it here's how you can also make a "transparent" circle around the small circle
struct CircleView: View {
var body: some View {
let bigCircleSize: CGFloat = 120
let smallCircleSize: CGFloat = 75
let borderWidth: CGFloat = 5
let maskCircleSize = smallCircleSize + borderWidth
let spacing: CGFloat = -20
ZStack {
LinearGradient(colors: [.mint, .cyan], startPoint: .topLeading, endPoint: .bottomTrailing)
HStack(spacing: spacing) {
ZStack {
Circle()
.frame(width: bigCircleSize, height: bigCircleSize)
.foregroundColor(.white)
Text("🥑")
.font(.system(size: 60))
}
.reverseMask {
Circle()
.frame(width: maskCircleSize, height: maskCircleSize)
.offset(x: bigCircleSize / 2 + spacing + smallCircleSize / 2)
}
ZStack {
Circle()
.foregroundColor(.white)
.background(Circle().fill(.white))
.frame(width: smallCircleSize, height: smallCircleSize)
.reverseMask {
VStack() {
Text("108")
Text("Days")
}
}
}
}
}
}
}
public extension View {
#inlinable
func reverseMask<Mask: View>(
alignment: Alignment = .center,
#ViewBuilder _ mask: () -> Mask
) -> some View {
self.mask {
Rectangle()
.overlay(alignment: alignment) {
mask()
.blendMode(.destinationOut)
}
}
}
}
The idea is to make another mask on the big circle. Calculate the x-offset and you're good to go
I try to draw a squared Image inside of a Circle to get something like that (without blue square here. It is just to show image squared border):
This code makes squared image over all circle.
ZStack() {
Circle()
.fill(.orange)
Image(systemName: "trash")
.resizable()
.foregroundColor(.blue)
.background(.orange)
}
This code makes a small image on the circle or no circle at all:
ZStack() {
Circle()
.fill(.orange)
Image(systemName: "trash")
.foregroundColor(.blue)
.background(.orange)
}
I try to find a solution to have squared image inside the circle. It should not goes outside of it.
Maybe, I would also need a small margin between image and circle's border.
Is there a way to do that easily? Or I have to use math to get circle border or something like that?
Asperi just beat me to it, but this view will just take an icon name and a radius and return a squared icon perfectly in a circle:
struct ImageOnCircle: View {
let icon: String
let radius: CGFloat
var squareSide: CGFloat {
2.0.squareRoot() * radius
}
var body: some View {
ZStack {
Circle()
.fill(.orange)
.frame(width: radius * 2, height: radius * 2)
Image(systemName: icon)
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.frame(width: squareSide, height: squareSide)
.foregroundColor(.blue)
}
}
}
Use:
ImageOnCircle(icon: "trash", radius: 150)
Here is a demo of possible approach - use overlay + geometry reader to calculate internal rectangle where image is injected.
Tested with Xcode 13 / iOS 15 (blue rect is of Preview one for selected image)
Circle()
.fill(.orange)
.overlay(GeometryReader {
let side = sqrt($0.size.width * $0.size.width / 2)
VStack {
Rectangle().foregroundColor(.clear)
.frame(width: side, height: side)
.overlay(
Image(systemName: "trash")
.resizable()
.foregroundColor(.blue)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(width: 100, height: 100)
I'm currently running into a problem of "harsh" edges when I try to create a rounded rectangle using a gradient.
In this gradient you can see around the corners it gets especially dark, and I'm not sure how to fix this.
I'm guessing it's stemming from using an extension to create the gradient so that I can use it as the foreground color instead of the background, but I'm not sure what I would use instead to fix this problem.
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 20)
.gradientForeground(colors: [Color("MainColor1"), Color("MainColor2")])
.frame(width: geometry.size.width * 0.9, height: 100, alignment: .center)
.padding(.leading, geometry.size.width * 0.05)
}
}
}
extension View {
public func gradientForeground(colors: [Color]) -> some View {
self.overlay(LinearGradient(gradient: .init(colors: colors),
startPoint: .topLeading,
endPoint: .bottomTrailing))
.mask(self)
}
}
Since you are working on Shape because you used RoundedRectangle, the right and correct way of coloring is fill, your issue is not connected to View to make an extension but it is about Shape.
extension Shape {
public func gradientForeground(colors: [Color]) -> some View {
self.fill(LinearGradient(gradient: .init(colors: colors), startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
Use case:
struct ContentView: View {
var body: some View {
RoundedRectangle(cornerRadius: 50)
.gradientForeground(colors: [Color.red, Color.yellow])
.frame(width: 300, height: 300, alignment: Alignment.center)
}
}
I wanted to try this down code to see if SwiftUI understand between RoundedRectangle and Rectangle, therefore I ran this code, SwiftUI could tell me the difference between Screen Background tap and Rectangle tap, but it is unable to understand between RoundedRectangle itself and Background of RoundedRectangle which is a Rectangle.
How could I solve this issue?
Goal: I want get print for white area on tap, as well I am getting for red and yellow area.
struct ContentView: View {
var body: some View {
ZStack {
Color.red
.ignoresSafeArea()
.onTapGesture{ print("you tapped on Screen Background! 🟥") }
RoundedRectangle(cornerRadius: 90)
.fill(Color.yellow)
.frame(width: 300, height: 300, alignment: .center)
.background(Color.white.onTapGesture{ print("you tapped on RoundedRectangle Background! ⬜️") })
.onTapGesture{ print("you tapped on RoundedRectangle! 🟨") }
}
}
}
This looks like a bug indeed.
As a workaround you can use a custom shape, e.g. RoundedCorner taken from here:
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
Then, it will work as expected:
struct ContentView: View {
var body: some View {
ZStack {
Color.red
.ignoresSafeArea()
.onTapGesture { print("you tapped on Screen Background! 🟥") }
Color.white
.frame(width: 300, height: 300, alignment: .center)
.onTapGesture { print("you tapped on RoundedRectangle Background! ⬜️") }
RoundedCorner(radius: 90)
.fill(Color.yellow)
.frame(width: 300, height: 300, alignment: .center)
.onTapGesture { print("you tapped on RoundedRectangle! 🟨") }
}
}
}
or, with Color.white as background:
struct ContentView: View {
var body: some View {
ZStack {
Color.red
.ignoresSafeArea()
.onTapGesture { print("you tapped on Screen Background! 🟥") }
RoundedCorner(radius: 90)
.fill(Color.yellow)
.frame(width: 300, height: 300, alignment: .center)
.onTapGesture { print("you tapped on RoundedRectangle! 🟨") }
.background(
Color.white
.onTapGesture { print("you tapped on RoundedRectangle Background! ⬜️") }
)
}
}
}
For a rating display I am trying to split an Image up into two parts, a black and a red one and I would like the red part to take up a specific percentage of the whole image. The problem I am having is that the Rectangle is aligned to the centre of the other image and when changing the alignment of the ZStack to .leading, the Rectangle does move but unfortunately the masked area of the image does not change.
ZStack {
Image("Car")
Rectangle()
.foregroundColor(ColorManager.brand)
.frame(width: 20.0, height: 20.0)
.mask(Image("Car"))
}
Without alignment
ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)) {
Image("Car")
Rectangle()
.foregroundColor(ColorManager.brand)
.frame(width: 20.0, height: 20.0)
.mask(Image("Car"))
}
With alignment
How could I change the alignment of the Rectangle to .leading, while also masking the leading part of the image?
edit: Desired effect
You need just make clipping at needed location. Here is a complete demo of possible approach (on custom CarView as I don't have car image like your).
Prepared with Xcode 12.1 / iOS 14.1
struct CarView: View {
var body: some View {
Image(systemName: "car").resizable()
.frame(width: 200, height: 80)
}
}
struct MaskShape: Shape {
var alignment: HorizontalAlignment = .center
func path(in rect: CGRect) -> Path {
switch alignment {
case .leading:
return Rectangle().path(in: rect.divided(atDistance: rect.width / 3, from: .minXEdge).slice)
case .center:
return Rectangle().path(in: rect.insetBy(dx: rect.width / 3, dy: 0))
case .trailing:
return Rectangle().path(in: rect.divided(atDistance: rect.width / 3, from: .maxXEdge).slice)
default:
return Rectangle().path(in: rect)
}
}
}
struct TestCarMaskView: View {
var body: some View {
VStack {
CarView()
.overlay(
Rectangle()
.foregroundColor(.red)
.mask(CarView())
.clipShape(MaskShape())
)
CarView()
.overlay(
Rectangle()
.foregroundColor(.red)
.mask(CarView())
.clipShape(MaskShape(alignment: .leading))
)
CarView()
.overlay(
Rectangle()
.foregroundColor(.red)
.mask(CarView())
.clipShape(MaskShape(alignment: .trailing))
)
}
}
}
so your code could look like (of course sizes of mask you can tune for your needs)
Image("Car")
.overlay(
Rectangle()
.foregroundColor(ColorManager.brand)
.mask(Image("Car"))
.clipShape(MaskShape(alignment: .leading))
)