/ ios

Data Classes on Swift

TL;DR: How do I get that?

While working on the latest project at work, I had the chance to work on a project where we tried some similar but different design patterns for our Android and iOS apps:

  • iOS app, written mainly on Swift: MVVM was used, making everything reactive and subscribing to events emitted by the observables in the ViewModel.
  • Android app, using only Kotlin: MVI with a state reducer was used on Android, so the view observed a single Observable which relayed the current state to render.

After using the 2 architectures extensively across both apps I ended up liking MVI and its rendering of immutable states a lot more than MVVM.

Note: the code has been simplified to show in the blog post.

iOS approach:

class MyController: UIViewController {

    func observe() {
        viewModel.userObservable.observeNotNil { user in
            self.nameLabel.text = user.name
        }
        
        viewModel.contactsObservable.observeNotNil { contacts in
            self.contacts = contacts
            self.tableView.reload()
        }
        
        // Continue for n more observable values
        ...
    }
    
    func loadData() {
        viewModel.loadUser()
        viewModel.loadContacts()
    }
}

Android approach:

class MyController: Controller { // Conductor used here
    fun observe() {
        presenter.state
            .mapNotNull { it.user }
            .distinctUntilChanged()
            .subscribeWithView ({ user -> renderUser(user) })
            
        presenter.state
            .distinctUntilChanged()
            .subscribeWithView ({ user -> renderContacts(contacts) })
            
        // Continue for n more observable values
        ...
    }
    
    fun loadData() {
        presenter.loadUser()
        presenter.loadContacts()
    }
}

I guess this is probably a matter or personal taste, but having everything inside a single State made the code a lot easier to read and predict for me since you could actually read the current state of the view to be rendered.

State was stored in a data class, so creating a new mutated copy was as easy as:

state = state.copy(user = newUser)

I liked the idea so much that I tried to apply it to our Swift code too, as a proof of concept. However, the state immutable struct was a lot more tedious to copy:

state = State(user: newUser, 
    contacts: state.contact, 
    loading: state.loading,
    ..., 
    error: state.error)

Every single value from the old state had to be passed again to the State struct's initializer. So I thought: wouldn't it be great to have data classes and copy on Swift too?

data class internals

To try to figure out how to actually code a Swift version of a data class, first I had to figure out how it's actually implemented on Kotlin. So I took one of our data classes and decompiled it to see its implementation in Java code. To do that, I simply opened a data class, used Tools > Kotlin > Show Kotlin Bytecode and then selected Decompile.

class State {
    private final User user;
    private final List<User> contacts;
    
    @NotNull
    public State copy(User var1, List<User> var2, ...) {
        return new State(var1, var2, ...);
    }
    
    @NotNull
    public static State copy$default(
            State previousState,
            User var1, 
            List<User> var2, 
            CustomError var3,
            ..., 
            int flag, 
            Object varN) {
        
        if ((flag & 1) != 0) var1 = previousState.user;
        if ((flag & 2) != 0) var2 = previousState.contacts;
        if ((flag & 4) != 0) var3 = previousState.error;
        // This will continue with (flag & (1 << i)) != 0
        ...
        
        return previousState.copy(var1, var2, var3, ...);
    }   
}

From what I saw, I infered that what happens when you call copy(value=...) on Kotlin is the next:

  1. Instead of calling copy(a, b, c) method directly, copy$default(previousState, a, b, c, flag, varN) will be called.
  2. The flag variable is used as a bitmask to know which values are new and which must be copied from previousState - oddly, a true value means the value will be copied, insted of marking that the value was passed.
  3. Now copy(a, b, c) is called, with both the new and the old values as needed.

Also, no, I don't know what the varN value is used for. The generated code never used it and I couldn't find anything on the docs or on Kotlin's generateCopyFunction method. If you do know please ping me, I'm curious about this!

You might be thinking: why use the flag arg to mark which values must be copied instead of just passing a null value?

Well, you might want to actually set one of the properties of the data class to null, so if you do pass a null value then the function would have no way to tell what you meant by doing this. So the flag arg is actually needed.

So, to sum up:

  • copy method is generated at build time for each data class.
  • We need a way to only pass some args, not all of them when copy is called.
  • Select which values will be changed and which copied.

One step at a time

Now that we figured out how copy works, let's try to find out how to achieve those on Swift.

Generating a copy method on build time

Oh, boy. Swift is one of the worst languages when you want to do some reflection or some kind of metaprogramming. It heavily relies on ObjC Runtime to do this, which means structs can't be used.

Luckily, there's an awesome project called Sourcery - awesome name, by the way - that allows us to generate code using templates written in several languages, including Swift.

Passing only some args to that method

Well, we also hit a wall here. Swift doesn't have Kotlin's named arguments, which allow you to pass any arg by just writing:

myFunc(argName = value)

However, when you use default arguments on Swift you can achieve something quite close:

func defaultArgsFunc(a: Int = 0, b: Int = 1, c: Int = -1) { ... }

// All args
defaultArgsFunc(a: 1, b: 2, c: 3)
// Only first
defaultArgsFunc(a: 1)
// Omit last one
defaultArgsFunc(a: 1, b: 2)
// Omit second one
defaultArgsFunc(a: 1, c: 2)
// Only second
defaultArgsFunc(b: 1)

Actually, any combination of args passed that follows the natural order - even if there are gaps - can be used:

// Is fine
defaultArgsFunc(a: 1, c: 2) 

// ERROR! This won't build, as 'a' must always be passed before 'c'
defaultArgsFunc(c: 1, a: 2)

So we got this covered - with a little restriction.

Select which values will be changed and which will be copied

Ok, this is probably the trickiest part. We don't have any way to modify the built code to make our copy(a = value) call some copy$default(a: value, flag) function. So... what do we do now?

We could always use default args that = nil, like this:

struct DataClassExample {

    func copy(a: Int? = nil, b: Int? = nil, c: Int? = nil) -> DataClassExample {
        ...
    }
}

But we face the same issue as in Kotlin: how would I actually pass a nil value?

After giving it some thought, I though enums would be a great solution here:

enum CopyValue<T> {
    case new(T)
    case same
    case `nil`
}

struct DataClassExample {

    func copy(a: CopyValue<Int> = .same,
        b: CopyValue<Int> = .same,
        c: CopyValue<Int> = .same) -> DataClassExample {
        
        let new_a: Int?
        switch (a) {
        case let .new(value):
            // Use new value
            new_a = value
        case .nil:
            // Actually use nil
            new_a = nil
        case .same:
            // Copy value
            new_a = self.a
        }
        
        // And so on with the rest ...
        return DataClassExample(a: new_a, b: new_b, c: new_c)
    }
}

// Yay!
let copiedDataClass = myDataClass.copy(a = .new(1), c = .nil)

That looks almost usable! However, there are some values that just shouldn't be allowed to be set to nil and the compiler will stop if you try to generate code that forces this, so we might want to refactor that to:

// To use with Optional values
enum OptionalCopyValue<T> {
    case new(T)
    case same
    case `nil`
}

// Use with non-optional values
enum CopyValue<T> {
    case new(T)
    case same
}

Putting it all together

I ended up with this template I called DataStruct.swifttemplate:

<% for type in types.structs where type.inheritedTypes.contains("DataStruct") { -%>
<%_ -%>
// Copyable extension for <%= type.name %>
public extension <%= type.name %> {

    public func copy(
    <% for variable in type.storedVariables { -%>
        <%_ if variable.typeName.isOptional { -%>
        <%= variable.name %> copied_<%= variable.name %>: OptionalCopyValue<<%= variable.unwrappedTypeName %>> = .same<% -%>
        <%_ } else { -%>
        <%= variable.name %> copied_<%= variable.name %>: CopyValue<<%= variable.typeName %>> = .same<% -%>
        <%_ } -%>
        <%_ if variable != type.storedVariables.last { %>, <% } %>
    <% } -%>
    ) -> <%= type.name %> {

        <% for variable in type.storedVariables { -%>
            <%_ if variable.typeName.isOptional { -%>
                <%_ %>let <%= variable.name %>: <%= variable.typeName %> = setValueOptional(copied_<%= variable.name %>, self.<%= variable.name %>)
            <%_ } else { -%>
                <%_ %>let <%= variable.name %>: <%= variable.typeName %> = setValue(copied_<%= variable.name %>, self.<%= variable.name %>)
            <%_ } -%>
        <% } -%>
    <%_ %>return <%= type.name %>(
    <% for variable in type.storedVariables { -%>
        <%= variable.name %>: <%= variable.name -%>
        <%_ if variable != type.storedVariables.last { %>, <% } _%>
    <% } -%>
    )
    }

}

Hideous, huh? Well, it does serve its purpose, so don't be too picky. Hopefully you won't ever have to edit it.

And I used this protocol and extension for the auxiliar functions:

public protocol DataStruct {}

public extension DataStruct {
    
    func setValueOptional<T>(_ value: OptionalCopyValue<T>, _ defaultValue: T?) -> T? {
        switch(value) {
        case let .new(content):
            return content
        case .same:
            return defaultValue
        default:
            return nil
        }
    }
    
    func setValue<T>(_ value: CopyValue<T>, _ defaultValue: T) -> T {
        switch(value) {
        case let .new(content):
            return content
        case .same:
            return defaultValue
        }
    }
    
}

public enum OptionalCopyValue<T> {
    case new(T)
    case same
    case `nil`
}

public enum CopyValue<T> {
    case new(T)
    case same
}

To get this working you might want to take a look at Sourcery's GH repo, but to sum up, you'd have to:

  1. Get a copy of Sourcery, either by downloading the binary, installing with brew or using Cocoapods.
  2. Add a build phase to execute Sourcery - or execute manually, if you prefer it.
  3. Either use command line args or a sourcery.yml file like this:
sources:
    - Your/Sources/Path
templates:
    - Your/Templates/Path
output:
    path: Generated/Code/Path

Also, don't forget to add those generated files to the sources to use on the build process.

  1. Implement DataStruct protocol on any struct you want to add the copy method to:
struct State: DataStruct {
    let user: User?
    let contacts: [User]
    ...
}

This will generate:

extension State {
    
    func copy(user: OptionalCopyValue<User> = .same,
            contacts: CopyValue<[User]> = .same,
            ...) -> State {
        let copied_user = setValueOptional(user, self.user)
        let copied_contacts = setValue(contacts, self.contacts)
        ...
        return State(user: copied_user, contacts: copied_contacts, ...)
    }
}

Conclusion

We can actually have the equivalent of data class on Swift with a bit of hacking, so using immutable structs shouldn't be such a nuisance now.

However, I know this setup is still a bit hard, so I'm working on making a pod which include this as well as autogenerated equality and hashValue.

I hope you find this useful!