This week’s SwiftUI tutorial is focusing on a popular running and cycling app, Strava. In this tutorial, I’ll walk through how I recreated the activity history graph which is displayed inside the Strava app. As usual, I’ll break it down into bite-size chunks and explain along the way.
If you found this tutorial helpful, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, please come check us out sometime!
We’ll break this post up into a few different parts. …
Here’s an easy quick tip for this week. Have you ever wanted to customize the TabView
's TabBar in SwiftUI? Don't reinvent the component from scratch. The method I show below simply uses the native TabView
in SwiftUI and overlays your custom TabBar component on top. Your component will handle the user taps and selections by passing them on to the native TabView
via its selection
parameter. See below for how it's done!
If you found this tutorial helpful, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, please come check us out sometime!
struct RootView: View {
// Hold the state for which tab is active/selected
@State var selection: Int = 0
var body: some View {
// Your native TabView here
TabView(selection: $selection) {
HomeTab()
.tag(0)
FavoritesTab()
.tag(1)
SettingsTab()
.tag(2)
}
.overlay( // Overlay the custom TabView component here
Color.white // Base color for Tab Bar
.edgesIgnoringSafeArea(.vertical)
.frame(height: 50) // Match Height of native bar
.overlay(HStack {
Spacer()
// First Tab Button
Button(action: {
self.selection = 0
}, label: {
Image(systemName: "house.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(Color(red: 32/255, green: 43/255, blue: 63/255))
.opacity(selection == 0 ? 1 : 0.4)
})
Spacer()
// Second Tab Button
Button(action: {
self.selection = 1
}, label: {
Image(systemName: "heart.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(Color(red: 32/255, green: 43/255, blue: 63/255))
.opacity(selection == 1 ? 1 : 0.4)
})
Spacer()
// Third Tab Button
Button(action: {
self.selection = 2
}, label: {
Image(systemName: "gear")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(Color(red: 32/255, green: 43/255, blue: 63/255))
.opacity(selection == 2 ? 1 : 0.4)
})
Spacer()
})
,alignment: .bottom) // Align the overlay to bottom to ensure tab bar stays pinned. …
Today, I decided to mess around with a SwiftUI view that allows me to easily create a sidebar menu for content inside my apps. After designing a few prototypes, I generalized it into a single custom view class called SideBarStack
, as shown below:
As you can see, the custom view utilizes the @ViewBuilder
property wrapper twice to allow you to pass in custom content for both the sidebar and actual view content.
The custom SideBarStack
also takes in two parameters: sidebarWidth
and showSidebar
. The first allows the view to translate both the sidebar and main content correctly when the sidebar opens and closes. …
Welcome back! This week’s articles cover an assortment of SwiftUI micro-interactions that I’ve made for my apps. The benefits these interactions bring can really help make your app feel polished and simple to use.
Today’s micro-interaction tutorial is about creating a custom button for asynchronous tasks such as downloading, sending, or loading data.
Before we get programming, let’s explain the different states of AsyncButton
:
inactive
— User has not started asynchronous task.inProgress
— Actively processing asynchronous task.isComplete
— Asynchronous task complete.During each of these states, the AsyncButton
will show a different View
describing the asynchronous task:
ScrollView
Welcome back! This week’s posts cover an assortment of SwiftUI micro-interactions that I’ve made for my apps. The benefits these interactions bring can really help make your app feel polished and simple to use. Today’s micro-interactions are all based on my custom Wave
shape.
If you found this tip helpful, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, please come check us out sometime!
Wave
ShapeThese animations all start with one thing in common and that’s my custom SwiftUI Shape struct, Wave
. The way this shape works is by drawing a continuous wave from the leading to the trailing side of the frame. …
AlignmentControl
with animationsFor the next few posts, I’m going to cover a few micro-interactions that I’ve made for my apps. The benefits these interactions bring can really help make your app feel polished and simple to use. As we get started these examples should help get you thinking about what’s possible in your app. As we dive in, I encourage you to mess around with the code to really get a view for what’s possible and looks good.
If you found this tip helpful, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, …
My SwiftUI quick tip for this week covers custom Toggle
views! You can easily apply your own style to SwiftUI toggles by using the ToggleStyle
protocol. The best part is you don't need to worry about implementing any of the backing properties of the Toggle
. Simply toggle the isOn
property inside the Configuration
instance that's passed from the makeBody(configuration:)
function.
ToggleStyle
Start off by creating a new struct, and make sure to inherit from the ToggleStyle
Protocol. Then, implement the makeBody(configuration:)
function. This is where you'll construct your custom View
to be shown in place of the default switch.
import SwiftUI
struct MyToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
// Insert custom View code here. …
The other day I started messing around with converting 2D designs into isometric views in Figma. I thought it might be neat to create a ViewModifier
in SwiftUI that does the same thing. After posting a screenshot of my work on twitter, I decided to write this tutorial.
I’ve broken the tutorial into two parts (both below):
Before getting started, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, please come check us out sometime!
Throughout this tutorial, you’re going to see my best 2D square drawing of a watermelon slice. Yes…I apologize in advance, but there’s one sitting in front of me as I write this tutorial, so that’s the image you’re going to get……
One of the biggest things I’m missing with SwiftUI is the ability to snap to views as I scroll in a ScrollView
. Below is a technique I use to implement snapping on my HStack
with a custom ViewModifier
.
Before getting started, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, please come check us out sometime!
Instead of using a ScrollView
, this technique uses an HStack
with a dynamically changing x-offset to simulate scrolling. To change the offset I use a DragGesture
to calculate the scroll distance. You'll see below inside the DragGesture
onChanged()
closure that I update the dragOffset
. This makes the HStack
appear to scroll as the user drags across the screen. Finally, when the DragGesture
function onEnded()
is called, I calculate which view item is closest and then apply the final offset. …
Quick SwiftUI tip for today. If you don’t like that annoying white space a user sees when they scroll past the top of your ScrollView
, a Sticky Header works perfectly!
Before getting started, please consider subscribing using this link, and if you aren’t reading this on TrailingClosure.com, please come check us out sometime!
Place this StickyHeader
component at the top of a ScrollView
and any content you put inside will stretch in size to fill that gap when a user scrolls to the top. See the video below for an example!
struct StickyHeader<Content: View>: View {
var minHeight: CGFloat
var content: Content
init(minHeight: CGFloat = 200, @ViewBuilder content: () -> Content) {
self.minHeight = minHeight
self.content = content()
}
var body: some View {
GeometryReader { geo in
if(geo.frame(in: .global).minY <= 0) {
content
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
} else {
content
.offset(y: -geo.frame(in: .global).minY)
.frame(width: geo.size.width, height: geo.size.height + geo.frame(in: .global).minY)
}
}.frame(minHeight: …