Translation of www.hackingwithswift.com/articles/22…
SwiftUI is a large and complex framework. Programming with this framework is undoubtedly enjoyable, but the opportunity to make mistakes is not uncommon. In this post I will take you through some of the most common mistakes SwiftUI beginners make and offer solutions to correct them.
Some of these errors are the result of simple misunderstandings. Since SwiftUI is so large, this is actually easy to do. Others are related to a deep understanding of how SwiftUI works, and some are the result of old thinking — you might have spent a lot of time writing Views and modifier, but didn’t think to simplify the results SwiftUI way.
Cut to the chase. You don’t have to guess what I’m going to prepare for you. Here are eight misuses that I’ll simply list as follows, and then we’ll expand them one by one:
-
Add unnecessary Views and modifiers
-
@observedobject is used where @stateObject is needed
-
Wrong sequence of Modifier
-
Add a property observer to the property wrapper
-
Stroke shapes are used where stroke borders are needed
-
Use of Alert and Sheet and optional states
-
Try changing things behind the SwiftUI view
-
Dynamically create views with the wrong scope
1 Add unnecessary Views and Modifier
Let’s start with one of the most common misuses that leads us to write more SwiftUI code. Part of the reason for this misuse is often that we write a lot of code to solve the problem, but then forget to clean it up. Other times, it’s just old habits, especially if the writer has moved to SwiftUI from UIKit or some other UI framework.
For example, you might want to fill the screen with a red rectangle, right? Then you write code like this:
Rectangle()
.fill(Color.red)
Copy the code
Indeed, the code above works — it gets exactly what you want. But half of the code is unnecessary, because you can achieve the same effect just by writing it like this:
Color.red
Copy the code
This is because in SwiftUI, all colors and shapes automatically follow the View protocol and you can use them directly as views.
You might also see shape clipping a lot, because it’s natural to apply clipShape() to achieve a particular shape. For example, we can give our red rectangle rounded corners like this:
Color.red
.clipShape(RoundedRectangle(cornerRadius: 50))
Copy the code
But this is also unnecessary — with a cornerRadius() modifier, the code can be simplified as follows:
Color.red
.cornerRadius(50)
Copy the code
Removing this kind of redundant code takes time because you need to change your mindset, which is even more difficult for SwiftUI beginners. So, if you start with these longer versions of code, don’t worry.
2 is used when needed@StateObject
Is used@ObservedObject
SwiftUI provides a number of property wrappers to help us build data-responsive user interfaces, the most important of which are @State, @StateObject, and @Observedobject. It is important to understand their usage scenarios, because misuse of them can cause all kinds of problems in your code.
The first is straightforward: @state is used for a value type property, and the property is owned by the current view. So integers, strings, arrays, etc., are all great scenarios for @State.
The latter two are a bit confusing, and you’ll probably often see code like this:
class DataModel: ObservableObject {
@Published var username = "@twostraws"
}
struct ContentView: View {
@ObservedObject var model = DataModel(a)var body: some View {
Text(model.username)
}
}
Copy the code
To be clear, doing so is wrong and likely to cause problems in your application.
To say that this must be wrong, based on snippets of code, is a loose statement. The author should implicitly assume that the ContentView is the top-level view of your application (and it is, in general, if you don’t change the default output of your project template). For top-level views, SwiftUI 2.0 should use @StateObject, which resolves the ownership of @Observedobject or @environmentobject objects. But for the view hierarchy attached to the top-level view, the data source for each sub-view can be @observedobject or @Environmentobject, because their life cycle is managed by the top-level view, which in turn ensures the availability of data.
As I said earlier, @state indicates that a value type property is owned by the current view, and ownership is important here. The @stateObject is equivalent to the @state version of the reference type.
Therefore, the code above should look like this:
@StateObject model = DataModel(a)Copy the code
When you use @observedobject to create an object instance, your view does not own the object instance, that is, the instance can be destroyed at any time. Cunningly, objects are only occasionally destroyed when the view still needs them, so you might think your code is perfect.
The important thing to remember is that @State and @StateObject mean “view owns data,” while @observedobject and @environmentobject do not.
3 Incorrect sequence of Modifier
The order of modifiers is critical in SwiftUI. The wrong order can not only cause the layout to be visually incorrect, but also cause its behavior to be incorrect.
The classic example of this is the use of padding and background, as follows:
Text("Hello, World!")
.font(.largeTitle)
.background(Color.green)
.padding()
Copy the code
Since we apply the padding after the background color, the color will only be applied directly around the text, not around the text after the white space is added. If you want white space and a green text background, change the code to something like this:
Text("Hello, World!")
.font(.largeTitle)
.padding()
.background(Color.green)
Copy the code
This principle makes things more interesting when you try to adjust view positions.
For example, offset() modifier modifies the position where a view is rendered, but does not actually change the position of the view. That is, applying the modifier after offset acts as if the offset never happened.
Try the following code:
Text("Hello, World!")
.font(.largeTitle)
.offset(x: 15, y: 15)
.background(Color.green)
Copy the code
You will notice that the text is offset, but the background color is not. Now, try swapping offset() and background() :
Text("Hello, World!")
.font(.largeTitle)
.background(Color.green)
.offset(x: 15, y: 15)
Copy the code
Now you can see that the text and the background have moved.
In addition, position() modifier changes the rendering position of a view in its parent by first applying a stretchable frame around the view.
Try the following code:
Text("Hello, World!")
.font(.largeTitle)
.background(Color.green)
.position(x: 150, y: 150)
Copy the code
You’ll notice that the background color is snugly around the text and the entire view is placed in the upper left corner. Now, try swapping background() and position() :
Text("Hello, World!")
.font(.largeTitle)
.position(x: 150, y: 150)
.background(Color.green)
Copy the code
This time you’ll notice that the whole screen turns green. Again, because Position () requires SwiftUI to place a scalable frame around the text view, the view automatically fills up all the available space. And then we color the view green, so the whole screen is green.
Most of the modifier you should create a new view — when you apply a Position or background, you’re essentially wrapping an existing view around it. We can use the modifier multiple times, such as adding multiple layers of white space and background:
Text("Hello, World!")
.font(.largeTitle)
.padding()
.background(Color.green)
.padding()
.background(Color.blue)
Copy the code
Or apply multiple shadows to create deep shadows:
Text("Hello, World!")
.font(.largeTitle)
.foregroundColor(.white)
.shadow(color: .black, radius: 10)
.shadow(color: .black, radius: 10)
.shadow(color: .black, radius: 10)
Copy the code
Add a property observer to the property wrapper
In some cases you might add a property observer like didSet to the property wrapper, but it won’t work as you expect.
For example, if you were using a slider and wanted to perform an action when the slider value changed, you might write code like this:
struct ContentView: View {
@State private var rating = 0.0 {
didSet {
print("Rating changed to \(rating)")}}var body: some View {
Slider(value: $rating)}}Copy the code
However, the didSet property observer is never called because the property’s value is modified directly by the binding, rather than creating a new value each time.
For this, SwiftUI’s native way is to use onChange() Modifier, as follows:
struct ContentView: View {
@State private var rating = 0.0
var body: some View {
Slider(value: $rating)
.onChange(of: rating) { value in
print("Rating changed to \(value)")}}}Copy the code
However, I personally prefer a different approach: I use a binding-based extension to return the new Binding, where get and set wrap the same values as before, but also call handlers when the new value arrives:
extension Binding {
func onChange(_ handler: @escaping (Value) - >Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
)
}
}
Copy the code
With this extension, we can attach the bound action directly to the slider view:
struct ContentView: View {
@State private var rating = 0.0
var body: some View {
Slider(value: $rating.onChange(sliderChanged))
}
func sliderChanged(_ value: Double) {
print("Rating changed to \(value)")}}Copy the code
Pick the one that works best for you.
5 Stroke shapes are used where stroke borders are needed
Not understanding the difference between stroke() and strokeBorder is a common mistake beginners make. Try the following code:
Circle()
.stroke(Color.red, lineWidth: 20)
Copy the code
Notice how the left and right edges of the circle are missing. (the translator: This is because the Stroke () modifier centers the stroke on the shape’s outline, so a 20-point red stroke draws 10 points outside the shape’s edge line, Ten points are inside the edge line — this causes you to see the left and right sides of the circle off the screen.
StrokeBorder (), by contrast, draws the entire stroke inside the shape, so it doesn’t enlarge the border of the shape.
Circle()
.strokeBorder(Color.red, lineWidth: 20)
Copy the code
One advantage of using stroke() over strokeBorder() is that it returns a new shape instead of a new view. This allows you to create effects that would otherwise be difficult to achieve, such as stroke a shape twice:
Circle()
.stroke(style: StrokeStyle(lineWidth: 20, dash: [10]))
.stroke(style: StrokeStyle(lineWidth: 20, dash: [10]))
.frame(width: 280, height: 280)
Copy the code
6 Use of Alert and Sheet and optional states
When you’re learning to use sheet and be selectable, it’s easy to think of binding sheet’s presentation to a Boolean like this:
struct User: Identifiable {
let id: String
}
struct ContentView: View {
@State private var selectedUser: User?
@State private var showingAlert = false
var body: some View {
VStack {
Button("Show Alert") {
selectedUser = User(id: "@twostraws")
showingAlert = true
}
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Hello, \(selectedUser!.id)"))}}}Copy the code
Of course, this works correctly — and the scheme is easy to understand. But once you get past the initial stage, you should consider switching to a scalable implementation. This scheme removes Boolean and does not force unpacking. The only requirement is that the target you are monitoring needs to follow the similarly.
For example, we can display a warning popup whenever a selectedUser changes, like this:
struct ContentView: View {
@State private var selectedUser: User?
var body: some View {
VStack {
Button("Show Alert") {
selectedUser = User(id: "@twostraws")
}
}
.alert(item: $selectedUser) { user in
Alert(title: Text("Hello, \(user.id)"))}}}Copy the code
This will make your code easier to read and write and avoid the hassle of forced unpacking.
7 Try changing things behind the SwiftUI view
One of the most common mistakes that SwiftUI beginners make is that they often try to change the background of the SwiftUI view. The code usually looks like this:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.background(Color.red)
}
}
Copy the code
This displays a white screen with a red background color in the middle of the text view that matches only the text area. Most people actually want the whole screen to be red. At this point, they’re thinking, what kind of UIKit view is behind SwiftUI?
Of course, there must be a UIKit view behind it, and it’s managed by a UIHostingController, and its role is like a UIKit view controller. But if you try to step into UIKit territory with SwiftUI, your changes will most likely give SwiftUI strange results, or you won’t even be able to change UIKit directly.
In fact, to achieve what most people want, the SwiftUI implementation would look something like this:
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.ignoresSafeArea()
Copy the code
8 The range parameter of the dynamic view is incorrect
The fact that there are multiple Constructors for SwiftUI views that allow us to pass in scopes makes the creation of many complex views very simple.
For example, suppose we wanted to show a list of four items, we would simply write:
struct ContentView: View {
@State private var rowCount = 4
var body: some View {
VStack {
List(0..<rowCount) { row in
Text("Row \(row)")}}}}Copy the code
This is fine in itself, but it becomes a problem once you need to change the scope at run time. You see I’ve used the @state property wrapper to make the number of rows I want to change modifiable, so we can change its value with a button:
Button("Add Row") {
rowCount + = 1
}
.padding(.top)
Copy the code
Run the code, click the button, and the Xcode debug output will print a warning, but the list view won’t do anything — this scheme doesn’t work.
The problem is that you don’t provide either an Identifiable implementation for the parameters of the list, or a specified ID parameter, to tell SwiftUI that the scope changes dynamically. The Identifiable or ID parameter identifies how the two entries differ. Distinguishable items can detect change).
List(0..<rowCount, id: \.self) { row in
Text("Row \(row)")}Copy the code
So if I change the code to this, I’ll be fine.
For more content, please follow my public account “Swift Garden”.