Using Transitions in SwiftUI
Views in SwiftUI can be added to and removed from other views easily by checking a state property. When views are added or removed using just a normal bool check they are automatically given a fade in/out transition by default. That’s great for a lot of cases, but what if you want to do something different, like show a message coming in from the top?
Setting up
To show how to set a different transition to animate a view in and out from the top of the screen, I am going to use a message view I threw together. Since this tutorial isn’t about building custom views I’m not going to dive into detail on how this one is built, but you can check out my tutorial on building custom list rows to learn how to do something similar.
struct Message: View {
enum MessageStyle {
case error
case message
case success
case warning
var backgroundColor: Color {
switch self {
case .error: return Message.errorRed
case .message: return .white
case .success: return Message.successGreen
case .warning: return Message.warningYellow
}
}
var textColor: Color {
switch self {
case .error: return .white
case .message: return .black
case .success: return .white
case .warning: return .black
}
}
}
let title: String
let message: String
let borderColor: Color
let borderWidth: CGFloat
let style: MessageStyle
private let cornerRadius: CGFloat = 8
init(title: String, message: String, borderColor: Color = .black, borderWidth: CGFloat = 0, style: MessageStyle = .message) {
self.title = title
self.message = message
self.borderColor = borderColor
self.borderWidth = borderWidth
self.style = style
}
var body: some View {
VStack {
ZStack(alignment: .leading) {
style.backgroundColor
.cornerRadius(cornerRadius)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 2)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.foregroundColor(style.textColor)
Text(message)
.font(.subheadline)
.foregroundColor(style.textColor)
}
.padding()
}
.fixedSize(horizontal: false, vertical: true)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(borderColor, lineWidth: borderWidth)
)
.padding()
Spacer()
}
.transition(.move(edge: .top)) // 1
.animation(.spring()) // 2
}
}
extension Message {
static let errorRed = Color(red: 236/255, green: 32/255, blue: 32/255)
static let successGreen = Color(red: 19/255, green: 127/255, blue: 13/255)
static let warningYellow = Color(red: 236/255, green: 187/255, blue: 32/255)
}
Setting a transition
In the code for the Message view you’ll notice a couple of lines near the end with numbered comments. Let’s take a look at those lines a little closer:
This is where the transition is being set using the
.transition
modifier. For this example I have chosen to use the.move
transition, telling it to move using the top edge of the view. You could use a different edge if you’d like, such as.leading
if you want the view to animate in and out from the left side. You could also use a different transition likescale
orslide
. For a full list of out-of-the-box transitions take a look at Apple’s documentation here.Along with setting the transition I’m setting the animation curve to use for the Message view. I chose to use a
spring
animation to give it a little playful bounciness when it comes into view.
Running the transition animation
OK so now that we’ve got a Message view set up and have given it an animation and a transition all that’s left to do is drop it into another view. To do that we’ll use a button that toggles a bool
state property, then check that property to conditionally add the new Message view.
struct ContentView: View {
@State private var showMessage = false
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.showMessage.toggle()
}
}) {
Text("Show Message")
}
if showMessage {
Message(title: "Success!", message: "You successfully presented your first message using transitions.", style: .success)
}
}
}
}
In the button’s action make sure you’re wrapping your toggle()
in withAnimation
so that the transition animates. If you don’t then your new Message view will just appear and disappear when tapping on the button!