Skip to content

Commit

Permalink
🏗 Refactor ExperienceRenderer to support multiple ExperienceStateMach…
Browse files Browse the repository at this point in the history
…ine instances for non-modal Experiences

There is one sharedStateMachine for modal experiences and a mapping by experience ID to state machine for non-modal.
  • Loading branch information
mmaatttt committed Jun 14, 2023
1 parent d945dff commit 6972b97
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 77 deletions.
27 changes: 27 additions & 0 deletions Sources/AppcuesKit/Data/Extensions/Collection+Custom.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Collection+Custom.swift
// AppcuesKit
//
// Created by Matt on 2022-04-28.
// Copyright © 2022 Appcues. All rights reserved.
//

import Foundation

extension Collection {
func separate(predicate: (Iterator.Element) -> Bool) -> (matching: [Iterator.Element], notMatching: [Iterator.Element]) {
var separated: ([Iterator.Element], [Iterator.Element]) = ([], [])
for element in self {
if predicate(element) {
separated.0.append(element)
} else {
separated.1.append(element)
}
}
return separated
}

subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
15 changes: 0 additions & 15 deletions Sources/AppcuesKit/Data/Extensions/Collection+SafeIndex.swift

This file was deleted.

7 changes: 6 additions & 1 deletion Sources/AppcuesKit/Data/Models/Experience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ internal protocol PluginDecoder {
func decode<T: Decodable>(_ type: T.Type) -> T?
}

internal struct InstanceID: Identifiable, Hashable {
let id = UUID()
}

internal struct Experience {

@dynamicMemberLookup
Expand Down Expand Up @@ -145,7 +149,7 @@ internal struct Experience {
let nextContentID: String?

/// Unique ID to disambiguate the same experience flowing through the system from different origins.
let instanceID = UUID()
let instanceID = InstanceID()
}

extension Experience: Decodable {
Expand Down Expand Up @@ -187,6 +191,7 @@ extension Experience {
if let nextContentID = nextContentID {
actions.append(AppcuesLaunchExperienceAction(
appcues: appcues,
currentExperienceID: instanceID,
experienceID: nextContentID,
trigger: .experienceCompletionAction(fromExperienceID: self.id)
))
Expand Down
8 changes: 5 additions & 3 deletions Sources/AppcuesKit/Presentation/Actions/ActionRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ internal class ActionRegistry {

/// Enqueue an array of experience action data models to be executed. This version is for non-interactive action execution,
/// such as actions that execute as part of the navigation to a step.
func enqueue(actionModels: [Experience.Action], level: AppcuesExperiencePluginConfiguration.Level, completion: @escaping () -> Void) {
func enqueue(actionModels: [Experience.Action], level: AppcuesExperiencePluginConfiguration.Level, experienceID: InstanceID?, completion: @escaping () -> Void) {
let actionInstances = actionModels.compactMap {
actions[$0.type]?.init(configuration: AppcuesExperiencePluginConfiguration($0.configDecoder, level: level, appcues: appcues))
actions[$0.type]?.init(configuration: AppcuesExperiencePluginConfiguration($0.configDecoder, level: level, experienceID: experienceID, appcues: appcues))
}
execute(transformQueue(actionInstances), completion: completion)
}
Expand All @@ -84,18 +84,20 @@ internal class ActionRegistry {
func enqueue(
actionModels: [Experience.Action],
level: AppcuesExperiencePluginConfiguration.Level,
experienceID: InstanceID?,
interactionType: String,
viewDescription: String?
) {
let actionInstances = actionModels.compactMap {
actions[$0.type]?.init(configuration: AppcuesExperiencePluginConfiguration($0.configDecoder, level: level, appcues: appcues))
actions[$0.type]?.init(configuration: AppcuesExperiencePluginConfiguration($0.configDecoder, level: level, experienceID: experienceID, appcues: appcues))
}

// As a heuristic, take the last action that's `InteractionLoggingAction`, since that's most likely
// to be the action that we'd want to see in the event export.
let primaryAction = actionInstances.reversed().compactMapFirst { $0 as? InteractionLoggingAction }
let interactionAction = AppcuesStepInteractionAction(
appcues: appcues,
experienceID: experienceID,
interactionType: interactionType,
viewDescription: viewDescription ?? "",
category: primaryAction?.category ?? "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ internal class AppcuesCloseAction: AppcuesExperienceAction {
static let type = "@appcues/close"

private weak var appcues: Appcues?
private let experienceID: InstanceID?

private let markComplete: Bool

required init?(configuration: AppcuesExperiencePluginConfiguration) {
appcues = configuration.appcues
experienceID = configuration.experienceID

let config = configuration.decode(Config.self)
markComplete = config?.markComplete ?? false
Expand All @@ -32,6 +34,6 @@ internal class AppcuesCloseAction: AppcuesExperienceAction {
guard let appcues = appcues else { return completion() }

let experienceRenderer = appcues.container.resolve(ExperienceRendering.self)
experienceRenderer.dismissCurrentExperience(markComplete: markComplete) { _ in completion() }
experienceRenderer.dismiss(experienceID: experienceID, markComplete: markComplete) { _ in completion() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ internal class AppcuesContinueAction: AppcuesExperienceAction {
static let type = "@appcues/continue"

private weak var appcues: Appcues?
private let experienceID: InstanceID?

let stepReference: StepReference

required init?(configuration: AppcuesExperiencePluginConfiguration) {
appcues = configuration.appcues
experienceID = configuration.experienceID

let config = configuration.decode(Config.self)
if let index = config?.index {
Expand All @@ -42,6 +44,6 @@ internal class AppcuesContinueAction: AppcuesExperienceAction {
guard let appcues = appcues else { return completion() }

let experienceRenderer = appcues.container.resolve(ExperienceRendering.self)
experienceRenderer.show(stepInCurrentExperience: stepReference, completion: completion)
experienceRenderer.show(step: stepReference, experienceID: experienceID, completion: completion)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,24 @@ internal class AppcuesLaunchExperienceAction: AppcuesExperienceAction {
static let type = "@appcues/launch-experience"

private weak var appcues: Appcues?
private let currentExperienceID: InstanceID?

let experienceID: String
let launchExperienceID: String
private let trigger: ExperienceTrigger?

required init?(configuration: AppcuesExperiencePluginConfiguration) {
self.appcues = configuration.appcues

guard let config = configuration.decode(Config.self) else { return nil }
self.experienceID = config.experienceID
self.currentExperienceID = configuration.experienceID
self.launchExperienceID = config.experienceID
self.trigger = nil
}

init(appcues: Appcues?, experienceID: String, trigger: ExperienceTrigger) {
init(appcues: Appcues?, currentExperienceID: InstanceID?, experienceID: String, trigger: ExperienceTrigger) {
self.appcues = appcues
self.experienceID = experienceID
self.currentExperienceID = currentExperienceID
self.launchExperienceID = experienceID

// This is used when a flow is triggered as a post flow action from another flow.
// The trigger value is set during the StateMachine processing of the post-flow actions.
Expand All @@ -50,14 +53,15 @@ internal class AppcuesLaunchExperienceAction: AppcuesExperienceAction {
// that launches another flow from a button, for example.
let trigger = self.trigger ?? launchExperienceTrigger(appcues)

experienceLoading.load(experienceID: experienceID, published: true, trigger: trigger) { _ in
experienceLoading.load(experienceID: launchExperienceID, published: true, trigger: trigger) { _ in
completion()
}
}

private func launchExperienceTrigger(_ appcues: Appcues) -> ExperienceTrigger {
let experienceRendering = appcues.container.resolve(ExperienceRendering.self)
let currentExperienceId = experienceRendering.getCurrentExperienceData()?.id
return .launchExperienceAction(fromExperienceID: currentExperienceId)
// Self.currentExperienceID is the instance ID, but we want Experience.id
let experienceID = experienceRendering.experienceData(experienceID: currentExperienceID)?.id
return .launchExperienceAction(fromExperienceID: experienceID)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal class AppcuesStepInteractionAction: AppcuesExperienceAction {
static let type = "@appcues/step_interaction"

private weak var appcues: Appcues?
private let experienceID: InstanceID?

let interactionType: String
let viewDescription: String
Expand All @@ -26,8 +27,16 @@ internal class AppcuesStepInteractionAction: AppcuesExperienceAction {
return nil
}

init(appcues: Appcues?, interactionType: String, viewDescription: String, category: String, destination: String) {
init(
appcues: Appcues?,
experienceID: InstanceID?,
interactionType: String,
viewDescription: String,
category: String,
destination: String
) {
self.appcues = appcues
self.experienceID = experienceID
self.interactionType = interactionType
self.viewDescription = viewDescription
self.category = category
Expand All @@ -49,8 +58,8 @@ internal class AppcuesStepInteractionAction: AppcuesExperienceAction {
]
]

if let experienceData = experienceRenderer.getCurrentExperienceData(),
let stepIndex = experienceRenderer.getCurrentStepIndex() {
if let experienceData = experienceRenderer.experienceData(experienceID: experienceID),
let stepIndex = experienceRenderer.stepIndex(experienceID: experienceID) {
interactionProperties = LifecycleEvent.properties(experienceData, stepIndex).merging(interactionProperties)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ internal class AppcuesSubmitFormAction: AppcuesExperienceAction, ExperienceActio
static let type = "@appcues/submit-form"

private weak var appcues: Appcues?
private let experienceID: InstanceID?

let skipValidation: Bool

required init?(configuration: AppcuesExperiencePluginConfiguration) {
self.appcues = configuration.appcues
self.experienceID = configuration.experienceID

let config = configuration.decode(Config.self)
self.skipValidation = config?.skipValidation ?? false
Expand All @@ -37,8 +39,8 @@ internal class AppcuesSubmitFormAction: AppcuesExperienceAction, ExperienceActio
let experienceRenderer = appcues.container.resolve(ExperienceRendering.self)
let analyticsPublisher = appcues.container.resolve(AnalyticsPublishing.self)

guard let experienceData = experienceRenderer.getCurrentExperienceData(),
let stepIndex = experienceRenderer.getCurrentStepIndex(),
guard let experienceData = experienceRenderer.experienceData(experienceID: experienceID),
let stepIndex = experienceRenderer.stepIndex(experienceID: experienceID),
let stepState = experienceData.state(for: stepIndex) else { return }

let interactionProperties = LifecycleEvent.properties(experienceData, stepIndex).merging([
Expand Down Expand Up @@ -66,8 +68,8 @@ internal class AppcuesSubmitFormAction: AppcuesExperienceAction, ExperienceActio

let experienceRenderer = appcues.container.resolve(ExperienceRendering.self)

guard let experienceData = experienceRenderer.getCurrentExperienceData(),
let stepIndex = experienceRenderer.getCurrentStepIndex(),
guard let experienceData = experienceRenderer.experienceData(experienceID: experienceID),
let stepIndex = experienceRenderer.stepIndex(experienceID: experienceID),
let stepState = experienceData.state(for: stepIndex) else { return queue }

if stepState.stepFormIsComplete {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension AppcuesLinkAction: InteractionLoggingAction {
@available(iOS 13.0, *)
extension AppcuesLaunchExperienceAction: InteractionLoggingAction {
var category: String { "internal" }
var destination: String { experienceID }
var destination: String { launchExperienceID }
}

@available(iOS 13.0, *)
Expand Down
4 changes: 2 additions & 2 deletions Sources/AppcuesKit/Presentation/Debugger/UIDebugger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ extension UIDebugger: DebugViewDelegate {
}

private func captureScreen(authorization: Authorization) {
guard experienceRenderer.getCurrentExperienceData() == nil else {
experienceRenderer.dismissCurrentExperience(markComplete: false) { _ in
guard experienceRenderer.experienceData(experienceID: nil) == nil else {
experienceRenderer.dismiss(experienceID: nil, markComplete: false) { _ in
self.captureScreen(authorization: authorization)
}
return
Expand Down
Loading

0 comments on commit 6972b97

Please sign in to comment.