How to make inner shadow in SwiftUI? - swiftui

How can I use Inner Shadow on a Rectangle()?
Rectangle()
.foregroundColor(.green)
.frame(width: 400, height: 300)
I can only manage to do an Outer Shadow using .shadow
I'm looking for something like this

For this problem, I built a modifier for the View protocol and a extension, like below
View+innerShadow.swift
import SwiftUI
extension View {
func innerShadow(color: Color, radius: CGFloat = 0.1) -> some View {
modifier(InnerShadow(color: color, radius: min(max(0, radius), 1)))
}
}
private struct InnerShadow: ViewModifier {
var color: Color = .gray
var radius: CGFloat = 0.1
private var colors: [Color] {
[color.opacity(0.75), color.opacity(0.0), .clear]
}
func body(content: Content) -> some View {
GeometryReader { geo in
content
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .top, endPoint: .bottom)
.frame(height: self.radius * self.minSide(geo)),
alignment: .top)
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .bottom, endPoint: .top)
.frame(height: self.radius * self.minSide(geo)),
alignment: .bottom)
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .leading, endPoint: .trailing)
.frame(width: self.radius * self.minSide(geo)),
alignment: .leading)
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .trailing, endPoint: .leading)
.frame(width: self.radius * self.minSide(geo)),
alignment: .trailing)
}
}
func minSide(_ geo: GeometryProxy) -> CGFloat {
CGFloat(3) * min(geo.size.width, geo.size.height) / 2
}
}
And, for the inner shadow, you just need to add .innerShadow(color:radius)
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Rectangle()
.foregroundColor(.green)
.frame(width: 400, height: 300)
.innerShadow(color: Color.black.opacity(0.3), radius: 0.05)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

If you want to add shadow only to any specific side.
private struct InnerShadow: ViewModifier {
var color: Color = .gray
var topToBottomRadius = 0.0
var bottomToTopRadius = 0.0
var leadingToTrailingRadius = 0.0
var trailingToLeadingRadius = 0.0
private var colors: [Color] {
[color.opacity(0.75), color.opacity(0.0), .clear]
}
func body(content: Content) -> some View {
GeometryReader { geo in
content
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .top, endPoint: .bottom)
.frame(height: self.topToBottomRadius * self.minSide(geo)),
alignment: .top)
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .bottom, endPoint: .top)
.frame(height: self.bottomToTopRadius * self.minSide(geo)),
alignment: .bottom)
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .leading, endPoint: .trailing)
.frame(width: self.leadingToTrailingRadius * self.minSide(geo)),
alignment: .leading)
.overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .trailing, endPoint: .leading)
.frame(width: self.trailingToLeadingRadius * self.minSide(geo)),
alignment: .trailing)
}
}
func minSide(_ geo: GeometryProxy) -> CGFloat {
CGFloat(3) * min(geo.size.width, geo.size.height) / 2
}
}
extension View {
func innerShadow(color: Color, topRadius: CGFloat = 0.0, bottomRadius: CGFloat = 0.0, leftRadius: CGFloat = 0.0, rightRadius: CGFloat = 0.0) -> some View {
modifier(InnerShadow(color: color, topToBottomRadius: min(max(0, topRadius), 1), bottomToTopRadius: min(max(0, bottomRadius), 1), leadingToTrailingRadius: min(max(0, leftRadius), 1), trailingToLeadingRadius:min(max(0, rightRadius), 1)))
}
}

Related

add borders to 2 sides only

I have a View stack as Expanded white part and I have given the corner radius to it, I need to give radius to only top left and top right
My Code
struct loginView: View {
#State private var stringOfTextField: String = String()
var body: some View {
ZStack {
LinearGradient(colors: [Color("primarycolor"), Color("secondarycolor")],
startPoint: .top,
endPoint: .center).ignoresSafeArea()
VStack() {
Text("Login").foregroundColor(.white).font(
.system(size: 18)
)
Image("logo")
Spacer()
}
VStack {
Spacer(minLength: 230)
VStack{
HStack {
Image(systemName: "person").foregroundColor(.gray)
TextField("Enter Email", text: $stringOfTextField)
} .padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding(.bottom)
TextField("Enter Password", text: $stringOfTextField)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding(.bottom)
Text("Forgot Password ?")
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.bottom)
Button(action: {
print("sign up bin tapped")
}) {
Text("SIGN IN")
.frame(minWidth: 0, maxWidth: .infinity)
.font(.system(size: 18))
.padding()
.foregroundColor(.white)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("secondarycolor"), lineWidth: 1)
)
}
.background(Color("secondarycolor"))
.cornerRadius(25)
Spacer()
}
.padding(.vertical, 60)
.padding(.horizontal, 20)
.background(Color.white)
.cornerRadius(30)
}
}
.ignoresSafeArea(edges: [.bottom])
}
}
Preview
You can see on the end it's showing white border because border is given to all sides, I need to give it to top 2 sides only. I try by Roundedcorner, but that was not working.
You can try like this
struct ContentView: View {
var body: some View {
VStack{
//....
//your content
//.....
}
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 25))
}
}
struct CustomCorner: Shape {
var corners : UIRectCorner
var radius : CGFloat
func path(in rect: CGRect)->Path{
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}

SwiftUI: Transparent part of the View and text

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

SwiftUI transition is not working when in HSack

I have a VStack which wraps a number elements wrapped inside another HStack, I want to add a simple bottom to top transition on load, but it does not have any effect on the output:
var body: some View {
let bottomPadding = ScreenRectHelper.shared.bottomPadding + 150
GeometryReader { geometry in
ZStack {
VisualEffectViewSUI(effect: UIBlurEffect(style: .regular))
.edgesIgnoringSafeArea(.all)
VStack(alignment: .center) {
Spacer()
HStack {
VStack(alignment: .leading, spacing: 15) {
ForEach(items, id: \.id) { item in
HStack(alignment: .center) {
Image(item.imageName)
.resizable()
.scaledToFit()
.frame(width: 24)
Spacer()
.frame(width: 15)
Text("\(item.text)")
.foregroundColor(.white)
.transition(.move(edge: .bottom))
}
.contentShape(Rectangle())
.onTapGesture {
/// Do something
}
}
}
}
.frame(width: geometry.size.width, height: nil, alignment: .center)
}.zIndex(1)
.frame(width: geometry.size.width, height: nil, alignment: .center)
.padding(.bottom, bottomPadding)
}.background(
LinearGradient(gradient: Gradient(colors: [gradientColor.opacity(0),gradientColor]), startPoint: .top, endPoint: .bottom)
)
}.edgesIgnoringSafeArea(.all)
}
This SwiftUI view is added to a UIKit view controller and that view controller is presented modally.
.transition only works if you insert something conditionally into the view.
I'm not totally sure what you want to achieve, but if you want the whole view to slide from the bottom, this would work.
struct ContentView: View {
#State private var showText = false
let bottomPadding: CGFloat = 150
var body: some View {
ZStack {
if showText { // conditional view to make .transition work
// VisualEffectViewSUI(effect: UIBlurEffect(style: .regular))
// .edgesIgnoringSafeArea(.all)
VStack(alignment: .center) {
Spacer()
HStack {
VStack(alignment: .leading, spacing: 15) {
ForEach(0 ..< 3) { item in
HStack(alignment: .center) {
Image(systemName: "\(item).circle")
.resizable()
.scaledToFit()
.frame(width: 24)
Spacer()
.frame(width: 15)
Text("Item \(item)")
.foregroundColor(.white)
}
}
}
}
}
.frame(maxWidth: .infinity)
.padding(.bottom, bottomPadding)
.background(
LinearGradient(gradient: Gradient(colors: [.blue.opacity(0), .blue]),
startPoint: .top, endPoint: .bottom)
)
.edgesIgnoringSafeArea(.all)
.transition(.move(edge: .bottom)) // here for whole modal view
}
Button("Show modal") {
withAnimation {
showText.toggle()
}
}
}
}
}

New to SwiftUI trying to understand layout changes without (I guess it doesn't have ) Layout constraints

The problem is I don't understand how to fix my Content View to the screen and handle things like rotation or even how to make other screen sizes dynamic. Below is my entire View code. When I create a new app out of the box it looks fine in preview, the way I like it. However, screen sizes change and all I know to do is put magic numbers, because there is no superview or frame to size from. Also I can't seem to clip at screen edges. So my background view is way to big. How is this handled in SwiftUI?
import SwiftUI
import AuthenticationServices
struct LoginScreen: View {
var body: some View {
ZStack{
backgroundLayout
loginLayout
}
}
var loginLayout: some View {
VStack {
welcomeText
signInWithAppleButton
}
}
var backgroundLayout: some View {
ZStack{
Rectangle()
.foregroundColor(.init(.sRGB, red: 0, green: 0.750, blue: 0.750, opacity: 0.25))
Circle()
.foregroundColor(.blue)
.offset(x: 200, y: -200)
.aspectRatio(1/3, contentMode: .fill)
.clipped()
Circle()
.foregroundColor(.green)
.offset(x: -200, y: 200)
.aspectRatio(1/3, contentMode: .fill)
.clipped()
}
}
var welcomeText: some View {
HStack {
VStack {
Text("Sign Up")
.font(.largeTitle)
.bold()
Text("Sign In")
.font(.title)
.bold()
Text("Get Started!")
.font(.title2)
.bold()
}
.offset(x: -100, y: -150)
}
}
var signInWithAppleButton: some View {
SignInWithAppleButton(
.continue,
onRequest: { request in
request.requestedScopes = [.fullName, .email]
},
onCompletion: { result in
switch result {
case .success (let authenticationResults):
print("Authorization successful! :\(authenticationResults)")
case .failure(let error):
print("Authorization failed: " + error.localizedDescription)
}
}
)
.offset(x: 0, y: 150)
.frame(width: 200, height: 50, alignment: .center)
}
}
struct Login_Preview: PreviewProvider {
static var previews: some View {
LoginScreen()
}
}
After trying several things this is all I could come up with. It's not perfect and certainly not ideal, but at least it work somewhat as expected.
import SwiftUI
import AuthenticationServices
struct LoginScreen: View {
var body: some View {
loginLayout.background(backgroundLayout)
.statusBar(hidden: true)
}
var loginLayout: some View {
GeometryReader {geo in
VStack {
welcomeText.padding(.top, geo.size.height / 10)
Spacer()
signInWithAppleButton
.padding(.bottom, geo.size.height / 10)
}
}
}
var backgroundLayout: some View {
GeometryReader { geo in
let circleSize = min(geo.size.width, geo.size.height)
ZStack {
Rectangle()
.foregroundColor(.init(.sRGB, red: 0, green: 0.750, blue: 0.750, opacity: 0.25))
Circle()
.foregroundColor(.blue)
.offset(x: circleSize / 1.75,
y: -geo.size.height / 2.5)
.frame(width: circleSize, height: circleSize, alignment: .center)
Circle()
.foregroundColor(.green)
.offset(x: -circleSize / 1.75, y: geo.size.height / 2.5)
.frame(width: circleSize, height: circleSize, alignment: .center)
}
.frame(width: geo.size.width, height: geo.size.height, alignment: .top)
}
}
var welcomeText: some View {
GeometryReader { geo in
HStack {
VStack {
Text("Sign Up")
.font(.largeTitle)
.bold()
Text("Sign In")
.font(.title)
.bold()
Text("Get Started!")
.font(.title2)
.bold()
}
.padding(.leading, geo.size.width / 8)
Spacer()
}
}
}
var signInWithAppleButton: some View {
SignInWithAppleButton(
// Add this below for Continue with Apple button
.continue,
onRequest: { request in
request.requestedScopes = [.fullName, .email]
},
onCompletion: { result in
switch result {
case .success (let authenticationResults):
print("Authorization successful! :\(authenticationResults)")
case .failure(let error):
print("Authorization failed: " + error.localizedDescription)
}
}
)
.frame(width: 200, height: 50, alignment: .center)
}
}
struct Login_Preview: PreviewProvider {
static var previews: some View {
LoginScreen()
}
}

Map Inside VStack Causing Crash

I have a simple Map view:
struct MapContainer: View {
#EnvironmentObject var store: AppStore
#State private var region: MKCoordinateRegion = Location.defaultRegion
var events: [Event] {
store.state.getEventsFromSearchResults()
}
func select(id: UUID) {}
var body: some View {
Map(
coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: true,
annotationItems: events,
annotationContent: { event in
MapAnnotation(coordinate: event.coordinates) {
Image("MapMarker")
.resizable()
.scaledToFit()
.frame(width: 24)
.foregroundColor(
event.id == store.state.searchState.tapped
? Color("Accent")
: .black
)
.onTapGesture { select(id: event.id) }
}
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear { region = store.state.searchState.near.region }
}
}
And I have a screen that uses the above container:
struct MapScreen: View {
let height: CGFloat? = nil
var body: some View {
GeometryReader { geo2 in
ZStack(alignment: .top) {
VStack {
MapContainer()
.animation(.easeOut)
}
.frame(height: height != nil ? height : geo2.size.height / 2 + geo2.safeAreaInsets.top)
VStack(spacing: 0) {
VStack(spacing: 0) {
HStack {
Image(systemName: "minus")
.font(.system(size: 40))
.foregroundColor(Color("LightGray"))
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
MapList()
}
.frame(height: 500)
.background(Color.white.shadow(color: .gray, radius: 5, x: 0, y: 3))
}
.frame(maxHeight: .infinity, alignment: .bottom)
.animation(.easeOut)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.edgesIgnoringSafeArea(.vertical)
}
}
}
The above crashes with - Thread 1: EXC_BAD_ACCESS (code=1, address=0x38) - but if I assign a hard coded value to the MapContainer e.g.
MapContainer().frame(height: 400)
Then everything works fine. Anyone know what's causing the issue?