本文是投稿文章,译者: Andy矢倉
开发了App这么久,是不是觉得主题功能很繁琐,那么接下来良辰我将动用北京的势力来指导你如何为你的App增加主题功能。
原文: http://www.raywenderlich.com/108766/uiappearance-tutorial
作者: Essan Parto
主题教程:现在开始
虽然拟物化在iOS里已是过去式,这并不意味着你的iOS应用控件就限于原始外观。
虽然你可以从头开始开发自己自定义控件,Apple官方还是建议您使用标准UIKit控件并且利用这各种自定义技术优势。这是因为,UIKit的控制更高效,并且这种自定义控件是在给未来铺路。
在本UIAppearance教程中,您将使用一些基本的UI自定义技术来定制一个普通的宠物搜索应用,并使其脱颖而出! :]
那就开始吧
本教程已经为你准备好了 初始工程 ,该应用包含许多标准的UIKit控件,看起来非常丝滑。。。
打开项目,了解一下它的工程结构目录。让他跑起来,看看宠物搜索(Pet Finder)的主要用户界面元素:
这里我们利用了导航控栏(navigation bar)和标签栏(tab bar),主屏幕的列表展示小宠物们;随意点击一个可以进入查看详情。除了有一个搜索页,你的应用也应该能有主题切换,难道不是吗?听起来就有一个非常良好的开端!
主题支持
许多应用都不允许用户选择主题,没有主题的应用让人看起来感觉怪怪的,挺煞笔,处女座有点不能忍。如果你对你应用的内容控制的不够好,那么你会发现内容与主题的冲突让人看起来有多low逼。但是,您可能希望在开发过程中测试不同的主题来看看哪些跟你的应用最搭配,也许是雨天跟巧克力。或者给不同的用户测试,看看哪种风格才能潮爆墨西哥。
在本UIAppearance教程中,我们将打造一批主题,来让你的Application看起来有够丝滑。
选择File/New/File…并选中iOS/Source/Swift文件。点击下一步,然后输入Theme作为文件名。最后点击下一步,然后创建。Xcode会自动打开新的文件,可以看到其中只包含一行代码。
删除并替换为下面的代码:
import UIKit enum Theme: Int { case Default, Dark, Graphical 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: 242.0/255.0, green: 101.0/255.0, blue: 34.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) } } }
给你的应用增加一个包含不同主题的enum。现在,所有的主题都通过mainColor具体到特定的主题。
接下来,添加下面的struct:
struct ThemeManager { }
马上你的应用就会拥有主题了。虽然它现在没代码,接下来我们就把它撸进去!
接着,添加以下行enum声明:
let SelectedThemeKey = "SelectedTheme"
现在,把下列方法加到ThemeManager里:
static func currentTheme() -> Theme { if let storedTheme = NSUserDefaults.standardUserDefaults().valueForKey(SelectedThemeKey)?.integerValue { return Theme(rawValue: storedTheme)! } else { return .Default } }
这里并没有什么过于复杂的东西:这个方法就是为了搞定你App风格,它使用NSUserDefaults来持久化当前主题数据,以便每次启动的时候好使用它。
来测试一下看能不能行,打开AppDelegate.swift在application(_:didFinishLaunchingWithOptions):里面加上
println(ThemeManager.currentTheme().mainColor)
Let’s it 跑起来,控制台会输出一下信息:
UIDeviceRGBColorSpace 0.341176 0.737255 0.372549 1
这个时候你可以管理你拥有的三套主题来改变你的App了。
主题应用
返回到Theme.swift, 添加下列方法到ThemeManager:
static func applyTheme(theme: Theme) { // 1 NSUserDefaults.standardUserDefaults().setValue(theme.rawValue, forKey: SelectedThemeKey) NSUserDefaults.standardUserDefaults().synchronize() // 2 let sharedApplication = UIApplication.sharedApplication() sharedApplication.delegate?.window??.tintColor = theme.mainColor }
我们来快速浏览下代码:
使用NSUserDefaults来记录你选择的主题
接着获取当前主题并把main color应用到整个应用窗口,接下来我们学习关于tintColor的更多知识。
现在,你需要做的唯一一件事就是调用此方法。没有哪里比在AppDelegate.swift里这么做更好了。
覆盖掉println()并录入下面的代码:
let theme = ThemeManager.currentTheme() ThemeManager.applyTheme(theme)
让我们跑起来,就会看到如下鸟样:
放眼望去,满脸绿!但是,你没有改变任何的控制器或视图。一秒变绿,真是神奇的一逼!
自从iOS7开始,UIView就开始暴露tintColor属性,它通常被用来定义在整个应用中用于指示应用程序界面元素的选择和交互的状态的原始色。
当您为视图指定的色彩时,色调会自动传播到该视图层次中的所有子视图。因为UIWindow继承自UIView的原因,你可以调用applyTheme()方法来设置窗口的tintColor,从而调整整个应用程序的色调。
点击你的应用程序的左上角的齿轮图标;用分段控制幻灯片表格视图,但是当你选择一个不同的主题,点击应用,也并没卵用。那么接下来我们就来解决这个问题。
打开SettingsTableViewController.swift并把下列代码添加到applyTheme()函数内,dismiss()上面:
if let selectedTheme = Theme(rawValue: themeSelector.selectedSegmentIndex) { ThemeManager.applyTheme(selectedTheme) }
这里选择的主题颜色设置到根视图的tintColor属性上调用的函数已经添加到了ThemeManager里,
接着,在viewDidLoad()的底部添加下面这句,用于第一次加载视图控制器时把主题数据写入NSUserDefaults内:
themeSelector.selectedSegmentIndex = ThemeManager.currentTheme().rawValue
运行起来看看,选择setting按钮,再选择Dark,最后点击Apply,主题颜色就会从绿变蓝:
眼尖的读者可能已经注意到这些颜色早就定义在ThemeType的mainColor()里了。
别急,你选择了Dark,但这并没有变暗。为了得到这种效果,你必须自定义更多的工作。
自定义导航栏
打开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的风格和主题所对应的背景图像给导航栏。
把下面两行添加到applyTheme()的底部:
UINavigationBar.appearance().barStyle = theme.barStyle UINavigationBar.appearance().setBackgroundImage(theme.navigationBackgroundImage, forBarMetrics: .Default)
好了 - 为什么在这里做这项工作,而不是更早的时候呢?
UIKit中有一个非正式的协议称为UIAppearance控制大部分外观。当你在UIKit上调用appearance() - 并非实例化 - 它返回一个UIAppearance代理类。当您更改此代理的属性,该类的所有实例自动获得相同的值。这是非常方便,因为您不必再需要手动去控制样式。
跑起来,选择黑暗的主题和导航栏现在应该更加黑暗:
这看起来好一点,但是还没完。
接下来,将定制返回的箭头。 iOS虽然默认采用了chevron效果,不过不要怕,良辰我会动用北京的势力来帮你。。。
自定导航栏的返回图标
继续在Themes.swift的applyTheme()最后添加下面两行,方便此更改适用于所有的主题:
UINavigationBar.appearance().backIndicatorImage = UIImage(named: "backArrow") UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrowMask")
在这里,我们简单地设置图像和过渡掩模图像用作返回图标。
跑起来,随便点击只宠物,你就可以看到新的返回图标:
打开Images.xcassets并在导航组中找到backArrow图像。该图像是全黑的,不过没事,他只是配合主题色。
如何才能只改变按钮项的图像颜色,在iOS里有三种渲染模式:
Original:务必使用图像“原样”,和原来的颜色。
Template:忽略颜色,只需使用图像作为模板。在这种模式下,iOS使用仅图像的形状,因此只会通过主题颜色和您提供的图像形状在屏幕进行渲染。
Automatic:这取决于你使用的图像环境,再由系统决定来该使用original还是template模式。对于返回图标,导航控制器按钮项和标签选择项,iOS会默认忽略图像的颜色,除非你改变渲染模式。
回到应用程序,随便点击个宠物,然后点击Adopt。仔细观察导航栏中返回按钮的动画。你能看到这个问题?
当返回文本转换到左边,箭头重叠了,这样看起来很糟糕:
为了解决这个问题,你必须改变的过渡掩模图像。
在applyTheme()里更新backIndicatorTransitionMaskImage设置:
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrow")
再次运行,点击Adopt,这样是不是看起来更好了呢:
这样文本和图标就不会再转换重叠,那这里面到底发生了什么呢?
iOS使用非转换像素经行返回图标绘制时,它与过渡掩模图像完全不同:它掩盖与过渡屏蔽图像的非透明像素的箭头,使得当文本向左移动,箭头仅在这些区域中可见。
在最开始实现时,要提供背面箭头图像覆盖在整个表面上,以便在文本过渡时仍然可见图像。但现在你正在使用的箭头图像本身,文本消失在最右边,而不是在其下面。
在Images.xcassets里看看这个箭头图像修复版本,你将看到完美覆盖:
让黑色部分显示,红色部分隐藏起来。
再次更新applyTheme()函数最后的代码:
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrowMaskFixed")
再次运行,再点击Adopt返回的时候,看起来是不是好多了,没有重叠也没有占位的效果:
现在你的导航栏就完美了,那么接下来就需要把精力放Tabbar上了。
自定义Tabbar
还是在Theme.swift,在Theme里添加如下属性:
var tabBarBackgroundImage: UIImage? { return self == .Graphical ? UIImage(named: "tabBarBackground") : nil } var backgroundColor: UIColor { switch self { case .Default, .Graphical: return UIColor(white: 0.9, alpha: 1.0) case .Dark: return UIColor(white: 0.8, alpha: 1.0) } } var secondaryColor: UIColor { switch self { case .Default: return UIColor(red: 242.0/255.0, green: 101.0/255.0, blue: 34.0/255.0, alpha: 1.0) case .Dark: return UIColor(red: 34.0/255.0, green: 128.0/255.0, blue: 66.0/255.0, alpha: 1.0) case .Graphical: return UIColor(red: 140.0/255.0, green: 50.0/255.0, blue: 48.0/255.0, alpha: 1.0) } }
这些属性提供了相应的标签栏背景图片,背景颜色,和每个主题对应的二级颜色。
在applyTheme()里添加如下代码,就可以统一风格了:
UITabBar.appearance().barStyle = theme.barStyle UITabBar.appearance().backgroundImage = theme.tabBarBackgroundImage let tabIndicator = UIImage(named: "tabBarSelectionIndicator")?.imageWithRenderingMode(.AlwaysTemplate) let tabResizableIndicator = tabIndicator?.resizableImageWithCapInsets( UIEdgeInsets(top: 0, left: 2.0, bottom: 0, right: 2.0)) UITabBar.appearance().selectionIndicatorImage = tabResizableIndicator
前面看了那么就,设置barStyle和backgroundImage应该不用在熬诉了吧;做法就和设置UINavigationBar类似。
最后三行代码的意思就是,加载一个指示器图像,并以.AlwaysTemplate设置其渲染模式。这个例子iOS并没有自动使用模板渲染模式。
最后,创建一个可调整大小的图像,并将其设置为标签栏的selectionIndicatorImage。
跑起来。你会看到你的新主题的标签栏:
暗黑主题看起来是不是好多了! :]
看看Tabbar最下面的横线?这是你的指示器图像。虽然它的高只有6点和49点宽像素,iOS会帮你自动延生。
接下来我们讨论调整图像大小,以及它们如何工作。
自定义Segmented Control
现在唯一没发生变化的就是Segmented Control了,接着我们就来搞定它。
打开Theme.swift在applyTheme()底部添加如下代码:
let controlBackground = UIImage(named: "controlBackground")?.imageWithRenderingMode(.AlwaysTemplate).resizableImageWithCapInsets(UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3)) let controlSelectedBackground = UIImage(named: "controlSelectedBackground")?.imageWithRenderingMode(.AlwaysTemplate).resizableImageWithCapInsets(UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3)) UISegmentedControl.appearance().setBackgroundImage(controlBackground, forState: .Normal, barMetrics: .Default) UISegmentedControl.appearance().setBackgroundImage(controlSelectedBackground, forState: .Selected, barMetrics: .Default)
要理解上面的代码,先来看看资源目录里的controlBackground图像。该图像可能比较小,但iOS能明确地知道如何使用它来绘制UISegmentedControl的边界,因为它是被预先切分好来调整大小。
什么是sliced?看看下面的放大模型:
有四个3×3的正方形,一个在每个角落。这些正方形在调整图像大小时原封不动,但水平和垂直方向按要求的灰色像素被拉伸。
在你的素材当中,所有像素为黑色,并承担着你控件的着色。你通过使用UIEdgeInsets()来指导iOS如何从上下左右四个方向拉伸图像。
运行一盘。点击Gear图标,你会看到UISegmentedControl边框有了新的造型:
圆角消失了,被你3x3像素的边角取缔了。
现在你已经有了独特风格的分段控件,接着我们就来搞定剩下的。
关闭设置屏幕的应用,点击右上角的放大镜图标;你会看到另一个已经被你自定义好的分段控件,但是UIStepper,UISlider和UISwitch仍需要添加主题。
接下来我们就来画画吧! :]
自定义Steppers, Sliders和Switches
打开Theme.swift在applyTheme()底部添加如下代码:
UIStepper.appearance().setBackgroundImage(controlBackground, forState: .Normal) UIStepper.appearance().setBackgroundImage(controlBackground, forState: .Disabled) UIStepper.appearance().setBackgroundImage(controlBackground, forState: .Highlighted) UIStepper.appearance().setDecrementImage(UIImage(named: "fewerPaws"), forState: .Normal) UIStepper.appearance().setIncrementImage(UIImage(named: "morePaws"), forState: .Normal)
这里我们用和UISegmentedControl一样的边框来构建UIStepper边框,并且使用素材来自定义它,这样就展示更形象而不是每次都看到一成不变的+``-按钮了。
运行。打开搜索,看看到底发生了变化:
接着继续搞定UISlider和UISwitch。
还是在applyTheme()最后添加如下代码:
UISlider.appearance().setThumbImage(UIImage(named: "sliderThumb"), forState: .Normal) UISlider.appearance().setMaximumTrackImage(UIImage(named: "maximumTrack")?.resizableImageWithCapInsets(UIEdgeInsets(top: 0, left: 0.0, bottom: 0, right: 6.0)), forState: .Normal) UISlider.appearance().setMinimumTrackImage(UIImage(named: "minimumTrack")?.imageWithRenderingMode(.AlwaysTemplate).resizableImageWithCapInsets(UIEdgeInsets(top: 0, left: 6.0, bottom: 0, right: 0)), forState: .Normal) UISwitch.appearance().onTintColor = theme.mainColor.colorWithAlphaComponent(0.3) UISwitch.appearance().thumbTintColor = theme.mainColor
UISlider有三个需要定制的地方:滑块图标,最小指示点的轨迹和最大指示点的轨迹。
滑块图标直接使用项目内的素材,最大指示点的轨迹因为不需要改变颜色,所以使用原来的渲染模式。而最小的则需要制定Template渲染模式,以便跟主题色一致。
UISwitch则是通过你修改thumbTintColor和onTintColor这两个属性来定制。
运行。点击搜索,看看滑块与开关:
正如你看到的UISegmentedControl一样,appearance控制着所有实例。但是某些时候你只是想控制其中某一个 - 在这种情况下,你可以单独自定义。
自定义单一实例
打开SearchTableViewController.swift,在viewDidLoad()内添加如下代码:
speciesSelector.setImage(UIImage(named: "dog"), forSegmentAtIndex: 0) speciesSelector.setImage(UIImage(named: "cat"), forSegmentAtIndex: 1)
在这里,我们只是简单地设置分段控件的每个分段图像。
运行。点击搜索,分段控件就会如下所示:
iOS会自动为分段控件着色,而不需要您额外的做任何事;因为这是Template模式下自动搞定的。
怎么样选择性地改变你控件的字体?这很容易办到。
打开PetTableViewController.swift并在viewWillAppear()内添加如下代码:
view.backgroundColor = ThemeManager.currentTheme().backgroundColor tableView.separatorColor = ThemeManager.currentTheme().secondaryColor
接着在tableView(_:cellForRowAtIndexPath:)方法内,return之前加上下面这句:
cell.textLabel!.font = UIFont(name: "Zapfino", size: 14.0)
这里我们只改变了宠物名字的字体。
运行看一下效果:
我们再来看看前后对比,素不素超级赞:
何去何从
教程就讲到这里,你也可以下载完成的 项目 。
在Objective-C里,你可以指定特定的自定义设置应用到只有当他们包含在特定类的其他控件里。例如,您可以将自定义的UITextField放到UINavigationBar里。
但悲剧的是,swift不能这么干。不过有个好消息是,iOS9将添加此功能,请期待稍后更新本教程。