Want to learn it to some non-trivial extent (which I never have before) so that I can start actually using it in anger for apps and look for jobs using it.
Starting with Apple’s tutorial (the Landmarks app), 2024-02-16. Actually, it’s the “Introducing SwiftUI” tutorial (there are a few different tutorials, others e.g. more principles-based).
App
protocol, with @main
annotationthe ContentView
that it comes with has a #Preview
at the bottom:
#Preview {
ContentView()
}
(I think this is a macro that wraps PreviewProvider
which you can use for more advanced stuff)
.padding()
in the ContentView
that comes with the template is doingbody
has to return a single view — use stacks to return multiple.font(.title)
— get comfortable with using these type sizes (they then use .subheadline
for something else; perhaps use this tutorial as a reference for now)foregroundColor
is “Inherited” by defaultText
view by dragging it from the library (plus button in top right)(alignment: .leading)
Spacer
to instruct the layout to use the full width of the deviceNow we’re going to add an image and put some nice effects on it (mask, border, drop shadow), creating a CircleView
Asset catalogs are still the way of getting images into project
.clipShape(Circle())
modifier
Circle
is a Shape
which is a View
.overlay { ... }
modifier (which I need to understand better but which seems to be some shorthand for creating a ZStack
, end result being that overlay content ends up on top)Shape
’s .stroke()
modifier which gives you the outline of the shape.shadow(radius: 7)
modifier — not clear to me how this knows to give a circle-shaped shadowNext we add a map.
we use the Map
from from MapKit
When you import SwiftUI and certain other frameworks in the same file, you gain access to SwiftUI-specific functionality provided by that framework.
initialPosition
.frame(height: 300)
modifier — understand better how this modifier is usually used.offset
and negative .padding
modifiers to put one view on top of another (understand this idiom better)VStack
to push the content to the top of the screen.” (another idiom to understand).foregroundStyle(.secondary)
modifier on a couple of the labels, which (another idiom to understand, why you don’t just set the colour explicitly) gives a grey colour to the textAfter some initial faffing around…
LandmarkRow
resizable()
modifier (look into this) used on an Image
; I notice that without this the .frame(width: 50, height: 50)
modifier still shows a huge image#Preview
and see how Xcode’s canvas lets us switch between them#Preview
argument to add a nameGroup
Group
by itself offers no layout logic, but a preview with a group shows them stacked in the canvasList
and create a LandmarkList
viewIdentifiable
Now we’re going to add navigation.
NavigationSplitView
(they do this inside LandmarkList
)… - second argument of NavigationSplitView
is a placeholder (this is only relevant for iPad).navigationTitle("Landmarks")
modifier to the NavigationSplitView
content viewNavigationLink
to set up a transition
Notice that they pass an Image
directly into a view; this suggests that Image
is a thing you can use as a view or as a piece of data?
.constant(…)
binding, why?ScrollView
inside LandmarkDetail
(instead of a VStack
) so that it can be scrolled(How do we preview what a view will look like in a navigation stack?)
.navigationBarTitleDisplayMode(.inline)
on the detail view
Now we look at previews a bit more.
We’re going to add favouriting of places: a switch (sorry, a Toggle
) to filter the list by favourites, and a button to favourite an item.
if landmark.isFavorite
to conditionally include a viewImage(systemName: "star.fill")
(I think this is related to SF Symbols, something to look into).foregroundStyle(.yellow)
modifier on this filled star (remember we used it with .secondary
before); apparently this works “because system images are vector based”
.foregroundColor
modifier; why did they decide foregroundStyle
a better name?OK, so the filtering of the list is the first place we see state (showFavoritesOnly
on list, accompanied by a filteredLandmarks
computed property)
(Note that SwiftLint has a rule that makes sure that state is private, in line with the guidance given in this tutorial)
And now we see bindings (“a binding acts as a reference to a mutable state”). “You use the $ prefix to access a binding to a state variable, or one of its properties.” We pass it as the Toggle
’s isOn
.
ForEach
in the list and pass the collection to that instead.Toggle
’s second argument is a closure that provides the “label” (which can be any view, e.g. a Text
)Notice that there’s no animation of the list when we flip the switch. To solve this, they add a .animation(.default, value: filteredLandmarks)
modifier to the list. Hopefully we’ll learn more about this modifier in the Eidhof SwiftUI book, so won’t think much about it now; but it’s something to do with applying the animation when given value
changes.
So, now we change our static list of landmarks, stored inside the list, into an external data source. To do this, we create a ModelData
class and annotate it with the @Observable
macro (well, it says it’s a macro, but I can’t figure out how to expand it…).
SwiftUI updates a view only when an observable property changes and the view’s body reads the property directly.
(emphasis mine here — important to bear in mind that SwiftUI is paying attention to what gets accessed whilst a view’s body
is executing)
And now (for reasons I still need to understand — i.e. what might the alternatives be, why is this the right way to do things, how would it be in React?) we tell the LandmarkList
to grab this data from the environment:
@Environment(ModelData.self) var modelData
No idea why ModelData.self
can be used as the environment key here — think about it again after reading the environment chapter in the book.
At this point, the ContentView
preview just started crashing — with a macOS crash report dialog, with no useful information. Adding .environment(ModelData())
fixed this.
And now we do the same in the app, except there we create the ModelData
instance as part of the app’s state, and then pass it down through .environment(modelData)
(again, why does this not require a specific key — are types a special kind of key?). Before doing this, running the app crashes with:
SwiftUI/Environment+Objects.swift:32: Fatal error: No Observable object of type ModelData found. A View.environmentObject(_:) for ModelData may be missing as an ancestor of this view.
(I need to understand better why it’s important that this be @State
in the app.)
Just like SwiftUI initializes state in a view only once during the lifetime of the view, it initializes state in an app only once during the lifetime of the app.
Now we see how to manipulate model data in a reusable view, by passing a binding to a property of the observable ModelData
object.
@Binding var isSet: Bool
to the our new FavoriteButton
view
@Binding
is doingButton
isSet.toggle()
Label
type, whatever that is (is it a text plus an image plus some magic?)#Preview
, we pass a isSet: .constant(true)
binding. Notice that this means that when we press the button the appearance never changes in the preview.So, inside the LandmarkDetail
view, in order to get the binding to pass to FavoriteButton
, we find the index of the current landmark inside modelData.landmarks
. Then inside body
we declare an @Bindable var modelData = modelData
(the latter coming from the environment). No idea what this @Bindable
step is nor why we do it. And then we pass $modelData.landmarks[landmarkIndex].isFavorite
to the FavoriteButton
.
(What is @Bindable
?)
(It would be instructive to some time understand why this feels so much more complicated than React, in which you basically just have useState
and useContext
. Is it because I’ve not yet seen a full app created in React?)
OK, that concludes “SwiftUI essentials”. Lots of information there at the end. Now we’ll move back away from the data side of things and do some more presentation stuff.
We’re going to generate a nice-looking badge that the user gets when they visit a landmark.
Path
view. its initializer receives a closure that takes a path
arg — looks like we’re going to call methods on this to draw.fill(.black)
modifier to the path (need to understand this better)path.move(to:)
and then add lines to the path with path.addLine(to:)
path.addQuadCurve(to:control:)
to draw some Bézier curves (whatever they are; I’m a bit lost by this point but seems like something to look at later)Then to get the size of the path we’re meant to be drawing, we wrap the Path
in a GeometryReader
, which takes a closure that provides a geometry
object; we grab its size.width
and size.height
(again, one to understand better after seeing the layout chapter in the book)
(This seems like a rather bizarre part of an introduction to SwiftUI — there’s a lot of geometry and maths to plough through)
Now to make the background pretty, we replace the .fill(.black)
with a .fill(.linearGradient(…))
, passing a Gradient
(look into this)
Finally — in another step I don’t really want to try and understand now — they do .aspectRatio(1, contentMode: .fit)
Then another view, BadgeSymbol
, which is more drawing with Path
.
Next RotatedBadgeSymbol
. Note there’s a type called Angle
(why?). Also applies some negative padding to make the badge symbol not quite fit inside (is it meant to be artistic?). And we apply .rotationEffect(angle, anchor: .bottom)
modifier.
Then we combine the background and multiple RotatedBadgeSymbol
(with different angles) into a Badge
.
Hmm: why do you have to write some View
for return type of methods that return View
? one to look into
We also see the .opacity(0.5)
modifier
We put the symbol on top of the background with a ZStack
. The sizes of the two views are completely mismatched (not sure why) so we use a GeometryReader
and scale the badge symbols using (new to us, look into) .scaleEffect(1.0 / 4.0, anchor: .top)
and .position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height)
modifiers.
Also we see the .scaledToFit()
modifier; another mystery.
So, we are going to look at some hike data that it gives us, and a view that it gives us, which is a graph that represents ranges of values over time using blobby capsules.
‘When you use the
animation(_:)
modifier on an equatable view, SwiftUI animates any changes to animatable properties of the view.
What is an “equatable view”? They don’t seem to show us this, and instead say that when the view isn’t equatable, you can use the animation(_:value:)
modifier so that changes to the specified value cause an animation. They demonstrate it with the showDetail
button, which is a little toggle that rotates and grows (with .rotationEffect
and .scaleEffect
(again, look into these).
.easeInOut
to .spring()
, although I can’t tell the difference.So, that showed us how to animate a change to a single view. Now we see how to animate all the results of a given state change, by wrapping the state change in the withAnimation
function. Specifially, we do it inside the button action’s showDetail.toggle()
.
we also see how we can pass withAnimation
the same parameters as the animation(_:value:)
modifier.
we make the animation slow so we can see how animations are interruptible
Now we’re going to think about transitions, which I think means when a view is added to or removed from the screen. The default is a fade in/out. We see how we can customise it by using the .transition(_:)
modifier on the view in question, and passing e.g. .slide
or .move(edge:)
or .asymmetric(insertion:removal:)
. We also see the .combined(with:)
transition for combining them.
Now we’ll work on animation the capsules on the graph. We create a custom animation by adding a static property to Animation
. We start by returning .default
and go on from there.
Oh, so I notice that the GraphCapsule
view we’re now animating does indeed conform to Equatable
(see my ‘what is an “equatable view”?’ above), and we just use .animation(_:)
but no further explanation.
So, just adding .animation(_:)
modifier to each GraphCapsule
has made clicking between the different metrics (elevation, heart rate, pace) to do a nice animation between the graph states, which is nice.
Then they switch it to use .spring(dampingFraction: 0.5)
— this dampingFraction
change seems to make it, to my eyes, actually what I’d call “springy” (compare to before when I said I couldn’t tell what .spring()
was doing).
Then we do .speed(2)
(which I guess can generically speed up any animation).
(Something to look into sometime — why does the colour not also go springy between the two values?)
Then they do a cool thing where they add a .delay(:_)
based on the index of the capsule, giving a ripple effect.
(Note: this area of animation design is something I know nothing about, but it’s cool.)
(Note: I see that HikeGraph
uses a var path: KeyPath<Hike.Observation, Range<Double>>
to represent the current choice of data. I like KeyPath
and would like to know explore its uses. I see that they can often be used in place of functions.)
Another thing I missed earlier:
A
nil
animation for a particular value counteracts a non-nil
animation for the same value that appears higher in the view hierarchy.
HStack
in a ScrollView(.horizontal, showsIndicators: false)
).(I just noticed that Xcode now has a Vim mode; that’s nice.)
.cornerRadius(_:)
Image
, is .clipped()
.listRowInsets(_:)
, which we set to zero so that the content goes right up to the grey outside of the list (i.e. without any white padding inside the list)(My understanding of how something like .listRowInsets
(and most other modifiers) work is that this will put something into the environment, and then List
will read it)
Then some more navigation stuff. Note that, for example, they put the push from CategoryRow
to LandmarkDetail
directly inside CategoryRow
. Presumably there is some sort of architectural kinda-better practice for this?
Now, after putting a CategoryItem
as the NavigationLink
’s label
, we go and undo some of the automatic changes that this causes:
Text that you pass as the label for a navigation link renders using the environment’s accent color, and images may render as template images. You can modify either behavior to best suit your design.
(I don’t even know what a template image is.)
We do this by:
.renderingMode(.original)
on the image.foregroundStyle(.primary)
on the textNext we see how to use a TabView
. You pass it a binding to the selection
(i.e. a piece of state in the view, e.g. some Tab
enum of your creation)
We use the tabItem(label:)
modifier to set the tab button for each view — they use a Label("Featured", systemImage: "star")
, and both the text and image get displayed (I think that there are contexts, possibly some we’ve seen already, in which the label’s text is only used for accessibility).
They also show using the .tag(_:)
modifier on each of the tab’s views — not sure yet how this is useful.
Now we’re going to add a profile view and let the user edit (not sure exactly what that means yet) their profile.
You’ll work with a variety of common user interface controls for data entry, and update the Landmarks model types whenever the user saves their changes.
(emphasis mine)
(Aside: I was curious to see what happened if I tried to modify some non-@State
property of a view inside a button’s action. I get Cannot assign to property: 'self' is immutable
. Fair enough. So what’s special about @State
that we can assign to it even though we’re not in a mutating
context?)
Hmm, they have one Text
to which they do .bold()
and .font(.title)
. Is this something you’d do or would you normally trust the style to get it right? Should I care?
Another interesting thing I just saw was adding two Text
views together (?!):
Text("Goal Date: ") + Text(profile.goalDate, style: .date)
(and the style: .date
is interesting, too.)
Some sort of trick that’s used to show a badge at a certain size (I don’t know if this is a useful technique or some defect in the way that the badge code was written):
The badge’s drawing logic produces a result that depends on the size of the frame in which it renders. To ensure the desired appearance, render in a frame of 300 x 300 points. To get the desired size for the final graphic, then scale the rendered result and place it in a comparably smaller frame.
We also see the .hueRotation(_:)
modifier, whatever that does — seems to be use here as just a way to quickly vary the colours of a badge
For some reason apropos of nothing it also shows us the .listStyle(.inset)
modifier here, to remove the grey borders from the list (don’t know why it didn’t show this back when we were creating the list)
This one is interesting — what does .listStyle(_:)
do; why is it a modifier and not a property of the List
?
OK, now we’re going to see how to:
add a toolbar button:
We set the .toolbar
modifier on one of our views (I’m not sure the impact of which view you set this on). Its initializer just takes content, and we set it to a Button
(I guess another one of those appearance-depends-on-context things that I don’t know how they are implemented, is it environment?)
make that present the profile details view:
use the .sheet(isPresented:)
modifier. This takes a binding. The body of the modifier is the content you want to show.
Now we look at “edit mode”, whatever that is.
We grab the built-in @Environment(\.editMode)
, which gives a property of type Binding<EditMode>?
. How mysterious. Why is this a binding? What’s going on?
And to make things more confusing, we then use the built-in EditButton
view, which controls the same \.editMode
enironment value. (Based on what I saw, this button appears to change to “Done” during editing.)
(Note on data flow: We add a profile object to the environment, but we pass the profile page a copy of this profile (to avoid unintentional edits until confirmed). We store this draft in state in the profile page, and pass it as a binding to the ProfileEditor
that we’ll see in a second.)
Our view then reads editMode?.wrappedValue === .inactive
(this ?.wrappedValue
just looks wrong, so I need to understand why it’s not) to decide whether to show the profile or the editor.
Now we build our ProfileEditor
view:
List
of (HStack
, Spacer
, TextField
), which (magically?) looks like a nice form
.foregroundStyle(.secondary)
and .multilineTextAlignment(.trailing)
which now raises of the question of whether this is stuff you’ve gotta do whenever you want to make a generic-looking form?@Binding var profile: Profile
propertyTextField
; we pass it …, text: $profile.username)
TextField
have a first argument that is just a string? I thought that we usually have some sort of closure that takes generic label content?#Preview
we pass a .constant
binding. What does this mean for the text field? It appears that I can change the text but when I press the Enter key it reverts to the original value. Understand this better.Bool
property the switch in the preview just doesn’t switchPicker
control, which on iPhone starts off as the selected value and when tapped does a little popover for you to choose a value from. It takes a selection:
binding. And then — I thought it would be like a List
but the example we see doesn’t take an array, rather we use a ForEach
for the content. And we just use a simple Text
for each of the rows.
TextField
(maybe both options exist, I don’t know).tag(season)
on each of these Text
views. I think this allows the picker to decide which of these Text
should be selected. I guess that’s what it’s used for in a TabView
too (I was wondering earlier, remember?)Picker
access the value that I passed to the tag
modifier? (This is like the opposite of environment; it’s a parent trying to discover something about its child…)DatePicker
.
selection
binding, and you give it a range of dates it should allow, which is a ClosedRange<Date>
(I think that e.g. populating this is something that you’d want some sort of view model-ish thing for)TextField
and Picker
, this one takes its label via a closure and you’ve got to create a Text
(It would be worth at some point understanding exactly what a Binding
is and how SwiftUI interacts with one; I remember reading somewhere that it’s really just a pair of set/get functions.)
OK, we’re finally going to see how to delay edit propagation (well, we’ve already been doing that; I guess what we’re actually doing is seeing how to commit once done).
Right, first we add a cancel button, which has (…, role: .cancel)
(what’s that?), and when tapped resets the draft profile: draftProfile = modelData.profile
. It also does editMode?.animation().wrappedValue = .inactive
.
(What’s editMode?.animation()
? Something to look into when understanding Binding
.)
OK, and then the final step, where I expected something cool, was a bit odd. The profile page applies the (new to us) .onAppear
and .onDisappear
modifiers to its ProfileEditor
, and in onDisappear
(which it assumes it called because the user has tapped the Done
button — i.e. it relies on some effect of EditButton
which is a bit odd) it updates the global model with the draftProfile
.
TODO: OK, all that’s left for this tutorial is to do:
We”re going to embed a UIPageViewController
inside our SwiftUI view.
View
that conforms to the UIViewRepresentable
or UIViewControllerRepresentable
SwiftUI protocols.<Page: View>
and var pages: [Page]
(why is it important to have this be generic instead of any View
? I think this is a similar question to before; wouldn’t be surprised if it’s something to do with type metadata)#Preview
s for this view)UIViewControllerRepresentable
views don’t seem to implement the body
property; instead they provide makeUIViewController(context:)
and updateUIViewController(context:)
methodsUIPageViewController
, we use UIHostingViewController(rootView: ourSwiftUIView)
To implement the page view controller’s dataSource
, we implement UIViewControllerRepresentable
’s makeCoordinator()
method. This will return an object of our new class Coordinator: NSObject
, which will implement the Cocoa pattern.
This coordinator is then accessible inside the aforementioned make/updateUIViewController
via the context.coordinator
property.
So, we implement, on our coordinator, the pageViewController(_:viewControllerBefore/After:)
methods.
(Note: I’m not going to think about this UIKit stuff in too much detail right now, especially how the coordinator maintains a reference to the SwiftUI view that created it; it seems like a mix of a long-lived object with a throwaway template and it’s a mix that doesn’t seem immediately intuitive.)
OK, now we add a piece of state on the view that holds this view controller, in order to control the current page number. We then see how to update this state inside the page view controller delegate so that the binding is two way.
Next we see, similarly, how to wrap a UIPageControl
in a UIViewRepresentable
, and we similarly hook up target-action to the coordinator.
WatchLandmarks
) in our project, and we choose the “Watch App for Existing iOS App” optionLandmarkDetail
view to replace that in the main targetTIL you can have multiple asset catalogs in a target.
(TODO again we see use of the landmark.image.resizable()
and the .scaledToFill()
modifier; get comfortable with those)
Then there’s something I don’t understand to do with fiddling with the image so it behaves well in a scroll view
Note that the NavigationSplitView
on watchOS presents things in what I’d call a modal style, with a little list button to return to the list.
Now we’ll take a look at how to create a notification view (not sure exactly what that means; let’s see).
So, we first just create a normal SwiftUI view, and then we subclass WKUserNotificationHostingController
(parameterised by our view type) from the WatchKit
framework. We implement its body
to return our view, and we implement didReceive(_ notification: UNNotification)
(where UNNotification
is from the UserNotifications
framework) which populate some properties that somehow find their way into that SwiftUI view.
Then to hook this thing up to the app, seems like you have to include a WKNotificationScene
in your view hierarchy (I don’t know why it’s a view and not a modifier, shrug). They include it inside the app’s Scene
(is the exact location important?) using an #if os(watchOS)
.
Then we see how to request notification permissions, which we just do by calling UNUserNotifcationCenter#requestAuthorization
in the usual way. This is an async
method and we call it inside a .task
modifier.
It then tries to show us how to view the notification by creating a “Notification Simulation File” and dragging it on the simulator watch face, but this doesn’t work for me and I shan’t investigate it now.
We start by adding a macOS target.
.frame(minWidth:minHeight:)
we create another LandmarkDetail
view
You now have three files called
LandmarkDetail
. Each serves the same purpose in the view hierarchy, but provides an experience tailored to a particular platform.
.navigationBarTitleDisplayMode(_:)
doesn’t exist on macOS, so we delete that modifiercouple of things to note about this same view but on macOS:
NavigationSplitView
is a single window with a sidebarToggle
is a check box.frame(maxWidth: 700)
on a text container for readability at large widthsFavoriteButton
currently looks like an old-school button with a border; we do .buttonStyle(.plain)
(another one to look at) to make it look like it does on iOSNow,
The larger display gives you more room for additional features.
MKMapItem#openInMaps()
)alignment
you pass to e.g. ZStack
is actually an instance of Alignment
, which has an initializer they use as Alignment(horizontal: .trailing, vertical: .top)
in this example.frame(minWidth:)
on our NavigationSplitView
’s sidebar contentFor the row, using conditional compilation is appropriate because the differences are small.
OK, we’re going to now move the favourites toggle to a menu on macOS.
toolbar
modifier again, but this time we put two new views inside it: a ToolbarItem
(?) containing a Menu
(?), where Menu
has a label
and an action.(Oh, I see, I think that they mean a dropdown menu that pops up when you click a toolbar button, not a menu bar button.)
Picker
to this Menu
, for choosing a category of location. Interestingly, this Picker
appears as a list directly within the menu on iOS (another example of “hmm how exactly does that work”?) We decide that we want the same inline behaviour on macOS too, where we have to use the .pickerStyle(.inline)
modifier (as I think I asked about something else — or maybe this — earlier, why is this a modifier and not just an initializer property of the Picker
?)OK, I think now we’re going to look at menu bar menus.
We’re going to start by adding a menu command for restoring the sidebar (if the user accidentally closes it and ignoring there’s a button for doing so).
We create a LandmarkCommands
structure that extends SwiftUI’s Commands
. It has a var body: some Commands
. And then in this body, we include SidebarCommands
, which is a built-in command set, including a command for toggling the sidebar.
To make use of commands in an app, you have to apply them to a scene, which you’ll do next.
(What is a scene?) — I don’t know, but we apply the .commands
modifier to the WindowGroup
in the app.
Scene modifiers work like view modifiers, except that you apply them to scenes instead of views.
And now, magically, the View > Show Sidebar menu has appeared!
Now, having seen how to add a built-in menu command, we’ll add a custom one for toggling the favourited-ness of the selected landmark.
Now begins something that I’ll vaguely follow.
create struct SelectedLandmarkKey
which implements SwiftUI’s FocusedValueKey
protocol
no idea what this is, focus seems to be some sort of SwiftUI concept though, and I think that the thing we favourite will in some sense be a “focussed” view
FocusedValues
type, adding a selectedLandmark: Binding<Landmark>?
propertyapparently this is similar to what we do with environment (shrug, look at this once I understand that better, I guess):
The pattern for defining focused values resembles the pattern for defining new
Environment
values: Use a private key to read and write a custom property on the system-definedFocusedValues
structure.
LandmarkCommands
we add this new-to-us property wrapper @FocusedBinding(\.selectedLandmark) var selectedLandmark
(hopefully this will become clearer on a second look once I’m familiar with whatever this pattern that mimics Environment
is)CommandMenu("Landmark")
in the LandmarkComamnds
body
.selectedLandmark?.isFavorite
.disabled(_:)
modifier to disable the button if there’s no selectedLandmark
.keyboardShortcut("f", modifiers: [.shift, .option])
OK, so we’ve got the menu in place, and it’ll do stuff with the selectedLandmark
. How do we populate that based on what the user’s selected?
well, we see how we can pass List
’s initializer a selection
binding, which will be updated with the selected landmark
again, I think that it uses the view’s .tag(_:)
to identify the right one
now we (and am at a point of just trying to get to the end of this now) use the .focusedValue(\.selectedLandmark, $modelData.landmarks[index ?? 0])
modifier. There’s another use of the mysterious @Bindable
again as a local variable (seen that a few times now)
(Aside: I think I was wondering earlier about how to pass things up view hierarchy, e.g. how .tag
gets interrogated. Someone asked the same thing in this Reddit question. Sounds like Preferences
is the general mechanism for passing stuff up the view hierarchy.)
Finally, we’re going to add an in-app “MacLandmarks > Settings” menu (i.e. the standard settings menu). We’ll use it to control the initial map zoom level.
@AppStorage(_:)
property wrapper, which takes a string user defaults key and I guess takes care of reading and writing to user defaults? we add, inside our map view, @AppStorage("MapView.zoom") private var zoom: Zoom = .medium
(where Zoom
is an enum of our creation)@AppStorage
(I note that both views are defining their own default values; shouldn’t this be coordinated in some way, even if just through a shared constant?)Picker
. but it does so within the new-to-us Form
, saying:You typically use a
Form
to arrange controls in your settings view.
(Why only in the settings view?) (Also, I notice that the Picker
appears as a list of radio buttons; is this Form
’s doing? Or is that because we’ve used .pickerStyle(.inline)
? Or both? Ah, yeah, it’s because of .inline
; without that then the form shows a <select>
-style thingy.)
.navigationTitle(_:)
here sets the window’s title bar text.And then, finally, we add the new-to-us Settings
scene inside the app’s body
, with content of our settings view.
From the test at the end, note that the picker’s selection type must be Hashable
.
Well, that’s it for this tutorial! There was a lot to take in, and I definitely switched off for a decent chunk of it. But working with SwiftUI and discovering all the different APIs is something that seems very exciting and cool to me, so that’s a good thing I guess.
body
(i.e. their Body
is Never
).So, I understand two of the mechanisms that exist. When executing body
, SwiftUI tracks which properties are read, if they fall into one of the following categories:
@State
in the view@Observable
And then, when you mutate these, which you do explicitly by calling a setter, it knows that they have changed and acts accordingly
But, how does it know that you’ve changed a normal var
property of a view, i.e. something that the view doesn’t manage, and which is changed by calling the view’s initializer again, as opposed to a setter? This feels like a different kind of “change” and it’s one that I don’t yet know how it fits in.
For example:
struct ContentView: View {
@State var text: String = "put myText here"
var body: some View {
let _ = Self._printChanges()
VStack {
TextField("Text please:", text: $text)
MyLabel(text: text)
}
}
}
struct MyLabel: View {
var text: String
var body: some View {
let _ = Self._printChanges()
Text(text)
}
}
So, what’s the mechanism that means that when I type text into ContentView
, MyLabel
’s body
gets called again? Yes, MyLabel
’s text
has “changed” in some sense, but not in the sense of “it was mutated by a setter” that I mentioned above. I imagine that this is something to do with SwiftUI’s diffing mechanism, so perhaps reading the aforementioned Rens Breur article will shed some light.
Self._printChanges()
just says:
ContentView: _text changed. MyLabel: @self changed.
.task { … }
modifierSo, if you want to make the task be restarted when some value changes, you can use the .task(id:) { … }
modifier instead; the documentation says:
If the
id
value changes, SwiftUI cancels and restarts the task.
So, two questions:
task { … }
closure, right? I don’t see any examples doing this.e.g.
.task(id:) { … }
— “If the id
value changes, SwiftUI cancels and restarts the task.”.animation(_:value:)
— “Applies the given animation to this view when the specified value changes.”What does “change” mean? id
and value
are just something conforming to Equatable
; does it matter where this value comes from; does it have anything to do with the dependency mechanism, or is this just something that’s called each time the view’s body
is re-evaluated? These probably both link in to “How SwiftUI finds out that things have changed” above.