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)..
Related
I have a view in SwiftUI. This view has some random images on it in various random positions. Check the code below.
struct ContentView: View {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { _ in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: CGFloat.random(in: 0..<screenWidth),
y: CGFloat.random(in: 0..<screenHeight)
)
}
}
.ignoreSafeArea()
}
}
I need to get the exact position of these random added images and pass the positions to another transparent view that shows up with a ZStack on top of the previous view. In the transparent popup fullscreen ZStack view i need to point to the position of the images i randomly put in the previous view using arrow images. Is this somehow possible in swiftui? I am new in swiftui so any help or suggestion appreciated.
Store the random offsets in a #State var and generate them in .onAppear { }. Then you can use them to position the random images and pass the offsets to the overlay view:
struct ContentView: View {
#State private var imageOffsets: [CGPoint] = Array(repeating: CGPoint.zero, count: 5)
#State private var showingOverlay = true
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { index in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
.onAppear {
for index in 0..<5 {
imageOffsets[index] = CGPoint(x: .random(in: 0..<screenWidth), y: .random(in: 0..<screenHeight))
}
}
.overlay {
if showingOverlay {
OverlayView(imageOffsets: imageOffsets)
}
}
}
}
struct OverlayView: View {
let imageOffsets: [CGPoint]
var body: some View {
ZStack {
Color.clear
ForEach(0..<5) { index in
Circle()
.stroke(.blue)
.frame(width: 40, height: 40)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
}
}
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'm creating a vertical paging view via TabView following this
Everything is perfect except the strange right margin as highlighted in pic below.
Here is the code I use. Appreciate it if anyone could point out the root cause.
import SwiftUI
fileprivate struct VCompatibleTabView<Content: View>: View {
let proxy: GeometryProxy
let content: Content
init(proxy: GeometryProxy, #ViewBuilder content: () -> Content) {
self.proxy = proxy
self.content = content()
}
var body: some View {
if #available(iOS 15.0, *) {
// Credit to Gary Tokmen for this bit of Geometry Reader code: https://blog.prototypr.io/how-to-vertical-paging-in-swiftui-f0e4afa739ba
TabView {
content
.rotationEffect(.degrees(-90)) // Rotate content
.frame(
width: proxy.size.width,
height: proxy.size.height
)
}
.frame(
width: proxy.size.height, // Height & width swap
height: proxy.size.width
)
.rotationEffect(.degrees(90), anchor: .topLeading) // Rotate TabView
.offset(x: proxy.size.width) // Offset back into screens bounds
.tabViewStyle(
PageTabViewStyle(indexDisplayMode: .never)
)
} else {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
content
}
}
.frame(
width: proxy.size.width,
height: proxy.size.height) }
}
}
struct BBYouView: View {
var body: some View {
ZStack {
GeometryReader { proxy in
VCompatibleTabView(proxy: proxy) {
ForEach(0..<3, id: \.self) { item in
Rectangle().fill(Color.pink)
.frame(
width: proxy.size.width,
height: proxy.size.height
)
}
}
}
}
.background(Color.yellow)
}
}
Problem:I have a View that I needed to place multiple (2) views that contained: 1 Image + 1 Text. I decided to break that up into a ClickableImageAndText structure that I called on twice. This works perfectly if the image is a set size (64x64) but I would like this to work on all size classes. Now, I know that I can do the following:
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
but I am asking for both Different Size Classes and Same Size Classes such as the iPhone X and iPhone 13 which are the same.
Question:How do I alter the image for dynamic phone sizes (iPhone X, 13, 13 pro, etc) so it looks appropriate for all measurements?
Code:
import SwiftUI
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
struct InitialView: View {
var topView: some View {
Image("Empty_App_Icon")
.resizable()
.scaledToFit()
}
var bottomView: some View {
VStack {
ClickableImageAndText(
image: "Card_Icon",
text: "View Your Memories") {
print("Tapped on View Memories")
}
.padding(.bottom)
ClickableImageAndText(
image: "Camera",
text: "Add Memories") {
print("Tapped on Add Memories")
}
.padding(.top)
}
}
var body: some View {
ZStack {
GradientView()
VStack {
Spacer()
topView
Spacer()
bottomView
Spacer()
}
}
}
}
struct InitialView_Previews: PreviewProvider {
static var previews: some View {
InitialView()
}
}
Image Note:My background includes a GradientView that I have since removed (thanks #lorem ipsum). If you so desire, here is the GradientView code but it is unnecessary for the problem above.
GradientView.swift
import SwiftUI
struct GradientView: View {
let firstColor = Color(uiColor: UIColor(red: 127/255, green: 71/255, blue: 221/255, alpha: 1))
let secondColor = Color(uiColor: UIColor(red: 251/255, green: 174/255, blue: 23/255, alpha: 1))
let startPoint = UnitPoint(x: 0, y: 0)
let endPoint = UnitPoint(x: 0.5, y: 1)
var body: some View {
LinearGradient(gradient:
Gradient(
colors: [firstColor, secondColor]),
startPoint: startPoint,
endPoint: endPoint)
.ignoresSafeArea()
}
}
struct GradientView_Previews: PreviewProvider {
static var previews: some View {
GradientView()
}
}
Effort 1:Added a GeometryReader to my ClickableImageAndText structure and the view is automatically changed incorrectly.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader { reader in
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
}
Effort 2:Added a GeometryReader as directed by #loremipsum's [deleted] answer and the content is still being pushed; specifically, the topView is being push to the top and the bottomView is taking the entire space with the addition of the GeometryReader.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader{ geo in
VStack {
Image(image)
.resizable()
.scaledToFit()
//You can do this and set strict size constraints
//.frame(minWidth: 64, maxWidth: 128, minHeight: 64, maxHeight: 128, alignment: .center)
//Or this to set it to be percentage of the size of the screen
.frame(width: geo.size.width * 0.2, alignment: .center)
Text(text)
}.foregroundColor(.white)
//Everything moves to the left because the `View` expecting a size vs stretching.
//If yo want the entire width just set the View with on the outer most View
.frame(width: geo.size.width, alignment: .center)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
The possible solution is to use screen bounds (which will be different for different phones) as reference value to calculate per-cent-based dynamic size for image. And to track device orientation changes we wrap our calculations into GeometryReader.
Note: I don't have your images, so added white borders for demo purpose
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
#State private var size = CGFloat(32) // some minimal initial value (not 0)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
// .border(Color.white) // << for demo !!
.background(GeometryReader { _ in
// GeometryReader is needed to track orientation changes
let sizeX = UIScreen.main.bounds.width
let sizeY = UIScreen.main.bounds.height
// Screen bounds is needed for reference dimentions, and use
// it to calculate needed size as per-cent to be dynamic
let width = min(sizeX, sizeY)
Color.clear // % (whichever you want)
.preference(key: ViewWidthKey.self, value: width * 0.2)
})
.onPreferenceChange(ViewWidthKey.self) {
self.size = max($0, size)
}
.frame(width: size, height: size)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
I have HStack with some images shown via ForEach view. Each image has DragGesture applied. I can drag an image all over the screen and the animation shown correctly. But when I put my HStack with images into the ScrollView when I drag an image (not scroll) the animation of gragging shown only within the ScrollView area. How can I make it show within the whole screen again?
import SwiftUI
struct SwiftUIView: View {
#State var position = CGSize.zero
#GestureState var dragOffset: [CGSize]
init() {
let dragOffsets = [CGSize](repeating: CGSize.zero, count: 36)
_dragOffset = GestureState(wrappedValue: dragOffsets)
}
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach ((0..<player.playersCards.count), id: \.self) { number in
Image(player.playersCards[number].pic)
.resizable()
.frame(width: 93, height: 127)
.modifier(CardStyle())
.offset(dragOffset[number])
.gesture(
DragGesture(coordinateSpace: .global)
.updating($dragOffset, body: { (value, state, transaction) in
state[number] = value.translation
})
)
.animation(.spring())
}
}
}.offset(x: 15, y: 0)
}
}
You just need to pack your image into another view (group or ZStack)! Set the height of this view to the height of the screen, then the image will move inside the parent view:
struct SwiftUIView: View {
private let colors = [Color.blue, Color.yellow, Color.orange, Color.gray, Color.black, Color.green, Color.white]
#State var position = CGSize.zero
#GestureState var dragOffset: [CGSize]
init() {
let dragOffsets = [CGSize](repeating: CGSize.zero, count: 36)
_dragOffset = GestureState(wrappedValue: dragOffsets)
}
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach (Array(0...6), id: \.self) { number in
ZStack {
Text("\(number.description)")
.frame(width: 93, height: 127)
.background(colors[number])
.offset(dragOffset[number])
.gesture(
DragGesture(coordinateSpace: .global)
.updating($dragOffset, body: { (value, state, transaction) in
state[number] = value.translation
})
)
.animation(.spring())
}
.frame(width: 93, height: UIScreen.main.bounds.height)
}
}
}
.background(Color.red)
}
}