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
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.
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
Convert Generic Free Function into Array Extension
How to Rotate Only One View Controller to Landscape Orientation in iOS Swift 3
How to Install Xcode on an External Hard Drive Along with the iPhone Simulator.App
What Are 'Get' and 'Set' in Swift
Difference Between "Precondition" and "Assert" in Swift
How to Make the Memberwise Initialiser Public, by Default, for Structs in Swift
Remove All Non-Numeric Characters from a String in Swift
Custom Swift Encoder/Decoder for the Strings Resource Format
Select Multiple Rows in Tableview and Tick the Selected Ones
Child View Controller to Rotate While Parent View Controller Does Not
How to Detect Vertical Planes in Arkit
Lazy Loading Properties in Swift
Consume Swift Package for Multiple Targets and Platforms in a Project
Implicitly Lazy Static Members in Swift
Today Extension Failed to Inherit Coremedia Permissions From