Commit 77a779df by Daniel Jonathan Committed by GitHub

Merge pull request #1115 from OrkhanAlikhanov/theme

Introducing Theming to Material
parents edab4901 5ee47a0c
......@@ -170,6 +170,7 @@
96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9D00EBB4216675FB00DBCD69 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D00EBB3216675FB00DBCD69 /* Theme.swift */; };
9D054A6520D175AC00D0528D /* Material+UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6320D175AC00D0528D /* Material+UIButton.swift */; };
9D054A6620D175AC00D0528D /* Material+UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6420D175AC00D0528D /* Material+UILabel.swift */; };
9D39A81B20FE8ED100BA8FA1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */; };
......@@ -292,6 +293,7 @@
96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = "<group>"; };
96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = "<group>"; };
96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = "<group>"; };
9D00EBB3216675FB00DBCD69 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
9D054A6320D175AC00D0528D /* Material+UIButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIButton.swift"; sourceTree = "<group>"; };
9D054A6420D175AC00D0528D /* Material+UILabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UILabel.swift"; sourceTree = "<group>"; };
9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
......@@ -567,6 +569,7 @@
963FBF011D6696AB008F8512 /* Tab */,
966ECF2B1CF4C21B00BB0BDF /* Table */,
96090B031D9D709E00709CA6 /* Text */,
9D00EBB2216675A800DBCD69 /* Theme */,
963FBF001D66964F008F8512 /* Toolbar */,
9626CA951DAB5370003E2611 /* Transition */,
96BCB8061CB40FD000C806FE /* Type */,
......@@ -760,6 +763,14 @@
name = Animation;
sourceTree = "<group>";
};
9D00EBB2216675A800DBCD69 /* Theme */ = {
isa = PBXGroup;
children = (
9D00EBB3216675FB00DBCD69 /* Theme.swift */,
);
name = Theme;
sourceTree = "<group>";
};
9DE84D6E1FF0250E00586C8B /* ButtonGroup */ = {
isa = PBXGroup;
children = (
......@@ -1002,6 +1013,7 @@
966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */,
965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */,
9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */,
9D00EBB4216675FB00DBCD69 /* Theme.swift in Sources */,
965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */,
96328B971E05C0BB009A4C90 /* TableView.swift in Sources */,
965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */,
......
......@@ -20,6 +20,7 @@ open class BaseIconLayerButton: Button {
open override var isSelected: Bool {
didSet {
iconLayer.setSelected(isSelected, animated: false)
updatePulseColor()
}
}
......@@ -84,6 +85,7 @@ open class BaseIconLayerButton: Button {
open override func prepare() {
super.prepare()
layer.addSublayer(iconLayer)
iconLayer.prepare()
contentHorizontalAlignment = .left // default was .center
reloadImage()
}
......@@ -122,7 +124,7 @@ open class BaseIconLayerButton: Button {
///
/// This property affects `intrinsicContentSize` and `sizeThatFits(_:)`
/// Use `iconEdgeInsets` to set margins.
open var iconSize: CGFloat = 16 {
open var iconSize: CGFloat = 18 {
didSet {
reloadImage()
}
......@@ -136,12 +138,24 @@ open class BaseIconLayerButton: Button {
///
/// You can use `iconSize` and this property, or `titleEdgeInsets` and `contentEdgeInsets` to position
/// the icon however you want.
/// For negative values, behavior is undefined. Default is `5.0` for all four margins
open var iconEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) {
/// For negative values, behavior is undefined. Default is `8.0` for all four margins
open var iconEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) {
didSet {
reloadImage()
}
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
setIconColor(theme.secondary, for: .selected)
setIconColor(theme.onSurface.withAlphaComponent(0.38), for: .normal)
titleColor = theme.onSurface.withAlphaComponent(0.60)
selectedPulseColor = theme.secondary
normalPulseColor = theme.onSurface
updatePulseColor()
}
/// This might be considered as a hackish way, but it's just manipulation
......@@ -159,6 +173,18 @@ open class BaseIconLayerButton: Button {
UIGraphicsEndImageContext()
self.image = image
}
/// Pulse color for selected state.
open var selectedPulseColor = Color.white
/// Pulse color for normal state.
open var normalPulseColor = Color.white
}
private extension BaseIconLayerButton {
func updatePulseColor() {
pulseColor = isSelected ? selectedPulseColor : normalPulseColor
}
}
// MARK: - BaseIconLayer
......@@ -185,16 +211,6 @@ internal class BaseIconLayer: CALayer {
}
}
override init() {
super.init()
prepare()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepare()
}
func prepare() {
normalColor = { normalColor }() // calling didSet
selectedColor = { selectedColor }() // calling didSet
......
......@@ -51,7 +51,7 @@ private class MaterialTabBar: UITabBar {
}
}
open class BottomNavigationController: UITabBarController {
open class BottomNavigationController: UITabBarController, Themeable {
/// A Boolean that controls if the swipe feature is enabled.
open var isSwipeEnabled = true {
didSet {
......@@ -168,6 +168,21 @@ open class BottomNavigationController: UITabBarController {
prepareTabBar()
isSwipeEnabled = true
isMotionEnabled = true
applyCurrentTheme()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
tabBar.tintColor = theme.secondary
tabBar.barTintColor = theme.background
tabBar.dividerColor = theme.onSurface.withAlphaComponent(0.12)
if #available(iOS 10.0, *) {
tabBar.unselectedItemTintColor = theme.onSurface.withAlphaComponent(0.54)
}
}
}
......
......@@ -31,7 +31,7 @@
import UIKit
import Motion
open class Button: UIButton, Pulseable, PulseableLayer {
open class Button: UIButton, Pulseable, PulseableLayer, Themeable {
/**
A CAShapeLayer used to manage elements that would be affected by
the clipToBounds property of the backing layer. For example, this
......@@ -195,8 +195,8 @@ open class Button: UIButton, Pulseable, PulseableLayer {
*/
public init(image: UIImage?, tintColor: UIColor = Color.blue.base) {
super.init(frame: .zero)
prepare(with: image, tintColor: tintColor)
prepare()
prepare(with: image, tintColor: tintColor)
}
/**
......@@ -206,8 +206,8 @@ open class Button: UIButton, Pulseable, PulseableLayer {
*/
public init(title: String?, titleColor: UIColor = Color.blue.base) {
super.init(frame: .zero)
prepare(with: title, titleColor: titleColor)
prepare()
prepare(with: title, titleColor: titleColor)
}
open override func layoutSubviews() {
......@@ -281,7 +281,14 @@ open class Button: UIButton, Pulseable, PulseableLayer {
contentScaleFactor = Screen.scale
prepareVisualLayer()
preparePulse()
applyCurrentTheme()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) { }
}
extension Button {
......
......@@ -31,6 +31,12 @@ open class CheckButton: BaseIconLayerButton {
guard !isAnimating else { return }
setSelected(!isSelected, animated: true)
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
checkmarkColor = theme.onSecondary
}
}
internal class CheckBoxLayer: BaseIconLayer {
......@@ -86,6 +92,7 @@ internal class CheckBoxLayer: BaseIconLayer {
if isSelected {
borderLayer.borderWidth = borderLayerNormalBorderWidth
} else {
borderLayer.borderWidth = 0
borderLayer.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor
checkMarkLeftLayer.strokeEnd = 1
checkMarkRightLayer.strokeEnd = 1
......
......@@ -35,7 +35,7 @@ public enum EditorPlaceholderAnimation {
case hidden
}
open class Editor: View {
open class Editor: View, Themeable {
/// Reference to textView.
public let textView = TextView()
......@@ -173,11 +173,14 @@ open class Editor: View {
open override func prepare() {
super.prepare()
backgroundColor = nil
prepareDivider()
prepareTextView()
preparePlaceholderLabel()
prepareDetailLabel()
prepareNotificationHandlers()
applyCurrentTheme()
}
open override func layoutSubviews() {
......@@ -187,6 +190,21 @@ open class Editor: View {
layoutBottomLabel(label: detailLabel, verticalOffset: detailVerticalOffset)
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
placeholderActiveColor = theme.secondary
placeholderNormalColor = theme.onSurface.withAlphaComponent(0.38)
dividerActiveColor = theme.secondary
dividerNormalColor = theme.onSurface.withAlphaComponent(0.12)
detailColor = theme.onSurface.withAlphaComponent(0.38)
textView.tintColor = theme.secondary
}
@discardableResult
open override func becomeFirstResponder() -> Bool {
return textView.becomeFirstResponder()
......@@ -196,15 +214,6 @@ open class Editor: View {
open override func resignFirstResponder() -> Bool {
return textView.resignFirstResponder()
}
open override var inputAccessoryView: UIView? {
get {
return textView.inputAccessoryView
}
set(value) {
textView.inputAccessoryView = value
}
}
}
......
......@@ -93,4 +93,10 @@ open class ErrorTextField: TextField {
super.layoutSubviews()
layoutBottomLabel(label: errorLabel, verticalOffset: errorVerticalOffset)
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
errorColor = theme.error
}
}
......@@ -36,6 +36,14 @@ open class FABButton: Button {
depthPreset = .depth1
shapePreset = .circle
pulseAnimation = .centerWithBacking
backgroundColor = .white
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
backgroundColor = theme.secondary
titleColor = theme.onSecondary
tintColor = theme.onSecondary
pulseColor = theme.onSecondary
}
}
......@@ -35,4 +35,13 @@ open class FlatButton: Button {
super.prepare()
cornerRadiusPreset = .cornerRadius1
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
backgroundColor = .clear
titleColor = theme.secondary
tintColor = theme.secondary
pulseColor = theme.secondary
}
}
......@@ -30,9 +30,33 @@
import UIKit
public enum IconButtonThemingStyle {
/// Theming when background content is in background color.
case onBackground
/// Theming when background content is in primary color.
case onPrimary
}
open class IconButton: Button {
/// A reference to IconButtonThemingStyle.
open var themingStyle = IconButtonThemingStyle.onBackground
open override func prepare() {
super.prepare()
pulseAnimation = .center
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
switch themingStyle {
case .onBackground:
tintColor = theme.secondary
pulseColor = theme.secondary
case .onPrimary:
tintColor = theme.onPrimary
pulseColor = theme.onPrimary
}
}
}
......@@ -62,3 +62,66 @@ public extension UIColor {
self.init(argb: (0xff000000 as UInt32) | rgb)
}
}
internal extension UIColor {
/// A tuple of the rgba components.
var components: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
}
/**
Blends given coverColor over this color.
- Parameter with coverColor: A UIColor.
- Returns: Resultant color of blending.
*/
func blend(with coverColor: UIColor) -> UIColor {
/// Blends channels according to https://en.wikipedia.org/wiki/Alpha_compositing (see `over` operator).
func blendChannel(value: CGFloat, bValue: CGFloat, alpha: CGFloat, bAlpha: CGFloat) -> CGFloat {
return ((1 - alpha) * bValue * bAlpha + alpha * value) / (alpha + bAlpha * (1 - alpha))
}
let (r, g, b, a) = coverColor.components
let (bR, bG, bB, bA) = components
let newR = blendChannel(value: r, bValue: bR, alpha: a, bAlpha: bA)
let newG = blendChannel(value: g, bValue: bG, alpha: a, bAlpha: bA)
let newB = blendChannel(value: b, bValue: bB, alpha: a, bAlpha: bA)
let newA = a + bA * (1 - a)
return UIColor(red: newR, green: newG, blue: newB, alpha: newA)
}
/**
Adjusts brightness of the color by given value.
- Parameter by value: A CGFloat value.
- Returns: Adjusted color.
*/
func adjustingBrightness(by value: CGFloat) -> UIColor {
var h: CGFloat = 0
var s: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
getHue(&h, saturation: &s, brightness: &b, alpha: &a)
return UIColor(hue: h, saturation: s, brightness: (b + value).clamp(0, 1), alpha: 1)
}
/// A lighter version of the color.
var lighter: UIColor {
return adjustingBrightness(by: 0.1)
}
/// A darker version of the color.
var darker: UIColor {
return adjustingBrightness(by: -0.1)
}
}
......@@ -30,7 +30,7 @@
import UIKit
open class NavigationBar: UINavigationBar {
open class NavigationBar: UINavigationBar, Themeable {
/// Will layout the view.
open var willLayout: Bool {
return 0 < bounds.width && 0 < bounds.height && nil != superview
......@@ -168,7 +168,28 @@ open class NavigationBar: UINavigationBar {
let image = UIImage()
shadowImage = image
setBackgroundImage(image, for: .default)
backgroundColor = .white
applyCurrentTheme()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
backgroundColor = theme.primary
items?.forEach {
apply(theme: theme, to: $0)
}
}
/**
Applies the given theme to the navigation item.
- Parameter theme: A Theme.
- Parameter to item: A UINavigationItem.
*/
private func apply(theme: Theme, to item: UINavigationItem) {
Theme.apply(theme: theme, to: item.toolbar)
item.toolbar.backgroundColor = .clear
}
}
......@@ -182,8 +203,11 @@ internal extension NavigationBar {
return
}
if isThemingEnabled {
apply(theme: .current, to: item)
}
let toolbar = item.toolbar
toolbar.backgroundColor = .clear
toolbar.interimSpace = interimSpace
toolbar.contentEdgeInsets = contentEdgeInsets
......
......@@ -35,6 +35,14 @@ open class RaisedButton: Button {
super.prepare()
depthPreset = .depth1
cornerRadiusPreset = .cornerRadius1
backgroundColor = .white
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
backgroundColor = theme.secondary
titleColor = theme.onSecondary
pulseColor = theme.onSecondary
tintColor = theme.onSecondary
}
}
......@@ -120,6 +120,12 @@ open class StatusBarController: TransitionController {
super.prepare()
prepareStatusBar()
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
statusBar.backgroundColor = theme.primary.darker
}
}
fileprivate extension StatusBarController {
......
......@@ -30,12 +30,6 @@
import UIKit
@objc(SwitchStyle)
public enum SwitchStyle: Int {
case light
case dark
}
@objc(SwitchState)
public enum SwitchState: Int {
case on
......@@ -59,7 +53,7 @@ public protocol SwitchDelegate {
func switchDidChangeState(control: Switch, state: SwitchState)
}
open class Switch: UIControl {
open class Switch: UIControl, Themeable {
/// Will layout the view.
open var willLayout: Bool {
return 0 < bounds.width && 0 < bounds.height && nil != superview
......@@ -200,32 +194,6 @@ open class Switch: UIControl {
}
}
/// Switch style.
open var switchStyle = SwitchStyle.dark {
didSet {
switch switchStyle {
case .light:
buttonOnColor = Color.blue.darken2
trackOnColor = Color.blue.lighten3
buttonOffColor = Color.blueGrey.lighten4
trackOffColor = Color.grey.lighten2
buttonOnDisabledColor = Color.grey.lighten2
trackOnDisabledColor = Color.grey.lighten2
buttonOffDisabledColor = Color.grey.lighten2
trackOffDisabledColor = Color.grey.lighten2
case .dark:
buttonOnColor = Color.blue.lighten1
trackOnColor = Color.blue.lighten2.withAlphaComponent(0.5)
buttonOffColor = Color.grey.lighten2
trackOffColor = Color.blueGrey.lighten4.withAlphaComponent(0.5)
buttonOnDisabledColor = Color.grey.darken3
trackOnDisabledColor = Color.grey.lighten1.withAlphaComponent(0.2)
buttonOffDisabledColor = Color.grey.darken3
trackOffDisabledColor = Color.grey.lighten1.withAlphaComponent(0.2)
}
}
}
/// Switch size.
open var switchSize = SwitchSize.medium {
didSet {
......@@ -287,13 +255,12 @@ open class Switch: UIControl {
- Parameter style: A SwitchStyle value.
- Parameter size: A SwitchSize value.
*/
public init(state: SwitchState = .off, style: SwitchStyle = .dark, size: SwitchSize = .medium) {
public init(state: SwitchState = .off, size: SwitchSize = .medium) {
track = UIView()
button = FABButton()
super.init(frame: .zero)
prepare()
prepareSwitchState(state: state)
prepareSwitchStyle(style: style)
prepareSwitchSize(size: size)
}
......@@ -356,8 +323,24 @@ open class Switch: UIControl {
prepareTrack()
prepareButton()
prepareSwitchState()
prepareSwitchStyle()
prepareSwitchSize()
applyCurrentTheme()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
buttonOnColor = theme.secondary
trackOnColor = theme.secondary.withAlphaComponent(0.60)
buttonOffColor = theme.surface.blend(with: theme.onSurface.withAlphaComponent(0.15).blend(with: theme.secondary.withAlphaComponent(0.06)))
trackOffColor = theme.onSurface.withAlphaComponent(0.12)
buttonOnDisabledColor = theme.surface.blend(with: theme.onSurface.withAlphaComponent(0.15))
trackOnDisabledColor = theme.onSurface.withAlphaComponent(0.15)
buttonOffDisabledColor = buttonOnDisabledColor
trackOffDisabledColor = trackOnDisabledColor
}
}
......@@ -565,15 +548,6 @@ fileprivate extension Switch {
}
/**
Prepares the switchStyle property. This is used mainly to allow
init to set the state value and have an effect.
- Parameter style: The SwitchStyle to set.
*/
func prepareSwitchStyle(style: SwitchStyle = .light) {
switchStyle = style
}
/**
Prepares the switchSize property. This is used mainly to allow
init to set the size value and have an effect.
- Parameter size: The SwitchSize to set.
......
......@@ -614,6 +614,7 @@ internal extension TabBar {
*/
func finishLineTransition(isAnimated: Bool = true) {
line.motionViewTransition.finish(isAnimated: isAnimated)
line.transition([])
}
/**
......@@ -622,6 +623,7 @@ internal extension TabBar {
*/
func cancelLineTransition(isAnimated: Bool = true) {
line.motionViewTransition.cancel(isAnimated: isAnimated)
line.transition([])
}
}
......
......@@ -39,6 +39,12 @@ public enum TabBarAlignment: Int {
case bottom
}
public enum TabBarThemingStyle {
case auto
case primary
case secondary
}
extension UIViewController {
/// TabItem reference.
@objc
......@@ -161,10 +167,14 @@ open class TabsController: TransitionController {
/// The tabBar alignment.
open var tabBarAlignment = TabBarAlignment.bottom {
didSet {
updateTabBarAlignment()
layoutSubviews()
}
}
/// The tabBar theming style.
open var tabBarThemingStyle = TabBarThemingStyle.auto
/**
A UIPanGestureRecognizer property internally used for the interactive
swipe.
......@@ -218,11 +228,55 @@ open class TabsController: TransitionController {
prepareTabBar()
prepareTabItems()
prepareSelectedIndexViewController()
applyCurrentTheme()
}
open override func transition(to viewController: UIViewController, completion: ((Bool) -> Void)?) {
transition(to: viewController, isTriggeredByUserInteraction: false, completion: completion)
}
open override func apply(theme: Theme) {
super.apply(theme: theme)
switch tabBarThemingStyle {
case .auto where (parent is NavigationController || parent is ToolbarController) && tabBarAlignment == .top:
fallthrough
case .primary:
applyPrimary(theme: theme)
default:
applySecondary(theme: theme)
}
}
}
private extension TabsController {
/**
Applies theming taking primary color as base.
- Parameter theme: A Theme
*/
func applyPrimary(theme: Theme) {
tabBar.lineColor = theme.onPrimary.withAlphaComponent(0.68)
tabBar.backgroundColor = theme.primary
tabBar.setTabItemsColor(theme.onPrimary, for: .normal)
tabBar.setTabItemsColor(theme.onPrimary, for: .selected)
tabBar.setTabItemsColor(theme.onPrimary, for: .highlighted)
tabBar.isDividerHidden = true
}
/**
Applies theming taking secondary color as base.
- Parameter theme: A Theme
*/
func applySecondary(theme: Theme) {
tabBar.lineColor = theme.secondary
tabBar.backgroundColor = theme.background
tabBar.setTabItemsColor(theme.onSurface.withAlphaComponent(0.60), for: .normal)
tabBar.setTabItemsColor(theme.secondary, for: .selected)
tabBar.setTabItemsColor(theme.secondary, for: .highlighted)
tabBar.dividerColor = theme.onSurface.withAlphaComponent(0.12)
}
}
fileprivate extension TabsController {
......@@ -273,9 +327,14 @@ fileprivate extension TabsController {
/// Prepares the TabBar.
func prepareTabBar() {
tabBar.lineAlignment = .bottom == tabBarAlignment ? .top : .bottom
tabBar._delegate = self
view.addSubview(tabBar)
updateTabBarAlignment()
}
func updateTabBarAlignment() {
tabBar.lineAlignment = .bottom == tabBarAlignment ? .top : .bottom
tabBar.dividerAlignment = .bottom == tabBarAlignment ? .top : .bottom
}
/// Prepares the `tabBar.tabItems`.
......
......@@ -63,7 +63,7 @@ public protocol TextFieldDelegate: UITextFieldDelegate {
optional func textField(textField: TextField, didClear text: String?)
}
open class TextField: UITextField {
open class TextField: UITextField, Themeable {
/// Minimum TextField text height.
private let minimumTextHeight: CGFloat = 32
......@@ -459,6 +459,25 @@ open class TextField: UITextField {
prepareTargetHandlers()
prepareTextAlignment()
prepareRightView()
applyCurrentTheme()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
placeholderActiveColor = theme.secondary
placeholderNormalColor = theme.onSurface.withAlphaComponent(0.38)
leftViewActiveColor = theme.secondary
leftViewNormalColor = theme.onSurface.withAlphaComponent(0.38)
dividerActiveColor = theme.secondary
dividerNormalColor = theme.onSurface.withAlphaComponent(0.12)
detailColor = theme.onSurface.withAlphaComponent(0.38)
textColor = theme.onSurface.withAlphaComponent(0.87)
}
}
......
......@@ -87,7 +87,7 @@ public protocol TextViewDelegate : UITextViewDelegate {
optional func textView(textView: TextView, didProcessEditing textStorage: TextStorage, text: String, range: NSRange)
}
open class TextView: UITextView {
open class TextView: UITextView, Themeable {
/// A boolean indicating whether the text is empty.
open var isEmpty: Bool {
return 0 == text?.utf16.count
......@@ -289,6 +289,7 @@ open class TextView: UITextView {
prepareNotificationHandlers()
prepareRegularExpression()
preparePlaceholderLabel()
applyCurrentTheme()
}
open override var contentSize: CGSize {
......@@ -354,6 +355,15 @@ open class TextView: UITextView {
super.paste(sender)
fixTypingFont()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
textColor = theme.onSurface.withAlphaComponent(0.87)
placeholderColor = theme.onSurface.withAlphaComponent(0.38)
}
}
fileprivate extension TextView {
......
/*
* Copyright (C) 2015 - 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of CosmicMind nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import UIKit
import Motion
public protocol Themeable: class {
/**
Applies given theme.
- Parameter theme: A Theme.
*/
func apply(theme: Theme)
/// A boolean indicating if theming is enabled.
var isThemingEnabled: Bool { get set }
}
public struct Theme: Hashable {
/// The color displayed most frequently across the app.
public var primary = Color.blue.darken2
/// Accent color for some components such as FABMenu.
public var secondary = Color.blue.base
/// Background color for view controllers and some components.
public var background = Color.white
/// Background color for components such as cards, and dialogs.
public var surface = Color.white
/// Error color for components such as ErrorTextField.
public var error = Color.red.base
/// Text and iconography color to be used on primary color.
public var onPrimary = Color.white
/// Text and iconography color to be used on secondary color.
public var onSecondary = Color.white
/// Text and iconography color to be used on background color.
public var onBackground = Color.black
/// Text and iconography color to be used on surface color.
public var onSurface = Color.black
/// Text and iconography color to be used on error color.
public var onError = Color.white
/// An initializer.
public init() { }
}
public extension Theme {
/// Current theme for Material.
static private(set) var current = Theme.light
/// A light theme.
static var light = Theme()
/// A dark theme.
static var dark: Theme = {
var t = Theme()
t.primary = UIColor(rgb: 0x202020)
t.secondary = Color.teal.base
t.background = UIColor(rgb: 0x303030)
t.surface = t.background
t.onBackground = .white
t.onSurface = .white
return t
}()
}
public extension Theme {
/**
Applies theme to the entire app.
- Parameter theme: A Theme.
*/
static func apply(theme: Theme) {
current = theme
guard let v = Application.rootViewController else {
return
}
apply(theme: theme, to: v)
}
/**
Applies theme to the hierarchy of given view.
- Parameter theme: A Theme.
- Parameter to view: A UIView.
*/
static func apply(theme: Theme, to view: UIView) {
guard !((view as? Themeable)?.isThemingEnabled == false), !view.isProcessed else {
return
}
view.subviews.forEach {
apply(theme: theme, to: $0)
}
(view as? Themeable)?.apply(theme: theme)
}
/**
Applies theme to the hierarchy of given view controller.
- Parameter theme: A Theme.
- Parameter to viewController: A UIViewController.
*/
static func apply(theme: Theme, to viewController: UIViewController) {
guard !((viewController as? Themeable)?.isThemingEnabled == false) else {
return
}
viewController.allChildren.forEach {
apply(theme: theme, to: $0)
$0.view.isProcessed = true
}
apply(theme: theme, to: viewController.view)
viewController.allChildren.forEach {
$0.view.isProcessed = false
}
(viewController as? Themeable)?.apply(theme: theme)
}
/**
Applies provided theme for the components created within the given block
without chaging app's theme.
- Parameter theme: A Theme.
- Parameter for block: A code block.
*/
static func applying(theme: Theme, for execute: () -> Void) {
let v = current
current = theme
execute()
current = v
}
}
/// A memory reference to the isThemingEnabled for Themeable NSObject subclasses.
private var IsThemingEnabledKey: UInt8 = 0
public extension Themeable where Self: NSObject {
/// A boolean indicating if theming is enabled.
var isThemingEnabled: Bool {
get {
return AssociatedObject.get(base: self, key: &IsThemingEnabledKey) {
true
}
}
set(value) {
AssociatedObject.set(base: self, key: &IsThemingEnabledKey, value: value)
}
}
/// Applies current theme to itself if theming is enabled.
internal func applyCurrentTheme() {
guard isThemingEnabled else {
return
}
apply(theme: .current)
}
}
private extension UIViewController {
/// Returns all possible child view controllers.
var allChildren: [UIViewController] {
var all = children
if let v = self as? TabsController {
all += v.viewControllers
}
if let v = presentedViewController, v.presentingViewController === self {
all.append(v)
}
return all
}
}
/// A memory reference to the isProcessed for UIView.
private var IsProcessedKey: UInt8 = 0
private extension UIView {
/// A boolean indicating if view is already themed.
var isProcessed: Bool {
get {
return AssociatedObject.get(base: self, key: &IsProcessedKey) {
false
}
}
set(value) {
AssociatedObject.set(base: self, key: &IsProcessedKey, value: value)
}
}
}
......@@ -30,7 +30,7 @@
import UIKit
open class Toolbar: Bar {
open class Toolbar: Bar, Themeable {
/// A convenience property to set the titleLabel.text.
@IBInspectable
open var title: String? {
......@@ -63,6 +63,24 @@ open class Toolbar: Bar {
@IBInspectable
public let detailLabel = UILabel()
open override var leftViews: [UIView] {
didSet {
prepareIconButtons(leftViews)
}
}
open override var centerViews: [UIView] {
didSet {
prepareIconButtons(centerViews)
}
}
open override var rightViews: [UIView] {
didSet {
prepareIconButtons(rightViews)
}
}
/**
An initializer that initializes the object with a NSCoder object.
- Parameter aDecoder: A NSCoder instance.
......@@ -129,6 +147,29 @@ open class Toolbar: Bar {
prepareDetailLabel()
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
backgroundColor = theme.primary
(leftViews + rightViews + centerViews).forEach {
guard let v = $0 as? IconButton, v.isThemingEnabled else {
return
}
v.apply(theme: theme)
}
if !((titleLabel as? Themeable)?.isThemingEnabled == false) {
titleLabel.textColor = theme.onPrimary
}
if !((detailLabel as? Themeable)?.isThemingEnabled == false) {
detailLabel.textColor = theme.onPrimary
}
}
/// A reference to titleLabel.textAlignment observation.
private var titleLabelTextAlignmentObserver: NSKeyValueObservation!
}
......@@ -152,4 +193,12 @@ private extension Toolbar {
detailLabel.font = RobotoFont.regular(with: 12)
detailLabel.textColor = Color.darkText.secondary
}
func prepareIconButtons(_ views: [UIView]) {
views.forEach {
($0 as? IconButton)?.themingStyle = .onPrimary
}
applyCurrentTheme()
}
}
......@@ -30,7 +30,7 @@
import UIKit
open class ViewController: UIViewController {
open class ViewController: UIViewController, Themeable {
open override func viewDidLoad() {
super.viewDidLoad()
prepare()
......@@ -45,8 +45,8 @@ open class ViewController: UIViewController {
*/
open func prepare() {
view.clipsToBounds = true
view.backgroundColor = .white
view.contentScaleFactor = Screen.scale
applyCurrentTheme()
}
open override func viewWillLayoutSubviews() {
......@@ -61,4 +61,12 @@ open class ViewController: UIViewController {
have a certain need.
*/
open func layoutSubviews() { }
/**
Applies given theme to the view controller.
- Parameter theme: A Theme.
*/
open func apply(theme: Theme) {
view.backgroundColor = theme.background
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment