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.
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:
- New SwiftUI Controls: Discover the latest additions to the SwiftUI control library, allowing you to create rich and interactive interfaces with ease.
- 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.
- 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 thepredicate
argument. The predicate is constructed using the#Predicate
syntax, which allows you to define a predicate inline without explicitly creating aNSPredicate
object. The predicate filters the data based on the condition$0.isFav == false
, meaning it retrieves instances ofProductList
where theisFav
property isfalse
.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 ofProductList
where theisFav
property istrue
. - 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 twoDisclosureGroup
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 aHStack
with aText
view for each item in theitems
orfavourites
array, depending on the respectiveDisclosureGroup
.HStack
: It represents a horizontal stack, organizing the contents of the view horizontally. In this case, it contains aText
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 theList
, 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 theProductList
entity or model, representing a product item. TheproductName
property is set to "My new Product".context.insert(product)
: It adds the newly createdproduct
instance to the Core Data context. Thecontext
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. Thetry
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 usingprint(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.
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 theScrollView
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. Thespacing
parameter specifies the spacing between each item in the stack.ForEach(images, id: \.self) { item in }
: It iterates over theimages
collection and creates a view for each element. Theid: \.self
parameter indicates that each element itself is used as its own identifier.Image(item)
: It creates an image view using theitem
from theimages
collection. Theitem
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 theLazyHStack
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 aView
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 aRectangle
view for each value. Theid: \.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 theRectangle
view. ThetopLeading
andbottomTrailing
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 theRectangle
view. It sets the opacity of the view based on thephase
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 theTransition
protocol, allowing you to define your own transition effect.- The
body(content:phase:)
function is required by theTransition
protocol. It takes two parameters:content
, representing the view to which the transition is applied, andphase
, 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 thephase
value. rotation3DEffect
takes two main parameters:degrees
, specifying the rotation angle, andaxis
, indicating the axis of rotation. In this case, thedegrees
value is determined by multiplying thephase.value
with either -90.0 or 90.0 depending on whether thephase
is.willAppear
or not.- The
axis
parameter defines the axis of rotation, wherex: 1.0
indicates the X-axis,y: 0.0
indicates no rotation along the Y-axis, andz: 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 aView
struct that represents the main SwiftUI view of the custom transition example.- The
@State
property wrapper is used to create a state variable calledbounceView
. It tracks the state of whether theRectangle
view should be shown or hidden. - The body of the view contains a
VStack
that holds the content. - Inside the
VStack
, there is anif
condition checking the value ofbounceView
. If it istrue
, theRectangle
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 theRectangle
view. It applies the custom transition effect defined by theCreateCustomTransition
struct. - Below the
if
condition, there is aButton
view. When tapped, it triggers an action closure that toggles the value ofbounceView
with a spring animation usingwithAnimation(.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 theCustomAnimation
protocol, allowing you to define your own custom animation behavior.- The struct has a stored property
duration
of typeCGFloat
, representing the desired duration of the animation. - The
animate(value:time:context:)
function is required by theCustomAnimation
protocol. It takes three parameters:value
, the initial value to animate,time
, the elapsed time since the animation started, andcontext
, aninout
parameter of typeAnimationContext
that encapsulates the animation context. - In the implementation, the function first checks if the elapsed time
time
is greater than the specifiedduration
. If so, it returnsnil
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 thescaled(by:)
method on thevalue
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 aView
struct that represents the main SwiftUI view of the animation example.- The
@State
property wrapper is used to create a state variable calledisAnimate
. 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 anif
condition checking the value ofisAnimate
. If it istrue
, theImage
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 aButton
view. When tapped, it triggers an action closure that toggles the value ofisAnimate
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