Creating an initial app loading screen is probably not thought about too much when developing iOS apps. Today we'll take a quick look at how we can create a relatively simple and elegant loading screen.
We want to see your work! If you've built something using this tutorial, send us pics! Find us on Twitter @TrailingClosure, or email us at [email protected]
This tutorial is split into two parts:
- Creating the
ScaledMaskModifier
- Creating the
LoadingView
Create The ScaledMaskModifier
Our ScaledMaskModifier
is going to be used to animate the display of our loading view. It takes a Mask
as an input that will scale up over time to display the loading view. See below for a glimpse.
First, create a new ViewModifier
called ScaledMaskModifier
. We're going to add two variables to the class:
mask
- The Mask described earlier. It will grow over time to display the underlying loading view.progress
- The value to keep track of where in the animation we are.
Here is the template of the struct so far:
import SwiftUI
struct ScaledMaskModifier<Mask: View>: ViewModifier {
var mask: Mask
var progress: CGFloat
func body(content: Content) -> some View {
content
}
}
Next we need to calculate the height and width of the mask and then apply it to our content. The tricky thing here is we want to ensure that the mask will shape will grow to the full size of the content. Think of it similarly to how we use scaleToFill()
on an Image
.
- Create a function called
calculateSize(geometry: GeometryProxy)
. This will return the size of themask
at it's maximum (at the end of the animation). It will figure out whether or not the width or height of the content is larger and return that as the size.
// Calculate Max Size of Mask
func calculateSize(geometry: GeometryProxy) -> CGFloat {
if geometry.size.width > geometry.size.height {
return geometry.size.width
}
return geometry.size.height
}
- Apply the
mask
to our content while usingcalculateSize(geometry:)
andprogress
to scale the mask size over time.
import SwiftUI
struct ScaledMaskModifier<Mask: View>: ViewModifier {
var mask: Mask
var progress: CGFloat
func body(content: Content) -> some View {
content
.mask(GeometryReader(content: { geometry in
self.mask.frame(width: self.calculateSize(geometry: geometry) * self.progress,
height: self.calculateSize(geometry: geometry) * self.progress,
alignment: .center)
}))
}
// Calculate Max Size of Mask
func calculateSize(geometry: GeometryProxy) -> CGFloat {
if geometry.size.width > geometry.size.height {
return geometry.size.width
}
return geometry.size.height
}
}
Create the LoadingView
Next up we'll put the ScaledMaskModifier
to work by creating the LoadingView
. Also inside the LoadingView
is where we'll handle the animations.
- Create a new
View
and call itLoadingView
. Inside you need to define 3 variables: one for the contents of theLoadingView
, one for the progress of the animation, and another for the Y offset (bouncing animation). See below for an example.
import SwiftUI
struct LoadingView<Content: View>: View {
var content: Content
@Binding var progress: CGFloat
@State var logoOffset: CGFloat = 0 //Animation Y Offset
var body: some View {
content
}
}
- Next, we'll need to apply the
ScaledMaskModifier
to the content. Here we used aCircle
as the mask, but really you could use anyShape
orView
such as aRectangle
or any other custom SwiftUIView
.
var body: some View {
content
.modifier(ScaledMaskModifier(mask: Circle(), progress: progress))
}
- In addition the
ScaledMaskModifier
we will make thecontent
bounce up and down. To do this, we're going to animate its y offset.
var body: some View {
content
.modifier(ScaledMaskModifier(mask: Circle(), progress: progress))
.offset(x: 0, y: logoOffset)
}
- Time to add the animations. Here we'll animate the
content
's progress change from0
to1
, while at the same time bouncing it up and down.
import SwiftUI
struct LoadingView<Content: View>: View {
var content: Content
@Binding var progress: CGFloat
@State var logoOffset: CGFloat = 0 //Animation Y Offset
var body: some View {
content
.modifier(ScaledMaskModifier(mask: Circle(), progress: progress))
.offset(x: 0, y: logoOffset)
.onAppear {
withAnimation(Animation.easeInOut(duration: 1)) {
self.progress = 1.0
}
withAnimation(Animation.easeInOut(duration: 0.4).repeatForever(autoreverses: true)) {
self.logoOffset = 10
}
}
}
}
Creating an Example
Now we're going to put our LoadingView
to the test in an example. See below:
Here we've created a fake HomeView
that is displayed once the loading is finished. As you can see we use the variable doneLoading
to decide which vie to render on screen.
For the LoadingView
I've passed in a Image
that will be used as the content
of the loading screen.
Finally, if you're curious as to why I've used the onAppear()
function, it is so I can simulate the asynchronous nature of loading data for your app.
import SwiftUI
struct ContentView: View {
@State var progress: CGFloat = 0
@State var doneLoading: Bool = false
var body: some View {
ZStack {
if doneLoading {
HomeView()
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
} else {
LoadingView(content: Image("PathMaskLogo-Dark")
.resizable()
.scaledToFit()
.padding(.horizontal, 50),
progress: $progress)
// Added to simulate asynchronous data loading
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.progress = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation {
self.doneLoading = true
}
}
}
}
}
}
}
}
Support Future Tutorials Like This One!
Please consider subscribing using this link. If you aren't reading this on TrailingClosure.com, please come check us out sometime!
We want to see your work! If you've built something using this tutorial, send us pics! Find us on Twitter @TrailingClosure, or email us at [email protected]