Vertical Paging in SwiftUI - swiftui

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)
}
}

Related

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 - Hovering a View outside of a Content View

How can I hover a view outside of its content view ?
For e.g content View Height = 400, Hover View Height = 1000
Hover View will need to overlap the content view
struct ContentView: View {
#State var isHovering = false
var body: some View {
Button(action: {
print("tap")
}) {
Text("Hover Me")
}.onHover(perform: { hovering in
isHovering.toggle()
}).overlay(VStack {
if self.isHovering {
Rectangle()
.background(Color.red)
.frame(width: 400, height: 1000)
} else {
EmptyView()
}
}).frame(width: 400, height: 400)
}
}
In this case, hover view will match content view height.
When isHovering is true, the height and width of the Text object will be equal to the Rectangle object.
Button(action: {
print("tap")
}) {
Text("Hover Me")
.frame(width: self.isHovering ? 400 : .none, height: self.isHovering ? 1000 : .none)
}.onHover(perform: { hovering in
isHovering.toggle()
}).overlay(VStack {
if self.isHovering {
Rectangle()
.foregroundColor(.red)
.frame(width: 400, height: 1000)
} else {
EmptyView()
}
}).frame(width: 400, height: 400)

Adding button to a moving image SwiftUI

I have successfully added a button to an image however when I click the button and the image moves via CGAffineTransform, the image moves but the button stays in the same place. When I click the space where the image used to be before the transform, the state toggle reacts. How do I get the button to move along with the image?
struct SliderView: View {
#State var partyType: Bool = false
var body: some View {
ZStack{
GeometryReader{ geo in
VStack(alignment: .leading){
Button(action: {
withAnimation (){
self.partyType.toggle();
}
}) {
Image("chevronPointer")
.clipped()
.modifier(SlideEffect(offset: CGSize(width: partyType ? geo.size.width - 70 : 0, height: 0.0)))
.animation(.smooth())
.padding(.horizontal)
}
Image("sliderLine")
.resizable()
.scaledToFit()
.frame(width: geo.size.width, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
}
}
.frame(height: 40)
}
}
}
struct SlideEffect: GeometryEffect {
var offset: CGSize
var animatableData: CGSize.AnimatableData {
get { CGSize.AnimatableData(offset.width, offset.height) }
set { offset = CGSize(width: newValue.first, height: newValue.second) }
}
public func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(CGAffineTransform(translationX: offset.width, y: offset.height))
}
}
struct SliderView_Previews: PreviewProvider {
static var previews: some View {
SliderView()
}
}
Just move the modifier outside the button.
Button(action: {
withAnimation(){
self.partyType.toggle()
}
}) {
Image(systemName: "chevron.up")
.clipped()
.animation(.default)
.padding(.horizontal)
}
.modifier(SlideEffect(offset: CGSize(width: partyType ? geo.size.width - 70 : 0, height: 0.0))) /// here!

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 onTapGesture on Color.clear background behaves differently to Color.blue

I am making a custom Picker in the SegmentedPickerStyle(). I want to have the same behaviour but when I tap on the area between the content and the border of one of the possible selections the onTapGesture does not work. When I add a blue background it does work but with a clear background it doesn't.
Working with blue background
Not working with clear background
Not working code:
import SwiftUI
struct PickerElementView<Content>: View where Content : View {
#Binding var selectedElement: Int
let content: () -> Content
#inlinable init(_ selectedElement: Binding<Int>, #ViewBuilder content: #escaping () -> Content) {
self._selectedElement = selectedElement
self.content = content
}
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
// ##################################################################
// CHANGE COLOR HERE TO BLUE TO MAKE IT WORK
// ##################################################################
.background(Color.clear)
// ##################################################################
.border(Color.yellow, width: 5)
}
}
}
struct PickerView: View {
#Environment (\.colorScheme) var colorScheme: ColorScheme
var elements: [(id: Int, view: AnyView)]
#Binding var selectedElement: Int
#State var internalSelectedElement: Int = 0
private var width: CGFloat = 220
private var height: CGFloat = 100
private var cornerRadius: CGFloat = 20
private var factor: CGFloat = 0.95
private var color = Color(UIColor.systemGray)
private var selectedColor = Color(UIColor.systemGray2)
init(_ selectedElement: Binding<Int>) {
self._selectedElement = selectedElement
self.elements = [
(id: 0, view: AnyView(PickerElementView(selectedElement) {
Text("9").font(.system(.title))
})),
(id: 1, view: AnyView(PickerElementView(selectedElement) {
Text("5").font(.system(.title))
})),
]
self.internalSelectedElement = selectedElement.wrappedValue
}
func calcXPosition() -> CGFloat {
var pos = CGFloat(-self.width * self.factor / 4)
pos += CGFloat(self.internalSelectedElement) * self.width * self.factor / 2
return pos
}
var body: some View {
ZStack {
Rectangle()
.foregroundColor(self.selectedColor)
.cornerRadius(self.cornerRadius * self.factor)
.frame(width: self.width * self.factor / CGFloat(self.elements.count), height: self.height - self.width * (1 - self.factor))
.offset(x: calcXPosition())
.animation(.easeInOut(duration: 0.2))
HStack {
ForEach(self.elements, id: \.id) { item in
item.view
.gesture(TapGesture().onEnded { _ in
print(item.id)
self.selectedElement = item.id
withAnimation {
self.internalSelectedElement = item.id
}
})
}
}
}
.frame(width: self.width, height: self.height)
.background(self.color)
.cornerRadius(self.cornerRadius)
.padding()
}
}
struct PickerView_Previews: PreviewProvider {
static var previews: some View {
PickerView(.constant(1))
}
}
Change the color where I marked it.
Does anyone know why they behave differently and how I can fix this?
The one line answer is instead of setting backgroundColor, please set contentShape for hit testing.
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
// ##################################################################
// CHANGE COLOR HERE TO BLUE TO MAKE IT WORK
// ##################################################################
.contentShape(Rectangle())
// ##################################################################
.border(Color.yellow, width: 5)
}
}
Transparent views are not tappable by default in SwiftUI because their content shape is zero.
You can change this behavior by using .contentShape modifier:
Color.clear
.frame(width: 300, height: 300)
.contentShape(Rectangle())
.onTapGesture { print("tapped") }
It appears to be a design decision that any Color with an opacity of 0 is untappable.
Color.clear.onTapGesture { print("tapped") } // will not print
Color.blue.opacity(0).onTapGesture { print("tapped") } // will not print
Color.blue.onTapGesture { print("tapped") } // will print
Color.blue.opacity(0.0001).onTapGesture { print("tapped") } // will print
You can use the 4th option to get around this, as it is visually indistinguishable from the 1st.
I was struggling a similar problem to get the tap on a RoundedRectangle.
My simple solution was to set the opacity to a very low value and it worked
RoundedRectangle(cornerRadius: 12)
.fill(Color.black)
.opacity(0.0001)
.frame(width: 32, height: 32)
.onTapGesture {
...
}