Swiftui Set Position to Center of Different View

SwiftUI set position to center of different view

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

Sample Image

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

SwiftUI how to center position an item in VStack

A possible approach is to use Color.clear and overlay, like

VStack {
Color.clear

Text("Label") // << your label view here

Color.clear.overlay(Group {
if condition {
Button("Title") {} // << your button here
}
}, alignment: .top)

}

SwiftUI - set the centre of a text view to be in an absolutely fixed position at all times

You can put your View in a ZStack and the ":" on another Z-level, so it no longer depends on other elements in the same horizontal stack.

How to position views relative to other views in different coordinate systems in SwiftUI

After a few more trying around I found a solution that works, also no matter how many views are created.
Using a GeometryReader as the parent of each Circle I'm getting the .global and .local position of the object I want to drag. I subtract the global position from its local position and to this I add the global position of the target. That gives me the new absolute position of the object, its destination, in its local coordinate space.
My code also has the drag and drop implemented as well as a ViewModifier for the Circle for convenience and future use.
I'm using the two underlying CGRects of the Circles to test for intersection.
It's important to note that the initial positions of the circle are also set using the GeometryReader

If this can be simplified please post your comment or answer.

import SwiftUI

struct ContentView: View {

@State private var isDragging: Bool = false
@State private var atTarget: Bool = false

@State private var objectDragOffset: CGSize = .zero
@State private var objectPosition: CGPoint = .zero
@State private var objectGlobalPosition: CGPoint = .zero

@State private var targetGlobalPosition: CGPoint = .zero
@State private var newObjectPosition: CGPoint = .zero

var body: some View {

VStack {
HStack {
GeometryReader { geometry in
Circle()
.stroke(lineWidth: 3)
.fill(Color.blue)
.modifier(CircleModifier())
.position(CGPoint(x:geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY))
.onAppear() {
targetGlobalPosition = CGPoint(x:geometry.frame(in: .global).midX, y: geometry.frame(in: .global).midY)
}
}
}.background(Color.yellow)

HStack {
GeometryReader { geometry in
Circle()
.foregroundColor(.red)
.position(atTarget ? newObjectPosition : CGPoint(x:geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY))
.modifier(CircleModifier())
.onAppear() {
objectPosition = CGPoint(x:geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
objectGlobalPosition = CGPoint(x:geometry.frame(in: .global).midX, y: geometry.frame(in: .global).midY)
}
.offset(isDragging ? objectDragOffset : .zero)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged { drag in
isDragging = true
objectDragOffset = drag.translation

newObjectPosition = CGPoint(
x: objectPosition.x - objectGlobalPosition.x + targetGlobalPosition.x,
y: objectPosition.y - objectGlobalPosition.y + targetGlobalPosition.y
)
}
.onEnded { drag in
isDragging = false

let targetFrame = CGRect(origin: targetGlobalPosition, size: CircleModifier.frame)
let objectFrame = CGRect(origin: objectGlobalPosition, size: CircleModifier.frame)
.offsetBy(dx: drag.translation.width, dy: drag.translation.height)
.insetBy(dx: CircleModifier.frame.width * 0.1, dy: CircleModifier.frame.height * 0.1)

if targetFrame.intersects(objectFrame) {
// Will check for the intersection of the rectangles, not the circles. See above insetBy adjustment to get a good middle ground.
atTarget = true
} else {
atTarget = false
}
}
)
}
}
}
}
}

struct CircleModifier: ViewModifier {
static let frame = CGSize(width: 100.0, height: 100.0)

func body(content: Content) -> some View {
content
.frame(width: CircleModifier.frame.width, height: CircleModifier.frame.height, alignment: .center)
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Based on the above solution I created a modifier that uses an internal .offset to align views. Maybe someone finds this helpful:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
/// Positions the center of this view at the specified point in the specified
/// coordinate space using offset.
///
/// Use the `openRelativeOffset(_ position:in:)` modifier to place the center of a view at a
/// specific coordinate in the specified coordinate space using a
/// CGPoint to specify the `x`
/// and `y` position of the target CoordinateSpace defined by the Enum `coordinateSpace`
/// This is not changing the position of the view by internally using an offset, other views using auto layout should not be affected.
///
/// Text("Position by passing a CGPoint() and CoordinateSpace")
/// .openRelativeOffset(CGPoint(x: 175, y: 100), in: .global)
/// .border(Color.gray)
///
/// - Parameters
/// - position: The point in the target CoordinateSpace at which to place the center of this. Uses auto layout if nil.
/// view.
/// - in coordinateSpace: The target CoordinateSpace at which to place the center of this view.
///
/// - Returns: A view that fixes the center of this view at `position` in `coordinateSpace` .
func openRelativeOffset(_ position: CGPoint?, in coordinateSpace: CoordinateSpace) -> some View {
modifier(OpenRelativeOffset(position: position, coordinateSpace: coordinateSpace))
}
}

private struct OpenRelativeOffset: ViewModifier {

var position: CGPoint?
@State private var newPosition: CGPoint = .zero
@State private var newOffset: CGSize = .zero

let coordinateSpace: CoordinateSpace

@State var localPosition: CGPoint = .zero
@State var targetPosition: CGPoint = .zero

func body(content: Content) -> some View {

if let position = position {
return AnyView(
content
.offset(newOffset)
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
let localFrame = geometry.frame(in: .local)
let otherFrame = geometry.frame(in: coordinateSpace)

localPosition = CGPoint(x: localFrame.midX, y: localFrame.midY)

targetPosition = CGPoint(x: otherFrame.midX, y: otherFrame.midY)
newPosition.x = localPosition.x - targetPosition.x + position.x
newPosition.y = localPosition.y - targetPosition.y + position.y

newOffset = CGSize(width: newPosition.x - abs(localPosition.x), height: newPosition.y - abs(localPosition.y))
}
}
)
)
} else {
return AnyView(
content
)
}
}
}

The source code is also available in this repository with another version that internally uses .position.

https://github.com/marcoboerner/OpenSwiftUIViews

SwiftUI - how to get the exactly center coordinate/position of newly created Button

One way to accomplish this is utilising "anchor preferences".

The idea is, to create the bounds anchor of the button when it is created and store it into an anchor preference.

To get an actual bounds value, we need a GeometryProxy where we relate the bounds anchor and get the bounds value.

When we have the bounds value, we store it in a state variable where they are accessible when the button action executes.

The following solution creates a number of buttons where the bounds are accessible via a Dictionary where the key is the button's label.

import SwiftUI

struct ContentView: View {

let labels = (0...4).map { "- \($0) -" }

@State private var bounds: [String: CGRect] = [:]

var body: some View {
VStack {
ForEach(labels, id: \.self) { label in
Button(action: {
let bounds = bounds[label]
print(bounds ?? "")
}) {
Text(verbatim: label)
}
// Store bounds anchors into BoundsAnchorsPreferenceKey:
.anchorPreference(
key: BoundsAnchorsPreferenceKey.self,
value: .bounds,
transform: { [label: $0] })
}
}
.frame(width: 300, height: 300, alignment: .center)
.backgroundPreferenceValue(BoundsAnchorsPreferenceKey.self) { anchors in
// Get bounds relative to VStack:
GeometryReader { proxy in
let localBoundss = anchors.mapValues { anchor in
CGRect(origin: proxy[anchor].origin, size: proxy[anchor].size)
}
Color.clear.border(Color.blue, width: 1)
.preference(key: BoundsPreferenceKey.self, value: localBoundss)
}
}
.onPreferenceChange(BoundsPreferenceKey.self) { bounds in
// Store bounds into the state variable:
self.bounds = bounds
}
}

}

extension CGRect: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(origin.x)
hasher.combine(origin.y)
hasher.combine(size.width)
hasher.combine(size.height)
}
}

struct BoundsAnchorsPreferenceKey: PreferenceKey {
typealias Value = [String: Anchor<CGRect>]
static var defaultValue: Value = [:]

static func reduce(value: inout Value, nextValue: () -> Value) {
value = value.merging(nextValue()) { (_, new) in new }
}
}

struct BoundsPreferenceKey: PreferenceKey {
typealias Value = [String: CGRect]
static var defaultValue: Value = [:]

static func reduce(value: inout Value, nextValue: () -> Value) {
value = value.merging(nextValue()) { (_, new) in new }
}
}


import PlaygroundSupport
PlaygroundPage.current.setLiveView(
NavigationView {
ContentView()
}
.navigationViewStyle(.stack)
)

The solution as is, looks a bit elaborated - but doesn't use any "tricks". We may alleviate this somewhat with using ViewModifiers.

Can not get correct position of view in SwiftUI

It seems to work just fine with .global coordinates. See my demo below. Maybe there is a problem with the use of xAxis in TabBarShape ... can't tell because you didn't share it.

Sample Image

import SwiftUI

enum Tab: CaseIterable {
case home
case search
case media
}

struct ContentView: View {
@State var selectedTab: Tab = .home
@State private var xAxis: CGFloat = 0

private let home = Text("home")
private let search = Text("search")
private let media = Text("media")

var body: some View {
ZStack(alignment: .center) {
TabView(selection: $selectedTab) {
switch selectedTab {
case .home:
home
.ignoresSafeArea()
case .search:
search
.ignoresSafeArea()
case .media:
media
.ignoresSafeArea()
}
}
VStack {
Spacer()
HStack(spacing: 0) {
ForEach(Tab.allCases, id: \.self) { tab in
if tab == .search { Spacer(minLength: 0) }

GeometryReader { proxy in
Button {
withAnimation {
selectedTab = tab
xAxis = proxy.frame(in: .global).midX // .global should work fine
}
} label: {
Image(systemName: "house")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 25, height: 25)
.foregroundColor(tab == selectedTab ? Color.blue : Color.gray)
}
.onAppear {
if tab == selectedTab {
xAxis = proxy.frame(in: .global).midX // set for initial selection
}
}
}
.frame(width: 25, height: 25)

if tab == .search { Spacer(minLength: 0) }
}
}
.padding(.horizontal, 60)
.padding(.vertical)
.background(
ZStack {
Color.green
Circle().fill(.white).frame(width: 50)
.position(x: xAxis) // applied here
}
)
.cornerRadius(50)
// .padding(.horizontal, idiomIsPhone() ? nil : tabBarHorizontalPaddingForIpad())
Spacer()
.frame(height: 5)
}
}
}
}

How To Position Views Relative To Their Top Left Corner In SwiftUI

@Asperi 's answer will solve the problem. But, I think we should use Spacer() rather than Color.clear and ZStack.

Spacer is specifically designed for these scenarios and makes the code easier to understand.

struct HomeView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Top Text")
.font(.system(size: 20))
.fontWeight(.medium)

Text("Bottom Text")
.font(.system(size: 12))
.fontWeight(.regular)
Spacer()
}
Spacer()
}
}
}

SwiftUI layout system is different from UIKit.

It asks each child view to calculate its own size based on the bounds of its parent view. Next, asks each parent to position its children within its own bounds.

https://www.hackingwithswift.com/books/ios-swiftui/how-layout-works-in-swiftui



Related Topics



Leave a reply



Submit