Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Flag Tools πŸ”¨ #705

Merged
merged 19 commits into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 0 additions & 1 deletion Kickstarter-iOS/Library/Storyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ public enum Storyboard: String {
case Activity
case Backing
case BackerDashboard
case BetaTools
case Checkout
case Comments
case Dashboard
Expand Down
294 changes: 222 additions & 72 deletions Kickstarter-iOS/Views/Controllers/BetaToolsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,119 +5,186 @@ import Prelude
import SafariServices
import UIKit

internal final class BetaToolsViewController: UIViewController {
fileprivate let viewModel: BetaToolsViewModelType = BetaToolsViewModel()
fileprivate let helpViewModel: HelpViewModelType = HelpViewModel()

@IBOutlet fileprivate var doneButton: UIBarButtonItem!
@IBOutlet fileprivate var betaDebugPushNotificationsButton: UIButton!
@IBOutlet fileprivate var betaFeedbackButton: UIButton!
@IBOutlet fileprivate var betaTitleLabel: UILabel!
@IBOutlet fileprivate var languageSwitcher: UIButton!
@IBOutlet fileprivate var languageTitleLabel: UILabel!
@IBOutlet fileprivate var environmentSwitcher: UIButton!
@IBOutlet fileprivate var environmentTitleLabel: UILabel!
internal final class BetaToolsViewController: UITableViewController {
private let viewModel: BetaToolsViewModelType = BetaToolsViewModel()

// MARK: - Properties

private let betaFeedbackButton = UIButton(type: .custom)
private var betaToolsData: BetaToolsData?
private let helpViewModel: HelpViewModelType = HelpViewModel()

internal static func instantiate() -> BetaToolsViewController {
return Storyboard.BetaTools.instantiate(BetaToolsViewController.self)
return BetaToolsViewController(style: .plain)
}

override func viewDidLoad() {
super.viewDidLoad()

_ = self
|> \.title .~ "Beta tools"

_ = self.tableView
|> \.dataSource .~ self

self.configureFooterView()
self.betaFeedbackButton.addTarget(
self, action: #selector(self.betaFeedbackButtonTapped),
for: .touchUpInside
)

let doneButton = UIBarButtonItem(
title: Strings.Done(), style: .done, target: self,
action: #selector(self.doneButtonTapped)
)

_ = self.navigationItem
?|> \.rightBarButtonItem .~ doneButton

self.viewModel.inputs.viewDidLoad()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

self.navigationItem.setRightBarButton(self.doneButton, animated: false)
self.tableView.ksr_sizeHeaderFooterViewsToFit()
}

override func bindStyles() {
_ = self.navigationController
?|> UINavigationController.lens.isNavigationBarHidden .~ false

_ = self.betaDebugPushNotificationsButton
|> UIButton.lens.titleColor(for: .normal) .~ .ksr_soft_black
|> UIButton.lens.titleLabel.font .~ .ksr_body()
|> UIButton.lens.contentHorizontalAlignment .~ .left
|> UIButton.lens.title(for: .normal) .~ "Debug push notifications"

_ = self.betaFeedbackButton
|> greenButtonStyle
|> UIButton.lens.title(for: .normal) .~ "Submit feedback for beta"
}

_ = self.betaTitleLabel
|> settingsTitleLabelStyle
|> UILabel.lens.text .~ "Beta tools"

_ = self.languageSwitcher
|> UIButton.lens.titleLabel.font .~ .ksr_headline(size: 15)
|> UIButton.lens.titleColor(for: .normal) .~ .ksr_text_dark_grey_500
|> UIButton.lens.title(for: .normal) .~ AppEnvironment.current.language.displayString
override func bindViewModel() {
self.viewModel.outputs.reloadWithData
.observeForUI()
.observeValues { [weak self] data in
self?.betaToolsData = data

_ = self.languageTitleLabel
|> settingsTitleLabelStyle
self?.tableView.reloadData()
}

_ = self.environmentSwitcher
|> UIButton.lens.titleLabel.font .~ .ksr_headline(size: 15)
|> UIButton.lens.contentHorizontalAlignment .~ .left
|> UIButton.lens.titleColor(for: .normal) .~ .ksr_text_dark_grey_500
|> UIButton.lens.title(for: .normal) .~
AppEnvironment.current.apiService.serverConfig.environment.rawValue
self.viewModel.outputs.goToPushNotificationTools
.observeForControllerAction()
.observeValues { [weak self] in
self?.goToDebugPushNotifications()
}

_ = self.environmentTitleLabel
|> settingsTitleLabelStyle
}
self.viewModel.outputs.goToFeatureFlagTools
.observeForControllerAction()
.observeValues { [weak self] in
self?.goToFeatureFlagTools()
}

override func bindViewModel() {
self.environmentSwitcher.rac.title = self.viewModel.outputs.environmentSwitcherButtonTitle
self.viewModel.outputs.goToBetaFeedback
.observeForControllerAction()
.observeValues { [weak self] in
self?.goToBetaFeedback()
}

self.languageSwitcher.rac.title = self.viewModel.outputs.currentLanguage
.map { $0.displayString }
self.viewModel.outputs.showChangeEnvironmentSheetWithSourceViewIndex
.observeForControllerAction()
.observeValues { [weak self] index in
self?.showEnvironmentActionSheet(sourceViewIndex: index)
}

self.viewModel.outputs.currentLanguage
.observeForUI()
.observeValues { [weak self] language in self?.languageDidChange(language: language) }
self.viewModel.outputs.showChangeLanguageSheetWithSourceViewIndex
.observeForControllerAction()
.observeValues { [weak self] index in
self?.showLanguageActionSheet(sourceViewIndex: index)
}

self.viewModel.outputs.goToBetaFeedback
self.viewModel.outputs.updateLanguage
.observeForControllerAction()
.observeValues { [weak self] in self?.goToBetaFeedback() }
.observeValues { [weak self] language in
self?.updateLanguage(language: language)
}

self.viewModel.outputs.betaFeedbackMailDisabled
self.viewModel.outputs.updateEnvironment
.observeForControllerAction()
.observeValues { [weak self] in self?.showMailDisabledAlert() }
.observeValues { [weak self] environment in
self?.updateEnvironment(environment: environment)
}

self.viewModel.outputs.logoutWithParams
.observeForControllerAction()
.observeValues { [weak self] in self?.logoutAndDismiss(params: $0) }
.observeValues { [weak self] in
self?.logoutAndDismiss(params: $0)
}

self.viewModel.outputs.showMailDisabledAlert
.observeForControllerAction()
.observeValues { [weak self] in
self?.showMailDisabledAlert()
}
}

@IBAction fileprivate func betaFeedbackButtonTapped(_: Any) {
// MARK: - Selectors

@objc private func betaFeedbackButtonTapped() {
self.viewModel.inputs.betaFeedbackButtonTapped(canSendMail: MFMailComposeViewController.canSendMail())
}

@IBAction fileprivate func betaDebugPushNotificationsButtonTapped(_: Any) {
@objc private func doneButtonTapped() {
self.navigationController?.dismiss(animated: true)
}

// MARK: Private Helper Functions

private func configureFooterView() {
let containerView = UIView(frame: .zero)

/* Silences autolayout warnings between conflicting table view frame-based sizing and our
tableFooterView's autolayout constraints
*/
let priority = UILayoutPriority(rawValue: 999)

_ = (self.betaFeedbackButton, containerView)
ifbarrera marked this conversation as resolved.
Show resolved Hide resolved
|> ksr_addSubviewToParent()
|> ksr_constrainViewToMarginsInParent(priority: priority)

_ = self.tableView
|> \.tableFooterView .~ containerView

let widthConstraint = self.betaFeedbackButton.widthAnchor
.constraint(equalTo: self.tableView.layoutMarginsGuide.widthAnchor)
|> \.priority .~ .defaultHigh

let heightConstraint = self.betaFeedbackButton.heightAnchor
.constraint(greaterThanOrEqualToConstant: Styles.minTouchSize.height)
|> \.priority .~ .defaultHigh

NSLayoutConstraint.activate([widthConstraint, heightConstraint])
}

private func goToDebugPushNotifications() {
self.navigationController?.pushViewController(
Storyboard.DebugPushNotifications.instantiate(DebugPushNotificationsViewController.self),
animated: true
)
}

@IBAction func languageSwitcherTapped(_: Any) {
self.showLanguageActionSheet()
}
private func goToFeatureFlagTools() {
let featureFlagToolsViewController = FeatureFlagToolsViewController.instantiate()

@IBAction func environmentSwitcherTapped(_: Any) {
self.showEnvironmentActionSheet()
self.navigationController?.pushViewController(featureFlagToolsViewController, animated: true)
}

@IBAction func doneTapped(_: Any) {
self.navigationController?.popViewController(animated: true)
}

// MARK: Private Helper Functions
private func showLanguageActionSheet(sourceViewIndex: Int) {
guard let sourceView = self.tableView
.cellForRow(at: IndexPath(row: sourceViewIndex, section: 0))?.detailTextLabel else {
return
}

private func showLanguageActionSheet() {
let alert = UIAlertController.alert(
title: "Change Language",
preferredStyle: .actionSheet,
sourceView: self.languageSwitcher
sourceView: sourceView,
sourceRect: sourceView.bounds
)

Language.allLanguages.forEach { language in
Expand All @@ -135,16 +202,22 @@ internal final class BetaToolsViewController: UIViewController {
self.present(alert, animated: true, completion: nil)
}

private func showEnvironmentActionSheet() {
private func showEnvironmentActionSheet(sourceViewIndex: Int) {
guard let sourceView = self.tableView
ifbarrera marked this conversation as resolved.
Show resolved Hide resolved
.cellForRow(at: IndexPath(row: sourceViewIndex, section: 0))?.detailTextLabel else {
return
}

let alert = UIAlertController.alert(
title: "Change Environment",
preferredStyle: .actionSheet,
sourceView: self.environmentSwitcher
sourceView: sourceView,
sourceRect: sourceView.bounds
)

EnvironmentType.allCases.forEach { environment in
alert.addAction(UIAlertAction(title: environment.rawValue, style: .default) { [weak self] _ in
self?.viewModel.inputs.environmentSwitcherButtonTapped(environment: environment)
self?.viewModel.inputs.setEnvironment(environment)
})
}

Expand All @@ -155,13 +228,24 @@ internal final class BetaToolsViewController: UIViewController {
self.present(alert, animated: true, completion: nil)
}

private func languageDidChange(language: Language) {
private func updateLanguage(language: Language) {
AppEnvironment.updateLanguage(language)

NotificationCenter.default.post(
name: Notification.Name.ksr_userLocalePreferencesChanged,
object: nil,
userInfo: nil
)

self.navigationController?.popViewController(animated: true)
}

private func updateEnvironment(environment: EnvironmentType) {
let serverConfig = ServerConfig.config(for: environment)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we are updating the environment on the view controller? Here we could pass the environment using and input like didUpdateEnvironment(environment) and let the ViewModel handle the updating action, this way we take this responsibility out of the view controller.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one thing to bear in mind when doing that sort of thing inside the view model is - if the environment is involved in the chain of signals that perform that replacement you may run the risk of the environment being switched out from underneath and the subscriptions being lost. Little tricky to explain but I had this happen once before something like:

AppEnvironment.current.signal
  .map { // transform something... }
  .observeValues { // replace environment }

Because this depends on the current environment doing the replacement here can be problematic. This might not exactly apply in what @Scollaco is suggesting πŸ˜„ My feeling is to do the replacement outside of the view model as it is now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I thought about this and my feeling is that actually updating the environment is more of a side effect. This way, we can be certain that the right EnvironmentType is being sent, and we've separately tested that side-effect behavior works as expected.

We also do it this way in AppDelegate


AppEnvironment.updateServerConfig(serverConfig)

self.viewModel.inputs.didUpdateEnvironment()
}

private func goToBetaFeedback() {
Expand Down Expand Up @@ -207,7 +291,59 @@ internal final class BetaToolsViewController: UIViewController {
// Refresh the discovery screens
NotificationCenter.default.post(.init(name: .ksr_environmentChanged))

self.navigationController?.dismiss(animated: true, completion: nil)
self.navigationController?.popViewController(animated: true)
}
}

// MARK: - UITableViewDelegate

extension BetaToolsViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let row = BetaToolsRow(rawValue: indexPath.row) else {
return
}

self.viewModel.inputs.didSelectBetaToolsRow(row)

tableView.deselectRow(at: indexPath, animated: true)
}
}

// MARK: - UITableViewDataSource

extension BetaToolsViewController {
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
return BetaToolsRow.allCases.count
}

override func numberOfSections(in _: UITableView) -> Int {
return 1
}

override func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let row = BetaToolsRow.init(rawValue: indexPath.row), let rowData = self.betaToolsData else {
fatalError("Cannot create cell")
}

let cell = UITableViewCell(style: row.cellStyle, reuseIdentifier: nil)
|> \.selectionStyle .~ row.selectionStyle

if let imageName = row.rightIconImageName {
let image = UIImage(named: imageName)

_ = cell
|> \.accessoryView .~ UIImageView(image: image)
}

_ = cell.textLabel
?|> titleLabelStyle
?|> \.text .~ row.titleText

_ = cell.detailTextLabel
?|> detailLabelStyle
?|> \.text .~ row.detailText(from: rowData)

return cell
}
}

Expand All @@ -221,3 +357,17 @@ extension BetaToolsViewController: MFMailComposeViewControllerDelegate {
controller.dismiss(animated: true, completion: nil)
}
}

private let detailLabelStyle: LabelStyle = { label in
label
|> \.font .~ .ksr_headline(size: 15)
|> \.textColor .~ .ksr_text_dark_grey_500
|> \.lineBreakMode .~ .byWordWrapping
|> \.textAlignment .~ .right
}

private let titleLabelStyle: LabelStyle = { label in
label
|> \.textColor .~ .ksr_soft_black
|> \.font .~ .ksr_body()
}
Loading