Skip to content

Conversation

liamnichols
Copy link
Contributor

@liamnichols liamnichols commented Jan 6, 2022

Background

When prototyping the integration within a real iOS app, I found using UserDefaults.ValueContainer() somewhat repetitive.

We defined a Configuration type that held a series of properties that related to UserDefaults, but to map that into the value container resulted in a lot of repetitive code:

Screenshot 2022-01-06 at 23 42 59

It would be great to avoid this somehow.

Description

In this PR, I'm proposing two new types:

  1. The LaunchArgumentEncodable protocol
  2. The @UserDefaultOverride property wrapper

They work in conjunction with each other and build on top of the functionality already provided by UserDefaults.ValueContainer. LaunchArgumentEncodable is pretty simple:

/// A protocol used by container types that can have their representations encoded into launch arguments via the ``encodeLaunchArguments()`` method.
///
/// This protocol works exclusively in conjunction with the ``UserDefaultOverride`` property wrapper.
public protocol LaunchArgumentEncodable {
    /// Additional values to be apended to the result of `collectLaunchArguments()`.
    ///
    /// A default implementation is provided that returns an empty array.
    var additionalLaunchArguments: [String] { get }
}

public extension LaunchArgumentEncodable {
    /// Collects the complete array of launch arguments from the reciever.
    ///
    /// The contents of the return value is built by using Reflection to look for all `@UserDefaultOverride` property wrapper instances. See ``UserDefaultOverride`` for more information.
    ///
    /// In addition to overrides, the contents of `additionalLaunchArguments` is apended to the return value.
    func encodeLaunchArguments() throws -> [String]
}

But it is somewhat magic. The encodeLaunchArguments() method will use Swift's reflection apis to find and iterate all of the @UserDefaultOverride properties defined on the type. It'll then collect the keys and values and pass them into a ValueContainer that is then used to produce the array of launch arguments.

In use, it looks something like this:

class ExampleUITests: XCTestCase {
    struct Configuration: LaunchArgumentEncodable {
        @UserDefaultOverride(.contentTitle)
        var title: String = "Example App (Test)"

        @UserDefaultOverride(.contentItems)
        var items: [Date] = []

        @UserDefaultOverride(.contentSortOrder)
        var sortOrder: ContentSortOrder = .descending

        var deviceLocale: Locale = Locale(identifier: "en_US")

        var additionalLaunchArguments: [String] {
            // Type `Locale` doesn't match how we want to represent the `AppleLocale` UserDefault so we'll encode it manually
            var container = UserDefaults.ValueContainer()
            container.set(deviceLocale.identifier, forKey: UserDefaults.Key("AppleLocale"))

            return container.launchArguments
        }
    }

    func testNoItemsPlaceholder() throws {
        // Override the relevant values
        var configuration = Configuration()
        configuration.deviceLocale = Locale(identifier: "fr_FR")
        configuration.sortOrder = .ascending
        configuration.title = "Example App"

        // Launch the app with the user defaults
        let app = XCUIApplication()
        app.launchArguments = try configuration.encodeLaunchArguments()
        app.launch()

        // ...
    }
}

In addition to this change, I updated UserDefaults.ValueContainer so that launchArguments returns key value pairs ordered based on when they were inserted. This helps us predictably test the output.

@liamnichols liamnichols self-assigned this Jan 7, 2022
@liamnichols liamnichols requested a review from ryanpato January 7, 2022 07:57
@liamnichols liamnichols changed the title Introduce new @UserDefaultOverride property wrapper to make UI Testing configuration easier Introduce new @UserDefaultOverride property wrapper for better UI testing support Jan 7, 2022
Copy link
Contributor

@ryanpato ryanpato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀🚀🚀 Really cool, excited to use it!

@liamnichols liamnichols force-pushed the ln/user-default-override branch from 980f59d to 756f916 Compare January 7, 2022 10:17
@liamnichols liamnichols force-pushed the ln/user-default-override branch from 9aca796 to 4c64df2 Compare January 7, 2022 11:27
@liamnichols
Copy link
Contributor Author

I just pushed some slight tweaks in the last two commits - https://github.com/cookpad/swift-user-defaults/pull/8/files/f9e9c3e9a6b03a84e19c224edf26036bbb7bfac1..4c64df2a637f90dea5b1d221addb0230236e571f

They change the UserDefaultsKeyValueRepresentable protocol to a public UserDefaultsOverrideRepresentable along with some tweaks to how LaunchArgumentEncodable works internally to help make it a bit more customisable if required in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants