Category Archives: SwiftUI

Properties, Binding, Property Wrappers and MVVM with Swift

Swift includes properties within the language (see Properties), but also supports (what we’d call in C#) property or data binding mechanisms.

Before we get started, I’m writing this post as a learning exercise, I’d suggest you checkout the references at the bottom of the post for a much more in depth look at this subject.

If you’re still reading, then let’s get started…

To begin with we’ll look at how we declare properties in Swift first. For those coming from C# or similar languages, then the simplest stored property looks just like a field

Property Declarations

struct MyModel {
    var id: String;
}

The above declaration is known as a stored property, in that we can store values with the specific instance of the MyModel type, this is really the equivalent of having a property with a default getter and setter.

We can also declare properties as readonly like this

struct MyModel {
    var id: String {
        return "SomeId"
    }
}

Both of the above examples are basically properties using “shorthand syntax”, like C# we can also create properties with get and set keywords – of course these are useful if the setting of the property (for example) does more than just assign a value, hence we have a form (notice you can use return or in these example we’re using implicit return

struct MyModel {
    private var _id: String = "SomeId"
        
    var id: String {
        get {
            _id
        }
        set {
            _id = newValue
        }
    }
}

Property Observer

Property observers allow us to monitor a property change. For example if you have a struct/class with the shorthand syntax for a property then we can willSet and/or didSet to our property, for example

struct MyModel {
    var id: String = "" {
        willSet(newTitle) {
            print("About to set value")
        }
        didSet {
            if title != oldValue {
                print("Set Value")
            }
        }
    }
}

Now when the property is about to be set, the willSet code is called and when a value changes didSet will be called. Obviously you wouldn’t want to do anything within these methods that could affect performance.

Property Wrapper

A Property Wrapper allows us to (well as the name suggests) wrap a property within a type that we can separate the storage of the property from functionality that acts upon that property. For example if our title has a length constraint, we might create a property wrapper to implement this functionality

@propertyWrapper
struct LengthConstraint {
    private var value = ""
    var wrappedValue: String {
        get { value }
        set { value = String(newValue.prefix(5)) }
    }
}

Property Binding

So what’s this got to do with binding/data binding/property binding? Well we’ve now seen how we declare propeties and can add property wrappers to the property. Within SwiftUI we can use the @Published property wrapper to our property which gets observed by a UI element and when the value changes updates the UI or in the case of TextField, when changes are made to the text field it’s placed into our property.

There are several different property wrappers for this type of functionality

  • @Published – this is used on property classes, such as ObservableObject subclasses.
  • @StateObject – this is used within a view to store an instance of an ObservableObject subclassed type
  • @State – this allows us to store bindable data within a view
  • @Binding – this is used in views which need to mutate a property owned by an ancestor view. In essences it’s like connecting to a parent’s property
  • @EnvironmentObject – this is a shared object stored within the environment and is used in situations where passing properties through a UI hierarchy or the likes becomes too cumbersome.
  • @Environment – this is used to connect to actual environment properties, such as reading whether the application is in dark theme or other SwiftUI properties

MVVM

We can declares @State and @Binding property wrappers (for example) within our view, but of course this can pollute the UI with view model state. If we prefer to go with a MVVM approach, i.e. separate the view from the view model, then we will start off by subclassing ObservableObject, for example here’s a minimal view model

@MainActor class ViewModel: ObservableObject {
    @Published var title: String = ""
}

@StateObject private var viewModel = ViewModel()

In the above code we declare and subclass of the ObservableObject and then we declare the properties that we wish to expose. The @StateObject line is what you’ll have in your ContentView to create an instance of the ObservableObject.

The @MainActor is used to denote that this class runs code on the main queue – essentially if you’re used to Windows UI programming this can be seen as equivalent to running code on the UI thread.

This of course might seem simpler to just declare properties like this, but then our model ends up more tightly coupled to the view than we might want

@State var title: String = ""

References

All SwiftUI property wrappers explained and compared
How to use @MainActor to run code on the main queue
What is the @Environment property wrapper?

Localization in SwiftUI

Let’s create a simple little Swift UI application (mine’s a Mac app) to try out the Swift UI localization options.

Once you’ve created an application, select the project in the Xcode project navigator. So for example my application’s name LocalizationApp, select this item in the project navigator. From the resultant view select the project (not the target) and this will display a section labelled Localizations. This will show your Development Language, in my case this is English. Beneath this you’ll see a + button which we can use to add further languages.

Click on the + as many times as you like and add the languages you want to support. I’ve just added French (Fr) for my example.

Adding localizable strings

Add a new file to the project, select a Strings file and name it Localizable (hence it will be Localizable.strings). This file will have key value pairs, where the key will be used as the key to the localised string and, as you probably guessed, the value will be the actual localised string, for example

"hello-world" = "Hello World";

Note: if you forget the semi-colon, you’ll get an error such as “validation failed: Couldn’t parse property list because the input data was in an invalid format”.

Now if you’ve created the default Mac application using Swift UI, go to the ContentView and replace the following

Text("Hello, world!")

with

Text("hello-world")

Wait a minute, we seemed to have replaced one string with another string, why isn’t the Text displaying “hello-world”?

The Swift UI Text control (and other controls) support LocalizedStringKey. This essentially means that the code above is an implicit version of this

Text(LocalizedStringKey("hello-world"))

So basically, we can think of this (at least in its implicit form) as first looking for the string within the .strings file and if it exists, replacing it with the LocalizedStringKey. If the string does not exist in the .strings file then use that string as it is.

We can also use string interpolation within the .strings file, so for example we might have

"my-name %@" = "My name is %@";

and we can use this in this way

Text("my-name \(name)")

The %@ is a string formatter and in this instance means we can display a string, but there are other formatters for int and other types, see String Format Specifiers.

What about variables and localization?

We’ve seen that Text and the controls allow an implicit use of LocalizedStringKey but variable assignment has no such implicit capability, so for example if we declared a variable like this

let variable = "hello-world"

Now if we use the variable like this (below) you’ll simply see the string “hello-world” displayed, which is predictable based upon what we know

Text(variable)

Ofcourse we can simply replace the variable initialization with

let variable = LocalizedStringKey("hello-world")

Adding other languages

Let’s now create a new language for our application by clicking on the Localizable.strings file and in the file inspector you’ll see a Localize button. As we’d already added a language view the project navigator (at the start of this post). You’ll now see both English and French listed. The Localizable.strings file now appears a parent to two Localizable files, one named Localizable (English) and one named Localizable (French).

In the French file we’ve added

"hello-world" = "Bonjour le monde";
"my-name %@" = "Mon nom est %@";

Note: What you’ll actually find is the within the application directory there will be two folders, one named en.lproj and fr.lproj each will have a Localizable.strings file.

Testing our localizations

Ofcourse if we now run our application we’ll still see the default language,, in my case English as that’s the locale on my Mac. So how do we test our French translation?

We can actually view the translations side by side (as it were) by amending our code like this

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .environment(\.locale, .init(identifier: "en"))
            ContentView()
                .environment(\.locale, .init(identifier: "fr"))
        }
    }
}

Or (much more likely) we can go to the application code and replace the WindowGroup with the following

var body: some Scene {
   WindowGroup {
      ContentView()
         .environment(\.locale, .init(identifier: "fr"))
   }
}

Actually, there’s also another way of change the locale.

Instead of coding the change, select the application name in Xcode’s top bar and a drop down will show “Edit Scheme”. Select the “Run” option on the left and then the tab “Options”. Locate the “App Language” picker and select French (or whichever language you added as non-default). Now run the application again and you’ll see the application using the selected language localization file.

Exporting and Importing localizations

Whilst we can use google translate or other online translation services, we might prefer to export the localization files so we can send them to a professional translation service.

Select the application within the project navigator, then select Product | Export Localizations… this will create a folder, by default named “<Your application name> Localizations” (obviously where Your application name is replaced by your actual app name). This folder contains en.xcloc and fr.xcloc files in my case.

After your translation service/department/friends complete the translations, we can now select the root in the project navigator (i.e. our application) then select Product | Import Localizations… from here select the folder and files you want to import. Click the “Import” button.

SwiftUI – why is my Mac window not resizable?

As a newbie to SwiftUI, I was surprised to find my simple little sample application would not resize. If you’re used to WPF, WinForms and even other non-Windows user interface libraries, you’ll notice by default the windows are usually resizable. So what’s the deal with SwiftUI

So my code looked like this

var body: some View {
   VStack {
      Text(viewModel.title)
         .padding()
      TextField("Enter", text: $viewModel.title)
   }
   .padding()
   .frame(minWidth: 500, minHeight: 300)
}

Simple enough, display a text label and input text field.

So what’s the problem, why is the Man window not resizable?

Spacer

The Spacer is key here. If we change the code to the following, the it all starts to work as I wanted

var body: some View {
   VStack {
      Text(viewModel.title)
         .padding()
      TextField("Enter", text: $viewModel.title)
      Spacer()
   }
   .padding()
   .frame(minWidth: 500, minHeight: 300)
}

The Spacer expands to fill space.

If you have a Spacer within a VStack the spacer fills space on the vertical and if on an HStack it fills horizontally. But the above code will expand on both the vertical and horizontal. In this case the TextField seems to combine with the Spacer to allow both the axis to expand.

Swift UI Circle Image

Here’s a simple circle image using SwiftUI. This is pretty much taken from the SwiftUI Tutorial with a few additions.

struct CircleImage: View {
    var image: Image
    var borderColor: Color = .white
    var shadowRadius: CGFloat = 10

    var body: some View {
        image
            .clipShape(Circle())
            .overlay(Circle().stroke(borderColor, lineWidth: 4))
            .shadow(radius: shadowRadius)
    }
}

In this case we pass and image to the CircleImage, clipping it into a circle shape. Then we display a line (in white by default) around it with a shadow (with radius 10 by default).

We simply clip the supplied image, i.e.

CircleImage(image: Image("imageFrom.Assets.xcassets"), 
   borderColor: .red, 
   shadowRadius: 5)

The image should be stored within the Assets.xcassets editor.