There are a lot of examples of bottom sheet out there for SwiftUI, however they all specify some type of maximum height the sheet can grow to using a GeometryReader. What I would like is to create a bottom sheet that becomes only as tall as the content within it. I've come up with the solution below using preference keys, but there must be a better solution. Perhaps using some type of dynamic scrollView is the solution?
struct ContentView: View{
#State private var offset: CGFloat = 0
#State private var size: CGSize = .zero
var body: some View{
ZStack(alignment:.bottom){
VStack{
Button(offset == 0 ? "Hide" : "Show"){
withAnimation(.linear(duration: 0.2)){
if offset == 0{
offset = size.height
} else {
offset = 0
}
}
}
.animation(nil)
.padding()
.font(.largeTitle)
Spacer()
}
BottomView(offset: $offset, size: $size)
}.edgesIgnoringSafeArea(.all)
}
}
struct BottomView: View{
#Binding var offset: CGFloat
#Binding var size: CGSize
var body: some View{
VStack(spacing: 0){
ForEach(0..<5){ value in
Rectangle()
.fill(value.isMultiple(of: 2) ? Color.blue : Color.red)
.frame(height: 100)
}
}
.offset(x: 0, y: offset)
.getSize{
size = $0
offset = $0.height
}
}
}
struct SizePreferenceKey: PreferenceKey {
struct SizePreferenceData {
let bounds: Anchor<CGRect>
}
static var defaultValue: [SizePreferenceData] = []
static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct SizePreferenceModifier: ViewModifier {
let onAppear: (CGSize)->Void
func body(content: Content) -> some View {
content
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { [SizePreferenceKey.SizePreferenceData( bounds: $0)] })
.backgroundPreferenceValue(SizePreferenceKey.self) { preferences in
GeometryReader { geo in
Color.clear
.onAppear{
let size = CGSize(width: geo.size.width, height: geo.size.height)
onAppear(size)
}
}
}
}
}
extension View{
func getSize(_ onAppear: #escaping (CGSize)->Void) -> some View {
return self.modifier(SizePreferenceModifier(onAppear: onAppear))
}
}
Talk about over engineering the problem. All you have to do is specify a height of 0 if you want the sheet to be hidden, and not specify a height when it's shown. Additionally set the frame alignment to be top.
struct ContentView: View{
#State private var hide = false
var body: some View{
ZStack(alignment: .bottom){
Color.blue
.overlay(
Text("Is hidden : \(hide.description)").foregroundColor(.white)
.padding(.bottom, 200)
)
.onTapGesture{
hide.toggle()
}
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
.layoutPriority(2)
}
}
.layoutPriority(1)
.frame(height: hide ? 0 : nil, alignment: .top)
.animation(.linear(duration: 0.2))
}.edgesIgnoringSafeArea(.all)
}
}
My approach is SwiftUI Sheet based solution feel free to check the gist
you just need to add the modifier to the view and let iOS do the rest for you, no need to re-do the math ;)
Plus you will have the sheet native behavior (swipe to dismiss) and i added "tap elsewhere" to dismiss.
struct ContentView: View {
#State var activeSheet: Bool = false
#State var activeBottomSheet: Bool = false
var body: some View {
VStack(spacing: 16){
Button {
activeSheet.toggle()
} label: {
HStack {
Text("Activate Normal sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
Button {
activeBottomSheet.toggle()
} label: {
HStack {
Text("Activate Bottom sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
}
.sheet(isPresented: $activeSheet) {
// Regular sheet
sheetView
}
.sheet(isPresented: $activeBottomSheet) {
// Responsive sheet
sheetView
.asResponsiveSheet()
}
}
var sheetView: some View {
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
}
}
}
iPhone:
iPad :
Related
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}
Is there a way to remove the cornerRadius of a sheet? I tried it like this:
.sheet(isPresented: $showModal) {
Modal().cornerRadius(0, corners: [.topLeft, .topRight])
}
but it didn't work.
I know I can just use fullScreenCover but I still want to know if there is a solution to this.
According to my comment above you can create your own slide-in menu.
In the example below I added a close button as well as gesture control to close the view.
//
//
// SlideInMenu.swift
// SlideInMenu
//
// Created by Sebastian on 21.09.22.
//
import SwiftUI
var bounds = UIScreen.main.bounds
struct ContentView: View {
#State var selectedItem: String = ""
#State var showMenu = false
var body: some View {
ZStack() {
MainView(selectedItem: $selectedItem, showMenu: $showMenu)
.blur(radius: showMenu ? 3 : 0)
SlideView(selectedItem: $selectedItem, showMenu: $showMenu)
}
.edgesIgnoringSafeArea(.all)
}
}
struct MainView: View {
#Binding var selectedItem: String
#Binding var showMenu: Bool
var body: some View {
HStack(){
Spacer()
VStack() {
Spacer()
Text("This is your main View")
.foregroundColor(.white)
.padding()
Button(action: {
withAnimation(.linear(duration: 0.3)) {
self.showMenu.toggle()
}
}) {
Text("Show Menu")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
}
Spacer()
}
Spacer()
}.background(Color.blue)
}
}
struct SlideView: View {
#Binding var selectedItem: String
#Binding var showMenu: Bool
#State private var viewOffest: CGFloat = 100
#State private var offset = CGSize.zero
#State private var isDragging = false
var body: some View {
let dragGesture = DragGesture()
.onChanged { value in
withAnimation(.linear(duration: 0.2)) {
if value.translation.height >= 0 {
offset = value.translation
}
}
}
.onEnded { _ in
withAnimation(.linear(duration: 0.2)) {
isDragging = false
if offset.height > (bounds.height - viewOffest)/3 {
showMenu.toggle()
}
offset = .zero
}
}
ZStack() {
Color.black
.opacity(showMenu ? 0.5 : 0)
VStack() {
HStack() {
Spacer()
Spacer()
}
VStack(alignment: .leading) {
HStack() {
Spacer()
Text("Here is the menu")
.foregroundColor(.black)
Spacer()
}
HStack() {
Spacer()
Button(action: {
withAnimation(.linear(duration: 0.3)) {
self.showMenu.toggle()
}
}) {
Text("Close Menu")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.red)
}
.padding()
Spacer()
}
Spacer()
}
.padding()
.background(Color.white)
.cornerRadius(0)
}
.offset(y: showMenu ? viewOffest + offset.height : bounds.height)
.gesture(dragGesture)
}
}
}
It is not possible for now perhaps we can in further update, nonetheless you can create your own custom view.
As the title says I am trying to create a grid of random numbers that must be clicked in ascending order and when pressed the background changes color so they're no longer visible.
I figured out how to create the grid from 0 to what ever size grid I want (5x5, 10x10, etc...)
I want the background to be white to start and then when the button is pressed it changes to black
The two biggest issues I'm having are all of the buttons turning black after I press on one button and the numbers randomize every time I press a button.
Any one have any ideas?
import SwiftUI
struct ContentView: View {
#State var buttonbackg = Color.white
#State var gridsize = 100
var body: some View {
//make grid
let cellCount = (gridsize/10)
let r = numbrand(min: 00, max: ((gridsize/10) * (gridsize/10) - 1))
ZStack{
Rectangle()
.ignoresSafeArea()
.background(Color.black)
VStack{
Text("Concentration Grid")
.font(.title)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom)
Spacer()
}
VStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { i in
HStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { c in
let a = r()
ZStack {
Button(action:{
if (self.buttonbackg == .white) {
self.buttonbackg = .black
}
}){
Text("\(a)")
.foregroundColor(Color.black)
.frame(width: 28, height: 28)
.background(buttonbackg)
.cornerRadius(5)
.multilineTextAlignment(.center)
.scaledToFit()
.padding(.all, -2)
Spacer()
}
Spacer()
}
}
}
Spacer()
}
Spacer()
}
}
}
//^grid
func numbrand(min: Int, max:Int) -> () -> Int{
//let array: [Int] = Array(min...max)
var numbers: [Int] = []
return {
if numbers.isEmpty {
numbers = Array(min...max)
}
let index = Int(arc4random_uniform(UInt32(numbers.count)))
return numbers.remove(at: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Every-time you click a button, view redraws itself and thus your randomArray is created again
You need to create a Struct here which holds the property whether the
button is selected or not.
Give it a try this way:
struct concentrationGame: Hashable {
var id = UUID()
var number: Int = 0
var isSelected: Bool = false
}
class RandomNumbers: ObservableObject {
#Published var randomNumbers : [concentrationGame]
init() {
self.randomNumbers = (1...100).map{_ in arc4random_uniform(100)}.map({ concentrationGame(number: Int($0)) })
}
}
struct ContentView: View {
#ObservedObject var randomNumbers = RandomNumbers()
private var gridItemLayout = [GridItem(.adaptive(minimum: 50))]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach($randomNumbers.randomNumbers, id: \.self) { $randomNumbers in
Button(String(randomNumbers.number)) { randomNumbers.isSelected.toggle() }
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.foregroundColor(.white)
.background( randomNumbers.isSelected ? .white : .black)
.cornerRadius(10)
}
}
}
.padding()
.ignoresSafeArea(edges: .bottom)
.navigationTitle("Concentration Grid")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have an extension (.alertLinearProgressBar) like an .alert view to display a unzipping progress.
Before iOS 14 was working well, now if I put this alert, the navigation back button doesn't work anymore (if I drag right, is working and it come back to the list, so I guess the problem is a conflict with back button bar, only that button doesn't work, other button in top bar are working).
Anyone knows this problem?
import SwiftUI
var chatData = ["1","2","3","4"]
struct ContentView: View {
#State private var selection = 0
#State private var isShowingAlert = false
var body: some View {
TabView (selection: $selection) {
NavigationView {
ZStack (alignment: .bottom) {
MasterView()
.navigationBarTitle(Text("Chat"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
//
}
) {
Image(systemName: "plus.circle")
.contentShape(Rectangle())
})
.background(Color.clear)
}
}
//IF I comment this line below, the navigation is working well
.alertLinearProgressBar(isShowing: self.$isShowingAlert, progressValue: .constant(0.5), barHeight: 8, loadingText: .constant(""), titleText: .constant(NSLocalizedString("unzipping", comment: "")),isShowingActivityIndicator: .constant(true)).offset(x: 0, y: 1)
}
}
}
struct MasterView: View {
var body: some View {
ZStack {
List {
ForEach(chatData, id: \.self) { chat in
NavigationLink(
destination:
//Test2()
DetailView(text: chat)
) {
ChatRow(text: chat)//, progressValue: self.$progressValue)
}
}
}
}
}
}
struct ChatRow: View {
var text: String
var body: some View {
Text(text)
}
}
struct DetailView: View {
var text: String
var body: some View {
Text(text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AlertLinearProgressBar<Presenting>: View where Presenting: View {
#Binding var isShowing: Bool
#Binding var progressValue:Float
#State var barHeight: Int
#Binding var loadingText: String
#Binding var titleText: String
#Binding var isShowingProgressBar: Bool
#Binding var isShowingActivityIndicator: Bool
let presenting: () -> Presenting
var body: some View {
GeometryReader { geometry in
self.presenting()
.blur(radius: self.isShowing ? 2 : 0).offset(y:1)
.disabled(self.isShowing)
ZStack {
Rectangle()
.frame(width: geometry.size.width * 0.8,
height: self.titleText == "" ? 70:100)
.foregroundColor(Color.white)
.cornerRadius(15)
.shadow(radius: 20)
.overlay(
GeometryReader { geometry in
VStack {
if self.titleText != "" {
Text(self.titleText)
.bold()
.offset(x: 0, y: 0)
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
HStack {
Text("\(self.loadingText) " + "\(self.isShowingProgressBar ? self.progressValue.getPercentage(to: 1):"")")
.font(.caption)
ActivityIndicator(isAnimating: .constant(true), isShowing: self.$isShowingActivityIndicator, style: .medium)
}
//.font(.system(size: 13))
Spacer()
.frame(height:6)
ZStack(alignment: .leading) {
Rectangle()
.frame(width: geometry.size.width, height: CGFloat(self.barHeight))
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
.cornerRadius(5.0)
Rectangle()
.frame(width: min(CGFloat(self.progressValue)*geometry.size.width, geometry.size.width), height: CGFloat(self.barHeight))
.foregroundColor(Color.blue)
.animation(.linear)
.cornerRadius(5.0)
}.opacity(self.isShowingProgressBar ? 1 : 0)
}
}
.padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15))
)
.padding()
}
.frame(width: self.isShowing ? geometry.size.width:0,
height: self.isShowing ? geometry.size.height:0)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
extension Float {
//number of decimal
func round(to places: Int) -> Float {
let divisor = pow(10.0, Float(places))
return (self * divisor).rounded() / divisor
}
func getPercentage(to digits: Int) -> String {
if self >= 1 {
return String(Int(self * 100)) + "%"
}
return String(format: "%.\(digits)f", self * 100) + "%"
}
}
extension View {
func alertLinearProgressBar(isShowing: Binding<Bool>,
progressValue: Binding<Float>,
barHeight: Int, loadingText: Binding<String>, titleText: Binding<String>=Binding.constant(""), isShowingProgressBar: Binding<Bool>=Binding.constant(true), isShowingActivityIndicator:Binding<Bool>=Binding.constant(false)) -> some View {
AlertLinearProgressBar(isShowing: isShowing, progressValue: progressValue, barHeight: barHeight, loadingText: loadingText, titleText: titleText, isShowingProgressBar: isShowingProgressBar, isShowingActivityIndicator:isShowingActivityIndicator, presenting: {self})
}
}
struct ActivityIndicator: UIViewRepresentable {
#Binding var isAnimating: Bool
#Binding var isShowing: Bool
let style: UIActivityIndicatorView.Style
var color:UIColor?
func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
uiView.isHidden = isShowing ? false:true
if color != nil {
uiView.color = color!
}
}
}
It looks like your AlertLinearProgressBar even with the opacity set to 0 blocks the NavigationBar.
You can see that the overlay's position when hidden is in the top left corner and overlapping with the navigation bar (try setting .opacity(self.isShowing ? 1 : 0.5)).
What you can do is to truly hide it with the hidden modifier.
Here is a possible solution using the if modifier:
struct AlertLinearProgressBar<Presenting>: View where Presenting: View {
// ...
var body: some View {
GeometryReader { geometry in
ZStack {
// ...
}
.frame(width: self.isShowing ? geometry.size.width : 0,
height: self.isShowing ? geometry.size.height : 0)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0.5)
.if(!isShowing) {
$0.hidden() // use `hidden` here
}
}
}
}
extension View {
#ViewBuilder func `if`<T>(_ condition: Bool, transform: (Self) -> T) -> some View where T: View {
if condition {
transform(self)
} else {
self
}
}
}
Alternatively you can just display the view conditionally:
struct AlertLinearProgressBar<Presenting>: View where Presenting: View {
//...
var body: some View {
GeometryReader { geometry in
self.presenting()
.blur(radius: self.isShowing ? 2 : 0).offset(y: 1)
.disabled(self.isShowing)
if isShowing {
// ...
}
}
}
}
I have two different views, one red rect and one black rect that is always positioned on the bottom of the screen. When I click the red rect it should position itself inside the other rect.
Currently the red rect is positioned statically: .position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50). Is there a way to replace the 210 and 777 dynamically to the position of the black rects center position?
I know that I can use the GeometryReader to get the views size, but how do I use that size to position a different view? Would this even be the right way?
struct ContentView: View {
#State private var tap = false
var body: some View {
ZStack {
VStack {
Spacer()
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
}
.padding()
VStack {
ZStack {
Rectangle()
.foregroundColor(.red)
Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
}
.frame(width: 50, height: 50)
.position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}
}
}
}
First define some structure where to store .center position of some View
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
The build-in mechanism to save such data and expose them to parent View is to set / read (or react) on values which conforms to PreferenceKey protocol.
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
To be able to read the center positions of View we can use well known and widely discussed GeometryReader. I define my PositionReader as a View and here we can simply save its center position in our preferences for further usage. There is no need to translate the center to different coordinate system. To identify the View its tag value must be saved as well
struct PositionReader: View {
let tag: Int
var body: some View {
// we don't need geometry reader at all
//GeometryReader { proxy in
Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.tag, center: anchor)]
}
//}
}
}
To demonstrate how to use all this together see next simple application (copy - paste - run)
import SwiftUI
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
struct PositionReader: View {
let tag: Int
var body: some View {
Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.tag, center: anchor)]
}
}
}
struct ContentView: View {
#State var tag = 0
var body: some View {
ZStack {
VStack {
Color.green.background(PositionReader(tag: 0))
.onTapGesture {
self.tag = 0
}
HStack {
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 1))
.onTapGesture {
self.tag = 1
}
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 2))
.onTapGesture {
self.tag = 2
}
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 3))
.onTapGesture {
self.tag = 3
}
}
}
}.overlayPreferenceValue(Positions.self) { preferences in
GeometryReader { proxy in
Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
}
}
}
func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
let p = preferences.filter({ (p) -> Bool in
p.id == tag
})[0]
return proxy[p.center]
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The code is almost self explanatory, we use .background(PositionReader(tag:)) to save the center position of View (this could be avoided by applying .anchorPreference directly on the View) and
.overlayPreferenceValue(Positions.self) { preferences in
GeometryReader { proxy in
Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
}
}
is used to create small black rectangle which will position itself at center of other Views. Just tap anywhere in green or red rectangles, and the black one will move immediately :-)
Here is view of this sample application running.
Here is possible approach (with a bit simplified your initial snapshot and added some convenient View extension).
Tested with Xcode 11.2 / iOS 13.2
extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
struct ContentView: View {
#State private var tap = false
#State private var bottomRect: CGRect = .zero
var body: some View {
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
.padding()
.rectReader($bottomRect, in: .named("board"))
Rectangle()
.foregroundColor(.red)
.overlay(Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
)
.frame(width: 50, height: 50)
.position(x: self.tap ? bottomRect.midX : 50,
y: self.tap ? bottomRect.midY : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}.coordinateSpace(name: "board")
}
}