Skip to content

Instantly share code, notes, and snippets.

@Snowy1803
Last active July 28, 2023 01:37
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Snowy1803/c077b60190c2543a823526a0c73ff142 to your computer and use it in GitHub Desktop.
Save Snowy1803/c077b60190c2543a823526a0c73ff142 to your computer and use it in GitHub Desktop.
This code allows you to use matchedGeometryEffect in SwiftUI while keeping iOS 13 compatibility in your app.

NamespaceWrapper

This gist allows you tu use matchedGeometryEffect in SwiftUI 1, with an iOS 13 deployment target. iOS 13 users will get a fade animation, while iOS 14 users can get a beautiful matched geometry effect.

Instead of making a @Namespace, just use the .namespaced() modifier in your common view, and instead of .matchedGeometryEffect, use .namespacedMatchedGeometryEffect. All the namespace handling is done in .namespaced(), so you don't pass a Namespace.ID to your effect modifier.

The only missing property is the properties argument. It can be added pretty easily, by using its rawValue as argument (because its type is iOS 14-only).

//
// ContentView.swift
// Example of using matchedGeometryEffect in iOS 13 code
// matchedGeometryEffect example code taken and adapted from :
// https://sarunw.com/posts/a-first-look-at-matchedgeometryeffect/
//
// Created by Emil Pedersen on 16/10/2020.
//
struct ContentView: View {
@State private var isExpanded = false
var body: some View {
Group {
if isExpanded {
VStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: 60, height: 60)
.namespacedMatchedGeometryEffect(id: "rect")
Text("Hello SwiftUI!").fontWeight(.semibold)
.namespacedMatchedGeometryEffect(id: "text")
}
} else {
HStack {
Text("Hello SwiftUI!").fontWeight(.semibold)
.namespacedMatchedGeometryEffect(id: "text")
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.pink)
.frame(width: 60, height: 60)
.namespacedMatchedGeometryEffect(id: "rect")
}
}
}.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}.namespaced()
}
}
//
// NamespaceWrapper.swift
// NamespaceWrapper for using matchedGeometryEffect in iOS 13 code
//
// Created by Emil Pedersen on 16/10/2020.
//
import SwiftUI
@available(iOS 14, *)
struct NamespaceWrapper<Content: View>: View {
@Namespace var namespace
var content: Content
var body: some View {
content.environment(\.namespace, namespace)
}
}
@available(iOS 14, *)
struct NamespaceReader<Content: View, ID: Hashable>: View {
@Environment(\.namespace) var namespace
var content: Content
var id: ID
var anchor: UnitPoint = .center
var isSource: Bool = true
var body: some View {
content.matchedGeometryEffect(id: id, in: namespace!, anchor: anchor, isSource: isSource)
}
}
@available(iOS 14, *)
struct NamespaceKey: EnvironmentKey {
static let defaultValue: Namespace.ID? = nil
}
@available(iOS 14, *)
extension EnvironmentValues {
var namespace: Namespace.ID? {
get {
self[NamespaceKey.self]
}
set {
self[NamespaceKey.self] = newValue
}
}
}
extension View {
func namespaced() -> AnyView {
if #available(iOS 14, *) {
return AnyView(NamespaceWrapper(content: self))
} else {
return AnyView(self)
}
}
func namespacedMatchedGeometryEffect<ID>(id: ID, anchor: UnitPoint = .center, isSource: Bool = true) -> some View where ID : Hashable {
if #available(iOS 14, *) {
return AnyView(NamespaceReader(content: self, id: id, anchor: anchor, isSource: isSource))
} else {
return AnyView(self)
}
}
}
@ggrell
Copy link

ggrell commented Feb 5, 2021

Nice job, thanks for sharing this!

@Metilli
Copy link

Metilli commented Jul 7, 2022

That's working well. Thank you for saving a lot of time!

@shaps80
Copy link

shaps80 commented Jul 13, 2022

I actually have a library – https://github.com/shaps80/SwiftUIBackports – that includes many full backports and will soon include support for this as well, as in the same feature will work in iOS 13 as well. Almost all backports work on the expected platforms as well (i.e. iOS, macOS, tvOS, watchOS)


Can I also suggest a couple small improvements regardless. You can drop the AnyView which is less performant and problematic at times with things like animation.

@ViewBuilder // you're missing this
func namespaced() -> some View {
    if #available(iOS 14, *) {
        NamespaceWrapper(content: self)
    } else {
        self
    }
}

@ViewBuilder
func namespacedMatchedGeometryEffect<ID>(id: ID, anchor: UnitPoint = .center, isSource: Bool = true) -> some View where ID : Hashable {
    if #available(iOS 14, *) {
        NamespaceReader(content: self, id: id, anchor: anchor, isSource: isSource)
    } else {
        self
    }
}

Also in NamespaceReader you have namespace! – you could instead just set your defaultValue in your EnvironmentKey to a default instance and get rid of the potential crash 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment