QYTheme 1.0.0

QYTheme 1.0.0

Joeyoung 维护。



QYTheme 1.0.0

  • 作者
  • qiaoyoung

QYTheme

CI Status Version License Platform

一、设计

1. 为什么需要暗黑模式

  1. 让用户更好地关注 App 内的内容展现
  2. 在暗光环境下减少用户的视觉疲劳,降低对周围他人的影响
  3. OLED 屏幕可以大幅降低屏幕的耗电量
  4. 用户对深色用户界面的喜爱
  5. 在不大幅度改动 UI 界面的前提下,为用户提供全新的视觉体验

2. iOS 设计团队为暗黑模式设定了以下五个设计原则:

  1. 保持视觉风格的熟悉感
  2. 平台一致性
  3. 清晰的信息架构
  4. 辅助功能
  5. 便于开发者采用

3. 提交您的 iPhone 应用程序到 App Store

全球范围内,iOS 13 现在运行在过去的四年中推出的所有 iOS 设备的 77%。通过无缝集成深色模式、苹果登录以及 ARKit 3、Core ML 3 和 Siri 的最新进展,提供卓越的用户体验。自 2020 年 4 月 30 日起,所有提交到 App Store 的 iPhone 应用都必须使用 iOS 13 SDK 或更高版本开发。利用 Xcode 的功能,如故事板(包括启动故事板)、自动布局以及 SwiftUI,确保您的应用程序界面元素和布局能够自动适配所有 iPhone 模型的显示,无论其尺寸或屏幕比例如何。自 2020 年 4 月 30 日起,所有提交给 App Store 的应用程序必须使用 Xcode 的故事板来提供启动画面,并且所有 iPhone 应用必须支持所有 iPhone 的屏幕。

  • 关于苹果 2020 年 3 月 4 日发布的官方公告,分析如下:
    1. 自 2020 年 4 月 30 日起,所有提交给 App Store 的 iPhone 应用程序都必须使用 iOS 13 SDK 或更高版本构建。(必须使用 Xcode 11 或更高版本进行打包发布。)
    2. 自 2020 年 4 月 30 日起,所有提交给 App Store 的应用程序都必须使用 storyboard 来提供应用程序的启动屏幕。
    3. 所有 iPhone 应用都必须支持所有 iPhone 的屏幕。

4. 微信适配了暗黑模式(2020.03.09)

二、适配

1. 启动页:

自 2020 年 4 月 30 日起,所有提交给 App Store 的应用程序都必须使用 storyboard 来提供应用程序的启动屏幕。

方法一:
  • 在 storyboard 中拖入 UIImageView 控件,设置好约束。
  • Assets.xcassets 中新建 New Image Set。默认新建的 Image 不支持多种机型,打开该 Image 的 Contents.json 文件,复制下面的代码覆盖该文件: 将 "filename" 对应的 value 替换为本地对应的图片名称即可(_dark 结尾的图片对应的是暗黑模式)。
{
  "images" : [
    {
      "idiom" : "iphone",
      "scale" : "1x"
    },
    {
      "idiom" : "iphone",
      "scale" : "1x",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ]
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_4.png",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_4_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ]
    },
    {
      "idiom" : "iphone",
      "subtype" : "retina4",
      "scale" : "1x"
    },
    {
      "idiom" : "iphone",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "retina4",
      "scale" : "1x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_5.png",
      "subtype" : "retina4",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_5_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "retina4",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "subtype" : "retina4",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "retina4",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_7P.png",
      "subtype" : "736h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_7P_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "736h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_7.png",
      "subtype" : "667h",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_7_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "667h",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_X.png",
      "subtype" : "2436h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_X_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "2436h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_Xs Max.png",
      "subtype" : "2688h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_Xs Max_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "2688h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_XR.png",
      "subtype" : "1792h",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "filename" : "iPhone_XR_dark.png",
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "subtype" : "1792h",
      "scale" : "2x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

  • 在 LaunchScreen.storyboard 中选择自己创建的 image。

  • 存在缓存问题(待解决)
方法二:

对于简单的启动图页面,直接在 LaunchScreen.storyboard 中自己画。创建图标、标签等控件,设置好约束,就不用担心屏幕适配问题了。

2. UITraitCollection:

在iOS 13中,我们可以通过UITraitCollection来判断当前系统所处的模式。UIScreen, UIWindow, UIViewController, UIPresentationController, UIView 都遵循了 UITraitEnvironment 协议。

/*! Trait environments expose a trait collection that describes their environment. */
@protocol UITraitEnvironment <NSObject>
@property (nonatomic, readonly) UITraitCollection *traitCollection API_AVAILABLE(ios(8.0));

/*! To be overridden as needed to provide custom behavior when the environment's traits change. */
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection API_AVAILABLE(ios(8.0));
@end

当用户更改系统外观时,系统会自动要求每个窗口和视图重绘自身。在此过程中,系统将为 macOS 和 iOS 调用下表中列出的几种众所周知的方法来更新您的内容。系统在调用这些方法之前会更新特征环境,所以只有在下列方法中获取 traitCollection 属性才是准确的。

如果您在这些方法之外进行外观敏感的更改,则您的应用可能无法针对当前环境正确绘制其内容。

在 iOS 13 中,UIView、UIViewController 、UIWindow 有了一个 overrideUserInterfaceStyle 的新属性,可以覆盖系统的模式。

单个页面或视图关闭暗黑模式,设置 overrideUserInterfaceStyle 为对应的模式,强制限制该视图与其子视图以设置的模式进行展示,不跟随系统模式改变而改变。

self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

设置此属性会影响当前 view/viewController/window 以及它下面的任何内容。

如果您希望一个子视图监听系统的模式,请将 overrideUserInterfaceStyle 属性设置为.unspecified。

3. 颜色:

  • 系统的语义化颜色

iOS 默认提供了 9 个彩色色板(TintColor),在 iOS 13 中为了保证深色模式下的对比度效果,每个 TintColor 都新增了深浅模式两种版本,在调用时也应使用语义化的颜色名称,比如 SystemBlue,在浅色模式下指的是 #007AFF,在深色模式下则是 #0A84FF。

  • 自定义颜色:通过在 Assets.xcassets 中新建 New Color Set 来操作。使用 [UIColor colorNamed:@""] 方法调用。

  • 通过代码自定义颜色:

+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider;
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider;

4. 图片:

  • SF Symbols

SF Symbols 是 iOS 13 中引入的非常重要的新特性。由于 Dark Mode 下所有图标都需要两套颜色,使用静态图片切片会导致图片素材数量激增,因此苹果制作了这一整套1500多个图标的矢量图标库。与 iOS 中的基底层、框架层、语义化颜色、Vibrancy(鲜亮化)等动态颜色处理配合,使用 SF Symbols 可以在深浅模式中自动获得最佳展示效果。

SF Symbols 的原理与 Iconfont 类似,都是将 SVG 矢量图形以 Unicode 字符的形式打包在字体文件中。SF Symbols 内置集成在苹果当前的系统默认字体 San Francisco 字体系列中,开发者只需引用 Symbol 的名称即可快速调用 SF Symbols 提供的图标。同时,设计师也可以利用 SF Symbols 官方提供的 SVG 模板制作自定义的图标供 App 调用。 为您的应用创建自定义符号图像

+ (nullable UIImage *)imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle withConfiguration:(nullable UIImageConfiguration *)configuration
  • 自定义图片:通过在 Assets.xcassets 中新建 New Image Set 来操作。使用 [UIImage imageNamed:@""] 方法调用。

5. 模糊效果:

在 iOS 13 中,我们需要让这些模糊效果在系统模式的切换中也相应切换。UIKit 提供了新的模糊样式且是动态的,会随着系统模式的更新而自动匹配。

利用 UIVisualEffectView 创建一些类似模糊的效果时,不要设置带有明确颜色的效果,如 UIBlurEffectStyleExtraLight,而是设置 UIKit 中新提供的动态样式效果,比如 UIBlurEffectStyleSystemThinMaterialLight 或 UIBlurEffectStyleSystemThinMaterialDark。

UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial];
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];

6. WebView页面:

多数流行的浏览器新版本都已经支持了“prefers-color-scheme”参数来检测系统当前的外观是浅色还是深色模式。配合使用类似Semantic Color的方法来定义网页样式表中同一颜色在深浅两种模式下的颜色值,Web内容也可以获得与原生App一样的自动适配深浅模式效果。

Dark Mode Support in WebKit


7. 封装适配工具:

针对iOS13暗黑模式的适配工作,编写了几个分类,方便项目统一管理适配工作,功能如下:

    1. UIWindow+QYTheme.h 当 UIUserInterfaceStyle 发生变化时的通知,可用于无法准确获取 TraitCollection 的地方
if (@available(iOS 13.0, *)) {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(qy_doSomething:)
                                                 name:QYUserInterfaceStyleWillChangeNotification
                                               object:nil];
}  
    1. UIColor+QYTheme.h 统一处理颜色的分类,iOS13以前取 anyColor
/// 获取动态颜色
/// @param anyColor 默认颜色
/// @param darkColor 暗黑颜色
+ (UIColor *)qy_setColorWithAny:(UIColor *)anyColor dark:(UIColor *)darkColor;
// 使用方法:
self.view.backgroundColor = [UIColor qy_setColorWithAny:[UIColor whiteColor] dark:[UIColor blackColor]];
    1. UIImageView+QYTheme.h 统一处理图片的分类,可处理本地和网络图片,网络图片依赖 SDWebImage 框架。
/// 设置图片 [处理本地图片]
/// @param anyImage 默认图片
/// @param darkImage 暗黑图片
- (void)qy_setImageWithAny:(UIImage *)anyImage darkIamge:(UIImage *)darkImage;

/// 设置图片 [处理网络图片]
/// @param anyUrl 默认url
/// @param darkUrl 暗黑图片url
/// @param placeholder 通用占位图
- (void)qy_setImageWithURL:(NSURL *)anyUrl darkImageWithURL:(NSURL *)darkUrl placeholderImage:(UIImage *)placeholder;

/// 设置图片 [处理网络图片]
/// @param anyUrl 默认url
/// @param placeholder 默认占位图
/// @param darkUrl 暗黑图片url
/// @param darkPlaceholder 暗黑占位图
- (void)qy_setImageWithURL:(NSURL *)anyUrl placeholderImage:(UIImage *)placeholder darkImageWithURL:(NSURL *)darkUrl darkPlaceholderImage:(UIImage *)darkPlaceholder;
// 使用方法:
// 本地图片
[self.imgView qy_setImageWithAny:[UIImage imageNamed:@"anyImage"] darkIamge:[UIImage imageNamed:@"darkImage"]];
// 网络图片
[self.imgView qy_setImageWithURL:[NSURL URLWithString:@"https://xxx"]
                darkImageWithURL:[NSURL URLWithString:@"https://xxx"]
                placeholderImage:[UIImage imageNamed:@"image"]];

目前工具正在完善,如有不足,欢迎指正~

参考文档

写给设计师的指南:iOS 13 Dark Mode 深度解析 Apple Design Resources


Submit Your iPhone Apps to the App Store Supporting Dark Mode in Your Interface How To Adopt Dark Mode In Your iOS App 使用 QMUITheme 实现换肤并适配 iOS 13 Dark Mode

Requirements

安装

QYTheme可以通过CocoaPods获取。要安装它,只需将以下行添加到您的Podfile中

pod 'QYTheme'

作者

qiaoyoung, [email protected]

许可

QYTheme遵循MIT许可。更多信息请参阅LICENSE文件。