Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
SwiftUI 4.0: New and updated APIs with code examples

SwiftUI 4.0: New and updated APIs with code examples

Aug 22, 2022

Written by Rudrank Riyam

Since Apple started using SwiftUI in its operating systems, it has been experiencing a noticeable evolution, from redesigns of apps to deep system integrations. The team behind SwiftUI has taken into account the feedback they got on Twitter and other social networking sites, resulting in some fantastic APIs to work and play with this year!

Here’s a quote from the session on “What’s new in SwiftUI”:

With this year’s APIs, we’ve gone deeper. We’ve made more custom experiences possible. We’ve introduced some amazing new graphical techniques. We’ve architected a new SwiftUI app structure and much more.

So, let’s dive into everything that SwiftUI 4.0 has to offer you!

Swift Charts

One of the biggest surprises this year for SwiftUI has been the announcement of an entirely new framework: Swift Charts, a data visualization framework written in SwiftUI that enables you to create beautiful charts.

Custom charts that would otherwise take hundreds of lines of code to create can now be simplified to a few lines of code. For example, here is a chart that fetches the data from the Codemagic REST API and displays the build times using Swift Charts!

And here is the corresponding code for the whole view:

NavigationView {
  VStack {
    Chart(times) { time in
      BarMark(x: .value("Type", time.instanceType),
              y: .value("Value", time.buildTime))
      .foregroundStyle(gradient)
    }

    Text("Build time in seconds")
      .font(.caption2).bold()
  }
  .navigationTitle("Xcode Benchmark")
  .padding()
}

The Swift Charts framework also handles localization, Dark Mode, and Dynamic Type for use while working across all of Apple’s platforms, ranging from the big Apple TV screens all the way to the tiny Apple Watch display.

Since the first version of SwiftUI was released in 2019, developers have complained about the navigation part of the framework and pushed for better APIs for this. That wish has been fulfilled this year! There is a new, data-driven way to deal with navigation in SwiftUI programmatically.

With a few lines of code, you can also pop to the root view of the navigation hierarchy. Here is a practical example that uses the new stack and destination APIs:

struct RecommendationsView: View {
  @State private var response: MusicPersonalRecommendationsResponse?
  @State private var selectedItems: [MusicPersonalRecommendation.Item] = []

  var body: some View {
    NavigationStack(path: $selectedItems) {
      ScrollView(showsIndicators: false) {
        ForEach(response?.recommendations ?? []) { recommendation in
          Text(recommendation.title ?? "")
            .font(.title2)
            .bold()

          ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack {
              ForEach(recommendation.items) { item in
                NavigationLink(value: item) {
                  RecommendationCardView(item: item)
                }
              }
            }
          }
        }
      }
      .navigationTitle("Recommendations")
      .navigationDestination(for: MusicPersonalRecommendation.Item.self) { item in
        switch item {
          case .album(let album):
            AlbumDetailView(with: album, path: $selectedItems)
          case .playlist(let playlist):
            PlaylistDetailView(with: playlist, path: $selectedItems)
          case .station(let station):
            StationDetailView(with: station, path: $selectedItems)
        }
      }
    }
    .task {
      let request = MusicPersonalRecommendationsRequest()
      self.response = try? await request.response()
    }
  }
}

/// Example of implementation of pop-to-root
struct StationDetailView: View {
  var station: Station
  @Binding var path: [MusicPersonalRecommendation.Item]

  init(with station: Station, path: Binding<[MusicPersonalRecommendation.Item]>) {
    self.station = station
    self._path = path
  }

  var body: some View {
    VStack {
      Text(station.name)
      
      /// Station details...

      if path.count > 1 {
        Button("Back to recommendations") {
          path.removeSubrange(1...)
        }
      }
    }
  }
}

Here is another example that uses the split view and three-column navigation in SwiftUI:

struct RecommendationsView: View {
  @State private var response: MusicPersonalRecommendationsResponse?
  @State private var selectedRecommendation: MusicPersonalRecommendation?
  @State private var selectedRecommendationItem: MusicPersonalRecommendation.Item?

  var body: some View {
    NavigationSplitView(sidebar: {
      List(response?.recommendations ?? [], selection: $selectedRecommendation) { recommendation in
        NavigationLink(value: recommendation) {
          Text(recommendation.title ?? "")
            .font(.title2)
            .bold()
        }
      }
      .navigationTitle("Recommendations")
    }, content: {
      List(selectedRecommendation?.items ?? [], selection: $selectedRecommendationItem) { item in
        NavigationLink(value: item) {
          Text(item.title)
        }
      }
      .navigationTitle(selectedRecommendation?.title ?? "")
    }, detail: {
      NavigationStack {
        if let selectedRecommendationItem {
          switch selectedRecommendationItem {
            case .album(let album):
              AlbumDetailView(with: album)
            case .playlist(let playlist):
              PlaylistDetailView(with: playlist)
            case .station(let station):
              StationDetailView(with: station)
          }
        }
      }
    })
    .task {
      let request = MusicPersonalRecommendationsRequest()
      self.response = try? await request.response()
    }
  }
}

The new navigation APIs from SwiftUI 4.0 look promising and make it less complicated to work with deep linking. Unfortunately, like most of the new stuff that’s been released, the new SwiftUI navigation APIs are for iOS 16+ only.

Advanced controls

In the new SwiftUI 4.0, there has also been an update to the TextField, which now automatically resizes to fit the text and caps the line to a certain extent:

TextField("Heading", text: $viewModel.heading)
  .limit(1)
TextField("Description", text: $viewModel.description, axis: .vertical)
  .limit(2...4)

You also get a new MultiDatePicker view in SwiftUI 4.0, which allows you to select multiple dates.

@State private var holidays: Set<DateComponents> = []

var body: some View {
  MultiDatePicker("Holidays", selection: $holidays)
}

PhotosPicker

Previously, you needed to wrap a PHPickerViewController in a UIViewControllerRepresentable. But this year, you can now use a native view that displays a PhotosPicker for choosing assets from the photo library. The new privacy-preserving PhotosPicker in SwiftUI is a part of the PhotoKit framework. To use it, you must import PhotosUI.

struct PhotosPicker<Label> where Label: View

Here is an example that gets one image from the photo library and sets it to imageData:

import PhotosUI

PhotosPicker(selection: $selection, matching: .images, maxSelectionCount: 1, photoLibrary: .shared()) {
  Text("Select Profile Picture")
}
.onChange(of: selectedItems) { selectedItems in
  if let selectedItem = selectedItems.first {
    selectedItem.loadTransferable(type: Data.self) { result in
      switch result {
        case .success(let imageData):
          if let imageData {
            self.imageData = imageData
          } else {
            print("No supported content type found.")
          }
        case .failure(let error):
          fatalError(error.localizedDescription)
      }
    }
  }
}

The SwiftUI team has been working hard on making the UIKit counterpart views available natively in SwiftUI, and the PhotosPicker is one of many examples of this.

Shape styles

Color has a new gradient property that adds a gradient based on the specified color. It automatically adapts to light/dark mode for system colors and your asset colors.

.fill(Color.indigo.gradient)

There is another API for dealing with shadows that enables you to add shadows to explicit views like this:

.foregroundStyle(.cyan.shadow(.drop(radius: 10, y: 15)))

Bottom sheets

Apple introduced UISheetPresentationController in iOS 15 to facilitate the presentation of an expandable bottom sheet. You may have seen this in the Apple Maps app, where you can drag the bottom sheet to display more information. However, this was not available in the previous version of SwiftUI.

For iOS 16, SwiftUI also supports customized bottom sheets using presentationDetents. The presentationDetents() modifier lets you create sheets that slide up and occupy only part of the view, and you can choose how much of the screen it occupies.

There are two system detents: .medium, which is approximately half the screen’s height, and .large, the regular sheet you use, showing on the entire screen. The view gets a resize handle so that the user can adjust the sheet between those two sizes. The default value is .large if no detent is specified. If you only provide .medium or .large, then there is no resize handle. Here is an example that shows how to use the modifier for a sheet:

And here’s the SwiftUI 4.0 code behind it:

.sheet(isPresented: $showSearchSongView) {
  SearchSongView()
    .presentationDetents([.medium, .large])
}

While there are system detents, SwiftUI 4.0 also gives you the option to play with custom detents:

  • fraction: for the specified fractional height
  • height: for the specified height
  • custom: for the calculated height

You can even programmatically update the sheet’s height by conditionally changing the detent. This is a welcome feature in SwiftUI, and it shows that SwiftUI is not far behind UIKit when it comes to introducing new features.

Grid

In iOS 14, SwiftUI introduced the LazyHGrid and LazyVGrid for working with a grid-based layout. However, their lazy nature (which means that they are created only when they should appear on screen) made it difficult to lay out and constrain the view’s size.

In iOS 16, you get a new Grid view that helps you create static grid-based layouts. This allows for the creation of more intricate layouts. Grid measures its subviews in advance to enable cells that span several columns and automatic alignment across rows and columns. 

Grid, GridRow, and the gridCellColumns modifier allow for the composition and construction of grids. You can use them to create simple and typical layouts and switch to lazy variants when dealing with a considerable amount of data to ease the memory pressure. These simple layout types will suffice most of the time, but occasionally, you’ll need to use imperative layout code, such as the size, minX, and frame (e.g., (origin.x - frame.midX)/2 - 3).

Here is an example that uses Grid and GridRow:

struct MainListView: View {
  @Binding var activeSheet: MainViewGridItemType?

  var body: some View {
    Grid(horizontalSpacing: 8, verticalSpacing: 8) {
      GridRow(alignment: .top) {
        MainViewGridItem(type: .arcade)

        MainViewGridItem(type: .challenge)
      }

      GridRow(alignment: .top) {
        MainViewGridItem(type: .casual)

        MainViewGridItem(type: .viewer)
      }
    }
    .accentColor(.red)
  }
}

ViewThatFits

Newly introduced in SwiftUI 4.0, this view helps you to easily manage and adapt your views in the given space without using GeometryReader. The definition from the documentation describes it as:

A view that adapts to the available space by providing the first child view that fits.

This means that you provide the axis to which ViewThatFits should constrain the content. If you do not provide this parameter, ViewThatFits constrains both the horizontal and vertical axes.

Then, inside ViewThatFits, you provide the views in the order you want them to be constrained. The documentation mentions that this order is largest to smallest, but since a view might fit along one constrained axis but not the other, this is not always the case.

For example, let’s assume there’s a view that you want to be vertical on iPhone SE, but it looks great horizontally on iPhone 13 Pro Max. For large and accessibility sizes, it only shows the description. You put them in the ViewThatFits like this:

ViewThatFits(in: .horizontal) {
  HStack {
    card
  }
  
  VStack {
    card
  }
    
  description
}

private var card: some View {
  Group {
    image
    
    description
  }
}

private var image: some View {
  AImage(url: url) { image in
    image
      .scaledToFit()
      .transition(.opacity.combined(with: .scale))
  }
  .cornerRadius(12.0)
  .frame(width: 120, height: 120.0)
}

private var description: some View {
  VStack(alignment: .leading, spacing: 4.0) {
    Text(name)
      .fontWeight(.bold)
      .font(.callout)
    
    Text(artistName)
      .fontWeight(.light)
      .font(.caption)
  }
  .foregroundColor(.white)
  .frame(maxWidth: .infinity, alignment: .leading)
  .multilineTextAlignment(.leading)
}

ViewThatFits helps you to create flexible UI layouts without the overhead of dealing with unintended consequences like GeometryReader.

AnyLayout

You may have noticed that while animating between views on environment changes, the view’s state is destroyed, resulting in glitchy behavior.

SwiftUI’s new AnyLayout structure aims to solve this by dynamically changing the type of a layout container without destroying the state of the subviews. AnyLayout can create layouts that respond to a user’s interaction or changes in the environment.

Here’s an example of dynamically changing the view from vertical to horizontal based on the accessibility size that uses AnyLayout:

struct MainViewGridItem: View {
  var image: String
  var mode: String
  var action: () -> Void
  
  @Environment(\.dynamicTypeSize) var size
  
  var body: some View {
    let layout = size >= .xxLarge ? AnyLayout(VStackLayout(spacing: 8)) : AnyLayout(HStackLayout(spacing: 8))
    
    Button(action: action) {
      layout {
        Text(image)
          .font(.system(size: 40))
          .frame(maxWidth: .infinity)
        
        Text(mode.lowercased())
          .font(type: .poppins, weight: .regular, style: .body)
      }
      .padding(.vertical)
    }
    .buttonStyle(HomeButtonStyle())
    .padding(8)
  }
}

Note that as of Xcode 14 Beta 5, VStack and HStack do not conform to the Layout protocol, as previously stated. For conditional layouts, you can now use VStackLayout and HStackLayout and continue using the older stack views you used earlier.

ImageRenderer

Another handy new class introduced this year in SwiftUI 4.0 is ImageRenderer. It helps you to create images from SwiftUI views. Previously, you had to write complex code to accomplish this, but the new API makes it easy to export your views as UIImage, NSImage, CGImage, or even PDF context.

Below is a simple example of saving the score view to the user’s camera roll with ImageRenderer:

Button("Share") {
  let scoreView = ScoreView()
  let imageRenderer = ImageRenderer(content: scoreView)
  imageRenderer.scale = 2.0
  
  if let image = imageRenderer.uiImage {
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
  }
}

If you need to export your views as images or PDF documents, ImageRenderer is a convenient tool that makes this process easier.

Hiding TabBar

One of this year’s underrated modifiers may help you if you want to hide the tab bar on any of the screens. I submitted feedback at the end of April 2020 for an API to hide the TabBar like the hidesBottomBarWhenPushed property in UIKit.

I got a response two weeks back that you can now hide or show the tab bar using the new toolbar(_:for:) modifier. Here is an example of how to do this:

struct SubHomeScreen: View {
  var body: some View {
    Text("Sub-Home")
      .toolbar(.hidden, for: .tabBar) /// <-- Hiding the TabBar for a particular view.
  }
}

struct HomeScreen: View {
  var body: some View {
    NavigationStack {
      VStack {
        Text("Welcome")
        NavigationLink("Sub home", destination: SubHomeScreen())
      }
    }
  }
}

To show the full-screen view, you may use this new handy modifier to hide the TabBar.

Conclusion

This year’s WWDC has been filled with many updates for SwiftUI, and there is much to learn from the 4.0 iteration of Apple’s flagship UI framework.

During the Platforms State of the Union talk this year, Apple announced the following:

The best way to build an app is with Swift and SwiftUI.

So, Apple has clarified the future of working with UI on Apple platforms. You developers have enough on your plate to spend the next year updating, making the best of the new SwiftUI APIs, and creating the best possible user experiences for your users.

For those of you who didn’t have the time to go through the sessions related to SwiftUI, we hope our article helped you catch up! Please share your favorite SwiftUI feature with us by mentioning @codemagicio on Twitter. Have a great year ahead exploring and experimenting with the new features!

Reminder: M1 Mac mini build machines are already available on Codemagic for you to take advantage of faster iOS and macOS builds. If you’re on the pay-as-you-go plan, use instance_type: mac_mini_m1 to try them out. If you’re on the Professional plan and want to set up a trial for the Mac mini M1, reach out to our customer engineering team to help you get fast green builds on the new machines!

Related articles

Latest articles

Show more posts