I've been thinking about how I would go about replacing the analytics in my app using a "Swifty" and "PointFree" way of approaching the problem.
At the moment the analytics in the app is very ad-hoc and whenever it is done it needs an AnalyticsProvider
passing in and then we manually log the event etc... to the provider.
The provider then wraps multiple third party analytics SDKs. (Don't ask why multiple... )
Anyway... going for the basic one of just Firebase...
I created a higher order Reducer like...
struct Event {
let name: String
let parameters: [String: Any]?
static func screenView(screenName: String, screenClass: String = "") -> Self {
.init(name: AnalyticsEventScreenView, parameters: [
AnalyticsParameterScreenName: screenName,
AnalyticsParameterScreenClass: screenClass
])
}
}
protocol AnalyticsAction {
var event: Event { get }
}
extension AnalyticsAction {
var event: Event {
.init(name: "\(self)", parameters: nil)
}
}
extension Reducer where Action: AnalyticsAction {
func firebaseAnalytics() -> Reducer {
return .init { state, action, environment in
let effects = self.run(&state, action, environment)
return .merge(
.fireAndForget {
Analytics.logEvent(action.event.name, parameters: action.event.parameters)
},
effects
)
}
}
}
So I can essentially turn EVERY action in the app into an AnalyticsAction. (Hmm... possibly I should make this optional... but that's a question for another time).
So now I can create this like...
let appReducer = ...
.debug()
.firebaseAnalytics()
And my actions can conform to this by having something like...
enum NumberAction: Equatable, AnalyticsAction {
case dismissAlertTapped
case onAppear
case incrementTapped
case decrementTapped
case getRandomNumberTapped
case randomNumberReceived(Result<Int?, Never>)
case getFactTapped
case factReceived(Result<NumberFact?, Never>)
var event: Event {
switch self {
case let .randomNumberReceived(.success(number?)):
return .init(name: "randomNumberReceived", parameters: ["number": number])
case let .factReceived(.success(fact?)):
return .init(
name: "factReceived",
parameters: [
"number": fact.number,
"fact": fact.text
]
)
case .onAppear:
return .screenView(screenName: "Number App View", screenClass: "ContentView")
default:
return .init(name: "\(self)", parameters: nil)
}
}
}
This is working so far. But I was coming here to run this idea past more people to get your input onto it? At the moment it's a fairly basic and naive implementation. Is this something worth exploring further? Or is there a nicer way of doing this?
Also... I noticed that the debug
reducer uses a toLocalState
and toLocalAction
to transform the incoming data. But I'm not entirely sure what that's used for an if that's something I might need also?
Thanks