How can I clip a Shape with another Shape in SwiftUI? - swift

I want to clip the red square with the green half-circle. I tried .mask and .clipShape but
i can't get this to work. Can someone share how its done please?
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color.red)
.frame(width: 500, height: 500)
Circle()
.trim(from: 0.5, to: 1.0)
.fill(Color.green)
.rotationEffect(Angle(degrees: 90))
.frame(width: 500, height: 500)
.offset(x: -250)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}

with using eoFill like in the code:
struct ContentView: View {
var body: some View {
CustomShape()
.fill(Color.purple, style: FillStyle(eoFill: true, antialiased: true))
.frame(width: 200, height: 200, alignment: .center)
.shadow(radius: 10.0)
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: rect.height/2, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: true)
path.addRect(CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height))
}
}
}

Create a struct that conforms to Shape.
Implement func path(in rect: CGRect) -> Path
Create a Path. Add the rectangle, then the circle.
Fill the shape with isEOFilled set to true

My code is this basic one. Maybe someone finds this useful so i am attaching it here as well.
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
BoxCutOut()
.foregroundColor(Color.red)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
struct BoxCutOut: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x:0, y: 0))
path.addLine(to: CGPoint(x:rect.width, y:0))
path.addLine(to: CGPoint(x:rect.width, y:rect.height))
path.addLine(to: CGPoint(x:0, y:rect.height))
path.addArc(center: CGPoint(x: 0, y: rect.height/2), radius: rect.height/2, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true )
}
}
}

Related

Custom Menu/Tab bar in SwiftUI

I want to make tab bar like this and do not know where to start. Please help me I'm a newbie!
It navigates to different views.
Here's a pretty functional version. This could be made better to further mirror SwiftUI's TabBar interface. Let me know if you run into any issues with this.
(full res)
protocol TopTab: RawRepresentable, Identifiable, CaseIterable, Hashable where AllCases == Array<Self> {
var title: String { get }
}
struct TabBackground: Shape {
var cornerRadius: CGFloat = 8
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: .zero + cornerRadius, y: .zero))
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: .zero))
path.addQuadCurve(to: CGPoint(x: rect.width, y: .zero + cornerRadius), control: CGPoint(x: rect.width, y: .zero))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - cornerRadius))
path.addQuadCurve(to: CGPoint(x: rect.width + cornerRadius, y: rect.height), control: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: .zero - cornerRadius, y: rect.height))
path.addQuadCurve(to: CGPoint(x: .zero, y: rect.height - cornerRadius), control: CGPoint(x: .zero, y: rect.height))
path.addLine(to: CGPoint(x: .zero, y: cornerRadius))
path.addQuadCurve(to: CGPoint(x: .zero + cornerRadius, y: .zero), control: .zero)
return path
}
}
struct TopScrollingTabBarPicker<Tab: TopTab>: View {
#Binding var selection: Tab
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(Tab.allCases) { tab in
Group {
if tab == selection {
Text(tab.title)
.bold()
.foregroundColor(.accentColor)
.padding(12)
.background {
TabBackground(cornerRadius: 16)
.fill(Color.white)
}
} else {
Text(tab.title)
.padding(4)
.foregroundColor(.white)
}
}
.tag(tab)
.onTapGesture {
selection = tab
}
}
}
.padding(.horizontal, 10)
}
.background(Color.accentColor)
}
}
struct TopScrollingTabBar<Tab: TopTab, Content: View>: View {
var alignment: HorizontalAlignment = .leading
#Binding var selection: Tab
#ViewBuilder var content: (_ tab: Tab) -> Content
var body: some View {
GeometryReader { geometry in
VStack(alignment: alignment) {
TopScrollingTabBarPicker(selection: $selection)
.frame(height: 44)
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
self.content(selection)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
}
}
Usage:
enum PreviewTab: String, TopTab {
case saved
case booked
case pending
case confirmed
case archived
var id: String {
rawValue
}
var title: String {
switch self {
case .saved:
return "Saved"
case .booked:
return "Booked"
case .pending:
return "Pending"
case .confirmed:
return "Confirmed"
case .archived:
return "Archived"
}
}
}
struct ContentView: View {
#State var selection: PreviewTab = .saved
var body: some View {
TopScrollingTabBar(selection: $selection.animation()) { tab in
// For demo purposes
List {
Text(tab.title)
.bold()
.font(.largeTitle)
Text("Something that is \(tab.title)")
}
.listStyle(.plain)
// In a full app I'd switch on the Tab enum and generate the appropriate view.
// switch tab {
// case .saved:
// SavedThings()
// case .booked:
// ...etc
// }
}
}
}
Hope this helps! <3

SwiftUI Custom Shape doesn't work correctly

I have a custom separator as a Shape. I also have a view on which I put my separator (I add it 3 times and separated by spacers).
Below I added a capsule with spacing 4. According to the idea my custom separator and capsule should have the same spacings.
Why doesn’t it work like that?
// Separator struct
struct ProgressBarSeparator: View{
struct RightShape: Shape {
func path(in rect: CGRect) -> Path {
let offsetX = rect.maxX - rect.width / 4
let crect = CGRect(origin: .zero, size: CGSize(width: 4, height: 4)).offsetBy(dx: offsetX, dy: .zero)
var path = Rectangle().path(in: rect)
path.addPath(Circle().path(in: crect))
return path
}
}
struct LeftShape: Shape {
func path(in rect: CGRect) -> Path {
let offsetX = rect.minX - rect.width / 4
let crect = CGRect(origin: .zero, size: CGSize(width: 4, height: 4)).offsetBy(dx: offsetX, dy: .zero)
var path = Rectangle().path(in: rect)
path.addPath(Circle().path(in: crect))
return path
}
}
var body: some View {
Rectangle()
.frame(width: 8, height: 4)
.foregroundColor(.gray)
.mask(RightShape().fill(style: .init(eoFill: true)))
.mask(LeftShape().fill(style: .init(eoFill: true)))
}
}
// Progress bar view
private var progressBar: some View {
GeometryReader { proxy in
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(ColorName.black400.color)
.frame(height: 4)
Capsule()
.fill(gradient)
.frame(width: (proxy.size.width - 12) * 0 / 4, height: 4)
.animation(.easeInOut)
HStack.zeroSpacing {
Spacer()
ProgressBarSeparator()
Spacer()
ProgressBarSeparator()
Spacer()
ProgressBarSeparator()
Spacer()
}
}
}
}
// Example capsule
private var procentCashback: some View {
HStack(spacing: 4) {
ForEach(0 ... 3) { index in
ZStack(alignment: .leading) {
Capsule()
.fill(.orange)
.frame(height: 4)
}
}
}
}
My broken view

SwiftUI animate opacity in gradient mask

I want to animate a mountain line chart (see screenshot) so that the line appears smoothly from left to right. I tried animating the endPoint of a LinearGradient in a mask. This works as it should, but leaves the right end of the line with an opacity value < 1. I also tried animating the opacity value itself, but that doesn't animate anything. This is the View:
import SwiftUI
struct ContentView: View {
private var datapoints: [CGFloat] = [100, 250, 200, 220, 200, 250, 350, 300, 325, 250]
#State private var pct: Double = 0.0
var body: some View {
GeometryReader { geometry in
ZStack {
Path() { path in
var x = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
}
.stroke()
Path() { path in
var x: Double = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height))
}
.fill(LinearGradient(colors: [.black.opacity(0.25), .black.opacity(0)], startPoint: .top, endPoint: .bottom))
}
.mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(pct), location: 1)], startPoint: .leading, endPoint: .trailing))
.onAppear() {
withAnimation(.linear(duration: 3)) {
pct = 1.0
}
}
// .mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(0), location: 1)], startPoint: .leading, endPoint: pct == 1 ? .trailing : .leading))
// .onAppear() {
// withAnimation(.linear(duration: 3)) {
// pct = 1.0
// }
// }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is the result if uncomment the commented out lines:

Path transparent only on macOS

Why are paths slightly transparent on macOS and not iOS? How can I make them not transparent on macOS?
Minimal reproducible example:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 150, y: 150))
path.addLine(to: CGPoint(x: 150, y: 300))
}
.stroke(lineWidth: 8)
Path { path in
path.move(to: CGPoint(x: 150, y: 200))
path.addLine(to: CGPoint(x: 150, y: 450))
}
.stroke(lineWidth: 8)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}

SwiftUI: Change the start position for animated progress bar (or Shape drawing in general)

The code below begins to draw a rectangle from the bottom left corner. How can I change it to begin to draw from the top middle? Hopefully there is a simple setting I am missing here.
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
Rectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.rotationEffect(Angle(degrees: 270.0))
.frame(width: 100, height:100)
Rectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
.frame(width: 100, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
You don't need to rotate it (moreover it might be not square), just create own rectangular shape with start point anywhere needed.
Demo prepared with Xcode 12.4 / iOS 14.4
struct MyRectangle: Shape {
func path(in rect: CGRect) -> Path {
Path {
$0.move(to: CGPoint(x: rect.midX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
}
}
}
struct ContentView: View {
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
MyRectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.frame(width: 200, height:100)
MyRectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.animation(.linear)
.frame(width: 200, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
}