Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Example-Swift/Tests/FeaturesInteractorSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class FeatureInteractorSpec: QuickSpec {
describe("Feature Registry") {

it("Initializes properly") {
let presenter = FeaturesPresenter(withFeatures: [])
let presenter = FeaturesPresenter(withFeatures: [],
colorProvider: DefaultFeatureStateColorProvider())
let interactor = FeaturesInteractor(withPresenter: presenter)

expect(interactor).to(be(interactor))
Expand All @@ -48,7 +49,8 @@ class FeatureInteractorSpec: QuickSpec {
let registry = TestRegistry(withFeatureStore: nil)

context("shouldHighlightRowAt") {
let presenter = FeaturesPresenter(withFeatures: registry.featureItems)
let presenter = FeaturesPresenter(withFeatures: registry.featureItems,
colorProvider: DefaultFeatureStateColorProvider())
let interactor = FeaturesInteractor(withPresenter: presenter)

it("false for cells") {
Expand All @@ -70,7 +72,8 @@ class FeatureInteractorSpec: QuickSpec {
var output: UIViewController!

beforeEach {
presenter = FeaturesPresenter(withFeatures: registry.featureItems)
presenter = FeaturesPresenter(withFeatures: registry.featureItems,
colorProvider: DefaultFeatureStateColorProvider())
interactor = FeaturesInteractor(withPresenter: presenter)
testTableView = TestTableView(frame: .zero, style: .plain)
}
Expand Down Expand Up @@ -137,7 +140,8 @@ class FeatureInteractorIosSpec: QuickSpec {
let registry = TestRegistry(withFeatureStore: nil)
var features = registry.featureItems
features.append(CustomFeature())
let presenter = FeaturesPresenter(withFeatures: features)
let presenter = FeaturesPresenter(withFeatures: features,
colorProvider: DefaultFeatureStateColorProvider())
let interactor = FeaturesInteractor(withPresenter: presenter)

it("does not configure swipe for groups") {
Expand Down
74 changes: 71 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,14 +222,82 @@ Now using `FConfigFeature` in our feature registry is just more of the same:
Override ships with a simple table view controller – `FeaturesViewController` – that provides a generic user interface for managing feature flags. The view controller shows a list of available features from a given `FeatureRegistry`. It also allows you to find features using text search. Each feature state is depicted visually, and swipe gestures are installed that allow for convienant feature control.

Feature state is conveyed visually:
- Overridden *enabled* features are in green
- Overridden *disabled* features are shown in red
- Underlined features – which are either green or red – are overriding the defaults
- Overridden *enabled* features are in green (or custom enabled color if a color provider is specified)
- Overridden *disabled* features are shown in red (or custom disabled color if a color provider is specified)
- Underlined features – which are either green or redare overriding the defaults

Feature state is controlled by gesture:
- Slide left to reveal the "on" and "off" force overrides
- Slide right to restore the default state of the feature

#### Customizing Feature State Colors

You can customize the colors used for enabled and disabled feature states by providing a `FeatureStateColorProvider`. This is particularly useful for visually distinguishing between different types of feature flags (e.g., server-side vs client-side flags).

**Using a Closure Provider (Swift):**

```swift
let colorProvider = ClosureFeatureStateColorProvider { feature in
// Distinguish server-side vs client-side flags
if feature.key?.hasPrefix("server_") == true {
return FeatureStateColors(enabledColor: .blue, disabledColor: .orange)
} else if feature.key?.hasPrefix("client_") == true {
return FeatureStateColors(enabledColor: .green, disabledColor: .red)
} else {
return FeatureStateColors.defaultStateColors
}
}

let viewController = FeaturesTableViewController(
features: registry.features,
colorProvider: colorProvider
)
```

**Custom Implementation (Swift):**

```swift
class MyColorProvider: NSObject, FeatureStateColorProvider {
func colors(for feature: AnyFeature) -> FeatureStateColors {
// Custom logic based on feature properties
if feature.requiresRestart {
return FeatureStateColors(enabledColor: .systemYellow, disabledColor: .systemRed)
}
return FeatureStateColors.defaultStateColors
}
}

let viewController = FeaturesTableViewController(
features: registry.features,
colorProvider: MyColorProvider()
)
```

**Objective-C:**

```objc
@interface MyColorProvider : NSObject <FeatureStateColorProvider>
@end

@implementation MyColorProvider
- (FeatureStateColors *)colorsForFeature:(id<AnyFeature>)feature {
if ([feature.key hasPrefix:@"server_"]) {
return [[FeatureStateColors alloc] initWithEnabledColor:[UIColor blueColor]
disabledColor:[UIColor orangeColor]];
}
return FeatureStateColors.defaultStateColors;
}
@end

MyColorProvider *provider = [[MyColorProvider alloc] init];
FeaturesTableViewController *vc = [[FeaturesTableViewController alloc]
initWithFeatures:registry.features
colorProvider:provider];
```


If no color provider is specified, the default colors (green for enabled, red for disabled) are used.

#### Support "Restart Required" Features

Sometimes enabling or disabling a feature is unsafe or impractical to do after the app has finished loading.
Expand Down
56 changes: 29 additions & 27 deletions Source/UI/Cells/FeatureSwitchCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,7 @@ class FeatureSwitchCell: FeatureTableViewCell {
let underlineStyleSingle = NSUnderlineStyle.styleSingle
#endif

var labeledFeature: LabeledFeatureItem? {
didSet {
guard let feature = labeledFeature?.feature,
let label = labeledFeature?.label,
let textLabel = textLabel
else { return }

let labelColor = feature.enabled ? UIColor.mulah : UIColor.swedishFish

let labelString = NSMutableAttributedString(string: label.unCamelCased)
var attrs: [AttributedStringKey: Any] = [
AttributedStringKey.font: textLabel.font as UIFont,
AttributedStringKey.foregroundColor: labelColor
]

// add emphasis if this is locally overridden by underlining the label
if feature.override != .featureDefault {
attrs[AttributedStringKey.underlineStyle] = underlineStyleSingle.rawValue
attrs[AttributedStringKey.underlineColor] = labelColor
}
labelString.addAttributes(attrs, range: NSRange(location: 0, length: labelString.length))

textLabel.attributedText = labelString

self.setNeedsLayout()
}
}
private var labeledFeature: LabeledFeatureItem?

var featurePath: [LabeledGroupItem]? {
didSet {
Expand Down Expand Up @@ -78,4 +52,32 @@ class FeatureSwitchCell: FeatureTableViewCell {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func configure(with labeledFeature: LabeledFeatureItem?,
colorProvider: FeatureStateColorProvider) {
guard let feature = labeledFeature?.feature,
let label = labeledFeature?.label,
let textLabel = textLabel
else { return }

let colors = colorProvider.colors(for: feature)
let labelColor = feature.enabled ? colors.enabledColor : colors.disabledColor

let labelString = NSMutableAttributedString(string: label.unCamelCased)
var attrs: [AttributedStringKey: Any] = [
AttributedStringKey.font: textLabel.font as UIFont,
AttributedStringKey.foregroundColor: labelColor
]

// add emphasis if this is locally overridden by underlining the label
if feature.override != .featureDefault {
attrs[AttributedStringKey.underlineStyle] = underlineStyleSingle.rawValue
attrs[AttributedStringKey.underlineColor] = labelColor
}
labelString.addAttributes(attrs, range: NSRange(location: 0, length: labelString.length))

textLabel.attributedText = labelString

self.setNeedsLayout()
}
}
56 changes: 56 additions & 0 deletions Source/UI/FeatureStateColorProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2019, Oath Inc.
// Licensed under the terms of the MIT license. See LICENSE file in https://github.com/yahoo/Override for terms.

import Foundation
import UIKit

/// A container for feature state colors
@objc public class FeatureStateColors: NSObject {
@objc public let enabledColor: UIColor
@objc public let disabledColor: UIColor

@objc public init(enabledColor: UIColor, disabledColor: UIColor) {
self.enabledColor = enabledColor
self.disabledColor = disabledColor
super.init()
}

@objc public static let defaultStateColors: FeatureStateColors = {
FeatureStateColors(enabledColor: .mulah, disabledColor: .swedishFish)
}()
}

/// A protocol for providing colors based on feature state.
/// Implement this protocol to customize colors for enabled and disabled feature states.
@objc public protocol FeatureStateColorProvider: NSObjectProtocol {
/// Returns the colors to use for a given feature's enabled and disabled states.
///
/// - Parameter feature: The feature to resolve colors for
/// - Returns: A FeatureStateColors object containing the enabled color and disabled color
func colors(for feature: AnyFeature) -> FeatureStateColors
}

/// Default implementation that uses the standard mulah (green) and swedishFish (red) colors.
@objc public class DefaultFeatureStateColorProvider: NSObject, FeatureStateColorProvider {
@objc public override init() {
super.init()
}

@objc public func colors(for feature: AnyFeature) -> FeatureStateColors {
FeatureStateColors.defaultStateColors
}
}

/// Adapter that allows using a closure as a FeatureStateColorProvider
public class ClosureFeatureStateColorProvider: NSObject, FeatureStateColorProvider {
private let resolver: (AnyFeature) -> FeatureStateColors

public init(_ resolver: @escaping (AnyFeature) -> FeatureStateColors) {
self.resolver = resolver
super.init()
}

public func colors(for feature: AnyFeature) -> FeatureStateColors {
return resolver(feature)
}
}
9 changes: 6 additions & 3 deletions Source/UI/FeaturesInteractor_iOS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ extension FeaturesInteractor { /* UITableViewDelegate */
completion: callback)
}

action.backgroundColor = labeledFeature.feature.defaultState ? UIColor.mulah : UIColor.swedishFish
let colors = presenter.colorProvider.colors(for: labeledFeature.feature)
let backgroundColor = labeledFeature.feature.defaultState ? colors.enabledColor : colors.disabledColor
action.backgroundColor = backgroundColor
return UISwipeActionsConfiguration(actions: [action])
}

Expand Down Expand Up @@ -64,8 +66,9 @@ extension FeaturesInteractor { /* UITableViewDelegate */
completion: callback)
}

enableAction.backgroundColor = UIColor.mulah
disableAction.backgroundColor = UIColor.swedishFish
let colors = presenter.colorProvider.colors(for: labeledFeature.feature)
enableAction.backgroundColor = colors.enabledColor
disableAction.backgroundColor = colors.disabledColor

var actions: [UIContextualAction]
switch labeledFeature.feature.override {
Expand Down
15 changes: 11 additions & 4 deletions Source/UI/FeaturesPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ class FeaturesPresenter: NSObject, UITableViewDataSource {

var features: [LabeledItem] { filteredFeatures ?? allFeatures }

let colorProvider: FeatureStateColorProvider

private let allFeatures: [LabeledItem]
private var filteredFeatures: [LabeledSearchResultItem]?

init(withFeatures features: [LabeledItem]) {
init(withFeatures features: [LabeledItem],
colorProvider: FeatureStateColorProvider) {
self.allFeatures = features
self.colorProvider = colorProvider
}

/// If the feature require restart (see `featuresRequiringRestart`), presents
Expand Down Expand Up @@ -77,7 +81,8 @@ extension FeaturesPresenter { /* UITableViewDataSource */
let cellID = FeaturesTableViewController.FeatureCellIdentifier.switchCell.rawValue
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
if let cell = cell as? FeatureSwitchCell {
cell.labeledFeature = item.result
cell.configure(with: item.result,
colorProvider: colorProvider)
cell.featurePath = item.groupStack
}
return cell
Expand All @@ -87,7 +92,8 @@ extension FeaturesPresenter { /* UITableViewDataSource */
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)

if let cell = cell as? FeatureSwitchCell {
cell.labeledFeature = labeledItem as? LabeledFeatureItem
cell.configure(with: labeledItem as? LabeledFeatureItem,
colorProvider: colorProvider)
}
return cell
}
Expand Down Expand Up @@ -140,7 +146,8 @@ extension FeaturesPresenter { /* UITableViewController Support Methods */

func present(_ tableView: UITableView, groupAtIndexPath indexPath: IndexPath) {
guard let labeledGroup = features[indexPath.row] as? LabeledGroupItem else { return }
let groupTableViewController = FeaturesTableViewController(features: labeledGroup)
let groupTableViewController = FeaturesTableViewController(features: labeledGroup,
colorProvider: colorProvider)

if let navController = output?.navigationController {
navController.pushViewController(groupTableViewController, animated: true)
Expand Down
25 changes: 17 additions & 8 deletions Source/UI/FeaturesTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ import UIKit

let interactor: FeaturesInteractor

init<C: Collection>(features: C) where C.Element == LabeledItem {
let colorProvider: FeatureStateColorProvider

init<C: Collection>(features: C,
colorProvider: FeatureStateColorProvider? = nil) where C.Element == LabeledItem {
let arrayFeatures = Array(features)

presenter = FeaturesPresenter(withFeatures: arrayFeatures)
self.colorProvider = colorProvider ?? DefaultFeatureStateColorProvider()
presenter = FeaturesPresenter(withFeatures: arrayFeatures,
colorProvider: self.colorProvider)
interactor = FeaturesInteractor(withPresenter: presenter)

super.init(style: UITableView.Style.plain)
Expand Down Expand Up @@ -91,9 +96,11 @@ extension FeaturesTableViewController {
/// allows use of this table view controller without the navigation
/// controller provided by FeaturesViewController.
///
/// - Parameter featureRegistry: The feature registry to use
convenience init(featureRegistry: FeatureRegistry) {
self.init(features: featureRegistry.features)
/// - Parameters:
/// - featureRegistry: The feature registry to use
/// - colorProvider: Optional color provider for customizing feature state colors
convenience init(featureRegistry: FeatureRegistry, colorProvider: FeatureStateColorProvider? = nil) {
self.init(features: featureRegistry.features, colorProvider: colorProvider)
}
}

Expand All @@ -103,8 +110,10 @@ public extension FeaturesTableViewController {
/// allows use of this table view controller without the navigation
/// controller provided by FeaturesViewController.
///
/// - Parameter featureRegistry: The feature registry to use
convenience init(registry: FeatureRegistry) {
self.init(features: registry.features)
/// - Parameters:
/// - registry: The feature registry to use
/// - colorProvider: Optional color provider for customizing feature state colors
convenience init(registry: FeatureRegistry, colorProvider: FeatureStateColorProvider? = nil) {
self.init(features: registry.features, colorProvider: colorProvider)
}
}
Loading