SwiftUI provides several powerful mechanisms for layout control and view coordination. Let’s explore some key concepts:
matchedGeometryEffect
matchedGeometryEffect
allows you to create smooth animations between views that share the same identity. This is particularly useful for transitions where you want elements to morph from one state to another.
In the example below, we create a highlight effect that follows selected buttons:
struct MatchedGeometryView: View {
@Namespace var namespace
let groupID = "highlight"
var body: some View {
VStack {
Button("Sign Up") {}
.matchedGeometryEffect(id: groupID, in: namespace)
Button("Log In") {}
//.matchedGeometryEffect(id: groupID, in: namespace)
}
.overlay {
Ellipse()
.strokeBorder(Color.red, lineWidth: 1)
.padding(-5)
.matchedGeometryEffect(id: groupID, in: namespace, isSource: false)
}
}
}
#Preview("MatchedGeometryView") {
MatchedGeometryView()
}
struct CoordinateSpaceView: View {
var body: some View {
VStack {
Text("Hello")
Text("Second")
.overlay { GeometryReader { proxy in
let _ = print([
proxy.frame(in: .global),
proxy.frame(in: .local),
proxy.frame(in: .named("Stack"))
])
Color.clear
}}
.scaleEffect(0.5)
}
.coordinateSpace(name: "Stack")
}
}
#Preview("CoordinateSpaceView") {
CoordinateSpaceView()
}
Coordinate Spaces
SwiftUI offers different coordinate spaces for positioning and measuring views:
.global
: Coordinates relative to the device screen.local
: Coordinates relative to the view’s immediate parent.named
: Custom coordinate space you define
The example above demonstrates how to access frame information in different coordinate spaces using GeometryReader.
struct DecorationView: View {
var input: [String] = ["Dallas","San Francisco"]
@State var maxWidth: CGFloat = 50
var body: some View {
VStack {
ForEach(input, id: \.self) { item in
Text(item)
.overlay {
GeometryReader { proxy in
Color.clear
.preference(key: MaxWidthKey.self, value: proxy.size.width)
}
}
}
Rectangle()
.frame(width: maxWidth, height: 5)
.padding()
}
.onPreferenceChange(MaxWidthKey.self) { value in
maxWidth = value
}
}
}
struct MaxWidthKey: PreferenceKey {
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
static var defaultValue: CGFloat = 0
}
#Preview("Preference-DecorationView") {
DecorationView()
}
## Preferences and PreferenceKey
Preferences provide a way for child views to pass data up the view hierarchy to parent views. This is useful for:
- Collecting information from child views
- Coordinating layouts
- Sharing data between non-adjacent views
The `DecorationView` example shows how to:
1. Define a PreferenceKey for tracking maximum width
2. Use GeometryReader to measure view dimensions
3. Propagate values up the view hierarchy
4. React to preference changes with onPreferenceChange
struct CollectView: View {
@State var superTitle: String = "None"
var body: some View {
VStack {
Text("\(superTitle)")
.font(.headline)
List {
ChildView()
ChildView()
ChildView()
}
}
.onPreferenceChange(titleKey.self) { value in
superTitle = "\(value)"
}
}
}
struct ChildView: View {
@State var value: Int = 0
var body: some View {
Text("\(value)")
.onAppear {
value = (Int.random(in: 1000...5000))
}
.preference(key: titleKey.self, value: value)
}
}
struct titleKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
let _ = print(nextValue())
value = value + nextValue()
}
}
#Preview("Preference-CollectView") {
CollectView()
}
## Value Collection with Preferences
The `CollectView` demonstrates how to:
- Collect and aggregate values from multiple child views
- Use PreferenceKey's reduce function to combine values
- Update parent view state based on collected values
struct InteractionUI: View {
var body: some View {
HStack(alignment: .firstTextBaseline) {
image
Text("Pencil")
.badge {
Text("100")
}
}
}
var image: some View {
Image(systemName: "pencil.circle.fill")
.alignmentGuide(.firstTextBaseline) { dimension in
dimension[VerticalAlignment.center]
}
}
}
#Preview {
InteractionUI()
}
## Custom Alignment Guides
AlignmentGuide allows you to customize how views align relative to each other. You can:
- Define custom alignment behavior
- Override default alignment positions
- Create complex layouts with precise control
The `InteractionUI` example shows:
- Custom baseline alignment for images with text
- Badge positioning using alignment guides
- Creating reusable view modifiers with alignment customization
extension View {
func badge<Badge: View>(@ViewBuilder content: () -> Badge) -> some View {
self.overlay(alignment: .topTrailing) {
content()
.alignmentGuide(.top, computeValue: { dim in
dim.height/2
})
.alignmentGuide(.trailing, computeValue: { dim in
dim.width/2
})
.padding(3)
.background {
RoundedRectangle(cornerRadius: 5).fill(.teal)
}
.fixedSize()
}
}
}