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

Refactor FXIOS-8971 Tweaks to redux actions #19823

Merged
merged 14 commits into from
Apr 30, 2024
21 changes: 11 additions & 10 deletions BrowserKit/Sources/Redux/Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@

import Foundation

/// Are a declarative way of describing a state change. Actions don’t contain any code,
/// they are consumed by the store and forwarded to reducers. Are used to express intended
/// state changes. Actions include a context object that includes information about the
/// action, including a UUID for a particular window. If an Action is truly global in
/// nature or can't be associated with a UUID it can send either the UUID for the active
/// window (see `WindowManager.activeWindow`) or if needed `WindowUUID.unavailable`.
public protocol Action {
var windowUUID: UUID { get }
}
/// Used to describe an action that can be dispatched by the redux store
open class Action {
public var windowUUID: UUID
public var actionType: ActionType

public init(windowUUID: UUID, actionType: ActionType) {
self.windowUUID = windowUUID
self.actionType = actionType
}

extension Action {
func displayString() -> String {
let className = String(describing: Self.self)
let actionName = String(describing: self).prefix(20)
return "\(className).\(actionName)"
}
}

public protocol ActionType {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe it would be beneficial to use enums for actions, mainly for two reasons. Firstly, the autocomplete feature in Xcode significantly enhances action discoverability. It conveniently allows you to view all options in one place. Secondly, using enums helps ensure that every new action added is handled properly because the compiler will flag an error if any action is overlooked. This approach helps avoid the potential oversight that might occur with a default case in switch statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The objects that conform to ActionType are enums.

Copy link
Collaborator

Choose a reason for hiding this comment

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

When you switch on ActionType, you loose this capability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may not be understanding. Are you proposing we have one big ActionType enum for all actions in the project?

29 changes: 20 additions & 9 deletions BrowserKit/Tests/ReduxTests/Utilities/FakeReduxAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,30 @@ import XCTest

@testable import Redux

enum FakeReduxAction: Action {
class FakeReduxAction: Action {
let counterValue: Int?
let privateMode: Bool?

init(counterValue: Int? = nil,
privateMode: Bool? = nil,
windowUUID: UUID,
actionType: ActionType) {
self.counterValue = counterValue
self.privateMode = privateMode
super.init(windowUUID: windowUUID,
actionType: actionType)
}
}

enum FakeReduxActionType: ActionType {
// User action
case requestInitialValue
case increaseCounter
case decreaseCounter

// Middleware actions
case initialValueLoaded(Int)
case counterIncreased(Int)
case counterDecreased(Int)
case setPrivateModeTo(Bool)

var windowUUID: UUID {
return UUID(uuidString: "D9D9D9D9-D9D9-D9D9-D9D9-CD68A019860B")!
}
case initialValueLoaded
case counterIncreased
case counterDecreased
case setPrivateModeTo
}
33 changes: 20 additions & 13 deletions BrowserKit/Tests/ReduxTests/Utilities/FakeReduxMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,33 @@
import Foundation

@testable import Redux

class FakeReduxMiddleware {
lazy var fakeProvider: Middleware<FakeReduxState> = { state, action in
switch action {
case FakeReduxAction.requestInitialValue:
switch action.actionType {
case FakeReduxActionType.requestInitialValue:
let initialValue = self.generateInitialValue()
DispatchQueue.main.async {
store.dispatch(FakeReduxAction.initialValueLoaded(initialValue))
}
case FakeReduxAction.increaseCounter:
let action = FakeReduxAction(counterValue: initialValue,
windowUUID: windowUUID,
actionType: FakeReduxActionType.initialValueLoaded)
store.dispatch(action)
OrlaM marked this conversation as resolved.
Show resolved Hide resolved

case FakeReduxActionType.increaseCounter:
let existingValue = state.counter
let newValue = self.increaseCounter(currentValue: existingValue)
DispatchQueue.main.async {
store.dispatch(FakeReduxAction.counterIncreased(newValue))
}
case FakeReduxAction.decreaseCounter:
let action = FakeReduxAction(counterValue: newValue,
windowUUID: windowUUID,
actionType: FakeReduxActionType.counterIncreased)
store.dispatch(action)

case FakeReduxActionType.decreaseCounter:
let existingValue = state.counter
let newValue = self.decreaseCounter(currentValue: existingValue)
DispatchQueue.main.async {
store.dispatch(FakeReduxAction.counterDecreased(newValue))
}
let action = FakeReduxAction(counterValue: newValue,
windowUUID: windowUUID,
actionType: FakeReduxActionType.counterDecreased)
store.dispatch(action)

default:
break
}
Expand Down
16 changes: 9 additions & 7 deletions BrowserKit/Tests/ReduxTests/Utilities/FakeReduxState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ struct FakeReduxState: StateType, Equatable {
var isInPrivateMode = false

static let reducer: Reducer<Self> = { state, action in
switch action {
case FakeReduxAction.initialValueLoaded(let value),
FakeReduxAction.counterIncreased(let value),
FakeReduxAction.counterDecreased(let value):
return FakeReduxState(counter: value,
guard let action = action as? FakeReduxAction else { return state }
Copy link
Collaborator

Choose a reason for hiding this comment

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

One minor concern: it seems like with this change, reducers/middlewares that are interested in handling several different Action subclasses are going to have to check the class type of the incoming action against every subclass they want to handle 1 or more types for. Not a big deal, but that might lead to some clunky boilerplate of its own since we'll need to have multiple switches against the .actionType for each individual Action subclass, wrapped by if statements. (Hopefully that made sense the way I phrased it)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree this is a concern. It touches on one of the other actions items I have to follow up on and that is how we have broken down and organized our states/middlewares and reducers. They are too big and broad right now, we have some switch statements that are already hundreds of lines long.
Ideally a reducers that wants to reduce actions from more than one Action subclass should be the exception rather than the norm. I'm hoping that will avoid this approach turning into a mess of casting attempts and fallbacks. Open to discussing further if you think that won't be enough.


switch action.actionType {
case FakeReduxActionType.initialValueLoaded,
FakeReduxActionType.counterIncreased,
FakeReduxActionType.counterDecreased:
return FakeReduxState(counter: action.counterValue ?? state.counter,
isInPrivateMode: state.isInPrivateMode)
case FakeReduxAction.setPrivateModeTo(let value):
case FakeReduxActionType.setPrivateModeTo:
return FakeReduxState(counter: state.counter,
isInPrivateMode: value)
isInPrivateMode: action.privateMode ?? state.isInPrivateMode)
default:
return state
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import UIKit

@testable import Redux

let windowUUID = UUID(uuidString: "D9D9D9D9-D9D9-D9D9-D9D9-CD68A019860B")!

class FakeReduxViewController: UIViewController, StoreSubscriber {
typealias SubscriberStateType = FakeReduxState

Expand All @@ -27,7 +29,9 @@ class FakeReduxViewController: UIViewController, StoreSubscriber {

func subscribeToRedux() {
store.subscribe(self)
store.dispatch(FakeReduxAction.requestInitialValue)

let action = FakeReduxAction(windowUUID: windowUUID, actionType: FakeReduxActionType.requestInitialValue)
store.dispatch(action)
}

func unsubscribeFromRedux() {
Expand All @@ -42,14 +46,19 @@ class FakeReduxViewController: UIViewController, StoreSubscriber {
// MARK: - Helper functions

func increaseCounter() {
store.dispatch(FakeReduxAction.increaseCounter)
let action = FakeReduxAction(windowUUID: windowUUID, actionType: FakeReduxActionType.increaseCounter)
store.dispatch(action)
}

func decreaseCounter() {
store.dispatch(FakeReduxAction.decreaseCounter)
let action = FakeReduxAction(windowUUID: windowUUID, actionType: FakeReduxActionType.decreaseCounter)
store.dispatch(action)
}

func setPrivateMode(to value: Bool) {
store.dispatch(FakeReduxAction.setPrivateModeTo(value))
let action = FakeReduxAction(privateMode: value,
windowUUID: windowUUID,
actionType: FakeReduxActionType.setPrivateModeTo)
store.dispatch(action)
}
}
7 changes: 4 additions & 3 deletions firefox-ios/Client/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,10 @@ class SettingsCoordinator: BaseCoordinator,
}

func pressedTheme() {
store.dispatch(ActiveScreensStateAction.showScreen(
ScreenActionContext(screen: .themeSettings, windowUUID: windowUUID)
))
let action = ScreenAction(windowUUID: windowUUID,
actionType: ScreenActionType.showScreen,
screen: .themeSettings)
store.dispatch(action)
router.push(ThemeSettingsController(windowUUID: windowUUID))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,29 @@
import Foundation
import Redux

class GeneralBrowserContext: ActionContext {
class GeneralBrowserAction: Action {
let selectedTabURL: URL?
let isPrivateBrowsing: Bool
init(selectedTabURL: URL?,
isPrivateBrowsing: Bool,
windowUUID: WindowUUID) {
let isPrivateBrowsing: Bool?
let toastType: ToastType?
let showOverlay: Bool?

init(selectedTabURL: URL? = nil,
isPrivateBrowsing: Bool? = nil,
toastType: ToastType? = nil,
showOverlay: Bool? = nil,
windowUUID: UUID,
actionType: ActionType) {
self.selectedTabURL = selectedTabURL
self.isPrivateBrowsing = isPrivateBrowsing
super.init(windowUUID: windowUUID)
self.toastType = toastType
self.showOverlay = showOverlay
super.init(windowUUID: windowUUID,
actionType: actionType)
}
}

enum GeneralBrowserAction: Action {
case showToast(ToastTypeContext)
case showOverlay(KeyboardContext)
case updateSelectedTab(GeneralBrowserContext)

var windowUUID: UUID {
switch self {
case .showToast(let context as ActionContext):
return context.windowUUID
case .showOverlay(let context as ActionContext):
return context.windowUUID
case .updateSelectedTab(let context as ActionContext):
return context.windowUUID
}
}
enum GeneralBrowserActionType: ActionType {
case showToast
case showOverlay
case updateSelectedTab
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ extension BrowserViewController: URLBarDelegate {
)
},
andActionForButton: {
store.dispatch(FakespotAction.show(windowUUID.context))
let action = FakespotAction(windowUUID: windowUUID,
actionType: FakespotActionType.show)
store.dispatch(action)
},
overlayState: overlayManager)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,77 +71,91 @@ struct BrowserViewControllerState: ScreenState, Equatable {
// Only process actions for the current window
guard action.windowUUID == .unavailable || action.windowUUID == state.windowUUID else { return state }

switch action {
case PrivateModeMiddlewareAction.privateModeUpdated(let context):
let privacyState = context.boolValue
if let action = action as? FakespotAction {
return BrowserViewControllerState.reduceStateForFakeSpotAction(action: action, state: state)
} else if let action = action as? PrivateModeAction {
return BrowserViewControllerState.reduceStateForPrivateModeAction(action: action, state: state)
} else if let action = action as? GeneralBrowserAction {
return BrowserViewControllerState.reduceStateForGeneralBrowserAction(action: action, state: state)
} else {
return BrowserViewControllerState(
searchScreenState: state.searchScreenState,
showDataClearanceFlow: state.showDataClearanceFlow,
fakespotState: FakespotState.reducer(state.fakespotState, action),
showOverlay: state.showOverlay,
windowUUID: state.windowUUID,
reloadWebView: false,
browserViewType: state.browserViewType)
}
}

static func reduceStateForFakeSpotAction(action: FakespotAction,
state: BrowserViewControllerState) -> BrowserViewControllerState {
return BrowserViewControllerState(
searchScreenState: state.searchScreenState,
showDataClearanceFlow: state.showDataClearanceFlow,
fakespotState: FakespotState.reducer(state.fakespotState, action),
windowUUID: state.windowUUID,
browserViewType: state.browserViewType)
}

static func reduceStateForPrivateModeAction(action: PrivateModeAction,
state: BrowserViewControllerState) -> BrowserViewControllerState {
switch action.actionType {
case PrivateModeActionType.privateModeUpdated:
let privacyState = action.isPrivate ?? false
var browserViewType = state.browserViewType
if browserViewType != .webview {
browserViewType = privacyState ? .privateHomepage : .normalHomepage
}
return BrowserViewControllerState(
searchScreenState: SearchScreenState(inPrivateMode: privacyState),
showDataClearanceFlow: privacyState,
fakespotState: state.fakespotState,
fakespotState: FakespotState.reducer(state.fakespotState, action),
windowUUID: state.windowUUID,
reloadWebView: true,
browserViewType: browserViewType)
case FakespotAction.pressedShoppingButton,
FakespotAction.show,
FakespotAction.dismiss,
FakespotAction.setAppearanceTo,
FakespotAction.settingsStateDidChange,
FakespotAction.reviewQualityDidChange,
FakespotAction.highlightsDidChange,
FakespotAction.tabDidChange,
FakespotAction.tabDidReload,
FakespotAction.surfaceDisplayedEventSend,
FakespotAction.adsImpressionEventSendFor,
FakespotAction.adsExposureEventSendFor:
default:
return state
}
}

static func reduceStateForGeneralBrowserAction(action: GeneralBrowserAction,
state: BrowserViewControllerState) -> BrowserViewControllerState {
switch action.actionType {
case GeneralBrowserActionType.showToast:
guard let toastType = action.toastType else { return state }
return BrowserViewControllerState(
searchScreenState: state.searchScreenState,
showDataClearanceFlow: state.showDataClearanceFlow,
fakespotState: FakespotState.reducer(state.fakespotState, action),
windowUUID: state.windowUUID,
browserViewType: state.browserViewType)
case GeneralBrowserAction.showToast(let context):
let toastType = context.toastType
return BrowserViewControllerState(
searchScreenState: state.searchScreenState,
showDataClearanceFlow: state.showDataClearanceFlow,
fakespotState: state.fakespotState,
toast: toastType,
windowUUID: state.windowUUID,
browserViewType: state.browserViewType)
case GeneralBrowserAction.showOverlay(let context):
let showOverlay = context.showOverlay
case GeneralBrowserActionType.showOverlay:
let showOverlay = action.showOverlay ?? false
return BrowserViewControllerState(
searchScreenState: state.searchScreenState,
showDataClearanceFlow: state.showDataClearanceFlow,
fakespotState: state.fakespotState,
fakespotState: FakespotState.reducer(state.fakespotState, action),
showOverlay: showOverlay,
windowUUID: state.windowUUID,
browserViewType: state.browserViewType)
case GeneralBrowserAction.updateSelectedTab(let context):
return BrowserViewControllerState.resolveStateForUpdateSelectedTab(context, state: state)
case GeneralBrowserActionType.updateSelectedTab:
return BrowserViewControllerState.resolveStateForUpdateSelectedTab(action: action, state: state)
default:
return BrowserViewControllerState(
searchScreenState: state.searchScreenState,
showDataClearanceFlow: state.showDataClearanceFlow,
fakespotState: state.fakespotState,
showOverlay: state.showOverlay,
windowUUID: state.windowUUID,
reloadWebView: false,
browserViewType: state.browserViewType)
return state
}
}

static func resolveStateForUpdateSelectedTab(_ context: GeneralBrowserContext,
static func resolveStateForUpdateSelectedTab(action: GeneralBrowserAction,
state: BrowserViewControllerState) -> BrowserViewControllerState {
let isAboutHomeURL = InternalURL(context.selectedTabURL)?.isAboutHomeURL ?? false
let isAboutHomeURL = InternalURL(action.selectedTabURL)?.isAboutHomeURL ?? false
var browserViewType = BrowserViewType.normalHomepage
let isPrivateBrowsing = action.isPrivateBrowsing ?? false

if isAboutHomeURL {
browserViewType = context.isPrivateBrowsing ? .privateHomepage : .normalHomepage
browserViewType = isPrivateBrowsing ? .privateHomepage : .normalHomepage
} else {
browserViewType = .webview
}
Expand Down
Loading