Welcome to Part 4 of our SwiftUI series! After understanding previous part of SwiftUI, it’s time to focus on building beautiful, dynamic, and interactive user interfaces with SwiftUI. This part is all about taking your UI skills to the next level by exploring essential components and techniques for crafting seamless app experiences.
So, let’s start your Swift journey and understand each topic in detail!
State Management in SwiftUI
State Management means managing and tracking the data of your app. In SwiftUI, state is such data that updates your UI. When the data changes, SwiftUI automatically reflects it on the UI. Through state management, we control which view the data is in, how it is being updated, and how it is being passed from one view to another.
@State: Local View State
We use @State to manage the local state of any view. This is a simple and lightweight way when we only need to change data within a view.
import SwiftUI
struct ContentView: View {
@State private var counter = 0 // Local state to track counter
var body: some View {
VStack {
Text("Counter: \(counter)") // Display counter value
.font(.largeTitle)
.padding()
Button(action: {
counter += 1 // Update the counter when button is pressed
}) {
Text("Increment Counter")
.font(.title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
}
- @state: The counter is a local state maintained inside the ContentView. The view updates automatically when its value changes.
@Binding: Passing State Between Views
We use @Binding to pass the state of a parent view to a child view. This allows us to modify the state of one view directly in another view.
import SwiftUI
struct ContentView: View {
@State private var counter = 0 // Parent state
var body: some View {
VStack {
Text("Counter: \(counter)")
.font(.largeTitle)
.padding()
// Passing counter to child view using @Binding
ChildView(counter: $counter)
}
}
}
struct ChildView: View {
@Binding var counter: Int // Binding to parent state
var body: some View {
Button(action: {
counter += 1 // Modify the counter in parent view
}) {
Text("Increment Counter from Child")
.font(.title)
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
@Binding: Here the counter in the ChildView is passed through @Binding. This allows the counter in the ChildView to be modified, and this change will be reflected in the parent view.
@ObservedObject: Observing External Data Changes
We use @ObservedObject to observe any external data. When the data changes, the view that is observing this object gets updated automatically.
import SwiftUI
import Combine
class CounterModel: ObservableObject {
@Published var counter = 0 // Observable data
func increment() {
counter += 1 // Method to update counter
}
}
struct ContentView: View {
@StateObject private var model = CounterModel() // Create an instance of CounterModel
var body: some View {
VStack {
Text("Counter: \(model.counter)")
.font(.largeTitle)
.padding()
Button(action: {
model.increment() // Update the counter using model
}) {
Text("Increment Counter")
.font(.title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
}
- @ObservedObject: We have observed the CounterModel with @ObservedObject, which updates the data. When the counter changes, the view is automatically updated.
- @Published: This property makes the state observable inside the wrapper model.
@EnvironmentObject: Global State Management
We use @EnvironmentObject to manage global state that is shared across multiple views of the app. This is a centralized way to manage app-wide data.
Custom Animations and Transitions
In SwiftUI, we can make our app more interactive and visually appealing by using custom animations and transitions. With animations, we can smoothly move or transform any element, while with transitions we can control the appearance and disappearance of the view.
Let’s understand custom animations and transitions.
Custom Animations Using Animation
and withAnimation()
With animation, we can animate the properties of a view (like size, position, opacity, etc.). SwiftUI has built-in animations like .easeIn, .linear, .spring, but we can also create our own custom animations.
withAnimation():
Through withAnimation(), we can apply animation with any action that the user performs (like a button press).
import SwiftUI
struct ContentView: View {
@State private var scale: CGFloat = 1.0 // Initial scale
var body: some View {
VStack {
Text("Tap to Scale")
.font(.largeTitle)
.padding()
// Animated Circle
Circle()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
.scaleEffect(scale) // Applying scale
.animation(.easeInOut(duration: 1), value: scale) // Custom animation on scale
Button(action: {
// Trigger animation with `withAnimation()`
withAnimation {
scale = scale == 1.0 ? 1.5 : 1.0 // Toggle scale value
}
}) {
Text("Animate Circle")
.font(.title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
}
- scaleEffect(scale): This modifier scales the circle.
- withAnimation(): When the user presses the button, then the scale value changes and the animation is applied smoothly.
- Custom Animation: We have used easeInOut animation, which makes the animation smooth.
Combining Transitions for a Polished User Experience
We use transitions to animate the appearance or disappearance of a view. We apply transitions through the transition() modifier, such as slide, opacity, scale, etc.
Custom Transitions:
We can also define our own custom transitions, in which views are animate in a specific way.
import SwiftUI
struct ContentView: View {
@State private var isVisible = false // State to control visibility
var body: some View {
VStack {
Button(action: {
withAnimation {
isVisible.toggle() // Toggle visibility with animation
}
}) {
Text(isVisible ? "Hide Circle" : "Show Circle")
.font(.title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
// Animated Circle with Transition
if isVisible {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(.green)
.transition(.scale) // Applying scale transition
}
}
}
}
- isVisible.toggle(): This toggles the isVisible state, which shows or hides the circle.
- transition(.scale): When the circle is visible, we have applied a scale transition, which scales the circle up or scales down when it appears or disappears.
Combining Animations and Transitions for Complex Effects
If you want to combine animations and transitions, you can apply transitions within an animation. This allows you to create complex visual effects.
import SwiftUI
struct ContentView: View {
@State private var showCircle = false // State to toggle visibility
var body: some View {
VStack {
Button(action: {
withAnimation(.spring()) {
showCircle.toggle() // Toggle visibility with spring animation
}
}) {
Text(showCircle ? "Hide Circle" : "Show Circle")
.font(.title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
if showCircle {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(.purple)
.transition(AnyTransition.scale.combined(with: .opacity)) // Combined transition
.animation(.easeInOut(duration: 1)) // Apply animation on combined transition
}
}
}
}
- transition(AnyTransition.scale.combined(with: .opacity)): Here we have combined scale and opacity transitions such that the circle smoothly scales and fades in/out.
- withAnimation(.spring()): Spring animation has been used which gives a natural bounce effect when the circle is shown or hidden.
Integrating UIKit with SwiftUI
SwiftUI has made iOS development quite easy, but there are times when we need to integrate UIKit components into our SwiftUI apps. For example, if we need to use an existing UIKit component or do some complex UI customizations.
So today we will see how we can embed UIKit components into SwiftUI using UIViewControllerRepresentable and UIViewRepresentable classes.
What is UIViewControllerRepresentable and UIViewRepresentable?
- UIViewControllerRepresentable: It is used to embed UIKit’s UIViewController into SwiftUI view.
- UIViewRepresentable: It is used to integrate UIKit’s UIView into SwiftUI view.
Both of these protocols give SwiftUI the ability to wrap UIKit components, so that you can use them in your SwiftUI view hierarchy.
Using UIViewControllerRepresentable
Assume we need to add a UIKit UIViewController to our SwiftUI view, such as a UIImagePickerController (which is an image picker). To do this we will use UIViewControllerRepresentable.
Step 1:Create a SwiftUI Wrapper for UIImagePickerController:
import SwiftUI
import UIKit
// UIViewControllerRepresentable ko conform karte hain
struct ImagePickerController: UIViewControllerRepresentable {
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
var parent: ImagePickerController
init(parent: ImagePickerController) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
parent.selectedImage = selectedImage
}
parent.isImagePickerPresented = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.isImagePickerPresented = false
}
}
@Binding var selectedImage: UIImage?
@Binding var isImagePickerPresented: Bool
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
// makeUIViewController method UIImagePickerController create karta hai
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
return picker
}
// updateUIViewController ko hum normally use nahi karte jab hum sirf image picker dikhane wale hain
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}
Step 2: Usage in SwiftUI View:
struct ContentView: View {
@State private var selectedImage: UIImage?
@State private var isImagePickerPresented: Bool = false
var body: some View {
VStack {
if let image = selectedImage {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
} else {
Text("Select an image")
}
Button("Open Image Picker") {
isImagePickerPresented = true
}
}
.sheet(isPresented: $isImagePickerPresented) {
ImagePickerController(selectedImage: $selectedImage, isImagePickerPresented: $isImagePickerPresented)
}
}
}
- We have created a SwiftUI view named ImagePickerController which uses UIKit’s UIImagePickerController.
- Using Coordinator, we are handling delegate methods which are associated with UIImagePickerController.
- When the image picker is finished, we are displaying the selected image in the SwiftUI view.
Key Points to Remember
- Using UIViewControllerRepresentable we wrap UIKit’s UIViewController in SwiftUI.
- Using UIViewRepresentable we wrap UIKit’s UIView in SwiftUI.
- In both protocols you have to implement makeUIViewController / makeUIView and updateUIViewController / updateUIView methods.
- You can use Coordinator to handle delegate methods of UIKit components.
Leave a Reply