MacKuba

🍎 Kuba Suder's blog on Mac & iOS development

SwiftUI betas - what changed before 1.0

Categories: Cocoa, Mac, SwiftUI, iPhone Comments: 0 comments

In the last few weeks I’ve been trying to catch up on SwiftUI - watching WWDC videos, reading tutorials. Not the new stuff that was announced 2 months ago though - but the things that people have been using for the past year.

Last June, like everyone else I immediately started playing with SwiftUI like a kid with a new box of Legos. In the first month I managed to build a sample Mac app for switching dark mode in apps. However, after that I got busy with some other things, and never really got back to SwiftUI until recently, so by the time the “version 2” was announced at the online-only WWDC, I’ve already forgotten most of it. So in order to not get this all mixed up, I decided to first remember everything about the existing version, before I look at the new stuff.

Back then, when I was watching all the videos and doing the tutorial, I was taking a lot of notes about all the components, modifiers and APIs you can use, every single detail I noticed on a slide. However, I was surprised to see how many of those things I wrote down don’t work anymore. After the first version that most people have played with and that the videos are based on, there were apparently a lot of changes in subsequent betas (especially in betas 3 to 5). Classes and modifiers changing names, initializers taking different parameters, some things redesigned completely.

And the problem is that all those old APIs are still there in the WWDC videos from last year. But WWDC videos are usually a very good source of knowledge, people come back to them years later looking for information that can’t be found in the docs, Apple even often references videos from previous years in new videos, because they naturally can’t repeat all information every year.

This was bothering me enough that I decided to spend some time collecting all the major changes in the APIs that were presented in June 2019, but were changed later in one place. If you’re reading this in 2021 or 2022 (hopefully that damn pandemic is over!), watching the first SwiftUI videos and wondering why things don’t work when typed into Xcode - this is for you.

Here’s a list of what was changed between the beta 1 from June 2019 and the final version from September (includes only things that were mentioned in videos or tutorials):


NavigationButton

Appeared in: “Building Lists and Navigation” tutorial, “Platforms State of the Union”

ForEach(store.trails) { trail in
    NavigationButton(destination: TrailDetailView(trail)) {
        TrailCell(trail)
    }
}

Replaced with: NavigationLink

ForEach(store.trails) { trail in
    NavigationLink(destination: TrailDetailView(trail)) {
        TrailCell(trail)
    }
}

PresentationButton / PresentationLink

Appeared in: “Composing Complex Interfaces” tutorial, “Platforms State of the Union”

.navigationBarItems(trailing:
    PresentationButton(
        Image(systemName: "person.crop.circle"),
        destination: ProfileScreen()
    )
)

Replaced with: PresentationLink, which was later removed and replaced with .sheet:

.navigationBarItems(trailing:
    Button(action: { self.showingProfile.toggle() }) {
        Image(systemName: "person.crop.circle")
    }
)
.sheet(isPresented: $showingProfile) {
    ProfileScreen()
}

SegmentedControl

Appeared in: “Working With UI Controls” tutorial

SegmentedControl(selection: $profile.seasonalPhoto) {
    ForEach(Profile.Season.allCases) { season in
        Text(season.rawValue).tag(season)
    }
}

Replaced with: Picker with SegmentedPickerStyle()

Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
    ForEach(Profile.Season.allCases) { season in
        Text(season.rawValue).tag(season)
    }
}
.pickerStyle(SegmentedPickerStyle())

TabbedView and tabItemLabel

Appeared in: “SwiftUI Essentials”, “SwiftUI on All Devices”

TabbedView {
    ExploreView().tabItemLabel(Text("Explore"))
    HikesView().tabItemLabel(Text("Hikes"))
    ToursView().tabItemLabel(Text("Tours"))
}

Replaced with: TabView and tabItem:

TabView {
    ExploreView().tabItem { Text("Explore") }
    HikesView().tabItem { Text("Hikes") }
    ToursView().tabItem { Text("Tours") }
}

DatePicker(selection:minimumDate:maximumDate:displayedComponents:)

Appeared in: “Working With UI Controls” tutorial

DatePicker(
    $profile.goalDate,
    minimumDate: startDate,
    maximumDate: endDate,
    displayedComponents: .date
)

The minimumDate and maximumDate parameters were replaced with a ClosedRange<Date>, and a label parameter was added:

DatePicker(
    selection: $profile.goalDate,
    in: startDate...endDate,
    displayedComponents: .date
) {
  Text("Goal Date")
}

You can also use this shorthand variant with a string label:

DatePicker(
    "Goal Date",
    selection: $profile.goalDate,
    in: startDate...endDate,
    displayedComponents: .date
)

TextField(_:placeholder:), SecureField(_:placeholder:)

Appeared in: “Platforms State of the Union”, “Working With UI Controls” tutorial

TextField($profile.username, placeholder: Text(“Username”))
SecureField($profile.password, placeholder: Text(“Password”))

Replaced with:

TextField("Username", text: $profile.username)
SecureField("Password", text: $profile.password)

ScrollView(showsHorizontalIndicator: false)

Appeared in: “Composing Complex Interfaces” tutorial

ScrollView(showsHorizontalIndicator: false) {
    HStack(alignment: .top, spacing: 0) {
        ForEach(self.items) { landmark in
            CategoryItem(landmark: landmark)
        }
    }
}

Replaced with: ScrollView(.horizontal, showsIndicators: false)

ScrollView(.horizontal, showsIndicators: false) {
    HStack(alignment: .top, spacing: 0) {
        ForEach(self.items) { landmark in
            CategoryItem(landmark: landmark)
        }
    }
}

It doesn’t seem to be possible now to have a scroll view that scrolls in both directions, but only shows indicators on one side (?)


List(_:action:)

Appeared in: Keynote, “Platforms State of the Union”

List(model.items, action: model.selectItem) { item in
    Image(item.image)
    Text(item.title)
}

Removed sometime in later betas - you can use selection: instead:

List(model.items, selection: $selectedItem) { item in
    Image(item.image)
    Text(item.title)
}

Text.color()

Appeared in: “Composing Complex Interfaces” tutorial, Keynote, “Platforms State of the Union”

Text(item.subtitle).color(.gray)

Replaced with: .foregroundColor()

Text(item.subtitle).foregroundColor(.gray)

Text.lineLimit(nil)

Appeared in: “Platforms State of the Union”, “SwiftUI on All Devices”

Text(trail.description)
    .lineLimit(nil)

Text used to have a default line limit of 1, so if you wanted to have a multi-line text control showing some longer text, you had to add .lineLimit(nil).

This was changed later and now no limit is the default. Instead, you may need to add .lineLimit(1) if you want to make sure that label contents don’t overflow into a second line if it’s too long.


Text(verbatim:)

Appeared in: “Building Lists and Navigation” tutorial, “SwiftUI on All Devices”

Text(verbatim: landmark.name)

It’s unclear if and when anything has changed in this API since beta 1. The current docs say that:

  • Text("string") used with a literal string is automatically localized
  • Text(model.field) used with variable is not localized
  • Text(verbatim: "string") should be used with a literal string that should not be localized

So verbatim: shouldn’t (or can’t) be used with variables like in the code above anymore, since in this variant the text will not be translated anyway. The parameter was removed from later versions of the tutorial code.

If you do want to localize a text that comes from a model property, use Text(LocalizedStringKey(value)).


.animation(…)

Appeared in: “Animating Views and Transitions” tutorial, “Introducing SwiftUI”, “SwiftUI on All Devices”

The .animation modifier has a number of different animation styles that you can choose from. This set of options has changed between the first beta and the final version.

In beta 1 you could do:

.animation(.basic())
.animation(.basic(duration: 5.0, curve: .linear))
.animation(.basic(duration: 5.0, curve: .easeIn))
.animation(.basic(duration: 5.0, curve: .easeInOut))
.animation(.basic(duration: 5.0, curve: .easeOut))

.animation(.default)
.animation(.empty)

.animation(.fluidSpring())
.animation(.fluidSpring(
  stiffness: 1.0, dampingFraction: 1.0, blendDuration: 1.0, timestep: 1.0, idleThreshold: 1.0
))

.animation(.spring())
.animation(.spring(mass: 1.0, stiffness: 1.0, damping: 1.0, initialVelocity: 1.0))

The .basic animations were replaced with options named after the selected curve:

.animation(.linear)
.animation(.linear(duration: 1.0))
.animation(.easeIn)
.animation(.easeIn(duration: 1.0))
.animation(.easeInOut)
.animation(.easeInOut(duration: 1.0))
.animation(.easeOut)
.animation(.easeOut(duration: 1.0))

(I’m not sure what .basic() without any parameters used to do exactly.)

What was called .spring is now .interpolatingSpring:

.animation(.interpolatingSpring(mass: 1.0, stiffness: 1.0, damping: 1.0, initialVelocity: 1.0))

And .fluidSpring is now either .spring or .interactiveSpring

.animation(.spring())
.animation(.spring(response: 1.0, dampingFraction: 1.0, blendDuration: 1.0))
.animation(.interactiveSpring())
.animation(.interactiveSpring(response: 1.0, dampingFraction: 1.0, blendDuration: 1.0))

.default is still available (I’m not sure what it does though) and .empty was removed.


.background(_:cornerRadius:)

Appeared in: “Platforms State of the Union”, “SwiftUI Essentials”

Text("🥑🍞")
    .background(Color.green, cornerRadius: 12)

Removed later - you can use separate .background and .cornerRadius modifiers to achieve the same effect:

Text("🥑🍞")
    .background(Color.green)
    .cornerRadius(12)

.identified(by:) method on collections

Appeared in: “Building Lists and Navigation” tutorial, “Platforms State of the Union”, “SwiftUI on All Devices”

ForEach(categories.keys.identified(by: \.self)) { key in
    CategoryRow(categoryName: key)
}

Replaced with: id: parameter

ForEach(categories.keys, id: \.self) { key in
    CategoryRow(categoryName: key)
}

.listStyle, .pickerStyle etc. with enum cases

Appeared in: “Introducing SwiftUI”, “SwiftUI Essentials”

.listStyle(.grouped)
.pickerStyle(.radioGroup)
.textFieldStyle(.roundedBorder)

Replaced with: creating instances of specific types

.listStyle(GroupedListStyle())
.pickerStyle(RadioGroupPickerStyle())
.textFieldStyle(RoundedBorderTextFieldStyle())

.navigationBarItem(title:)

Appeared in: “Platforms State of the Union”

NavigationView {
    List {
        ...
    }
    .navigationBarItem(title: Text("Explore"))
}

Replaced with: .navigationBarTitle

NavigationView {
    List {
        ...
    }
    .navigationBarTitle("Explore")
}

.onPaste, .onPlayPause, .onExit

Appeared in: “Integrating SwiftUI”

.onPaste(of: types) { provider in
    self.handlePaste(provider)
}
.onPlayPause {
    self.pause()
}
.onExit {
    self.close()
}

Replaced with .onPasteCommand, .onPlayPauseCommand, .onExitCommand

.onPasteCommand(of: types) { provider in
    self.handlePaste(provider)
}
.onPlayPauseCommand {
    self.pause()
}
.onExitCommand {
    self.close()
}

.tapAction

Appeared in: “Platforms State of the Union”, “Introducing SwiftUI”, “SwiftUI on All Devices”

Image(room.imageName)
    .tapAction { self.zoomed.toggle() }

MacLandmarkRow(landmark: landmark)
    .tapAction(count: 2) { self.showDetail(landmark) }

Replaced with: .onTapGesture

Image(room.imageName)
    .onTapGesture { self.zoomed.toggle() }

MacLandmarkRow(landmark: landmark)
    .onTapGesture(count: 2) { self.showDetail(landmark) }

BindableObject and didChange

Appeared in: “Handling User Input” tutorial, “Introducing SwiftUI”, “Data Flow Through SwiftUI”

class UserData: BindableObject {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavorites = false {
        didSet {
            didChange.send(self)
        }
    }
}

Replaced with: ObservableObject with objectWillChange (needs to be called before the change!). objectWillChange is automatically included, so you don’t need to declare it.

class UserData: ObservableObject {
    var showFavorites = false {
        willSet {
            objectWillChange.send(self)
        }
    }
}

In simple cases, you can use the @Published attribute instead which handles this automatically for you:

class UserData: ObservableObject {
    @Published var showFavorites = false
}

BindableObject was used together with:

@ObjectBinding

Appeared in: “Handling User Input” tutorial, “Introducing SwiftUI”, “Data Flow Through SwiftUI”

@ObjectBinding var store = RoomStore()

Replaced with: @ObservedObject

@ObservedObject var store = RoomStore()

Collection methods on Bindings

I don’t think this was mentioned in any talks or tutorials, but there were some tweets going around last June showing how you can do some cool tricks with bindings by calling methods on them, e.g.:

Toggle(landmark.name, isOn: $favorites.contains(landmarkID))

Sadly, this was removed in a later beta. The release notes include some extension code that you can add to your project to reimplement something similar.


Command

Appeared in: “SwiftUI on All Devices”

extension Command {
    static let showExplore = Command(Selector("showExplore"))
}

.onCommand(.showExplore) { self.selectedTab = .explore }

Replaced with: using Selector directly

.onCommand(Selector("showExplore")) { self.selectedTab = .explore }

Length

Appeared in: “Animating Views and Transitions” tutorial

var heightRatio: Length {
    max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}

Replaced with: CGFloat

var heightRatio: CGFloat {
    max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}

#if DEBUG

Appeared in: all tutorials, “Platforms State of the Union”, “Introducing SwiftUI”

This was automatically added around the preview definition in all SwiftUI view files created in beta versions of Xcode 11:

#if DEBUG
struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}
#endif

It was removed from the templates in one of the final betas - you no longer need to add that:

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}

Leave a comment

*

*
This will only be used to display your Gravatar image.

*

What's the name of the base class of all AppKit and UIKit classes?

*