ContainerRelativeShape in SwiftUI (iOS 14+)

DevTechie Inc
Jan 29, 2023


ContainerRelativeShape Modifier in SwiftUI (iOS 14+)

ContainerRelativeShape was introduced to the world of SwiftUI in iOS 14. ContainerRelativeShape is a shape that is replaced by an inset version of the current container shape. If no container shape was defined, is replaced by a rectangle.

This shape was introduced to match the corner radius for rounded rectangles. This feature was demoed in Widget development videos but its useful in other use cases as well.

Let’s take a look at this with an example.

struct DevTechieContainerRelativeShapeExample: View {
    var body: some View {
        Text("DevTechie")
            .font(.largeTitle)
            .padding()
            .background(.white, in: RoundedRectangle(cornerRadius: 20))
            .padding(5)
            .background(.mint, in: RoundedRectangle(cornerRadius: 20))
            .padding(5)
            .background(.orange, in: RoundedRectangle(cornerRadius: 20))
            .padding(5)
            .background(.indigo, in: RoundedRectangle(cornerRadius: 20))
    }
}
Notice that we have to have same RoundedRectangle corners to make sure that corners are concentric. In this case it’s easy because we know we are choosing the value but imagine the case where rounded corner is the system container, we can either apply brute force to find out the exact value or we can use this new shape. This is the case with Widgets as they have rounded corners but we don’t need to know exact value of corner radius.

We will look at a Widget view later but for now, let’s make use of this shape in our app’s view.

struct DevTechieContainerRelativeShapeExample: View {
    var body: some View {
        Text("DevTechie")
            .font(.largeTitle)
            .padding()
            .background(ContainerRelativeShape().fill(Color.white))
            .padding(5)
            .background(ContainerRelativeShape().fill(Color.mint))
            .padding(5)
            .background(ContainerRelativeShape().fill(Color.orange))
            .padding(5)
            .background(.indigo, in: RoundedRectangle(cornerRadius: 20))
    }
}
Notice the rounded corners with new shape are not only concentric but they are also distributed evenly.

Difference is more apparent when both views are side by side.

Widget Use Case
Let’s add Widget target to the app and add this new shape for its background view. After adding extension we will have this code for the extension.

import WidgetKit
import SwiftUIstruct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []// Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}struct SimpleEntry: TimelineEntry {
    let date: Date
}struct WidgetSamplesEntryView : View {
    var entry: Provider.Entryvar body: some View {
        Text(entry.date, style: .time)
    }
}@main
struct WidgetSamples: Widget {
    let kind: String = "WidgetSamples"var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WidgetSamplesEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}struct WidgetSamples_Previews: PreviewProvider {
    static var previews: some View {
        WidgetSamplesEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}
We are only interested in the View part of Widget so let’s update that and apply

struct WidgetSamplesEntryView : View {
    var entry: Provider.Entryvar body: some View {
        ZStack {
            ContainerRelativeShape()
                .fill(Color.orange.gradient)
                .padding(10)
            
            Text(entry.date, style: .time)
        }
    }
}
Our widget will look like this:



With that we have reached the end of this article. Thank you once again for reading. Don’t forget to subscribe our newsletter at https://www.devtechie.com