Best Practices in SwiftUI Composition

Well, you might be tempted to specify the footnote text in the first example as:struct FootnoteText : View { let text: String var body: some View { MultiLineText(text: text, alignment: .

center) .

foregroundColor(.

gray) .

font(.

footnote) }}Where you specified the color as gray because that’s the color you’ve wanted so far.

But the problem is that you’re now stuck with it, and attempting…FootnoteText(text: $model.

disclamer) .

foregroundColor(.

red)Still yields gray text.

Other problems abound with our example now, such as we’ve probably screwed up automatic Dark Mode adaptation by specifying a specific single text color value, and we’ve also probably screwed up the possibility of using our FootnoteText view on other platforms with differing appearances and color schemes.

So hold off on the purely visual appearance modifiers in your view components.

Especially color.

Use Semantic ColorBut if you must set colors, strongly consider using Semantic Colors.

Color.

primary, Color.

secondary, and Color.

accentColor are all just examples of colors provided by the system and environment.

Even a color like .

orange can and will properly adapt to light and dark mode, varying slightly in the process.

You can also define your own semantic colors in Xcode.

And like Apple, you can even adjust them for light and dark mode.

The nice thing with this approach is that you can easily change your color schemes and branding application-wide in a single spot.

You can even modify your schemes per platform without changing your code just by providing a different color set in that platform’s xcassets directory.

Speaking of which…Architect with other platforms in mindWith SwiftUI, it’s easier than ever to take the same code and use it across platforms.

To some extent you could do this before taking your models and API code and business logic from platform to platform, but now it’s entirely possible to use many of your UI elements across platforms as well.

This was readily apparent in Apple’s SwiftUI on All Devices presentation during WWDC.

I won’t go into a lot on this since the presentation covers it so well, but keep in mind that a lot of the cross-platform user interface sharing between iOS apps and iPadOS apps and macOS apps and tvOS apps come from plugging the same content views you’ve created on one platform into a different navigation structure on another platform.

So again, consider where you might want to specify things like fonts, colors, and the like.

SwiftUI has several mechanisms for providing or delaying those specifications.

We discussed semantic colors above, but here’s another example.

Group { MyCustomTextField($model.

username) MyCustomTextField($model.

password) } .

font(.

headline) .

background(Color.

white.

opacity(0.

5)) .

relativeWidth(1)Group is a powerful tool that lets you well, group things that need to be manipulated together or, in this case, that share common attributes.

In this sample the group modifiers are applied to each MyCustomTextField,and as such each one will pick up the headline font, the background color, and be sized to the full width of the parent container.

MyCustomTextField deals with functionality.

Let the context deal with style.

Let the system do it’s thingAlso remember that SwiftUI automatically translates views into the visual interface elements appropriate for a given platform.

A Toogle view, for example, renders differently — and correctly — on iOS, macOS, tvOS, and watchOS.

Further, SwiftUI will properly adjust colors, spacing, padding, and the like based on the platform, on the current screen and/or container size, on control state.

It will also take into consideration any changes needed for any accessibility features that might be enabled, do the right thing for Light/Dark mode, and more.

Just to pick one example at random, paragraph text padding presents one way on iOS, but tends to have more border space on an iPad’s screen… unless your content is now compressed by appearing in a Slide-Over view.

A lot of considerations that, to be honest, we don’t always take into account.

In UIKit we probably just plugged a value of 15 into the constraint field on the storyboard and moved on.

What that means for us in the here and now is that we need to relax and let the system do its thing.

Avoid obsessing trying to match pixel-perfect design layouts often created for a single “best case” layout on a specific screen size for the default accessibility mode.

Take a cue from web developers, who’ve moved from highly rigid design layouts to highly responsive ones, well suited to each platform and to the needs of the individual user.

This may mean several conversations with your UI/UX design team.

Apple’s gone to a lot of trouble to make sure SwiftUI does the right thing at the right time, just as long as we don’t jostle its elbow.

So don’t.

And I want to be clear here.

I’m not saying that you can’t, either.

Apple’s given us a lot of control over presentation.

We just need to know when to let go and save the special cases for very special cases.

Let the system do it’s thing, and you’ll not only write less code, but I’m willing to bet that you’ll have fewer bugs as well.

Bind state as low in the hierarchy as possibleAs mentioned earlier, I’m going to save most of my thoughts on view models and state management for later articles, but this concept fits in with the examples shown thus far, so I’ll wrap up with this one.

Observe our trusty FootnoteText view in the following code.

struct MyMainView : View { @ObjectBinding var model: MainViewModel @EnvironmentObject var settings: UserSettings var body: some View { VStack { MainContentView(model) MainContentButtons(model) FootnoteText(text: settings.

fullVersionString) } }}Note here that MyMainView is automatically importing an environment object named settings, and is passing settings.

fullVersionString to our FootnoteText view.

All well and good… but why does MyMainView know about UserSettings at all?.What if we did the following instead?struct MyMainView : View { @ObjectBinding var model: MainViewModel var body: some View { VStack { MainContentView(model) MainContentButtons(model) ApplicationVersionFootnote() } }}And defined ApplicationVersionFootnote elsewhere as…struct ApplicationVersionFootnote : View { @EnvironmentObject var settings: UserSettings var body: some View { FootnoteText(text: settings.

fullVersionString) }}Here our environment variable is acquired and used lower in the view hierarchy.

MyMainView knows nothing of UserSettings, nor should it care.

Nor does MainViewModel, for that matter.

The latter point is a key one.

For some time now we’ve attempted to avoid Massive-View-Controller syndrome by embracing some form of view model structure, be it MVVM, VIPER, or whatever.

Unless we were very careful, however, all we managed to do was replace our massive view controllers with massive view models.

In a more traditional MVVM implementation, our UserSettings object would probably be injected into our MainViewModel, and some function or variable or binding created to expose fullVersionString on our view model.

This complicates our view model, complicates our injection strategies and initialization code, and contributes to making our application’s components more tightly coupled and rigid.

But in our last example, our ApplicationVersionFootnote view is effectively a small, highly specific, special-purpose view model that couples our environment UserSettings data to a FootnoteText view.

I see a lot of potential for this sort of thing in the future, and it very much fits in with SOLID’s Single Responsibility Principle.

Completion BlockSo what do you think?.On point?.Disagree?.Am I missing something?As always, let me know in the comments below.

.

. More details

Leave a Reply