SwiftUI betas - what changed before 1.0
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 localizedText(model.field)
used with variable is not localizedText(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() } }