Daniel Steinberg Workshop: Bringing SwiftUI to your app
Live from Pragma Conference, Bologna, 9 October 2019
Registration done, now waiting for the workshop to start.
Daniel Steinberg just got on stage! Here are some of the key aspects of today!
- In Swift 5 if you have a function that has only one statement, we can omit the return
keyword
. VStack
have a function builder component where the last parameter accepts a closure.Spacer()
pushes views as far as possible from each other.Button
s do not talk toText
(Labels) anymore. Labels do not have names anymore… We need to create a property for the text inside of the label, then, inside thebody
property, we need to pass in aText(propertyName)
and then, in the last parameter of theButton
initialiser, writeself.message = "What you want to change goes here"
. But that will not compile unless we write@State
in front of the property that is controlling the text content outside… These are called property wrappers, new to Swift 5.1.- SwiftUI wants to make sure that anything you knew in the past doesn’t work anymore! Now buttons doesn’t have
isEnabled
anymore as a property, but a.disabled(\_: Bool)
method that requires another property to be passed inside. Of course this property will need another@State
property… - We can replace this call to
Text()
with a separate file which has an uninitialised constant calledmessage
which will then be called insideContentView
. - If we move also the Button to its own file we need it to have a
@Binding
property tomessage
, which will be called insideContentView
with$message
. The$
sign is the binding sign.
Happily enough, I found that the Documentation for this action is updated and it writes the following:
Summary
A manager for a value that provides a way to mutate it.
Declaration
Discussion
Use a binding to create a two-way connection between a view and its underlying model. For example, you can create a binding between a
Toggle
and aBool
property of aState
. Interacting with the toggle control changes the value of theBool
, and mutating the value of the Bool causes the toggle to update its presented state.You can get a binding from a
State
by accessing its binding property. You can also use the$
prefix operator with any property of aState
to create a binding.
Another quite new thing is that we now create our model as classes and our views as structs. The exact contrary of what we used to do.
There are many “manager”: we saw Binding
before, then we get @Published
, which is coming from the Combine framework and is so defined in the Documentation:
Generic Structure
Published
A type that publishes a property marked with an attribute.
Declaration
Overview
Publishing a property with the
@Published
attribute creates a publisher of this type. You access the publisher with the$
operator, as shown here:Important
The
@Published
attribute is class-constrained. Use it with properties of classes, not with non-class types like structures.
It seems that we have to use this with an ObservableObject
class whenever possible, even if it is not stated in that doc. Let’s try this one:
Protocol
ObservableObject
A type of object with a publisher that emits before the object has changed.
Declaration
Overview
By default an
ObservableObject
synthesizes anobjectWillChange
publisher that emits the changed value before any of its@Published
properties changes.
All this seems more advanced than what we looked at today, and very interesting. I think it is just natural that I am feeling confused right now, but I can’t wait to see what we are going to do this afternoon.
Please now try to read this and ping me if you understand what it means!
Generic Structure
EnvironmentObject
A dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.
Declaration
Overview
You must set a model object on an ancestor view by calling its
environmentObject(_:)
method.
In the last of this morning’s apps we used this method but only when we had to set the Previews, which in Mojave are totally irrelevant.
The last one we should look at before continuing the workshop is this one:
Generic Structure
State
A persistent value of a given type, through which a view reads and monitors the value.
Declaration
Overview
SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view.
A
State
instance isn’t the value itself; it’s a means of reading and mutating the value. To access a state’s underlying value, use its value property.Only access a state property from inside the view’s
body
(or from functions called by it). For this reason, you should declare your state properties asprivate
, to prevent clients of your view from accessing it.You can get a binding from a state with the
binding
property, or by using the$
prefix operator.
So, here we start with the afternoon half of the workshop.
State is being stored locally and is used to track the changes of a value. If we have other views that require to know about that value. If it just need to know about it, it can be just a let
, otherwise, if it changes it, we need to use Binding, which means that I need a 2-way connection. To access a value a
in another view we need to use the $
sign.
If we have a class a: A
and we need to see its changes elsewhere, we need to label it as ObservableObject
. Every property of a
must be marked as @Published
. Who watches a
to see what has changed? A content view is going to watch for changes, and also other views are going to do so. They need to register for a
but they need to be marked as @ObservedObject
s. We create an instance of a
then we create an instance of the extra views that need the dependency injection and pass the properties to their initialisers.
If we have an ObservableObject
class a
, with @Published
properties but we have a content view with a couple of tab views, we create two instances of the views we need to work on and, inside of our SceneDelegate
file, we add an instance of a
passed to the environmentObject
method of the contentView
property. At this point, we pull out the object from there using the @EnvironmentObject
manager.
Forgive me if this doesn’t sound too clear but this is what I understood from the recap after the launch-break! I promise I will go over this again and again in the future. I underline that this is my lack of experience that makes me slow and fall behind. Daniel’s talk (and drawings!!!) were so good! He was using a nice app on the iPad that I think was called Penultimate to show us the connections between different UI elements and their wrappers.
The next app we built was evolving from a project Daniel gave us… being an already working project that needed to be improved we needed to get a quick grasp on what was already there, thing that, unfortunately, I struggled a lot to do. All this is so different from UIKit (which is the only thing I knew before today) that I found the syntax quite chaotic. I know, closures and key paths are one of the most powerful features of Swift but I still need some good studying time to get comfortable with them.
The live coding was well done by Daniel (and thank you!, slow enough to be possible to follow it!) but after a while I was too tired to be able to follow, maybe also because of the very generous lunch!
I am really grateful to Mr. Steinberg for giving us the project files and the slides to allow us to go through them again and again and review things. Please do not forget to check out his website and his latest book (cannot find the link, use this for all his published books)!
100 Days of SwiftUI — Day 16
Project 1, part one
This first project aims to build a check-sharing app to calculate how much each one should pay and how much tip should be given. The name of the project is WeSplit. We are going to learn the basics of UI design, how to let users enter values and select from options, and how to track program state.
Introduction
Create a new SingleView App with interface based on SwiftUI, call it “WeSplit” and save it somewhere sensible. Make sure that you are not selecting the bottom three checkboxes and, optionally, check or leave unchecked the “Create a Git repository on this Mac” checkbox — I hit it.
Understanding the basic structure of a SwiftUI app
We start this section with an overview of the files in the project that we just created. The AppDelegate.swift file is very similar to what was before in UIKit
and it contains code used to manage our app. In the past we used to write code in here to manage windows, view controllers and so on, but nowadays it is less common to do so. If you look at it and compare it with one from a UIKit app it will be much trimmed down. There is a lot of UIScene
calls in there, which bring us to the next file.
The SceneDelegate.swift is the real new thing for SwiftUI
as it contains the code that will launch one window in our app. So, all the viewWillAppear
and its brothers are now here in the form of sceneWillEnterForeground
and similar. The most important method here is scene(_:willConnectTo:optios:)
which creates an instance of ContentView()
, downcasts the scene
as a UIWindowScene
, creates a UIWindow
with it, sets its rootViewController
to be a UIHostingController
with the content view as the root view, sets the window
of the SceneDelegate
class to be the just declared UIWindow
, finally making it key and visible.
The ContentView.swift replaces what the ViewController.swift file used to be and where we will be doing most of the work for this project. After the import SwiftUI
statement, which imports the new framework (instead of UIKit
). Then we find the struct ContentView: View
declaration, which is a struct conforming to the View
protocol. This is the basic protocol that must be adopted by anything we want to draw on the screen.
Protocol
View
A type that represents a SwiftUI view.
Declaration
Overview
You create custom views by declaring types that conform to the View protocol. Implement the required
body
computed property to provide the content and behavior for your custom view.
This is quite disappointing, right? Better to get used to it, though, this is SwiftUI by now. The new thing here is that computed property, body
, which returns some View
, an opaque return type introduced in Swift 5. That some
means that this property must return something that conforms to the View
protocol but it must always be the same kind of view (we cannot sometimes return one type of thing and other times a different one). I wonder then why it is called some
, if it is not some but precisely the same kind of thing!!! Paul promises further explanation in the future, so I will trust him (never being let down on that!).
The line Text("Hello World")
is what UILabel
were before. I am not sure I like that, I find it somehow confusing but it may just be the new thing. The part below is something about previews, a feature available in Catalina, which I don’t have (and by now don’t want), so I will leave this out.
Creating a form
To create a place for user to input text we need to use a dedicated view type called Form
, a scrolling list of static controls, capable though of accepting interactive controls (text fields, switches, etc…). Simply wrap the Text
instance inside a Form { }
one. Here is the Documentation for Form
:
Generic Structure
Form
A container for grouping controls used for data entry, such as in settings or inspectors.
Declaration
Overview
SwiftUI renders forms in a manner appropriate for the platform. For example, on iOS, forms appear as grouped lists. Use
Section
to group different parts of a form’s content.
And here is the link to Section
: an affordance for creating hierarchical view content. It is declared like this: struct Section<Parent, Content, Footer>
. Looks like grouped table views, maybe?
If we want more than 10 items in our Form
, we need to wrap them into Group
s. Interestingly enough, Groups
don’t change the way the user interface looks, they are just workarounds for SwiftUI limitations by now. If we want a real difference in the UI we should use Section
.
Adding a navigation bar
A great addition of SwiftUI is that it doesn’t allow anymore to place view by default outside of the Safe Area. The form we used, though, if scrolled, still goes outside of it. What we used to do in UIKit was to embed the view controller inside of a navigation controller. This, in SwiftUI is done through wrapping our views inside of a NavigationView
element. Right now SwiftUI is very slow in the auto-completion code but very fast at showing you incomprehensible errors before you can do anything about it. Right now the app shows a grey area about the Form but if we scroll up a white space appears and out Form disappears behind it.
We normally want to put some sort of title in the navigation bar, and we can do that by attaching a modifier to that navigation bar so that all what we have placed inside will be affected.. Modifiers are regular methods with just one difference: they always return a new instance of what we used them on. This is where SwiftUI can quickly become a lot of code-noise, but we will stick with it by now! After the closing braces of the Form
, add .navigationBarTitle(Text("SwiftUI"))
. This places a “label” inside the bar title, but it is also possible to pass it a StringProtocol
object, that is, a simple String. The result looks the same, so I cannot say what is better by now. The one we use is the only one which is actually fully documented.
Summary
Configures the title in the navigation bar for this view.
Declaration
Discussion
This modifier only takes effect when this view is inside of and visible within a NavigationView.
Parameters
title
A description of this view to display in the navigation bar.
Paul says that the other version is just a shortcut of this one. If we want to have a smaller font (what was largeTitle = .never
before), we need to add , displayMode: .inline)
. The options here are .automatic
, .inline
and .none
.
Modifying program state
“Our views are a function of their state”, is a motto for SwiftUI developers. The active collection of settings that describe how things are in this moment is called state! If we quit an app, its state will be saved and when we launch it again, its state will be loaded back. While we are using it, all the integers, Booleans, strings etc… will be called state and stored in RAM memory.
In SwiftUI what appears on the screen is determined by the state of our program. In UIKit the UI was driven by events called messages between every element of the app. This programming approach makes it very hard for an app to store its state so even very famous apps do not save the user’s state inside themselves.
Now remove all what we added previously inside the body
property and replace it with a Button with a text of “Tap Count: \(tapCount)”
, and a running closure of self.tapCount += 1
. Of course add a var tapCount = 0
as a property at the top of the struct.
This won’t compile as the left size of the modifier is immutable. We should add the mutating
keyword to the closure but this is not allowed for computer properties.
The solution is a property wrapper, an attribute to be placed before properties to enhance their possibilities. In this case, we need to add @State
before the var tapCount = 0
line. This modifier allows a value to be stored separately by SwiftUI in a place where it can indeed be modified.
As a wrap-up to this section, we get told that @State
is designed for simple properties that are stored in one view. Apple suggests that we add private
in front of these properties.
Binding state to user interface controls
The @State
property wrapper lets us do what mutating
used to, so that we update our project according to how our properties change.
To go back to the original intent of our app, we could create a Form with a TextField and a Text inside but that would not work because SwiftUI wants to be kept up to date with what to store in that text field. As a rule to remember for the future: as views are a function of their state, a view can only show something if it reflects a value stored somewhere in the program.
We can therefore create a name
property equal to an empty string , then inside our Form, create a TextField(<#T##title: StringProtocol##StringProtocol#>, text: <#T##Binding<String>#>)
(yes, autocompletion when copied out is that complicated, in practice is a (_ title: String, text: BindingString)
and then a Text("Hello World")
to keep it simple.
To help the code compile we should now add @State
in front of private var name = ""
but that is still not enough because Swift here makes a difference between showing the value of a property somewhere and doing that along with writing any changes back to the property! This is a huge change compared to what was done before. This is called two-way binding. To do so, we need to add $
in front of the name
call. Before running, change the Text to read "Your name is \(name)"
.
The absolutely amazing thing here is that it updates in real time! Also, when you hit Return on the keyboard, the keyboard is actually dismissed! Awesome!
Creating views in a loop
The type to create views in a loop from a piece of data is called ForEach
. The Documentation entry for this is very small and reads just this:
A structure that computes views on demand from an underlying collection of of identified data.
It is clear enough, though. Create an array of strings in a constant called students
filled with students from the Harry Popper movies and books, and an @State private var selectedStudent = "Harry"
for example.
Inside the body
property, create a Picker
, which gets three parameters, a String Protocol “Select your student”, a selection: $selectedStudent
and a closure that calls a ForEach(0 ..< students.count)
that creates a Text(self.students[$0])
. These are all closures and the syntax may look difficult but it is actually much simpler that what it could be because we are using trailing closures.
Here the students
array doesn’t need to be marked with @State
because it doesn’t change. The selectedStudent
property, instead, needs to be able to change and it is therefore marked with @State
. The Picker has a label, which is fully accessible, a two-way binding and a closure.
Now it’s time to clean the project and be ready to start anew, thing that closes today’s article!
Come back tomorrow for the second part!
That’s it for today.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!