How to clip background blur in SwiftUI - swiftui

I am trying to have a semi-transparent blurred overlaying view but the blur creates a fuzzy edge and I'd like it to be sharp outlining the overlay shape, ie clip it.
struct TestBlur : View {
var body : some View {
HStack {
VStack {
Spacer()
Text("Hello World")
Spacer()
}
.frame(width: 128)
.background(Color.red.opacity(0.5).blur(radius: 16).clipShape(Rectangle()))
Spacer()
}
.background(Color.yellow)
}
}
I want the edge of the red overlay to be like so:
In essence I want the red view to blur anything below it and have a sharp edge:
Using #Asperi's BackgroundBlurView also doesn't work, there's no blur done (maybe I am not using this right):
var body : some View {
ZStack {
VStack {
ForEach(0..<40) { i in
Text("ABCDEFGHIKLMNOPQRSTUVWXYZ ABCDEFGHIKLMNOPQRSTUVWXYZ ABCDEFGHIKLMNOPQRSTUVWXYZ ABCDEFGHIKLMNOPQRSTUVWXYZ ABCDEFGHIKLMNOPQRSTUVWXYZ ")
}
.rotationEffect(Angle.init(degrees: 0))
}
.opacity(0.9)
.background(Color.yellow)
HStack {
// Trying to make the below VStack have a semi-transparent background and blur all content below it
// but the VStack needs to have a sharp edge, not fuzzy.
VStack {
Spacer()
Text("Hello World")
Spacer()
}
.frame(width: 128)
.background(Color.red.opacity(0.5))
.background(BackgroundBlurView().opacity(0.8))
Spacer()
}
}
}
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Looks like this (lacks blur):

Related

Why do the views extend wider than the screen?

Edit: Substitute your "system name:" of choice. "pencil.circle" works fine. "edit" is not a valid SF Symbol.
(I've simplified my code so you can cut and paste. That's why you see .frame, resizable, etc. where much simpler code might your first instinct.)
I have created a view which is a vertical list of row items (table view).
Each row item has a horizontal view with two images inside it.
The images take up too much space and do not fit correctly on the screen:
import SwiftUI
#main
struct StackOverflowDemoApp: App {
var body: some Scene {
WindowGroup {
TandemView()
}
}
}
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(height: 80)
.aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(width: 200, height: 80)
}
.padding()
.fixedSize()
}
}
struct TandemView_Previews: PreviewProvider {
static var previews: some View {
TandemView()
}
}
The above is the closest I can get to the desired layout (it just needs to fit horizontally). I experimented with GeometryReader but that did not produce desired results.
Here are some things I tried:
The code as provided
NoConstraintsOnPencilOrHStack
NoConstraintsOnTandemView
NoConstraintsOnImageInPaddedViewButWithFrameConstraint
I am trying to get a row view which consists of two Images (my actual source consists of UIImage objects) that fits within the width of the screen.
Edit:
After Accepting cedricbahirwe's spot-on response, I was able to simplify the code further. New results:
I added at the top level
TandemView()
.padding(.horizontal)
I removed:
// Spacer()
at the end of PaddedImageView
updated TandemView -- changed both frames and removed 3 lines:
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(width: 80, height: 80)
// .aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(height: 80)
}
// .padding()
// .fixedSize()
}
}
This is happening because of the layout of PaddedImageView View, you can actually remove the Spacer since it is not needed there.
So change
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
to
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
}
}
Note:
SwiftUI Engine infers the layout of your view from the implementation of the body property. It's recommended to have one Parent View inside the body property.

SwiftUI measuring the height of a view

I tried measuring the height of a view by using coordinatespace on it but the value it returns is the height of the full screen. Any idea why my code isn't giving me what I want ?
struct GeoReader: View {
var body: some View {
GeometryReader { geo in
VStack {
ZStack {
Rectangle()
.foregroundColor(.blue)
Text("Heigt of full screen is \(geo.size.height)")
}
ZStack {
Rectangle()
.foregroundColor(.red)
.coordinateSpace(name: "Redbox")
Text("Height of red box is \(geo.frame(in: .named("Redbox")).height)")
}
}
}
}
}
The showing size is the dimension of the full view that is the container of the inner view.
You need to use another GeometryReader to get the inner dimension of a second ZStack.
struct ContentView: View {
var body: some View {
GeometryReader { geo in
VStack {
ZStack {
Rectangle()
.foregroundColor(.blue)
Text("Heigt of full screen is \(geo.size.height)")
}
GeometryReader { innterGeo in //<Here
ZStack {
Rectangle()
.foregroundColor(.red)
.coordinateSpace(name: "Redbox")
Text("Height of red box is \(innterGeo.frame(in: .named("Redbox")).height)")
}
}
}
}
}
}
if you need to use this in any places then you can use this approch.
First, create one struct and wrapped your content with GeometryReader.
struct GeometryContentSize<Content: View>: View {
public var content: (CGSize) -> Content
var body: some View {
GeometryReader { geo in
content(geo.size)
}
}
}
usage:
struct ContentView: View {
var body: some View {
GeometryReader { geo in
VStack {
ZStack {
Rectangle()
.foregroundColor(.blue)
Text("Heigt of full screen is \(geo.size.height)")
}
GeometryContentSize { (size) in //<--- Here
ZStack {
Rectangle()
.foregroundColor(.red)
Text("Height of red box is \(size.height)")
}
}
}
}
}
}
The geometry reader measures the entires screen, because you put the geometry reader right on top in the body of the view. It will therefore read the geometry of the view it returns. In this case the entire screen.
If you want to get the size of the two rectangles, you schoul put the geometry reader in the Stack of the rectangle and the text.
struct GeoReader: View {
var body: some View {
GeometryReader { geo1 in
VStack {
ZStack {
Rectangle()
.foregroundColor(.blue)
Text("Heigt of full screen is \(geo1.size.height)")
}
ZStack {
GeometryReader { geo2 in
Rectangle()
.foregroundColor(.red)
.coordinateSpace(name: "Redbox")
Text("Height of red box is \(geo2.frame(in: .named("Redbox")).height)")
}
}
}
}
}
}

Placing a rectangle in front of ScrollView affects scrolling

I need to place a translucent rectangle on front of ScrollView but when i put everything (Rectangle & ScrollView) inside of a ZStack, scroll & touch events stop working within this rectangle.
Atm I'm using .background modifier as it doesn't affect scrolling but I am still looking for way to make it work properly with rectangle placed over (in front of) my ScrollView.
Is there any way to put a View over ScrollView so it wouldn't affect it's functionality?
Here's the code i'm using now (i changed the colors and removed opacity to make the objects visible as my original rectangle is translucent & contains barely visible gradient)
struct ContentView: View {
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(0...100, id:\.self) { val in
ZStack {
Text("test")
.font(.system(size: 128))
} // ZStack
.background(Color.blue)
} // ForEach
}
}
.background(RadialGradient(gradient: Gradient(stops: [
.init(color: Color.blue, location: 0),
.init(color: Color.red, location: 1)]), center: .top, startRadius: 1, endRadius: 200)
.mask(
VStack(spacing: 0) {
Rectangle()
.frame(width: 347, height: 139)
.padding(.top, 0)
Spacer()
}
))
}
}
}
Here is a possible approach to start with - use UIVisualEffectView. And Blur view is taken from How do I pass a View into a struct while getting its height also? topic.
struct ScrollContentView: View {
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(0...100, id:\.self) { val in
ZStack {
Text("test")
.font(.system(size: 128))
} // ZStack
.background(Color.blue)
} // ForEach
}
}
Blur(style: .systemThinMaterialLight)
.mask(
VStack(spacing: 0) {
Rectangle()
.frame(width: 347, height: 139)
.padding(.top, 0)
Spacer()
}
)
.allowsHitTesting(false)
}
}
}
I decided to post a solution here.. it's based on an approach suggested by Asperi.
2Asperi: Thank you, i appreciate your help, as always.
I played a little bit with applying .opacity & mask to Blur but it didn't work.
So i applied mask to the .layer property inside makeUIView and it worked fine
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(0...100, id:\.self) { val in
ZStack {
Text("test")
.font(.system(size: 128))
} // ZStack
.background(Color.white)
} // ForEach
}
}
Blur(style: .systemThinMaterial)
.mask(
VStack(spacing: 0) {
Rectangle()
.frame(width: 347, height: 139)
.padding(.top, 0)
Spacer()
}
)
.allowsHitTesting(false)
}
}
}
}
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: style))
let gradientMaskLayer = CAGradientLayer()
gradientMaskLayer.type = .radial
gradientMaskLayer.frame = CGRect(x: 0, y: 0, width: 347, height: 256)
gradientMaskLayer.colors = [UIColor.black.cgColor, UIColor.clear.cgColor]
gradientMaskLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientMaskLayer.endPoint = CGPoint(x: 1, y: 1)
gradientMaskLayer.locations = [0 , 0.6]
blurEffectView.layer.mask = gradientMaskLayer
return blurEffectView
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
The only thing i don't understand is why startPoint and endPoint work only when i set them to [0.5,0] & [1,1] but not [0.5,0] & [0.5,1] - i expected that it should determine the direction of radial gradient and in my case it should go from .topCenter to .topBottom (which it does but i don't understand the nature of endPoint)..

SwiftUI NavigationView and NavigationLink changes layout of custom view

I have a DetailView() that displays an image, text, and then a map of the location of the displayed image. In my ContentView() I have a NavigationView and NavigationLink to go from the main view to my custom view. Everything works fine, except that the alignment of my DetailView() is not aligned properly as when I view the preview for DetailView(). The text description is showing well below the picture. I have been pulling my hair out for 2 days trying to figure this out, but haven't so far.
Picture of ContentView()
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(picture: "dunnottar-castle")) {
Text("Hello, World!")
Image(systemName: "sun.min.fill")
} .buttonStyle(PlainButtonStyle())
.navigationBarHidden(true)
}
}
}
=================== My DetailView()
struct MapView: UIViewRepresentable {
// 1.
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
MKMapView(frame: .zero)
}
// 2.
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
// 3.
let location = CLLocationCoordinate2D(latitude: 30.478340,
longitude: -90.037687)
// 4.
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: location, span: span)
uiView.setRegion(region, animated: true)
// 5.
let annotation = MKPointAnnotation()
annotation.coordinate = location
annotation.title = "Abita Springs"
annotation.subtitle = "Louisiana"
uiView.addAnnotation(annotation)
}
}
struct DetailView: View {
let picture: String
var body: some View {
VStack(spacing: -50.0){
// Picture and Title
ZStack (alignment: .bottom) {
//Image
Image(picture)
.resizable()
.aspectRatio(contentMode: .fit)
Rectangle()
.frame(height: 80)
.opacity(0.25)
.blur(radius: 10)
HStack {
VStack(alignment: .leading, spacing: 8.0) {
Text("EDINBURGH")
.foregroundColor(.yellow)
.font(.largeTitle)
}
.padding(.leading)
.padding(.bottom)
Spacer()
}
}.edgesIgnoringSafeArea(.top)
VStack{
// Description
Text("Edinburgh is Scotland's compact, hilly capital. It has a medieval Old Town and elegant Georgian New Town with gardens and neoclassical buildings. Looming over the city is Edinburgh Castle, home to Scotland’s crown jewels and the Stone of Destiny, used in the coronation of Scottish rulers. Arthur’s Seat is an imposing peak in Holyrood Park with sweeping views, and Calton Hill is topped with monuments and memorials.")
.font(.body)
.lineLimit(9)
.lineSpacing(5.0)
.padding()
// .frame(maxHeight: 310)
}
Spacer()
// Map of location
VStack {
MapView()
.edgesIgnoringSafeArea(.all)
.padding(.top)
.frame(maxHeight: 310)
// Image(systemName: "person")
.padding(.top)
}
}
}
}
I changed
NavigationLink(destination: DetailView(picture: "dunnottar-castle")) {
to
NavigationLink(destination: DetailView(picture: "dunnottar castle").edgesIgnoringSafeArea(.all)) {
and it works like I want it to now.
If you just want to have the normal layout, you can also use:
NavigationLink(destination: DetailView(picture: "dunnottar castle").navigationBarHidden(true))

How to scale the contentview to multiple screen sizes? SwiftUI

Newbie here! I am building a quiz app using Swiftui, I built the view controller by previewing it in an iPhone 11 simulator.
And I thought the controlview would fit other iPhone sizes, like iPhone 8. Because Swiftui has a built-in auto layout.
But when I run the iPhone 8 simulator some of the content in the control view is not visible because they are below the screen.
Is there a way to fix it?
I tried to play with multiple Spacer() and different paddings but I can't seem to make it look good on both screen at the same time.
This is my code:
import SwiftUI
struct questionOne: View {
#State var totalClicked: Int = 0
#State var showDetails = false
#State var isSelected = false
var body: some View {
VStack {
TRpic().frame(width: 350.0, height: 233.0).cornerRadius(10).padding(.top, 80)
Spacer()
Text(verbatim: "What's the capital of Turkey?")
.font(.title)
.padding(.bottom, 60)
.frame(height: 100.0)
Button(action: {}) {
Text("Istanbul")
}.buttonStyle(MyButtonStyle())
Spacer()
Button(action: {self.isSelected.toggle()}) {
Text("Ankara")
}.buttonStyle(SelectedButtonStyle(isSelected: $isSelected))
Spacer()
Button(action: {}) {
Text("Athens")
} .buttonStyle(MyButtonStyle())
Spacer()
NavigationLink(destination: questionTwo()) {
VStack {
Text("Next Question")
Adview().frame(width: 150, height: 60)
}
}
}.edgesIgnoringSafeArea(.top)
}
}
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration:
Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(configuration.isPressed ? Color.red : Color.gray)
.cornerRadius(10.0)
}
}
struct SelectedButtonStyle: ButtonStyle {
#Binding var isSelected: Bool
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(isSelected ? Color.green : Color.gray)
.cornerRadius(10.0)
}
}
enter image description here
Screenshot
Being in the given context I guess you do not want a scroll view, so regarding spacing I suggest using a VStack with spacing parameter VStack(alignment: .center, spacing: n){ ... } and remove the Spacers, if between 2 views you need another distance than n, just use padding to add some extra space.
This should adjust everything to fit the height of any screen, including the image, so do not need a fixed frame for it.
But, you might have a very wide image that could go beyond safe area, so, you could set a maximum width for the image as being the screen width
struct questionOne: View {
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .center, spacing: 20) {
TRpic().frame(maxWidth: geometryProxy.size.width, alignment: .center)
.padding([.leading, .trailing], 10)
.......
}
}
}