Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stuck on creating SwiftUI enums with 'dynamic' colors #1198

Open
tijs opened this issue May 15, 2024 · 7 comments
Open

Stuck on creating SwiftUI enums with 'dynamic' colors #1198

tijs opened this issue May 15, 2024 · 7 comments
Labels

Comments

@tijs
Copy link

tijs commented May 15, 2024

We have a working setup in our app generating design tokens for Android and iOS using style dictionary which is excellent. The only issue i'm running into is that instead of generating SwiftUI enums for light and dark mode separately i would love to just generate enums with a single dynamic color using the dark and light values directly. That seems like a better fit for SwiftUI anyway.

For context our generated tokens (iOS) look something like this now:

public protocol DividerDesignTokens {
    static var dividerColorDefault: Color { get }
    static var dividerColorInverse: Color { get }
    static var dividerSizeHeight: CGFloat { get }
}

public enum DividerLight: DividerDesignTokens {
    public static let dividerColorDefault = Color(red: 0.910, green: 0.910, blue: 0.910, opacity: 1)
    public static let dividerColorInverse = Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1)
    public static let dividerSizeHeight = CGFloat(1)
}

public enum DividerDark: DividerDesignTokens {
    public static let dividerColorDefault = Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1)
    public static let dividerColorInverse = Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1)
    public static let dividerSizeHeight = CGFloat(1)
}

This was pretty easy to setup and works reasonably well but to use it is still need to check the ColorScheme and pick the token from the correct enum each time. With a simple color extension:

public extension Color {
    init(light: Color, dark: Color) {
        self.init(light: UIColor(light), dark: UIColor(dark))
    }
}

i could have enums that look like this instead:

public enum DividerTokens: DividerDesignTokens {
    public static let dividerColorDefault = Color(light: Color(red: 0.910, green: 0.910, blue: 0.910, opacity: 1), dark:  Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1))
    public static let dividerColorInverse = Color(light:Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1), dark: Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1))
    public static let dividerSizeHeight = CGFloat(1)
}

But i'm having a very hard time figuring out how to generate the tokens with a custom color implementation. Anyone have pointers on where i should start or even some examples i could look into?

@jorenbroekema
Copy link
Collaborator

jorenbroekema commented May 15, 2024

What does your token structure look like (e.g. for this color example)? This is quite important to know for me to help you sketch out a custom format that gives you the desired output

@tijs
Copy link
Author

tijs commented May 16, 2024

ah yes of course, still trying to figure this stuff out. specifically for this divider an entry will look like this:

{
  "divider": {
    "size": {
      "height": {
        "value": "{alias.divider.height}",
        "type": "sizing"
      }
    },
    "color": {
      "default": {
        "value": "{divider.color.mode.default}",
        "type": "color"
      },
      "inverse": {
        "value": "{divider.color.mode.inverse}",
        "type": "color"
      }
    }
  }
}

which is a reference to the light and dark modes in separate light.json and dark.json token files which might make this harder perhaps? in the light.json for instance you would have a matching entry:

"divider": {
    "color": {
      "mode": {
        "default": {
          "value": "{alias.divider.color.default.dark}",
          "type": "color"
        },
        "inverse": {
          "value": "{alias.divider.color.inverse.dark}",
          "type": "color"
        }
      }
    }
  },

where that alias will point to a colors.json file with the correct color. so instead of splitting the output by mode as we do now i will also have to generate the iOS token files with the light and dark values combined somehow instead of a separate enum file for each mode.

Hope this makes sense?

@jorenbroekema
Copy link
Collaborator

ah I see so you've got different theme files (dark/light) which are defining the same tokens but with different values.
This means you usually have multiple style-dictionary runs, one for each theme, and thus different output files.

Would it be possible in the iOS implementation to have public enum DividerLight: DividerDesignTokens and public enum DividerDark: DividerDesignTokens living in separate files, and importing the correct one based on the theme selection?

@tijs
Copy link
Author

tijs commented May 20, 2024

Yeah that’s exactly what we do now 😁 my goal was to get rid of the extra overhead of needing some kind of token provider that switches themes based on the color scheme. Especially since you get that behavior for free if you have dynamic colors from the outset. Exporting a combined token file is not really an option since for Android & web the split fits well with how the platforms deal with theming.

@jorenbroekema
Copy link
Collaborator

jorenbroekema commented May 20, 2024

The issue with this format:

public enum DividerTokens: DividerDesignTokens {
    public static let dividerColorDefault = Color(light: Color(red: 0.910, green: 0.910, blue: 0.910, opacity: 1), dark:  Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1))
    public static let dividerColorInverse = Color(light:Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1), dark: Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1))
    public static let dividerSizeHeight = CGFloat(1)
}

is that a single SD instance is only aware of light OR dark context, so it's hard to output the above format without having this multi-theme context and multiple SD instances. I guess it's similar to this format in CSS:

:root {
  --divider-color-default: rgba(240, 240, 240, 1);
  --divider-color-inverse: rgba(50, 50, 50, 1);
}

html[theme="dark"] {
  --divider-color-default: rgba(50, 50, 50, 1);
  --divider-color-inverse: rgba(240, 240, 240, 1);
}

Which is a common request as well, but also incredibly difficult to do with how Style Dictionary is currently structured to have formats output to file destinations, if you have 2 SD instances (light/dark) writing to the same file output, they'd just overwrite one another.

What you could try is the following approach, circumventing the format hook and creating your own logic for format (I'm using CSS as an example coz I'm more familiar with it):

import StyleDictionary from 'style-dictionary';
import { formattedVariables } from 'style-dictionary/utils';

function getConfig(theme) {
  return {
    source: [
      'tokens/primitives.json',
      `tokens/${theme}.json`,
      // etc.
    ],
    platforms: {
      css: {
        transformGroup: 'css',
      }
    }
  }
}

const sdLight = new StyleDictionary(getConfig('light'));
const sdDark = new StyleDictionary(getConfig('dark'));

const [light, dark] = await Promise.all(sdLight.getPlatform('css'), sdDark.getPlatform('css')];

const lightCSS = `:root {
  ${formattedVariables(
    format: 'css',
    dictionary: light.dictionary
  )}
};
`;

const darkCSS = `html[theme="dark"] {
  ${formattedVariables(
    format: 'css',
    dictionary: dark.dictionary
  )}
};
`;

const output = `// maybe some fileheader here

${lightCSS}
${darkCSS}`;

The caveats with this approach is that:

  • files in your SD config is just not used, you're doing this process manually
  • filters are not executed, this logic is hard coupled to the buildFile function atm which is private API
  • a lot of the logging related to output/format is not executed
  • fileheaders are not present, you have to do this yourself now as well
  • format options are not passed, you'd have to pass them manually to formattedVariables helper (e.g. outputReferences)

So what I'd really like to have in the future for Style Dictionary is a method called formatPlatform/formatAllPlatforms which doesn't write to the filesystem but just returns the outputs as strings, or as some other data structure (in your case you'd want an array of objects, light/dark props, containing the iOS UIColor() strings), enabling users to do with that what they want, whether that's writing to the filesystem or something more custom (like combining the strings together from multiple instances, and outputting it as a file themselves, but there are many use cases you can think for this)

@jorenbroekema
Copy link
Collaborator

#1211 created an issue here, and also added your use case there

@tijs
Copy link
Author

tijs commented May 20, 2024

Thanks man, subscribed to that issue. I’ll play with it a bit more in the current setup but I’m hearing the “here be dragons” loud and clear 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants