Новый темный режим в iOS меня очень взволновал. Я мог использовать все свои любимые приложения, но теперь они были Dark 😎. Хорошо, не так уж и интересно, но я думаю, что для многих просто изменение презентации освежает.

Мне пришлось немедленно внести несколько поправок в наше приложение на работе, и я также подумал о том, как я могу развернуть эту функцию в своих собственных приложениях. Естественно, они оба следуют схожему шаблону, когда дело доходит до цветов (или цветов 🙄) и тематики.

Я собираюсь рассказать о подходе и настройке, которые я придумал для тематики и темного режима, и о том, как это можно адаптировать, чтобы сделать его еще более универсальным для пользователей, чтобы настроить свой опыт в вашем приложении.

Текущая настройка

В настоящее время мы централизовали наши цвета и тему в одном месте. В этой статье я буду говорить только о цветах, но, конечно, тематика может и, вероятно, должна содержать spacing, font, icon… все, что вы хотите сохранить в своем приложении с определенной степенью согласованности.

/src
-- /theme/index.js

Прямо в корне у нас есть theme. Здесь мы храним наши цвета (и все остальное, что вы хотите использовать для всего приложения). Вот пример того, как это выглядит;

// theme/index.js
const palette = {
  palette01: '#000000',
  palette02: 'rgba(255,255,255,1)',
}
export const colors = {
  paragraphText: palette.palette01,
  buttonPrimaryBg: palette.palette02,
  headingText: palette.palette01,
}

Зачем разделять палитру и цвета? Отличный вопрос. Итак, таким образом мы можем определять цвета на более детальном уровне в зависимости от их варианта использования и повторного использования из нашей палитры. Это гарантирует, что мы не придумаем 50 оттенков серого 😏. Конечно, в конечном итоге мы будем повторять использование палитры для всех цветов, но на самом деле это не имеет значения, и это своего рода суть. Это также означает, что когда дело доходит до тематики, мы можем изменить палитру или цвета по отдельности (или и то, и другое, если захотим).

Просто краткий пример того, как они используются для ясности;

// ButtonPrimary/index.js
...
import { colors } from 'theme'
const ButtonPrimary = ({ onPress, children }) => (
  <TouchableOpacity onPress={onPress} style={styles}>
    {children}
  </TouchableOpacity>
)
const styles = {
  backgroundColor: colors.buttonPrimaryBg,
}
export default ButtonPrimary

Здесь ничего интересного не происходит, но наша тема по сути отделена от самих компонентов. Отличное начало.

Темный режим

Темный режим является предметом этой короткой статьи и является эксклюзивным для iOS, но есть и другие настройки темы / пользовательского интерфейса для Android, такие как ночной режим. Я полагаю, что в будущем это станет более распространенным явлением, и независимо от предпочтений устройства эту же идею можно использовать для изменения внутренних настроек пользователя.

Если вы используете Expo, как и я (мне это очень нравится… но об этом в другой раз), вы можете использовать react-native-appearance для прослушивания пользовательских настроек устройства. В настоящее время это только для iOS и не учитывает Android, но в соответствии с документацией здесь;

Appearance API, вероятно, будет доступен в react-native@>=0.61

Так что это должно подготовить нас к использованию в будущем. Есть также другие модули, если вы не используете Expo, например react-native-dark-mode, которые используют аналогичный подход.

react-native-appearance дает нам несколько способов работы с пользовательскими предпочтениями, но наиболее простым и практичным мне кажется крючки.

Я не буду вдаваться в подробности использования хуков, но я рекомендую вам потратьте немного времени, чтобы прочитать о них. Они потрясающие.

Вот используемый хук react-native-appearance, прямо из документации;

...
import { useColorScheme } from 'react-native-appearance'
function MyComponent() {
  let colorScheme = useColorScheme()
  if (colorScheme === 'dark') {
   // render some dark thing
  } else {
  // render some light thing
  }
}

Итак, если мы возьмем наш предыдущий пример ButtonPrimary, мы могли бы сделать что-то вроде этого;

// ButtonPrimary/index.js
import { useColorScheme } from 'react-native-appearance'
import { colors } from 'theme'
const ButtonPrimary = ({ onPress, children }) => { 
const colorScheme = useColorScheme()
  return (
    <TouchableOpacity 
      onPress={onPress} 
      style={{ 
        backgroundColor: colorScheme === 'dark' 
          ? colors.buttonPrimaryBgDark 
          : colors.buttonPrimaryBg 
      }}
    >
      {children}
    </TouchableOpacity>
  )
export default ButtonPrimary

Я имею в виду ... это сработает. Но он не очень удобочитаем, и вся логика выбора цвета теперь присутствует в нашем компоненте и в каждом другом компоненте, на который мы, возможно, захотим повлиять, которые с темой могут быть наиболее презентационными компонентами.

Так что это непрактично. Но это легко исправить.

Я создам свою собственную ловушку, которая будет использовать ловушку useColorScheme, а также будет возвращать мои тематические цвета. Он также может вернуть цветовую схему, если она мне понадобится. Это будет означать пару изменений моего colors объекта, которые мы написали ранее, но не слишком радикальных. Глядя на это в первую очередь;

// theme/index.js
const palette = {
  palette01: '#000000',
  palette02: 'rgba(255,255,255,1)',
}
export const colors = {
  paragraphText: palette.palette01,
  buttonPrimaryBg: palette.palette02,
  headingText: palette.palette01,
}
export const themedColors = {
  default: {
    ...colors,
  },
  light: {
    ...colors,
  },
  dark: {
    ...colors,
    buttonPrimaryBg: palette.palette01,
    paragraphText: palette.palette02,
  },
}

Опять же, здесь не происходит ничего слишком интересного - просто добавляем новый объект themedColors, здесь мы и займемся тематикой. light и default делают то же самое в данный момент, просто распространяя исходный набор цветов, но теперь dark некоторые из этих исходных цветов перезаписываются набором для конкретной темы (обратите внимание, как цвета теперь инвертируются для dark против light.

Теперь о крючке.

// theme/hooks.js
import { useColorScheme } from 'react-native-appearance'
import { themedColors } from '.'
export const useTheme = () => {
  const theme = useColorScheme()
  const colors = theme ? themedColors[theme] : themedColors.default
  return {
    colors,
    theme,
  }
}

Мы возвращаем цвета и тему из нашего кастомного хука. colors уже будет назначен предпочтительной для пользователя теме (если она доступна), возвращаясь к default, если ничего не выбрано или не определено.

Использование этого хука становится поразительно похожим на нашу оригинальную реализацию ButtonPrimary.

// ButtonPrimary/index.js
...
import { useTheme } from 'theme/hooks'
const ButtonPrimary = ({ onPress, children }) => {
  const { colors } = useTheme()
  return (
    <TouchableOpacity 
      onPress={onPress} 
      style={{ 
        backgroundColor: colors.buttonPrimaryBg 
      }}
    >
      {children}
    </TouchableOpacity>
  )
}
export default ButtonPrimary

Все, что нам нужно сделать, это импортировать настраиваемый хук и деконструировать цвета оттуда, а не напрямую из нашей темы 🎉

Это масштабируемое и управляемое решение для широкого развертывания всего приложения.

Обратная сторона

Всегда есть один… мы должны написать наш цветовой стиль в строке, что немного обескураживает. При необходимости вы можете использовать и остальные стили, но это сделает его крошечным немного более беспорядочным. Мне кажется, что это хороший компромисс для тематических стилей повсюду.

Расширяя пример

Это просто показывает, как использовать пользовательские настройки устройства, и это ограничено только iOS. Вы можете расширить ловушку, включив в нее настройки пользователя с относительной легкостью, что означает, что пользователь может выбрать свои настройки в приложении, чтобы переопределить настройки устройства.