UIAppearance Tutorial: Getting Started

原文地址:
https://www.raywenderlich.com/156036/uiappearance-tutorial-getting-started
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。
翻译者:毛毛可

虽然skeuomorphism风格在iOS app中已经成为过去式,但也不意味着在你的iOS app中,不会被控件的普通样式所限制.
当然,你可以开发你自己的控件,从scratch获取样式风格,但是Apple推荐使用标准的UIKit控件,充分利用iOS中丰富的定制化技术优势.这是因为UIKit控件是非常高效的,而你对控件的定制化应该是面向未来的.

在本次UIAppearance教程中,你将使用一些基本的UI定制化技术去自定义一个Pet Finder app,使其格外耀眼! :]
再加一个福利,你将学习如何在夜间开启应用时自动切换到夜间模式.

Getting Started

首先下载该教程的初始工程这里.此app中有非常多的标准UIKit控件,看上去非常的不显眼.
打开项目,并熟悉一下整体的项目结构,运行app,你将会看到Pet Finder的UI页面.

UIAppearance Tutorial: Getting Started_第1张图片
plain-600x500.png

图中显示一个navigation bar 和 tab bar.主屏幕上显示了一组宠物列表.点击宠物会来到宠物的详情页面.还有一个搜索页面,允许你去选择指定的选项.这听上去是个不错的开始.

UIAppearance: Supporting Themes

大多数app不允许用户去选择主题,并且也不怎么建议发布的app有主题选择.但是,在某些方面主题选择会非常有用.你可能想要在开发过程中测试不同的主题,来观察在你app中哪个最合适.你或许会让你的用户做A/B测试,来选择最受欢迎的那个风格.或者你想只是想简单在夜间时添加对应的夜间模式.

在这份指南中,你将会创建一组主题,然后找出最美观的主题.
选择File\NEW\File...选择iOS\Source\Swfit文件.点击下一步然后将文件命名为Theme.最后点击创建.

将文件的内容替换成下面的内容:

import UIKit

enum Theme: Int {
  //1
  case `default`, dark, graphical
  
  //2
  private enum Keys {
    static let selectedTheme = "SelectedTheme"
  }

  //3
  static var current: Theme {
    let storedTheme = UserDefaults.standard.integer(forKey: Keys.selectedTheme)
    return Theme(rawValue: storedTheme) ?? .default
  }
}

让我们看看这段代码都做了什么:

  1. 定义三种主题 - 默认, 夜间, 图形化
  2. 定义一个常量来帮助你访问被选中的主题
  3. 给被选中的主题定义一个只读的计算性属性.它使用UserDefault去持久化当前的主题,如果之前没有选择的主题就返回默认主题.

现在你已经设置了Theme枚举,再添加一些样式上去.在Theme结束之前添加一下代码:

var mainColor: UIColor {
  switch self {
  case .default:
    return UIColor(red: 87.0/255.0, green: 188.0/255.0, blue: 95.0/255.0, alpha: 1.0)
  case .dark:
    return UIColor(red: 255.0/255.0, green: 115.0/255.0, blue: 50.0/255.0, alpha: 1.0)
  case .graphical:
    return UIColor(red: 10.0/255.0, green: 10.0/255.0, blue: 10.0/255.0, alpha: 1.0)
  }

定义mainColor属性来指定每种具体主题样式.

让我们看看这如何工作,打开AppDelegate.swift文件并添加代码到application(_:didFinishLaunchingWithOptions:)方法中:

print(Theme.current.mainColor)

运行app,你应该会在控制台看到以下的打印显示:

UIExtendedSRGBColorSpace 0.341176 0.737255 0.372549 1

此时,你可以通过Theme枚举控制三种主题.现在是时候将它应用到你的app中了.

Applying Themes to Your Controls

打开Theme.swift文件,添加以下代码到Theme的最后面

func apply() {
  //1
  UserDefaults.standard.set(rawValue, forKey: Keys.selectedTheme)
  UserDefaults.standard.synchronize()
    
  //2
  UIApplication.shared.delegate?.window??.tintColor = mainColor
}
  1. 使用UserDefaults来持久化被选中的主题
  2. 设置application的window的tintColor属性为 mainColor.稍后你将学到更多tintColor相关的知识.

现在你只需要调用这个方法.
打开AppDelegate.swift文件,将print()函数替换为以下代码:

Theme.current.apply()

运行app.你会看到app明显的变成绿色:

UIAppearance Tutorial: Getting Started_第2张图片
15197432662918.png

浏览一下app,到处都是绿色主题!但是你并没有改变任何一个controller或view.这是什么黑 - 额, 绿魔法? :]

Applying Tint Colors

自iOS7之后,UIView暴露了tintColor属性.它经常用作定义app页面元素的主要的颜色.

当你对view指定tintColor属性,view会自动传播给自身层级中的所有子view.因为UIWindow继承自UIView,所以你可以通过设置window的tintColor来影响这个app的tint color.这也就是上面的apply()方法的功能.

点击app左上角的Gear(齿轮).会展示一个新页面,包含一个tableView,带有UISegmentedControl控件.这时你选择不同的主题,然后点击Apply,什么事也不会发生.现在就去解决这个问题.
打开SettingsTableViewController.swift文件,添加以下代码到applyTheme(_:)方法中,dismissAnimated()方法上面.

if let selectedTheme = Theme(rawValue: themeSelector.selectedSegmentIndex) {
  selectedTheme.apply()
}

这里你调用Theme的方法,设置选中的主题的mianColor到window上.
接下来,添加以下代码到vidwDidLoad()方法最后面.在view controller第一次加载时选择UserDefaults保存的主题样式.

themeSelector.selectedSegmentIndex = Theme.current.rawValue

运行app.点击设置按钮,选择Dark,然后点击Apply.你的app的主题色将会从绿色变成橙色:

UIAppearance Tutorial: Getting Started_第3张图片
15197891167067.png

眼尖的读者可能注意到Theme文件中mainColor属性的定义.
但是,你选择了Dark,但是它并不怎么像夜间模式.想要实现这种效果,你还需要再做一些事情.

Customizing the Navigation Bar

打开Theme.swift文件,并添加两个属性到Theme类中:

var barStyle: UIBarStyle {
  switch self {
  case .default, .graphical:
    return .default
  case .dark:
    return .black
  }
}
  
var navigationBackgroundImage: UIImage? {
  return self == .graphical ? UIImage(named: "navBackground") : nil
}

这些方法很简单,为每一种主题返回合适的bar style 和 background image.
下面,添加代码到apply()方法的最后面:

UINavigationBar.appearance().barStyle = barStyle
UINavigationBar.appearance().setBackgroundImage(navigationBackgroundImage, for: .default)

为什么这段代码可以工作?难道不应该访问UINavigationBar实例对象吗?

UIKit中有一个非正式协议叫做UIAppearance,这是一个大部分控件都遵守的协议.当你调用UIKit 类的 appearance()方法-注意不是实例-会返回一个UIAppearance代理对象.当你改变这个代理的属性时,这个类的所有实例都会自动获得相同的值.这非常的方便,这样你不需要每个控件在实例化之后再手动修改样式.

运行app.选择Dark主题,navigation bar 会变得很暗;

UIAppearance Tutorial: Getting Started_第4张图片
15198253210269.png

这样看上去好点了,但是你仍然需要做一些工作.
下面,你会自定义back indicator(返回标记).iOS默认会使用chevron符号,但你可以通过代码编写的更好!:]

Customizing the Navigation Bar Back Indicator

这种更改方式会应用到所有的主题上,所以你只需要添加下面的代码到Theme.swift文件的apply()方法中.

UINavigationBar.appearance().backIndicatorImage = UIImage(named: "backArrow")
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrowMask")

你简单的设置了image和transition image.
运行app.点击宠物进入新页面,你将会看到新的返回按钮图案


UIAppearance Tutorial: Getting Started_第5张图片
15198686660710.png

打开 Image.xcasset,找到Navigation组下的backArrow image 图片.这张图片是黑色的,但你的app的window已经设置了tint color,图片颜色就自动更改了.

UIAppearance Tutorial: Getting Started_第6张图片
15198688582166.jpg

但为什么iOS只更改了bar button item的image color,却没有全部更改?
事实证明,iOS中的图像有三种渲染模式:

  • 原始模式:使用图像原本的颜色.
  • 模板模式:忽略颜色,使用图像作为一个模板,在这种模式下,iOS只会使用图像的轮廓外形,并且在将图像渲染到屏幕前,为图像着色.
  • 自动模式:依赖你使用的图像的上下文,系统自己决定是否应该使用原始模式还是模板模式.对于back indicators,navigation item和 tab bar images,iOS会默认忽略图像颜色.你可以覆盖这种行为来手动改变渲染模式.

回到app中,点击其中一个宠物进入详情页,然后点击Adopt按钮.仔细观察返回按钮的动画.你能看到有什么问题吗?

mask1.gif

返回的文字过渡到左边时,会和indicator重叠,这看上去非常糟糕:
为解决这个问题,你将要更改这个 transition mask image(属性).
更新Theme.swift文件里apply()方法里的backIndicatorTransitionMaskImage代码:

UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrow")

运行app.重新上一次的操作.这时你会发现过渡动画变得比之前好很多.

mask2.gif

文字不再被切断,更像是在indicator的底部.到底发生了什么?
iOS会使用back indicator image的所有不透明像素绘制indicator.然而,它和transition mask image做了完全不同的工作.它使用transition mask image的不透明像素遮盖了indicator.所以当文字向左移动时,indicator只显示在那个区域.

在原来的实现中,你提供了一个用来覆盖back indicator的整个表面.这就是为什么文本在过渡时,仍然可见的原因.现在你使用indicator image本身作为遮罩,看起来会更好.如果你仔细观察,你将会看到文字会在遮罩的右边缘消失,而不是在indocator的下方.

查看你的image资源文件中"fixed"的indicator图片.你会看到这个图片比其它的更完美.


UIAppearance Tutorial: Getting Started_第7张图片
15199744535932.png

黑色部分是你的back indicator,红色部分是你的遮罩.你想让文字只在红色部分下方时显示,其它的部分都隐藏.

要实现这一点,再一次更改apply()方法中的最后一行,这次试用更新过的遮罩:

UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrowMaskFixed")

运行程序.在点击宠物,点击Adopt.你会看到文字现在在image下面消失,就像预期的那样:

mask3.gif

现在你的navigation bar非常完美,是时候将关注点放在tab bar上了.

Customizing the Tab Bar

还是Theme.swift文件,添加属性到Theme类中:

var tabBarBackgroundImage: UIImage? {
  return self == .graphical ? UIImage(named: "tabBarBackground") : nil
}

这个属性将会为每个主题提供合适的 tab bar背景图片.
为了应用这些主题,添加下面代码到apply()方法中.

UITabBar.appearance().barStyle = barStyle
UITabBar.appearance().backgroundImage = tabBarBackgroundImage
    
let tabIndicator = UIImage(named: "tabBarSelectionIndicator")?.withRenderingMode(.alwaysTemplate)
let tabResizableIndicator = tabIndicator?.resizableImage(
  withCapInsets: UIEdgeInsets(top: 0, left: 2.0, bottom: 0, right: 2.0))
UITabBar.appearance().selectionIndicatorImage = tabResizableIndicator

现在设置 barStyle和 backgroundImage应该很熟悉.和之前的UInavigationBar相同.
在上面的代码的倒数三行,你接收一个资源图库的 indicator image并设置渲染模式.
最终,你创建了一个 resizable image 并把它设置给 tab bar的 selectionIndicatorImage属性.
运行app.你讲看到最新主题的 tab bar.


UIAppearance Tutorial: Getting Started_第8张图片
15202166146808.png

夜间模式变得更加完美!
看到选中 tab下面的横线了吗?这就是你的 indicator image.虽然它的高只有6个点,宽49个点,但是iOS会在运行时将图片拉伸到全尺寸.
下一节会提及resizeable image,探究它如何工作.

Customizing a Segmented Control

还有一个没有更改的元素,那就是显示当前选中主题的segmented control控件.
是时候把此控件也带到奇妙的主题中了.
添加一下代码到Theme.swift文件的apply()方法的最后.

let controlBackground = UIImage(named: "controlBackground")?
  .withRenderingMode(.alwaysTemplate)
  .resizableImage(withCapInsets: UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))

let controlSelectedBackground = UIImage(named: "controlSelectedBackground")?
  .withRenderingMode(.alwaysTemplate)
  .resizableImage(withCapInsets: UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))

UISegmentedControl.appearance().setBackgroundImage(controlBackground,
                                                   for: .normal,
                                                   barMetrics: .default)
UISegmentedControl.appearance().setBackgroundImage(controlSelectedBackground,
                                                   for: .selected,
                                                   barMetrics: .default)

要理解上面的代码,首先查看一下资源文件中的 controlBackground image.
此图像可能是很小,但是iOS知道如何使用它来绘制UISegmentedControl的边框,因为它已经被预分片,可以调整大小.
分片是什么意思? 请看下面的放大图:


UIAppearance Tutorial: Getting Started_第9张图片
15202357540528.png

这里有四个 3×3 正方形,分落在四个角落.调整image大小时,这些方块保持不变.灰色的像素区域,会被水平和垂直拉伸.在你的图像中,所有的像素都是黑色的,并且假设就是控件的 tint color.通过使用UIedgeInsets()方法指定iOS该如何拉伸图像.你给top,left,bottom,right参数都传入3,因此四个角落都是3×3.

运行app.点击左上角的齿轮图标,你将看到UIsegmentedControl最新的样式.

UIAppearance Tutorial: Getting Started_第10张图片
15202366617247.png

通过设置3×3的正方形,控件的圆角已经被替换.
现在你已经对你的 segmented control进行了着色和样式设计,剩下的就是对剩下的控件进行着色。
现在关闭app的设置页面,点击右上角的放大镜.你竟看到另一个 segmented control, 还有UIStepper, UISlider和UISwitch 都需要设置主题.

Customizing Steppers, Sliders, and Switches

改变 stepper的颜色,添加下面代码到Theme.swift文件的apply()方法中.

UIStepper.appearance().setBackgroundImage(controlBackground, for: .normal)
UIStepper.appearance().setBackgroundImage(controlBackground, for: .disabled)
UIStepper.appearance().setBackgroundImage(controlBackground, for: .highlighted)
UIStepper.appearance().setDecrementImage(UIImage(named: "fewerPaws"), for: .normal)
UIStepper.appearance().setIncrementImage(UIImage(named: "morePaws"), for: .normal)

你之前已经使用相同可拉伸的图像给到UISegementedControl控件.这里唯一不同的是,当UIStepper到达最大值或最小值时变成失效状态.你也可以在这种情况下指定图像.为了简单,你重用相同的图像对象.通过这些操作,不止改变了stepper的颜色,同样按钮的"+"和"-"被图片替代.

运行app.打开Search,观察stepper发生了怎样的变化:![]
UIAppearance Tutorial: Getting Started_第11张图片
15212656230140.png

UISlider和UISwitch同样也需要一些有趣的主题.
添加代码到apply()方法中.

UISlider.appearance().setThumbImage(UIImage(named: "sliderThumb"), for: .normal)
UISlider.appearance().setMaximumTrackImage(UIImage(named: "maximumTrack")?
  .resizableImage(withCapInsets:UIEdgeInsets(top: 0, left: 0.0, bottom: 0, right: 6.0)), for: .normal)
    
UISlider.appearance().setMinimumTrackImage(UIImage(named: "minimumTrack")?
  .withRenderingMode(.alwaysTemplate)
  .resizableImage(withCapInsets:UIEdgeInsets(top: 0, left: 6.0, bottom: 0, right: 0)), for: .normal)
    
UISwitch.appearance().onTintColor = mainColor.withAlphaComponent(0.3)
UISwitch.appearance().thumbTintColor = mainColor

UISlider有三个定制化的地方:slider(滑块)的'thumb','minimum track'(滑块左部分),'maximum track'(滑块右部分).
thumb使用了你的资源图片.maximum track 使用演示渲染模式的拉伸图片,在任何主题下始终保持黑色.minimum track 同样是使用拉伸图片,但是使用的是模板渲染模式,所以它会继承模板的tint color.
通过设置UISwitch的thumbTinTColor,修改它的主颜色.通过onTintColor设置浅亮的主颜色,提升对比度.
运行app.点击Search,此时slider和witch应该显示如下:

UIAppearance Tutorial: Getting Started_第12张图片
15213487094096.png

现在你的app变得非常新潮,但是夜间模式还缺少一些.table view的背景太亮了.让我们修改一下.

Customizing UITableViewCell

Theme.swift文件中,添加属性到Theme类里:

var backgroundColor: UIColor {
  switch self {
  case .default, .graphical:
    return UIColor.white
  case .dark:
    return UIColor(white: 0.4, alpha: 1.0)
  }
}

var textColor: UIColor {
  switch self {
  case .default, .graphical:
    return UIColor.black
  case .dark:
    return UIColor.white
  }
}

声明backgroundColor属性,将会用在table view cell和 label的text color属性上.

UITableViewCell.appearance().backgroundColor = backgroundColor
UILabel.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).textColor = textColor

第一行代码看上去很熟悉,它简单的设置了所有的UITableViewCell实例的backgroundColor.第二行代码很神奇.
UIAppearance能够让你根据条件去改变.在这里,你不想改变app中所有的text color.你只想改变UITableViewCell中的text color.通过使用whenContainedInstancesOf:方法来实现.你强制只有UITableViewCell中的UILabel允许改变.

运行app,选择dark theme,屏幕显示会变成这样:

UIAppearance Tutorial: Getting Started_第13张图片
15213547396992.png

现在,才是真正的dark theme!
正如你现在看到的,appearance proxy定制了多个类的实例.但有时你并不想使用全部的appearance来控制.这时,你可以只用一个单独的控件来自定义.

Customizing a Single Instance

打开SearchTableViewController.swift文件,并添加代码到viewDidLoad()方法中.

speciesSelector.setImage(UIImage(named: "dog"), forSegmentAt: 0)
speciesSelector.setImage(UIImage(named: "cat"), forSegmentAt: 1)
UIAppearance Tutorial: Getting Started_第14张图片
15214700792876.png

iOS将所选片段的图像上的颜色颠倒过来,而无需您做任何工作.这是因为图像会使用模板模式自动渲染.
如何选择性地更改控件的字体?这也很简单.
打开PetViewController.swift文件,并添加以下代码到viewDidLoad()方法底部.

view.backgroundColor = Theme.current.backgroundColor

运行app.选择一个宠物,观察效果:

UIAppearance Tutorial: Getting Started_第15张图片
15214707237729.png

你已经完成了主题设定.下面的图片展示了先后的不同效果:

UIAppearance Tutorial: Getting Started_第16张图片
15214714840035.png

我感觉你会觉得新版会比原生的更有趣.你为app添加了3中样式.
为什么不再进一步呢?比如你帮助用户在合适的时间选择正确的主题.像在黄昏时选择夜间模式.让我们看看如何操作.

Automating dark theme with Solar

作为本教程的这一部分,你将会使用一个叫做Solar的开源库.Solar接收位置和日期参数,返回当天的日出和日落时间.Solar已经被安装到了这份指南对应的starter project项目中了.

Note: For more on Solar, visit the library’s GitHub repository

提示:更多的Solar内容,请浏览GitHub repository库

打开*AppDelegate.swift(文件,在window属性下添加属性:

private let solar = Solar(latitude: 40.758899, longitude: -73.9873197)!

通过给定一个位置和今天的日期,声明一个Solar类型的属性.你可以更改位置,这样就可以根据用户的位置动态改变.

还是在AppDelegate文件中,添加下面两个方法:

//1
func initializeTheme() {
  //2
  if solar.isDaytime {
    Theme.current.apply()
    scheduleThemeTimer()
  } else {
    Theme.dark.apply()
  }
}
  
func scheduleThemeTimer() {
  //3
  let timer = Timer(fire: solar.sunset!, interval: 0, repeats: false) { [weak self] _ in
    Theme.dark.apply()

    //4
    self?.window?.subviews.forEach({ (view: UIView) in
      view.removeFromSuperview()
      self?.window?.addSubview(view)
    })
  }

  //5
  RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
}
  1. 声明一个方法用来初始化主题设置
  2. Solar实例对象有一个便利方法可以检查给定时间是否是白天.如果是这样,你会使用当前的主题,并安排一个Timer用于改变主题.
  3. 你会创建一个timer,设置在日落时启动.
  4. 注意这是很重要的.当UIAppearance的值发生改变时,并不会继续反射下去,知道子view重新渲染.通过移除,重新添加window上所有的子view,来保证它们重新渲染成新的主题.
  5. 将安排的timer添加到主runloop中.

最后,在application(_:didFinishLaunchingWithOptions:)方法中替换

Theme.current.apply()

initializeTheme()

在日落后打开app(如果是白天,你可以更改系统时间).app应该会显示为夜间模式主题.

Where to Go From Here?

你可以在这里下载本教程的最终项目.
除了你已经做的这些,UIAppearance也支持指定UITraitConnection(没找到此类 我认为是UITraitCollection)的自定义控件.这可以让你在不同的设备布局中使用多种主题.
我希望你喜欢本次的UIAppearance教程,并从中学习到如何简单的应用到UI中.

你可能感兴趣的:(UIAppearance Tutorial: Getting Started)