Skip to content

Case Study: Cloning Swift UI's Image view

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

Swift UI has a view called Image, which roughly approximates Flutter's Image 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 Image widget.

Investigating the Swift UI API

Let's begin with the most basic use of the Image view. In it's simplest form, an Image view displays a bitmap from the app's asset bundle, identified by a string name.

Image("logo")

Right off the bat we see a likely difference between a Flutter Image and a Swift UI Image. With a traditional Flutter image, the developer is likely to need to specify an asset directory path, has to specify the extension, and the developer needs to use the .asset() named constructor:

Image.asset("assets/images/logo.png");

We'd like for Flutter developers to be able to retain the concision of Swift UI, for example:

Image("logo");

To achieve this concision, the swift_ui Image widget must do a few things:

  • Offer a default constructor that takes a String as a required unnamed parameter.
  • Search the asset bundle for any file named "logo", regardless of extension

Additionally, to make it possible for the developer to avoid declaring a directory path, like "assets/images/", there needs to be some kind of tool to configure a search path for all images within a scope. Here's an example of what such a tool might look like:

SwiftUiImagePath(
  "assets/images",
  child: Any(
    child: Number(
      child: OfDescendants(
        child: Image("logo"),
      ),
    ),
  ),
);

To let the user skip the image extension, we need to inspect all assets available in the asset bundle. Flutter doesn't appear to provide any direct way to query the assets, but here's a supposed workaround: https://stackoverflow.com/questions/68862225/flutter-how-to-get-all-files-from-assets-folder-in-one-list. In addition to that workaround for querying assets, the Image widget needs to maintain a list of supported extensions. Furthermore, if multiple images exist with the same name, the Image widget either needs to document its priority selection process, or throw an exception.

Bundle Selection

The Image widget should make it possible to provide a desired bundle to search for the given image.

System Images

iOS includes various "system images". The Image widget should support displaying a system image by name.

Image(systemName: "swift")
  .resizable()
  .scaledToFit()
  .frame(width: 200, height: 200)

Image from custom rendering

The Image widget should include a constructor that takes a custom renderer.

Localizing image labels

By default, a Swift UI Image interprets the image name, e.g., "logo", as a label that's localized. For example, if the image asset is called "pencil" and the app is running in a Spanish locale, the image will be given an accessibility label of "lápiz". The label can also be set explicitly with the label property. In both cases, the value, which is localized, isn't known until run-time.

This feature conflicts with Flutter's current way of handling localization, which expects developers to request compile-time values, e.g., MaterialLocalizations.of(context).pencil.

As a temporary solution, we might be able to read the .arb file and index it ourselves.

There's a Flutter issue to provide key'ed localization at runtime: https://github.com/flutter/flutter/issues/105672

[init(size: CGSize, label: Text?, opaque: Bool, colorMode: ColorRenderingMode, renderer: (inout GraphicsContext) -> Void)](https://developer.apple.com/documentation/swiftui/image/init(size:label:opaque:colormode:renderer:))
Initializes an image of the given size, with contents provided by a custom rendering closure.

Spec'ing the Image widget

The Image widget should implement the following APIs.

From an asset by name, labeled:

Image(
  this.assetName,     // name of image file, also "label" value if no label is provided
  this.bundle,        // (optional) bundle to find the image asset
  this.label,         // (optional) accessibility label
  // -- following apply to most/all other configurations --
  this.orientation,   // (optional) image orientation (rotated, mirrored, flipped, etc)
  this.resizable,     // (optional) Sets the mode by which SwiftUI resizes an image to fit its space.
  this.antialiased,   // (optional) Specifies whether SwiftUI applies antialiasing when rendering the image.
  this.renderingMode, // (optional) Whether to replace transparent pixels with foreground color
  this.interpolation, // (optional) Specifies the current level of quality for rendering an image that requires interpolation.
  this.allowedDynamicRange, // (optional) The allowed dynamic range for the view
);

From an asset by name, decorative (unlabeled):

Image.decorative(
  this.assetName,     // name of image file, also "label" value if no label is provided
  this.bundle,        // (optional) bundle to find the image asset
  // -- add other common properties --
);

From a system image:

Image.system(
  this.systemImageName, // name of the system image, i.e., SF Symbol, e.g., "trash.square.fill"
  this.label,           // (optional) accessibility label
  this.variableValue,   // (optional) system symbol customization value
  // -- add other common properties --
);

From a dart:ui Image:

Image.fromImage(
  this.memoryImage,
);

From a custom painter:

Image.fromRenderer(
  this.renderer,  // The custom painter that renders the image
  this.size,      // The size of the rendered image
  this.label,     // (optional) accessibility label
  this.opaque,    // A Boolean value that indicates whether the image is fully opaque. 
                  // This may improve performance when true. Don’t render non-opaque 
                  // pixels to an image declared as opaque. Defaults to false.
  this.colorMode, // The working color space and storage format of the image. Defaults 
                  // to ColorRenderingMode.nonLinear.
);
Clone this wiki locally