Skip to content

Case Study: Cloning Swift UI's Text view

Matt Carroll edited this page Oct 21, 2023 · 7 revisions

Swift UI has a view called Text, which roughly approximates Flutter's Text widget. This case study is intended to help you clone other Swift UI views in this package by showing you the analysis and approach that was used to clone the Text widget.

Spec'ing the work

The first step when cloning a Swift UI view is to explore the existing API. You can't make effective Flutter API decisions without first understanding the breadth and variability of existing Swift UI features. Let's look at some Text examples from Swift UI.

Simplest usage:

// Note: this usage implicitly tries to use localization lookup
Text("Hello, World!")

Style a single Text with modifier methods:

Text("Hello, World!")
  .font(.system(size: 18, weight: .light, design: .serif))
  .italic()
  .bold()

Style multiple Texts with modifier methods on a group:

VStack() {
  Text("One")
  Text("Two")
  Text("Three")
}.bold()

Note the difference in localization behavior:

Text("This will be localized")

Text(verbatim: "This will NOT be localized")

// This will NOT be localized
Text(myTextVar)

// This will be localized
Text(LocalizedStringKey(myTextVar))

Porting text styling

Given that the swift_ui package isn't trying to bring modifier methods to Flutter, how should we port the text styling behavior?

The tricky part about the text styling behavior is that it might apply directly to a Text view, or it might apply to all Text views within some sub-tree of the UI.

At first, it might seem reasonable to implement all text styling via InheritedWidgets, for example:

Bold(
  Text("Hello, World!"),
);

The above approach isn't terrible when applying a single style property, but it quickly balloons into an unnecessarily massive tree:

Font(
  SystemFont.title,
  Bold(
    Italic(
      Strikethrough(
        Underline(
          Text("Hello, World!"),
        ),
      ),
    ),
  ),
);

The above example is technically achievable, but obviously it introduces far more visual clutter than should be necessary. In this case, there's a significant harm in code readability when achieving a trivial text styling use-case.

To maximize similarity to Swift UI, while also maximizing readability and convenience, we'll introduce three different ways to accomplish text styling goals. Keep in mind that in general, creating three ways to achieve the same goal is not a great idea. It breeds confusion. However, the swift_ui package aims to walk the line between familiarity with Swift UI APIs, as well as embracing fundamental Flutter practices. This unusual balancing act will result in some regrettable API decisions.

Proposed approach to direct text styling:

Text(
  "Hello, world", 
  stye: TextStyle(
    font: SystemFont(size: 18, weight: SystemFont.light, design: SystemFont.serif),
    bold: true,
    italic: true,
    strikethrough: Strikethrough(Color.red),
    underline: Underline(Color.purple),
  ),
);

Proposed approach to single-value group styling:

Bold(
  VStack([
    Text("One"),
    Text("Two"),
    Text("Three"),
  ]),
);

Proposed approach to multi-value group styling:

GroupTextStyle(
  VStack([
    Text("One"),
    Text("Two"),
    Text("Three"),
  ]),
  style: TextStyle(
    font: SystemFont(size: 18, weight: SystemFont.light, design: SystemFont.serif),
    bold: true,
    italic: true,
    strikethrough: Strikethrough(Color.red),
    underline: Underline(Color.purple),
  ),
);

Notice that this proposal includes three different ways to achieve text styles. It implies that the swift_ui package must implement:

  • A style property in every Text widget constructor
  • A GroupTextStyle InheritedWidget
  • An InheritedWidget for every individual style: Bold, Italic, Strikethrough, Underline, etc.

Porting localization

TODO




Update the following:



While the Text view is a fundamental piece of Swift UI, it doesn't implement a huge variety of behaviors. To spec the work, the Swift UI API docs were replicated in an issue ticket, along with some cloning instructions: https://github.com/Flutter-Bounty-Hunters/swift_ui/issues/2

The issue ticket includes some specification analysis, but we'll replicate that analysis here.

Constructors

A perpetual challenge when cloning Swift UI is dealing with Swift UI initializers. Swift UI, similar to many other languages, supports method overloading. This means methods, such as initializers, can declare different groups of parameters, without defining separate names. For example, here are a few Text initializers from Swift UI:

init(LocalizedStringKey, tableName: String?, bundle: Bundle?, comment: StaticString?)
Creates a text view that displays localized content identified by a key.

init(LocalizedStringResource)
Creates a text view that displays a localized string resource.

init<S>(S)
Creates a text view that displays a stored string without localization.

init(verbatim: String)
Creates a text view that displays a string literal without localization.

Notice that each of these initializers are defined via init(). They don't have different names. Dart can't do this, so the swift_ui package needs to use named constructors to support such situations.

At the time of cloning the Text view, this package lacked a number of concepts, such as a swift_ui package version of LocalizedStringKey and LocalizedStringResource. It's unclear whether those will be implemented, or how. But, to be safe, the initial implementation should include constructors for those cases. We can delete them later, if we don't need them.

There are generally two goals when cloning swift_ui initializers. All things being equal, create fewer named constructors than more named constructors, so Swift UI developers don't feel overwhelmed with the number of options. All things being equal, use shorter names over longer names, so that Swift UI developers don't feel like they're typing a lot more than they're used to.

In the case of Text, we chose the following constructors:

class Text extends StatefulWidget {
  /// Creates a `Text` widget that displays a string with default localization.
  Text(String localizedPlainText);

  /// Creates a `Text` widget that displays a string literal without localization.
  Text.verbatim(String verbatimPlainText);

  /// Creates a `Text` widget that displays text associated with a [localizedStringKey].
  Text.localString(this.localizedStringKey, {
    this.tableName,
    this.bundle,
    this.comment,
  });

  /// Creates a `Text` widget that displays attributed text, which might contain
  /// various styles throughout subsections of the text.
  Text.attributed(this.attributedText);

  /// Creates a `Text` widget that displays a localized range between two dates.
  Text.dateRange(this.dateRange);

  /// Creates a `Text` widget that displays a localized time interval.
  Text.timeInterval(this.timeInterval);

  /// Creates a `Text` widget that displays localized dates and times using a specific [style].
  Text.date(this.date, this.dateStyle);

  // TODO: Creating a text view with formatting
  // TODO: Creating a text view from an image
  // TODO: Creating a text view with a timer
}