1. The View protocol

If you aren’t already aware, SwiftUI uses the View protocol to create reusable interface elements. Views are value types, which means they use a Struct instead of a Class definition.

What does this actually mean in practice?

Structs do not allow inheritance. Although your structs conform to the View protocol, they do not inherit from a base class called View that Apple has provided.

This makes it different from UIView, from which almost everything in UIKit inherits. A UIView basically cannot be seen without being assigned a frame and being added as a subview of a UIViewController subclass.

If you create a new Xcode project that uses SwiftUI instead of Storyboard as the basis of its user interface, you’ll automatically be given an example of a SwiftUI View called ContentView.

You’ll notice that inside the ContentView struct, there is a variable called body. This is the sole requirement of the View protocol, and it makes use of the some keyword which is brand new to Swift 5.1.

You can rely on a Stack Overflow thread to explain what this keyword means, better than I ever could:

“You can think of this as being a “reverse” generic placeholder. Unlike a regular generic placeholder which is satisfied by the caller… An opaque result type is an implicit generic placeholder satisfied by the implementation… The main thing to take away from this is that a function returning some P is one that returns a value of a specific single concrete type that conforms to P.”

Let’s get started looking at the example views that Apple provides that conform to the View protocol.

2. Text

That example project that you get when creating a SwiftUI Xcode project includes probably the most simple building block for a view, and it’s called Text.

In most cases, you’ll be passing a String to the constructor of this, and that will be the content it displays.

Here are some examples of all the initializers for Text:

Note that the last initializer, the one that takes a LocalizedStringKey, tableName, bundle, and comment, requires a separate file that uses the .strings file extension.

As is mentioned in Apple’s documentation for this initializer, the only required parameter is the string for the key. I gave a verbose example mostly so that you can see what these other parameters require.

The default of tableName is Localizable, the standard name for a strings file. I deliberately named mine Local to show why I would need this parameter.

The bundle is the main bundle by default, so passing Bundle.main is redundant in this case.

The comment should give contextual information, but in this example, I’ve just given it the string Comment.

The Texts here are embedded in a VStack because the body variable’s opaque result type has to be set to one type. In other words, it can’t be set to several items because this could involve several types.

Although VStack can contain up to ten views inside it, each of these can be a VStack, HStack, or Group and each can have ten types inside it.

Scroll down to HStack, VStack, and ZStack for more details on these.

3. Text ViewModifiers

Text, like all Views, can be modified by structs that conform to the ViewModifier protocol.

Let’s see an example of a custom modifier so you can see what’s happening under the surface with these:

All of these Texts will look the same, but the second two use custom ViewModifiers

As you can see, it would be possible to add .modifier(YourModifier()) to call a ViewModifier, but it makes a lot more sense to use a View extension and give a clean call site.

This is the way the standard modifiers look, so making a View extension will make your modifiers look much more like the default ones.

It will be difficult to create a ViewModifier that is easier to write than the defaults without this, as starting your modifier with the word “modifier” and calling its constructor adds unnecessary complexity.

4. Standard text modifiers

All the sizes and weights of the default font

In this example, I put a red border around the VStack that holds the font alignments. This was to show that the boundaries of this VStack are limited because the Texts inside have a fixed maximum size.

Without this, alignment for the Texts has no effect, as the VStack container will expand to accommodate the Texts inside.

To align to a leading (left)or trailing (right) edge, we need to define where that leading or trailing edge is going to be. This can also be achieved by fixing the width of the VStack itself.

Notice how this example has created extensions on Text instead of View. This is because you can only guarantee that the received type will be a Text in this case.

It is impossible to do the equivalent to greenStrikethrough or redUnderline by creating a ViewModifier or View extension because these take a generic View that may not be a Text.

Now that you know this, you can make your own custom functions that customize Text, without the intermediary step of creating ViewModifiers I mentioned above.

5. TextField

If you want a user to enter text, you’ll need a binding to store that data. This first example uses a State variable, which stores the string locally in the SwiftUI struct and doesn’t actually save it anywhere you can use it or store it permanently.

If you want to hold onto your data and be able to give it a computed value, you’ll need an ObservableObject. This essentially gives you a regular Swift file to store your data in, which will get pretty useful once you start adding controls that can modify data values.

You don’t need to provide a didSet closure for the string value that you saved, I’m just providing an example to show when didSet runs. My DataModel class is a normal Swift class, so its didSet closure runs and prints the new value of the string.

However, since SwiftUI views are value types that are created dynamically, the didSet callback does not print anything to the console when you modify the local State variable.

Did you notice how some of the later initializers use a Float instead of a String?

These initializers will take any type, but be careful to pass in one of the provided Formatter classes, or make one yourself.

In my example, I use NumberFormatter, which will not let you input any character that isn’t a number. This makes it easy for me to save my Float without worrying that the app will crash because I can’t convert a string of letters into the Float that stores the TextField’s value.

The other initializers also have two closures, the first of which is onEditingChanged.

Apple’s documentation on this TextField initializer doesn’t mention what the bool inside the closure indicates, but testing seems to show that it relates to the TextField being given focus.

Have you ever called resignFirstResponder on a UITextField in UIKit?

This essentially dismisses the keyboard because the UITextField no longer needs focus. Even if you could bring the keyboard back at some point, text would not be inserted into that UITextField unless you made it the first responder again.

That all relates to UIResponder, an abstract interface from which UIView, UIViewController, and basically everything else in UIKit inherits.

We don’t know how SwiftUI events are handled to the same extent, but I’m using the phrase first responder as it should be familiar to anyone who has used UITextField.

The bool in onEditingChanged can be called fieldActive or anything else you want that makes it clear to you.

The important thing is that when you start to edit a TextField, onEditingChanged is called with a bool that is set to true. When you press the keyboard’s return key, the onCommit block is called, after which onEditingChanged is called with a bool that is set to false.

6. TextField ViewModifiers

For a more detailed explanation of what ViewModifiers are, see Text ViewModifiers.

You cannot currently change the foreground color of the placeholder text in a TextField. At the time that I’m writing this, you cannot use any keyboard type for a TextField other than the default when displaying TextFields in a List.

I found that trying to display TextFields in a List caused them to overlap each other too. Here are all of the keyboard types which seem to work pretty well when presented in a VStack:

7. SecureTextField

Essentially the same as TextField above with the added benefit of hiding the characters you enter, which is useful for passwords. As with TextField above, you can choose a variety of keyboard types, of which only numberPad is shown here.

8. SecureTextField ViewModifiers

For a more detailed explanation of ViewModifiers, see Text ViewModifiers.

Similar to TextField, you can change the foreground or background colors, add a border, and use different TextFieldStyles, but you cannot change the foreground color of the placeholder text at this time.

9. Font

I can’t expand much on what Apple’s documentation says about Font, so I’ve provided a simple way to use custom fonts in the same way as Apple’s standard fonts:

Notice how I’ve made extensions for both Font and View. You don’t have to use extensions, as you can see when I use Font.custom directly. All of these methods result in the same Text, so it’s just a matter of which code you find to be the cleanest.

The absolute easiest to write is the View extension, which doesn’t require you to pass anything into the function.

The Font extension is more consistent with the way standard Apple fonts are assigned, for example, .font(.headline).

10. Image

Images in SwiftUI are much easier than in UIKit. Instead of needing to create a UIImage(named: “Your file name”) and assigning it to yourUIImageView.image, Image is about as easy to create as Text.

Just pass it a String and it’ll set it to a file with that name. If you launch your app and it doesn’t have a file with that name, you’ll get a useful console message saying:

No image named ‘Your file name’ found in asset catalog for main bundle. 

If you find images not turning up in your app, you may want to search for this in the console.

Image is not resizable by default

You must call the .resizable() modifier on your Image before making changes to its size in subsequent modifiers.

The scaledToFit modifier will lock the aspect ratio of your image and scale it to the maximum size it can be without being too large for the screen.

The scaledToFill modifier also scales your image, but it does not lock the aspect ratio and, subsequently, is likely to stretch or shrink your image to fit the available space.

11. SF Symbols

If you aren’t familiar with them, SF Symbols is a library of over 1500 symbols that Apple provides in nine weights from ultralight to black.

To use these in your images, simply label the String you pass into your Image as systemName. It’s probably worth downloading the SF Symbols Mac app so that you can find out what the system name is for the symbols you want to use.

Using SF Symbols gives your app a consistent look that will probably be taking over the iOS ecosystem in the coming years due to the flexibility and accessibility of these free symbols.

12. Button

A Button has no appearance of its own. In other words, you will need to give your Button a Label, which itself is any concrete type that conforms to View.

The most obvious example of this is a Text that will give information on what your button will do. At the time that I’m writing this, the only thing Apple specifies in the documentation (other than how to create and style them) is that buttons are triggered differently depending on your operating system.

On iOS, you tap on it, on tvOS, you press enter when the button is selected, and in a macOS app with or without Catalyst, which Apple doesn’t mention, you click with a mouse or trackpad.

The constructor requires that you give an action. This can be an empty set of curly braces, but it has to be there in this form at the very least.

As well as specifying your functionality in the curly braces, which can get verbose pretty fast, you can also specify the name of a function without curly braces and without the () call operator. This is not binding the action to a variable, which means that you do not need the $ operator that you’ll find on controls that take a binding such as a Toggle.

13. ButtonStyle

Some controls allow you to choose existing styles, such as those that conform to ButtonStyle in this case. That also means that you can create your own custom styles for a Button, details of how to do that can be found on SwiftUI Lab’s Custom Styling tutorial.

As you might see in the comment I made on that post, SliderStyle does not currently exist (although it is documented on Apple’s website). Let’s go through the existing styles for buttons and see them in action.

Note that some are only available on MacOS.

14. NavigationView and NavigationLink

Embedding your Views in a NavigationView allows you to set a navigation title and link to other Views. Similarly to Button, a NavigationLink requires a Label which is basically any struct that conforms to the View protocol.

In most cases, this will probably be a Text or Image, but it can also be any custom view that you create.

The View that is the destination of your link slides in from the right on an iPhone, and each successive NavigationLink slides in the same way. When returning to the initial View, you can swipe from the left edge or use the back button in the top-left of the navigation bar.

This example is from my watch app Dog HQ that shows a scrolling list of full-sized dog photos, each of which links to a zoomed-in version.

This is why I need to pass the index to the constructor of my zoomed-in DogView, so that I know which dog I want to be the destination.

Combining a List, which scrolls vertically and expands to any size I want, with a ForEach allows me to create 50 rows and pass that index into the closure with a name I specify.

The iteration for the ForEach could be a sequence that has a maximum of the number of items in an array, or the array could be passed into the constructor for a List and accessed inside the closure with a name you specify.

Obviously, I’m just scratching the surface with this array. The array could contain complex types, such as a custom class that has a string property called imageName, a number value, or perhaps even an instance of another class, which you can access using the dot syntax.

The navigation bar at the top of the screen can contain a leading and trailing button. The main use for this seems to be adding an EditButton, which is described in detail below.

15. EditButton

An edit button is pretty useful when you have a List of items and you want to make it possible to delete some of them. Tapping it takes you into edit mode (unsurprisingly), showing a red circle with a horizontal line through it on each row.

Tapping Edit will slide the row to the left, revealing a delete button on the right end that acts as a final confirmation.

You still need to implement a function that will handle deleting the data from the list, otherwise, your changes will only be visual and your data won’t actually be deleted in the way you expect.

For more information on using EditButton, see List, ScrollView, ForEach, and DynamicViewContent.

16. MenuButton

Four menuButtonStyle options are currently available

MenuButton is only available on macOS apps, so I’ve provided a Mac app example that uses all of the standard .menuButtonStyle options.

From left to right, these styles are BorderlessButtonMenuButtonStyle, BorderlessPullDownMenuButtonStyle, and PullDownMenuButtonStyle.

If the one on the far right looks a lot like the one next to it, it’s because it uses DefaultMenuButtonStyle.

Since the default MenuButton has the appearance of PullDownMenuButtonStyle, these look exactly the same.

17. PasteButton

This control allows you to paste information on MacOS, but it is not available on iOS. It can take a variety of data types, which are expressed as UTI types. I’ve included a function in my example that lets you find the UTI string for any type, which will probably help you when implementing this button. Once you have decided what type identifiers you need, you will need to handle the data that you get from the NSItemProvider. I’ve shown an example where I only paste the first item in the array, but hopefully it makes it clear how you could handle other data types and multiple items.

Here’s a list of the types that conform to NSItemProviderWriting, and can therefore be used for pasting with the PasteButton:

You can also conform to this protocol with your own custom types, allowing you to paste custom types of data.

18. Toggle

Toggle is the SwiftUI equivalent of UISwitch in UIKit. Instead of having an IBAction function that links your Swift code to a UISwitch on a Storyboard and runs when its value changes, SwiftUI uses bindings.

Without marking a variable as State (within the struct) or Published (in an outside class conforming to ObservableObject), SwiftUI will not redraw the contents of the View when the value changes.

This is an essential part of the binding process, especially marking outside code as Published, as this is the only way that SwiftUI will even be aware of that variable’s existence.

19. Creating a custom ToggleStyle

I noticed that the initializer for Toggle can take a struct called ToggleStyleConfiguration, and I spent a while trying to figure out how to construct this myself.

What I found, with a lot of help from SwiftUI Lab’s excellent tutorial on custom styling, was that the protocol ToggleStyle provides the ability to make your own custom styles.

Part of the way it allows you to do this is the following line:

typealias ToggleStyle.Configuration = ToggleStyleConfiguration

This syntax perplexed me at first.

Using a typealias here is just a way of referring to the struct with a more succinct local name. This is probably so that the makeBody function, seen below, can have the same declaration signature as the similar protocols ButtonStyle, PickerStyle, and TextFieldStyle:

func makeBody(configuration: Self.Configuration) -> some View

I didn’t want to create a custom visual appearance for the Toggle here, as this is already covered pretty well by SwiftUI Lab’s tutorial.

Instead, I decided I would change how the label is treated by completely ignoring the label that is passed in and giving two dynamic labels that change based on the toggle’s isOn state:

There are two examples here, but they look exactly the same. One uses the rather verbose form using the .toggleStyle modifier, just as the standard ToggleStyles do.

The other uses an extension on Toggle that returns this verbose form, providing a clean call site but becoming inconsistent with the way the standard ToggleStyles look.

It’s up to you which of these you prefer. It goes without saying that you do not need to have local variables in the MyToggleStyle struct, a lack of which would remove the need to pass values into the constructor.

I only did this to show how you can pass custom values in, but you cannot change the signature of the makeBody function.

In other words, makeBody can only take a Self.Configuration parameter. By constructing a struct with uninitialized variables, we have another way to pass values alongside the isOn binding and Label from the Toggle constructor.

MyToggleStyle does not make use of configuration.label, which is the value of Text(“This label will never be seen”) we added. It isn’t necessary to add this label, as a Toggle can be constructed without it, but it was worth pointing out how a custom ToggleStyle can hide whatever it wants.

Since makeBody returns some View, you can return whatever you want. You could return a Text, Button, Image, or even a VStack, although I have no idea why you’d want to do that.

20. Picker

As was mentioned in the Hacking With Swift tutorial on Pickers, the default behavior of a Picker inside a Form is to take you to another where you can choose an option.

On iOS you must put the Form inside a NavigationView, otherwise, this navigation will not occur. Outside of a Form, the DefaultPickerStyle will be WheelPickerStyle.

I have also included SegmentedPickerStyle which has a similar appearance to UISegmentedControl in UIKit.

21. DatePicker

DatePicker is similar to Picker, but doesn’t have all the same styles. When used inside a Form, the DatePicker only takes up a single line.

As you can see in the screenshot above, the default DatePicker in a Form has a label and the current date. Tapping it will cause a DatePicker to slide out underneath.

The DatePicker that slides out is exactly the same as the WheelDatePickerStyle, which is why it looks like it is displayed now when actually I just have a WheelDatePickerStyle underneath it.

I added a Picker that you can use to try out different date formats, just to show how you could change the format of a DatePicker at runtime.

22. Slider

A slider allows you to swipe the thumb, a white circle, between a minimum and maximum value. This is similar to UISlider in UIKit. When you create it you have to set a closed range so that SwiftUI knows what the minimum and maximum values will be.

The step can be set to any amount, potentially saving you from needing to convert a long Float to an Int if you don’t need your value to be a decimal.

This also helps you to increase or decrease the amount of accuracy that the slider position is recorded in, potentially making it easier to make calculations by excluding decimal places past the step amount you specify.

23. Stepper

A Stepper in SwiftUI is basically identical to a UIStepper in UIKit. It consists of a connected minus and plus button.

Not all of the initializers require you to set a binding variable to store the value. Many of them take closures that are called when you decrement, increment, or edit the value of the Stepper.


Source link