Creating Users Dashboard with SwiftUI

In this article, you’ll learn the basics of the SwiftUI framework, which was created in 2019 by Apple and is now gaining on popularity, slowly replacing UIKit in building iOS-apps views. We will go through the basic components and build a simple user interface. The final result of our project will look like in the image below.

Stacks

In all views created with SwiftUI, we can observe one basic pattern.

Each contains Stacks in three axes: Vertical – VStack, Horizontal – HStack and along the Z-axis – ZStack.

They allow us to simply arrange the sub-views and organize them on the screen. With their help we will build the already mentioned user interface.

From elements to complete view

Building views in SwiftUI is very often based on creating single, reusable components that we can later combine into one complete view. This approach is also known as atomic-design in web development.

Geometry reader

One of the built-in „tools“ that assists iOS developers in their work is Geometry Reader. With its help, we can read many dimensions or offset positions measured in pixels from the screen we are currently on.

 
  
struct GeometryReaderView: View {

    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text("Some view")
            }
        }
    }
}
  

HeaderView

Let’s build our first component. It will be a HeaderView, which we will use later in the complete view of our application.

 
  
import SwiftUI

struct HeaderView: View {
  
  var width: CGFloat
  var height: CGFloat
  
  var body: some View {
    
    ZStack {
      VStack {
        HStack {
          Image(systemName: "person.circle")
            .font(.system(size: Constants.xlFont))
          Spacer()
          Text("Current balance")
            .font(.system(size: Constants.smallFont, weight: .semibold))
          Spacer()
          Image(systemName: "slider.horizontal.3")
            .font(.system(size: Constants.xlFont))
        }
        .foregroundColor(.white)
        .padding(.horizontal, Constants.medium)
        
        HStack {
          VStack {
            Text("4856,32$")
              .foregroundColor(.white)
              .font(.system(.largeTitle, weight: .bold))
              .shadow(color: .white.opacity(0.5), radius: 5, x: 0, y: 7)
          }
        }
        .padding(.horizontal, Constants.medium)
      }
    }
    .frame(width: width, height: height / 4)
    .background(Color.blue.gradient)
    .cornerRadius(Constants.cornerRadius)
  }
}

struct HeaderView_Previews: PreviewProvider {
  static var previews: some View {
    
    GeometryReader { geometry in
      HeaderView(width: geometry.size.width, height: geometry.size.height)
        .previewLayout(PreviewLayout.sizeThatFits)
    }
    .edgesIgnoringSafeArea(.top)
  }
}
  

As you may have already noticed, our view is built from the previously mentioned stacks. The main view is ZStack, with V and H-Stacks stored sequentially as sub views. This allows us to obtain the effect of overlapping elements. We also declared two variables – width and height. When we initialize this component in the main view, we will pass the values of the geometry reader to these variables. They are then assigned in the .frame modifier, specifying the height and width of our component. The preview below demonstrates initially how the geometry reader can be used to display the view.

If we wrote the above code correctly, our component should look like in the image below:

Category View

The next step will be the component construction, called CategoryView. We will use Xcode’s built-in Sf-Symbols. Our component will then be used for a horizontally scrolling view containing as many categories as we declare to it.

 
  
import SwiftUI

struct CategoryView: View {
  
  var image: String
  var text: String
  var color: Color
  
    var body: some View {
      VStack {
        Spacer()
        Image(systemName: image)
          .font(.system(size: Constants.xxlFont))
        Spacer()
        Text(text)
          .font(.system(size: Constants.mediumFont, weight: .semibold))
        Spacer()
      }
      .frame(width: Constants.cardWidth, height: Constants.cardHeight)
      .background(color.gradient)
      .foregroundColor(.white)
      .cornerRadius(Constants.cornerRadius / 2)
      .padding(.trailing, Constants.xlmedium)
    }
}

struct CategoryView_Previews: PreviewProvider {
    static var previews: some View {
      CategoryView(image: "calendar", text: "Calendar", color: Color.purple)
    }
}
  

Again, you may notice that here we are using very basic elements like VStack and Image. Modifiers such as background or foreground color allow us to easily give visual effects to our component.

Our component should look like on the image below:

Categories View

The next component is the above-mentioned horizontal scrollview containing several tiles (categoryView) we created. For this view, we declare three arrays containing colors, images and a description of each category. Next, we declare our scrollView with the axis marked as horizontal. Inside, it contains an HStack and a ForEach loop that generates our tiles with the values specified in the arrays.

 
  
import SwiftUI

struct CategoriesView: View {
  
  let colors = [Color.purple, .gray, .pink, .yellow, .cyan]
  let images = ["chart.bar", "calendar", "person.2", "envelope", "message"]
  let texts = ["Overview", "Calendar", "Customers", "Products", "Messages"]
  
  var width: CGFloat
  var height: CGFloat
  
  var body: some View {
    ScrollView(.horizontal, showsIndicators: false) {
      HStack {
        ForEach(colors.indices, id: \.self) { i in
          CategoryView(
            image: images[i],
            text: texts[i],
            color: colors[i]
          )
        }
      }
      .padding(.horizontal, Constants.medium)
    }
    .frame(width: width, height: height / 8)
    .offset(y: Constants.offset)
  }
}

struct CategoriesView_Previews: PreviewProvider {
  static var previews: some View {
    
    GeometryReader { geometry in
      CategoriesView(width: geometry.size.width, height: 150)
        .previewLayout(.sizeThatFits)
    }
    .padding(.top, 350)
    .edgesIgnoringSafeArea(.top)
  }
}
  

Chart View

One of the very nice features that were added to SwiftUI with iOS 16 are Swift Charts. With them we can easily visualize data in our view. Below, for the purpose of our small app, we declare a structure with values, which we will then use in our chart.

 
  
import Foundation

struct ChartItem: Identifiable {
  var id = UUID()
  var type: String
  var value: Double
}

let chart_items: [ChartItem] = [
  ChartItem(type: "1", value: 0),
  ChartItem(type: "2", value: 35),
  ChartItem(type: "3", value: 22),
  ChartItem(type: "4", value: 40),
  ChartItem(type: "5", value: 70),
  ChartItem(type: "6", value: 50),
  ChartItem(type: "7", value: 80),
  ChartItem(type: "8", value: 130),
  ChartItem(type: "9", value: 140),
]
  

Next, using PointMark and LineMark charts, which we will overlay on top of each other, we will display our chart. As you can see, for the x-axis we declare item.type and for the y-axis, item.value. Additionally, we assign colors using the foregroundStyle modifier and we’re done.

 
  
import SwiftUI
import Charts

struct ChartView: View {
  var width: CGFloat
  var height: CGFloat
  
  @State var chartItems: [ChartItem] = chart_items
  
  var body: some View {
    HStack {
      Chart(chartItems) { item in
        LineMark(
          x: .value("Department", item.type),
          y: .value("Profit", item.value)
        )
        .foregroundStyle(Color.purple.gradient)
        
        PointMark(
          x: .value("Department", item.type),
          y: .value("Profit", item.value)
        )
        .foregroundStyle(Color.purple.gradient)
      }
      .frame(width: width - 48, height: height / 4)
    }
  }
}

struct ChartView_Previews: PreviewProvider {
  static var previews: some View {
    ChartView(width: 400, height: 1000)
  }
}
  

Detail View

Let’s add another component. It will be a detail view, showing percentage changes in a day, week or month. In truth, here we can provide any data, but for the moment we are just focusing on the basics of SwiftUI. Here we similarly declare an array of values and in a ForEach loop we generate successive tiles of our view.

 
  
import SwiftUI

struct DetailView: View {
  
  var width: CGFloat
  var height: CGFloat
  
  let periods = ["Week", "Month", "Year"]
  let values = ["+48%", "+72%", "+93%"]
  let colors = [Color.gray, Color.green, Color.red]
  
    var body: some View {
      VStack {
        HStack(spacing: Constants.medium) {
          ForEach(periods.indices, id: \.self) { i in
            VStack(spacing: Constants.small) {
              Text(periods[i])
                .font(.system(size: Constants.mediumFont, weight: .semibold))
              Text(values[i])
                .font(.system(size: Constants.bigFont, weight: .bold))
            }
            .foregroundColor(.white)
            .frame(width: width / 4, height: height / 8)
            .background(colors[i].gradient)
            .cornerRadius(Constants.cornerRadius)
          }
        }
      }
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
      DetailView(width: 400, height: 800)
    }
}
  

Text View

One component that does not need to be separated is TextView. In order to keep our code and folder structure clean, we will isolate it into a separate component. This is already the last of our reusable components, in the next step we will put our view together.

 
  
import SwiftUI

struct TextView: View {
  
  var text: String
  
    var body: some View {
      HStack {
        Text(text)
          .font(.system(size: Constants.bigFont, weight: .semibold))
          .foregroundColor(.gray)
        Spacer()
      }
      .padding(.top, Constants.medium)
      .padding(.horizontal, Constants.medium)
    }
}

struct TextView_Previews: PreviewProvider {
    static var previews: some View {
      TextView(text: "Details: ")
        .border(.black)
    }
}
  

Final View

It’s time to build our complete view. As you can see in the code below, we start the structure by declaring GeometryReader. Then we assign the screen dimensions to the width and height variables. The next step is to place subcomponents in the ScrollView to make it scale well on screens of different sizes. ScrollView is also a Vstack so we don’t need to nest another view. Next, one below the other, we declare the components we built during this tutorial. Some of them take our width and height values from the GeometryReader as arguments. Additionally, we declare a variable labelled @State, which we will use to change the height of our chart. By using the ternary operator, the onTapGesture modifier and withAnimation, we have easily given our view some user interaction. It’s time to build the whole thing and run the simulator to see the results of our work.

 
  
import SwiftUI
import Charts

struct DashboardView: View {
  
  @State private var resize = false
  
  var body: some View {
    VStack {
      GeometryReader { geometry in
        
        let viewWidth = geometry.size.width
        let viewHeight = geometry.size.height
        
        ScrollView {
          HeaderView(width: viewWidth, height: viewHeight)
            
          CategoriesView(width: viewWidth, height: viewHeight)
          
          TextView(text: Constants.overview)
          
          ChartView(width: viewWidth, height: resize ? viewHeight * 2 : viewHeight)
            .onTapGesture {
              withAnimation(.spring()) {
                resize.toggle()
              }
            }

          TextView(text: Constants.details)
          
          DetailView(width: viewWidth, height: viewHeight)
        }
      }
      .edgesIgnoringSafeArea(.top)
    }
  }
}

struct DashboardView_Previews: PreviewProvider {
  static var previews: some View {
    DashboardView()
  }
}
  
Written by
Michał Gabrielczyk

Engaged in the Mobile team since 2021. Constantly developing his competence in the area of mobile applications for iOS. Enthusiast of simple and effective solutions provided by Apple.

No items found.