SwiftUI: Get height of content inside of ScrollView to update frame - swiftui

I need to update the height of my ScrollView depending on the content height inside of it. I have a working carousel below:
struct PostsPaging<Content>: View where Content: View {
#Binding var index: Int
let maxIndex: Int
let content: () -> Content
#State private var offset = CGFloat.zero
init(index: Binding<Int>, maxIndex: Int, #ViewBuilder content: #escaping () -> Content) {
self._index = index
self.maxIndex = maxIndex
self.content = content
}
var body: some View {
ZStack(alignment: .bottomTrailing) {
GeometryReader { geometry in
ScrollView (.horizontal, showsIndicators: false){
LazyHStack(spacing: 0) {
self.content()
.fixedSize()
.frame(width: geometry.size.width)
.clipped()
}
}
.content.offset(x: self.offset(in: geometry), y: 0)
.frame(width: geometry.size.width, alignment: .leading)
.animation(.spring())
}.clipped()
}
}
}
This is how it's called:
PostsPaging(index: self.$index.animation(), maxIndex: self.posts.count - 1) {
ForEach(self.posts, id: \.self) { post in
SmallPostCard(...)
}
}
This seems to only return the same value, over and over again:
.background(
GeometryReader { proxy in
Color.clear.onAppear { print(proxy.size.height) }
})
What would be the best way to find out the height of the content and update the height of the carousel on scroll?

Related

SwiftUI: Adapt size and position of one View to another View

I would like to adapt the position of one View (Rectangle) to another View (Button1). I only found solutions between a parent and a child-view or two child-views of the same parent view. Any tips how I could do that in this situation?
struct View: View {
var body: some View {
ZStack {
VStack {
Spacer()
HStack {
Spacer()
Button {} label: {
Text("1").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("2").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("3").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("4").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
}
Spacer()
}
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
Rectangle() // adapt this Rectangle to the button
.cornerRadius(10)
.frame(width: 200, height: 200)
.blendMode(.destinationOut)
}
.compositingGroup()
}
.background(Color.blue)
}
}
edit: I want to align the rectangle with the button to code a tutorial.
Thanks a lot!
This was the solution I found.
struct View: View {
#State private var sizeOfHole = CGSize()
#State private var positionOfHole = CGPoint()
var body: some View {
ZStack {
VStack {
Spacer()
HStack {
Spacer()
Button {
} label: {
Text("1").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
.readSize { size in
sizeOfHole = size
} // get the size of the button
.readPosition{ position in
positionOfHole = position
} // get the position of the button
Spacer()
Button {} label: {
Text("2").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("3").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("4").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
}
Spacer()
}
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
Rectangle()
.cornerRadius(10)
.frame(width: sizeOfHole.width + 10, height: sizeOfHole.height + 10) // set the size of the holw
.position(x: positionOfHole.x + (sizeOfHole.width / 2), y: positionOfHole.y - (sizeOfHole.height / 2)+1) // set the position of the hole
.blendMode(.destinationOut)
}
.compositingGroup()
}
.background(Color.blue)
}
}
// added this
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
extension View {
func readPosition(onChange: #escaping (CGPoint) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: PositionPreferenceKey.self, value: geometryProxy.frame(in: CoordinateSpace.global).origin)
}
)
.onPreferenceChange(PositionPreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
private struct PositionPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
}
Credit go to: Get width of a view using in SwiftUI
and https://stackoverflow.com/a/56520321/

SwiftUI View Overlap when integrated with Drag gesture

I am new to swiftUI and facing an issue when working with DragGesture functionality of view. I am trying to create a photo gallery app which have swipe functionality. Facing an issue with having a gesture view embedded in HStack with other 2 views. The 1st view of HStack get covered by drag view but the 3rd view of HStack is shown on top of Drag view.
How can I make the 1st view also to be shown on top of my gesture view.
The Review screen is the view where the Gesture view is created.
struct ReviewScreen: View {
#State var swipeHorizontalDirection: SwipeHorizontalDirection = .none { didSet { print(swipeHorizontalDirection) } }
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 20) {
HStack(spacing: 20) {
Image("red_rectangle")
.resizable()
.frame(width: 12, height: 100)
Image(uiImage: renderImage)
.resizable()
.frame(width: geometry.size.width * 0.75, height: geometry.size.height * 0.55)
.cornerRadius(10)
.onSwipe { direction in
switch direction {
case .right:
break
case .left:
self.presentationMode.wrappedValue.dismiss()
default:
break
}
}
Image("green_rectangle")
.resizable()
.frame(width: 12, height: 100)
}
Spacer()
}
.padding(20)
}
}
}
And the Swipe modifier code is -
struct SwipeModifier: ViewModifier {
let action: ((UISwipeGestureRecognizer.Direction) -> Void)?
#State var offset: CGSize = .zero
#State var startingOffsetX: CGFloat = UIScreen.main.bounds.width
init(perform action: ((UISwipeGestureRecognizer.Direction) -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
ZStack {
content
.offset(offset)
.scaleEffect(getScaleAmount())
.gesture(
DragGesture()
.onChanged({ value in
withAnimation(.spring()) {
offset = CGSize(width: value.translation.width, height: 0)
}
})
.onEnded({ value in
guard let action = action else {
return
}
withAnimation(.spring()) {
if value.location.x != 0,
value.startLocation.x < value.location.x && offset.width > 150 {
print("right call >> \(offset.width)")
action(.right)
}
else if value.startLocation.x > value.location.x && offset.width < -150 {
print("left call >> \(offset.width)")
action(.left)
}
offset = .zero
}
})
)
}
}
func getScaleAmount() -> CGFloat {
let max = UIScreen.main.bounds.width / 2
let currentAmount = abs(offset.width)
let percentage = currentAmount / max
return 1.0 - min(percentage, 0.5) * 0.5
}
}
extension View {
public func onSwipe(perform action: ((UISwipeGestureRecognizer.Direction) -> Void)? = nil) -> some View {
return self.modifier(SwipeModifier(perform: action))
}
}
enter image description here

Vertical Paging in 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)
}
}

ScrollView limits DragGesture animation

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

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 {
...
}