SwiftUI Pro Tips 1–3

Ryan Jennings
Ancestry Product & Technology
4 min readApr 6, 2022

At Ancestry we’ve been slowly transitioning to SwiftUI for our user interfaces and have picked up a couple of useful “pro tips” along the way. Here are a couple of these tips. I’ll follow up with more in the future.

This article assumes you have a basic understanding of SwiftUI and its mechanics. If you are completely unfamiliar with SwiftUI, I recommend you visit Paul Hudson’s “100 Days of SwiftUI” https://www.hackingwithswift.com/100/swiftui.

Pro Tip #1: Initializing @State variables

Usually state variables are initialized inline when you declare them:

@State private var speed: Double = 3.0

But sometimes you need to set them based on a value passed through the init() function. So, you can do this instead:

@State private var speed: Double
init(speed: Double) {
self._speed = State(initialValue: speed)
}

For more information on @State: https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-state-property-wrapper

Pro tip #2: Custom Bindings

Sometimes you’ll want to react to a binding when it’s updated. To do this, you can use the .onChange modifier:

.onChange(of: <binding>) { <binding value> in
<do something>
}

But this is only fired after the binding is updated. What if you want to react when (or immediately before) a binding is updated? You can’t add a didSave or willSave to the binding declaration. The binded variable doesn’t really belong to the struct it’s defined in. It is rather a variable that is shared with that struct, from another struct (an ancestor struct) where the key variable is defined as something like @State. You can add a didSave to a @State variable, but for a binding you have to get a little more creative.

Here’s what I did. I wanted a text binding (attached to a TextEditor) to limit its size to a certain number of characters. If I used onChange, and the user typed within the text editor, the character that was over the limit was allowed to be typed, but then would disappear. I instead wanted a hard stop, when you’re at the character limit future typing should just be blocked.To achieve this I created a custom binding. The “true” binding is defined as you normally would:

@Binding public var text: String

Then I declare another “custom” binding that sets and returns the value of the “true” binding, but whose “set” function is called when the custom binding is being set. And so further processing can happen at that time, and in my case I could limit the string to a certain number of characters.

let textCustomBinding = Binding(
get: { self.text },
set: { self.text = String($0.prefix(200)).components(separatedBy: .newlines).joined() }
)

My TextEditor then uses this custom binding instead of the true binding:

TextEditor(text: textCustomBinding)

And this allowed the text editor to behave exact how I wanted it to.

For more information on custom bindings: https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-custom-bindings

Pro tip #3: Dynamic TextEditor

By default a TextEditor will take up as much room as possible. It will fill all the available space of its parent. To limit the size of a TextEditor, apply a .frame(width: <value>, height: <value>) modifier.

But if you want the TextEditor to only be as tall as the amount of text typed into it, and grow as more text is entered, place the TextEditor inside the .overlay modifier of a Text view. The Text view will only display as tall as the amount of text it needs to display, and the embedded TextEditor will only fill that amount of space. And since both are sharing the same text @State variable, both will "grow" as more text is added to the variable.

@State private var text = ""...Text(text) // This transparent text view is used to size the text editor
.font(font)
.foregroundColor(.clear)
.padding(.horizontal, 5)
.padding(.vertical, 8) // This padding is to match the default padding of TextEditor
.frame(maxWidth: .infinity)
.background(Color.clear)
.overlay(
TextEditor(text: text)
.font(font)
)

Come back next week for more pro tips!

Bonus!

Here is a handy SwiftUI cheat sheet originally provided by Jared Sinclair (but slightly modified by one of our SwiftUI engineers, Scott Doerrfeld):

https://jaredsinclair.com/2020/05/07/swiftui-cheat-sheet.html

SwiftUI Cheat Sheet

  1. Use @State when your view needs to mutate one of its own properties.
  2. Use @Binding when your view needs to mutate a property owned by an ancestor view, or owned by an observable object that an ancestor has a reference to.
  3. Use @StateObject when your view instantiates an observable object itself.
  4. Use @ObservedObject when your view is dependent on an observable object that gets passed into that view’s initializer.
  5. Use @EnvironmentObject when it would be too cumbersome to pass an observable object through all the initializers of all your view’s ancestors.
  6. Use @Environment when your view is dependent on a type that cannot conform to ObservableObject.
  7. Also use @Environment when your views are dependent upon more than one instance of the same type, as long as that type does not need to be used as an observable object.
  8. If your view needs more than one instance of the same observable object class, you are out of luck. You cannot use @EnvironmentObject nor @Environment to resolve this issue.

If you’re interested in joining Ancestry, we’re hiring! Feel free to check out our careers page for more info.

--

--