iOS 17 updates for SwiftUI — Part 1

Welcome back to my Swift Journey! It’s been quite some time since we last connected, but I’m thrilled to have you here again. In this blog, we’ll dive back into the world of Swift programming and explore the latest updates, techniques, and best practices. SwiftUI has revolutionized the way we build user interfaces for Apple platforms, and with iOS 17, Apple continues to push the boundaries of what’s possible.

lyvennitha sasikumar
16 min readJun 10, 2023

Introduction

In this blog, we’ll delve into the cutting-edge features of iOS 17 and how they can be leveraged with SwiftUI to create stunning and intuitive user experiences. We’ll cover a wide range of topics, including:

  1. New SwiftUI Controls: Discover the latest additions to the SwiftUI control library, allowing you to create rich and interactive interfaces with ease.
  2. Enhanced Animation and Transitions: Animation plays a crucial role in creating delightful user experiences. With iOS 17, SwiftUI introduces new animation APIs and transitions, enabling us to bring our UIs to life with stunning visuals and fluid motion.
  3. Improved Data Modeling: Core Data’s data modeling capabilities have been enhanced in iOS 17, offering more flexibility and expressiveness.

SwiftData

SwiftData combines the power of Core Data’s proven persistence technology with the modern concurrency features of Swift, allowing you to effortlessly add persistence to your app. With minimal code and no external dependencies, SwiftData empowers you to create a robust and efficient model layer for your application.

By leveraging Swift’s modern language features, such as macros, SwiftData enables you to write concise and performant code. You can easily describe your entire app’s model layer, or object graph, using SwiftData’s intuitive syntax. The framework takes care of seamlessly storing the underlying model data, and if needed, it can even handle data synchronization across multiple devices.

While SwiftData excels at persisting locally created content, its applications extend beyond that. For instance, if your app fetches data from a remote web service, SwiftData can serve as a lightweight caching mechanism, providing limited offline functionality. This allows your app to gracefully handle scenarios where network connectivity is limited or unreliable.

With SwiftData, you can achieve fast and efficient persistence in your app while ensuring data safety and integrity. Whether you’re building a small-scale app or a complex, data-driven application, SwiftData simplifies the process of adding persistence and enables you to focus on delivering exceptional user experiences.

Experience the power and simplicity of SwiftData as you enhance your app’s data management capabilities and unlock new possibilities for offline functionality and seamless data synchronization. Let SwiftData streamline your persistence needs, making your app more resilient and responsive.

Now we gonna implement it using some examples.

@Model //<------------------------------------- Equals to entity 
class ProductList{
var productName: String?
var isFav: Bool?

init(productName: String?, isFav: Bool? = false) {
self.productName = productName
self.isFav = isFav
}
}

This @Model attribute make easy your work avoing adding attributes in Entity in coredata model file.

And here i just share how to create a list in SwiftUI using this model. First I just gonna share the model across Views.

struct iOS17FeaturesApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: ProductList.self, isUndoEnabled: true)
//^--------------------------
}
}
}

isUndoEnabled — Sets the model container in this view for storing the provided model type, creating a new container if necessary, and also sets a model context for that container in this view’s environment.

Now we can access in our view like

 @Environment(\.modelContext) private var context

And now am gonna add @Query descriptor.

@Query(FetchDescriptor(predicate: #Predicate { $0.isFav == false } ), animation: .bouncy) private var items: [ProductList]
@Query(FetchDescriptor(predicate: #Predicate { $0.isFav == true } ), animation: .bouncy) private var favourites: [ProductList]

The above code snippet demonstrates the usage of the @Query property wrapper in Swift, which is commonly used with data persistence frameworks like Core Data or SwiftData. Let's break down the code and explain its functionality.

In this line of code, a property named items is declared with the @Query property wrapper. This indicates that the items property is associated with a query to fetch data from a persistence store.

  • FetchDescriptor: It's a type that defines the fetch request parameters. In this case, it specifies a predicate for the fetch request using the predicate argument. The predicate is constructed using the #Predicate syntax, which allows you to define a predicate inline without explicitly creating a NSPredicate object. The predicate filters the data based on the condition $0.isFav == false, meaning it retrieves instances of ProductList where the isFav property is false.
  • animation: It specifies the animation to be used when updating the query results. In this case, the .bouncy animation is used, which suggests that the query results should be animated with a bouncy effect when they change.
  • Here, the favourites property is declared with the @Query property wrapper, fetching instances of ProductList where the isFav property is true.
  • By using the @Query property wrapper, you can bind a property to the query results and automatically keep it updated whenever the underlying data changes. This simplifies data fetching and ensures that your property always reflects the current state of the persisted data.

Now am gonna List favourites and products List under separate disclosure groups.

NavigationStack{
VStack {
List{
DisclosureGroup("New Products"){
ForEach(items) { item in
HStack{
Text(item.productName ?? "")
}
}
}
DisclosureGroup("Favouites"){
ForEach(favourites) { item in
HStack{
Text(item.productName ?? "")
}
}
}
}
.listStyle(.plain)
.navigationTitle("My Products")
}
}

The provided code snippet represents a SwiftUI view that displays a list of products categorized as “New Products” and “Favorites” using DisclosureGroup components. Let's break down the code and explain its functionality.

  • NavigationStack: It represents a navigation stack, indicating that this view will be part of a navigation hierarchy. It's typically used in conjunction with a navigation view to enable navigation between different screens.
  • VStack: It represents a vertical stack, organizing the contents of the view vertically.
  • List: It creates a list view that displays a collection of rows. In this case, it contains two DisclosureGroup views representing "New Products" and "Favorites".
  • DisclosureGroup: It creates an expandable/collapsible container that can reveal additional content when expanded. It takes a label as its first parameter, which in this case is a string representing the category name.
  • ForEach: It is a view builder that iterates over a collection and creates views dynamically based on the elements in the collection. Here, it creates a HStack with a Text view for each item in the items or favourites array, depending on the respective DisclosureGroup.
  • HStack: It represents a horizontal stack, organizing the contents of the view horizontally. In this case, it contains a Text view to display the product name.
  • Text: It is a view that displays a static text string.
  • .listStyle(.plain): It applies a plain list style to the List, removing any default styling such as inset separators.
  • .navigationTitle("My Products"): It sets the navigation title for the view to "My Products".

Overall, this code creates a view with a list of products categorized as “New Products” and “Favorites”. The products are displayed in a disclosure group format, allowing the user to expand and collapse each category. The view is presented within a navigation stack, indicating that it can be navigated to from other views within a navigation hierarchy.

No Save!🤯

We forgot to add something that we need items to show up in the List. So For now am adding button in navigation bar and using it to add item. For adding navigation button.

Navigation Toolbar update ! ➕➕

.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) { //<------------
Button("Add Item") {
let product = ProductList(productName: "My new Product")
context.insert(product)
do{
try context.save()
}catch{
print(error.localizedDescription)
}
}
}
})

.topBarTrailing — Places the item in the trailing edge of the top bar. New in iOS 17.

The above code snippet for button represents a button action that adds a new item to a list of products. Let’s break down the code and explain its functionality.

  • Button: It creates a button view with the label "Add Item". This button will trigger an action when tapped.
  • Closure: The closure within the button’s action parameter defines the code to be executed when the button is tapped.
  • let product = ProductList(productName: "My new Product"): It creates a new instance of the ProductList entity or model, representing a product item. The productName property is set to "My new Product".
  • context.insert(product): It adds the newly created product instance to the Core Data context. The context variable represents the managed object context associated with the Core Data stack.
  • try context.save(): It saves the changes made to the managed object context. The try keyword indicates that this operation may throw an error, so it's wrapped in a do-catch block to handle any potential errors that may occur during the save operation.
  • catch: It captures any errors thrown during the save operation. In this case, if an error occurs, the localized description of the error is printed to the console using print(error.localizedDescription).

In summary, when the “Add Item” button is tapped, it creates a new ProductList object with the name "My new Product" and inserts it into the Core Data context. Then, it saves the changes made to the context. If any errors occur during the save operation, the error's localized description is printed to the console.

And now am gonna add heart button to make it move across favourite grp and items.

Button(action: {
item.isFav?.toggle()
try? context.save()
}, label: {
Image(systemName: "heart.fill")
.foregroundColor((item.isFav ?? false) ? .red:.gray)

})

Now the final Output is here.

SwiftData Implementation

ScrollView New Paging API & Reading Scroll Position

In ScrollView, This time long waited things are added. Peaking scrollview. Initial blog on this is done in swift but i just want to recreate this in SwiftUI.

Now in Asset, am adding 5 images and created an array in view.

let images: [String] = ["cat1", "cat2", "cat3", "cat4", "cat5"]

The below code snippet represents a horizontal scrollable view with a collection of images. Let’s break down the code and explain its functionality.

ScrollView(.horizontal) {
LazyHStack(spacing: 25) {
ForEach(images, id: \.self) { item in
Image(item)
.resizable()
.frame(width: 300, height: 200)
.cornerRadius(12)
}
}
.padding(.horizontal, (size.width-300)/2)
}
  • ScrollView(.horizontal): It creates a scrollable view that allows horizontal scrolling. The content within the ScrollView will be horizontally scrollable if it exceeds the available width.
  • LazyHStack(spacing: 25): It creates a horizontal stack view (HStack) that lazily loads its contents. The spacing parameter specifies the spacing between each item in the stack.
  • ForEach(images, id: \.self) { item in }: It iterates over the images collection and creates a view for each element. The id: \.self parameter indicates that each element itself is used as its own identifier.
  • Image(item): It creates an image view using the item from the images collection. The item represents the name or identifier of an image.
  • .resizable(): It allows the image to be resizable, adjusting its size according to the frame or container it is placed in.
  • .frame(width: 300, height: 200): It sets the width and height of the image view to 300 and 200, respectively.
  • .cornerRadius(12): It applies a corner radius of 12 to the image view, giving it rounded corners.
  • .padding(.horizontal, (size.width-300)/2): It adds horizontal padding to the LazyHStack view, adjusting the padding dynamically based on the available width (size.width) and the width of the images (300).

Overall, this code creates a horizontally scrollable view (ScrollView) containing a lazy horizontal stack (LazyHStack). The stack iterates over the images collection and creates an Image view for each item. The images are resized, given a fixed frame size, and rounded corners are applied. The stack is horizontally padded to center-align the content within the available width.

I just want to identify the visible item in scroll view before but now i can do it. We can use scrollTargetLayout() modifier.

When used in conjunction with the ViewAlignedScrollTargetBehavior, this modifier ensures that scroll views align with view-based content. You can apply this modifier to layout containers such as LazyHStack or VStack that are contained within a ScrollView and hold the main repeating content of the ScrollView.

if we add the scrollTargetLayout() alone we can’t achieve what we want. As per above, we can add one more modifier with binding variable as we can get the data of what is visible in

.scrollPosition(id: $scrollToItem) //<---------- Binding variable i created

To track the active scroll view, utilize this modifier in conjunction with the View/scrollTargetLayout() modifier. As the scroll view is scrolled, the associated binding will be updated with the identity of the leading-most or top-most view. To designate the layout that includes your scroll targets, employ the View/scrollTargetLayout() modifier. In the provided example, the binding scrolledID will be updated with the identifier of the top-most ItemView as the scroll view is scrolled.

Yup we got it. To know this in overlay am adding text to get the visible content of scrollView.

.overlay(alignment: .bottom) {
VStack{
if let scrollToItem{
Text(scrollToItem )
}
}
}

And now am gonna make the scrollview behaviour as per for peeking view. scrollTargetBehavior — Sets the scroll behavior of views scrollable in the provided axes.

.scrollTargetBehavior(.viewAligned)

Yup heres the output.

ScrollTransition

This modifier applies the specified transition when this view appears and disappears within the visible region of the containing scroll view or another specified container using the coordinateSpace parameter. The transition animates between the different phases of the transition.

Here is an example.

struct ScrollTransition: View {
var body: some View {
ScrollView{
LazyVStack{
ForEach(1...30, id: \.self){ item in
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.teal)
.cornerRadius(12)
.scrollTransition(topLeading: .interactive, bottomTrailing: .interactive) { view, phase in
view.opacity(1-(phase.value < 0 ? -phase.value : phase.value))
}
}
}

}
}
}

The above code represents a SwiftUI view called ScrollTransition. Let's break down the code and explain its functionality.

  • ScrollTransition is a View struct that represents the SwiftUI view containing the scrollable content with a transition effect.
  • ScrollView creates a scrollable container to hold the content.
  • LazyVStack organizes the content vertically in a stack view. It is a lazily loaded vertical stack, meaning that only the visible items are loaded at a given time, improving performance.
  • ForEach iterates over a range of values (1 to 30 in this case) and creates a Rectangle view for each value. The id: \.self parameter indicates that the value itself is used as the identifier for each item.
  • Rectangle creates a rectangular shape with a width of 200 and height of 100. It is styled with a teal color and rounded corners using a corner radius of 12.
  • .scrollTransition(topLeading: .interactive, bottomTrailing: .interactive): This modifier applies the scroll transition effect to the Rectangle view. The topLeading and bottomTrailing parameters specify the anchor points where the transition should be applied. In this case, the transition is interactive, meaning it responds to user interaction with the scroll view.
  • { view, phase in view.opacity(1 - (phase.value < 0 ? -phase.value : phase.value)) }: This closure defines the transition effect applied to the Rectangle view. It sets the opacity of the view based on the phase value of the transition. As the view appears and disappears within the visible region of the scroll view, the opacity is adjusted accordingly.

Overall, this code creates a scrollable view with a vertical stack of rectangular items. Each item has a scroll transition effect applied to it, which modifies the opacity of the rectangle based on its visibility within the scroll view.

CustomTransitions

New thing in transition modifier is we can apply our own custom transitions by initialising the created custom transitions.

When applying a transition, it’s recommended to use one or more modifiers on the content. For symmetric transitions, you can utilize the isIdentity property on the phase parameter to modify the properties of the modifiers. On the other hand, for asymmetric transitions, you can directly use the phase parameter to modify those properties. It's important to note that transitions should avoid any identity-affecting changes, such as using .id, if, or switch statements on the content. These changes would reset the state of the view to which they are applied, resulting in unnecessary work and potentially unexpected behavior when the view appears and disappears.

Now am gonna create my own custom transition using Transition Protocol.

//Creating Custom Transition

struct CreateCustomTransition: Transition{
func body(content: Content, phase: TransitionPhase) -> some View {
content
.rotation3DEffect(.init(degrees: phase.value * (phase == .willAppear ? -90.0:90.0)),

axis: (x: 1.0, y: 0.0, z: 0.0)
)
}


}
  • CreateCustomTransition is a custom struct that implements the Transition protocol, allowing you to define your own transition effect.
  • The body(content:phase:) function is required by the Transition protocol. It takes two parameters: content, representing the view to which the transition is applied, and phase, indicating the current phase of the transition.
  • In the implementation, the content view is returned with the applied transition effect.
  • The transition effect in this example is a 3D rotation effect achieved through the rotation3DEffect modifier. It rotates the content view along the X-axis based on the phase value.
  • rotation3DEffect takes two main parameters: degrees, specifying the rotation angle, and axis, indicating the axis of rotation. In this case, the degrees value is determined by multiplying the phase.value with either -90.0 or 90.0 depending on whether the phase is .willAppear or not.
  • The axis parameter defines the axis of rotation, where x: 1.0 indicates the X-axis, y: 0.0 indicates no rotation along the Y-axis, and z: 0.0 indicates no rotation along the Z-axis.

Overall, this custom transition applies a 3D rotation effect to the content view. The rotation angle is determined by the phase value, and the axis of rotation is set to the X-axis. This custom transition can be used in SwiftUI animations to provide a unique visual effect during view transitions.

Am just applying this custom transition like

struct CustomTransitions: View {
@State var bounceView: Bool = false
var body: some View {
VStack{
if bounceView{
Rectangle()
.frame(width: 150, height: 150)
.cornerRadius(8)
.foregroundColor(.purple)
//custom transition
.transition(CreateCustomTransition())

}

Button(action: {
withAnimation(.spring()) {
bounceView.toggle()
}
}, label: {
Text("Button")
})

}
}
}
  • CustomTransitions is a View struct that represents the main SwiftUI view of the custom transition example.
  • The @State property wrapper is used to create a state variable called bounceView. It tracks the state of whether the Rectangle view should be shown or hidden.
  • The body of the view contains a VStack that holds the content.
  • Inside the VStack, there is an if condition checking the value of bounceView. If it is true, the Rectangle view is displayed. It has a width of 150, height of 150, and is styled with a purple color and rounded corners.
  • The .transition(CreateCustomTransition()) modifier is applied to the Rectangle view. It applies the custom transition effect defined by the CreateCustomTransition struct.
  • Below the if condition, there is a Button view. When tapped, it triggers an action closure that toggles the value of bounceView with a spring animation using withAnimation(.spring()).
  • The button displays the text “Button” as its label.

Overall, the code demonstrates how to use a custom transition effect in SwiftUI. The bounceView state variable controls the visibility of the Rectangle view, and the button allows the user to toggle its appearance with a spring animation. The custom transition effect is applied to the Rectangle view using the .transition modifier.

Tadaa!. Here is the output.

CustomAnimation

Similarly we can create custom animation and can apply the instance of the custom animated class.

CustomAnimation — A definition of how an animatable value should change over time.

//Creating Custom Animation

struct CreatingCustomAnimation: CustomAnimation{
let duration: CGFloat = 1.0
func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
//this goes beyond screen as we have it as time
// return value.scaled(by: time)
//to avoid this
if time > duration{return nil}
return value.scaled(by: time)
}
}
  • CreatingCustomAnimation is a struct that implements the CustomAnimation protocol, allowing you to define your own custom animation behavior.
  • The struct has a stored property duration of type CGFloat, representing the desired duration of the animation.
  • The animate(value:time:context:) function is required by the CustomAnimation protocol. It takes three parameters: value, the initial value to animate, time, the elapsed time since the animation started, and context, an inout parameter of type AnimationContext that encapsulates the animation context.
  • In the implementation, the function first checks if the elapsed time time is greater than the specified duration. If so, it returns nil to indicate that the animation is complete.
  • If the elapsed time is within the desired duration, the function applies the animation transformation to the value. In this case, it uses the scaled(by:) method on the value object, which scales the value by the elapsed time.
  • Finally, the transformed value is returned, and the animation continues until the elapsed time exceeds the duration.

Overall, this custom animation provides a scaling effect to the value over time. The animation starts from the initial value and gradually scales it based on the elapsed time. Once the elapsed time exceeds the specified duration, the animation is considered complete.

And we can apply like this to any item

 //Adding custom animation
withAnimation(.init(CreatingCustomAnimation())) {
animate.toggle()
}

Animation CompleteCallback

The newly introduced call back returns the result of recomputing the view’s body with the provided animation, and runs the completion when all animations are complete.

Here is the snippet where it explains a lot.

struct AnimationCompleteCallback: View {
@State var isAnimate: Bool = false
var body: some View {
VStack{
if isAnimate{
Image("cat2")
.resizable()
.frame(width: 300, height: 200)
.cornerRadius(12)
}
Button(action: {
withAnimation(.easeInOut, completionCriteria: .logicallyComplete) {
isAnimate.toggle()
} completion: {
print("animation completed")
}

}, label: {
Text("animateView")
})
}
}
}
  • AnimationCompleteCallback is a View struct that represents the main SwiftUI view of the animation example.
  • The @State property wrapper is used to create a state variable called isAnimate. It tracks the state of whether the image view should be shown or hidden.
  • The body of the view contains a VStack that holds the content.
  • Inside the VStack, there is an if condition checking the value of isAnimate. If it is true, the Image view with the name "cat2" is displayed. It is resizable and has a frame of width 300 and height 200, with rounded corners.
  • Below the if condition, there is a Button view. When tapped, it triggers an action closure that toggles the value of isAnimate with an ease-in-out animation.
  • The withAnimation function is used to apply the animation to the state change. The animation is specified as .easeInOut, indicating an ease-in and ease-out effect.
  • The completionCriteria parameter is set to .logicallyComplete, which means the animation will be considered complete once the state change is fully processed.
  • Inside the completion closure, the message “animation completed” is printed.
  • The button displays the text “animateView” as its label.

Overall, the code demonstrates how to animate the appearance and disappearance of an image view using SwiftUI. When the button is tapped, the isAnimate state variable is toggled, triggering the animation. Once the animation is logically complete, the completion closure is executed, printing a message to indicate that the animation has finished.

Update On OnChange Modifier

Now the onchange modifer preserves old values too.

struct UpdatedOnChangeModifier: View {
@State var value = "Hello"
var body: some View {
Button(action: {
value = "Hey"
}, label: {
Text("CurrentValue: \(value)")
})
.onChange(of: value, initial: true) { oldValue, newValue in
print(oldValue, newValue)
}
}
}

Yup It returns old and new value too!

Here am ending my PART I things of newly released features in SwiftUI. Lets see things in PART II. Here am gonna attach git link too.😉

https://github.com/LyvennithaQgen/WWDC23

Happy Coding!😺

--

--