Advanced Integration Patterns
This guide covers advanced integration patterns and architectural approaches for implementing StorifyMe in complex iOS applications.
Multi-Widget Management
Managing Multiple Widgets in Single View Controller
class MultiWidgetViewController: UIViewController {
private var widgets: [Int: StorifyMeWidget] = [:]
private let widgetIds = [123, 456, 789]
override func viewDidLoad() {
super.viewDidLoad()
setupMultipleWidgets()
}
private func setupMultipleWidgets() {
for (index, widgetId) in widgetIds.enumerated() {
let storifyMeWidget = StorifyMeWidget()
storifyMeWidget.eventHandler = MultiWidgetEventHandler(
widgetId: widgetId,
delegate: self
)
// Position widgets in stack view
addWidgetToView(storifyMeWidget, at: index)
storifyMeWidget.setWidgetId(widgetId: widgetId)
storifyMeWidget.load()
widgets[widgetId] = storifyMeWidget
}
}
private func addWidgetToView(_ widget: StorifyMeWidget, at index: Int) {
view.addSubview(widget)
widget.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
widget.leadingAnchor.constraint(equalTo: view.leadingAnchor),
widget.trailingAnchor.constraint(equalTo: view.trailingAnchor),
widget.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: CGFloat(index * 140)),
widget.heightAnchor.constraint(equalToConstant: 120)
])
}
}
extension MultiWidgetViewController: MultiWidgetDelegate {
func widgetDidLoad(widgetId: Int, stories: [StorifyMeStory]) {
print("Widget \(widgetId) loaded with \(stories.count) stories")
updateWidgetState(widgetId: widgetId, state: .loaded)
}
func widgetDidFail(widgetId: Int, error: String) {
print("Widget \(widgetId) failed: \(error)")
updateWidgetState(widgetId: widgetId, state: .error)
}
private func updateWidgetState(widgetId: Int, state: WidgetState) {
// Update UI indicators based on state
DispatchQueue.main.async { [weak self] in
// Update loading indicators, error views, etc.
}
}
}
protocol MultiWidgetDelegate: AnyObject {
func widgetDidLoad(widgetId: Int, stories: [StorifyMeStory])
func widgetDidFail(widgetId: Int, error: String)
}
class MultiWidgetEventHandler: NSObject, StorifyMeStoryEventProtocol {
private let widgetId: Int
private weak var delegate: MultiWidgetDelegate?
init(widgetId: Int, delegate: MultiWidgetDelegate) {
self.widgetId = widgetId
self.delegate = delegate
}
func onLoad(widgetId: Int, storyList: [StorifyMeStory]) {
delegate?.widgetDidLoad(widgetId: widgetId, stories: storyList)
}
func onFail(widgetId: Int, error: String) {
delegate?.widgetDidFail(widgetId: widgetId, error: error)
}
}
enum WidgetState {
case loading, loaded, error
}
Widget Factory Pattern
struct WidgetConfiguration {
let audioOptions: AudioConfiguration?
let playbackOptions: PlaybackConfiguration?
let posterSettings: PosterConfiguration?
struct AudioConfiguration {
let behaviour: StorifyMeStoryAudioBehaviour
let defaultState: StorifyMeStoryAudioState
}
struct PlaybackConfiguration {
let behaviour: StorifyMeStoryPlaybackBehaviour
}
struct PosterConfiguration {
let gifEnabled: Bool
let videoEnabled: Bool
}
}
class StorifyMeWidgetFactory {
static func createWidget(
widgetId: Int,
configuration: WidgetConfiguration
) -> StorifyMeWidget {
let widget = StorifyMeWidget()
// Apply configuration
widget.setWidgetId(widgetId: widgetId)
if let audioOptions = configuration.audioOptions {
widget.setWidgetAudioOptions(
options: StorifyMeStoryAudioOptions(
behaviour: audioOptions.behaviour,
defaultState: audioOptions.defaultState
)
)
}
if let playbackOptions = configuration.playbackOptions {
widget.setPlaybackOptions(
options: StorifyMeStoryPlaybackOptions(
behaviour: playbackOptions.behaviour
)
)
}
if let posterSettings = configuration.posterSettings {
widget.setGifPosterEnabled(posterSettings.gifEnabled)
widget.setVideoPosterEnabled(posterSettings.videoEnabled)
}
return widget
}
}
MVVM Pattern Integration
ViewModel Implementation
import Combine
class StoriesViewModel: ObservableObject {
@Published var widgetState: WidgetState = .loading
@Published var stories: [StorifyMeStory] = []
@Published var errorMessage: String?
private var storifyMeWidget: StorifyMeWidget?
private var cancellables = Set<AnyCancellable>()
func initializeWidget(widgetId: Int) {
widgetState = .loading
let widget = StorifyMeWidget()
widget.eventHandler = StoriesEventHandler { [weak self] result in
DispatchQueue.main.async {
self?.handleWidgetResult(result)
}
}
self.storifyMeWidget = widget
widget.setWidgetId(widgetId: widgetId)
widget.load()
}
private func handleWidgetResult(_ result: WidgetResult) {
switch result {
case .loaded(let stories):
self.widgetState = .loaded
self.stories = stories
self.errorMessage = nil
case .failed(let error):
self.widgetState = .error
self.errorMessage = error
case .storyOpened(let handle):
trackStoryOpen(handle: handle)
}
}
private func trackStoryOpen(handle: String) {
// Analytics tracking
print("Story opened: \(handle)")
}
func openStory(by handle: String) {
storifyMeWidget?.openWidgetStoryByHandle(handle) { result in
// Handle result
}
}
}
enum WidgetResult {
case loaded([StorifyMeStory])
case failed(String)
case storyOpened(String)
}
class StoriesEventHandler: NSObject, StorifyMeStoryEventProtocol {
private let resultHandler: (WidgetResult) -> Void
init(resultHandler: @escaping (WidgetResult) -> Void) {
self.resultHandler = resultHandler
}
func onLoad(widgetId: Int, storyList: [StorifyMeStory]) {
resultHandler(.loaded(storyList))
}
func onFail(widgetId: Int, error: String) {
resultHandler(.failed(error))
}
func onStoryOpen(widgetId: Int, storyId: Int, storyHandle: String) {
resultHandler(.storyOpened(storyHandle))
}
}
SwiftUI Integration
import SwiftUI
struct StoriesView: View {
@StateObject private var viewModel = StoriesViewModel()
let widgetId: Int
var body: some View {
VStack {
switch viewModel.widgetState {
case .loading:
ProgressView("Loading stories...")
.frame(height: 120)
case .loaded:
StorifyMeWidgetView(
widgetId: widgetId,
viewModel: viewModel
)
.frame(height: 120)
case .error:
ErrorView(message: viewModel.errorMessage ?? "Unknown error")
.frame(height: 120)
}
}
.onAppear {
viewModel.initializeWidget(widgetId: widgetId)
}
}
}
struct StorifyMeWidgetView: UIViewRepresentable {
let widgetId: Int
let viewModel: StoriesViewModel
func makeUIView(context: Context) -> StorifyMeWidget {
let widget = StorifyMeWidget()
widget.eventHandler = context.coordinator
widget.setWidgetId(widgetId: widgetId)
widget.load()
return widget
}
func updateUIView(_ uiView: StorifyMeWidget, context: Context) {
// Update if needed
}
func makeCoordinator() -> Coordinator {
Coordinator(viewModel)
}
class Coordinator: NSObject, StorifyMeStoryEventProtocol {
let viewModel: StoriesViewModel
init(_ viewModel: StoriesViewModel) {
self.viewModel = viewModel
}
func onLoad(widgetId: Int, storyList: [StorifyMeStory]) {
DispatchQueue.main.async {
self.viewModel.widgetState = .loaded
self.viewModel.stories = storyList
}
}
func onFail(widgetId: Int, error: String) {
DispatchQueue.main.async {
self.viewModel.widgetState = .error
self.viewModel.errorMessage = error
}
}
}
}
struct ErrorView: View {
let message: String
var body: some View {
VStack {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.orange)
.font(.title)
Text(message)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
}
Custom Analytics Integration
Analytics Wrapper
protocol AnalyticsTracker {
func trackStoryOpen(storyHandle: String, widgetId: Int)
func trackStoryClose(storyHandle: String, duration: TimeInterval)
func trackStoryAction(actionType: String, actionUrl: String)
func trackWidgetLoad(widgetId: Int, storyCount: Int, loadTime: TimeInterval)
}
class CompositeAnalyticsTracker: AnalyticsTracker {
private let trackers: [AnalyticsTracker]
init(trackers: [AnalyticsTracker]) {
self.trackers = trackers
}
func trackStoryOpen(storyHandle: String, widgetId: Int) {
trackers.forEach { $0.trackStoryOpen(storyHandle: storyHandle, widgetId: widgetId) }
}
func trackStoryClose(storyHandle: String, duration: TimeInterval) {
trackers.forEach { $0.trackStoryClose(storyHandle: storyHandle, duration: duration) }
}
func trackStoryAction(actionType: String, actionUrl: String) {
trackers.forEach { $0.trackStoryAction(actionType: actionType, actionUrl: actionUrl) }
}
func trackWidgetLoad(widgetId: Int, storyCount: Int, loadTime: TimeInterval) {
trackers.forEach { $0.trackWidgetLoad(widgetId: widgetId, storyCount: storyCount, loadTime: loadTime) }
}
}
class StorifyMeAnalyticsHandler: NSObject, StorifyMeStoryEventProtocol {
private let analyticsTracker: AnalyticsTracker
private var storyOpenTimes: [String: Date] = [:]
private var widgetLoadStartTime: Date?
init(analyticsTracker: AnalyticsTracker) {
self.analyticsTracker = analyticsTracker
}
func startLoadTimer() {
widgetLoadStartTime = Date()
}
func onLoad(widgetId: Int, storyList: [StorifyMeStory]) {
if let startTime = widgetLoadStartTime {
let loadTime = Date().timeIntervalSince(startTime)
analyticsTracker.trackWidgetLoad(widgetId: widgetId, storyCount: storyList.count, loadTime: loadTime)
}
}
func onStoryOpen(widgetId: Int, storyId: Int, storyHandle: String) {
storyOpenTimes[storyHandle] = Date()
analyticsTracker.trackStoryOpen(storyHandle: storyHandle, widgetId: widgetId)
}
func onStoryClose(widgetId: Int, storyId: Int, storyHandle: String) {
if let openTime = storyOpenTimes[storyHandle] {
let duration = Date().timeIntervalSince(openTime)
analyticsTracker.trackStoryClose(storyHandle: storyHandle, duration: duration)
storyOpenTimes.removeValue(forKey: storyHandle)
}
}
func onAction(widgetId: Int, storyId: Int, actionType: String, actionUrl: String) {
analyticsTracker.trackStoryAction(actionType: actionType, actionUrl: actionUrl)
}
}
Dynamic Configuration
Remote Configuration Manager
struct RemoteWidgetConfig: Codable {
let widgetId: Int
let enabled: Bool
let audioEnabled: Bool
let gifPostersEnabled: Bool
let videoPostersEnabled: Bool
let customTheme: String?
}
class DynamicConfigurationManager {
private var configCache: [Int: RemoteWidgetConfig] = [:]
private let urlSession = URLSession.shared
func getWidgetConfig(widgetId: Int) async throws -> RemoteWidgetConfig {
if let cachedConfig = configCache[widgetId] {
return cachedConfig
}
return try await fetchConfigFromServer(widgetId: widgetId)
}
private func fetchConfigFromServer(widgetId: Int) async throws -> RemoteWidgetConfig {
// Replace with your actual API endpoint
let url = URL(string: "https://your-api.com/widget-config/\(widgetId)")!
let (data, _) = try await urlSession.data(from: url)
let config = try JSONDecoder().decode(RemoteWidgetConfig.self, from: data)
configCache[widgetId] = config
return config
}
func applyConfiguration(_ storifyMeWidget: StorifyMeWidget, config: RemoteWidgetConfig) {
guard config.enabled else {
storifyMeWidget.isHidden = true
return
}
storifyMeWidget.isHidden = false
let audioOptions = StorifyMeStoryAudioOptions(
behaviour: config.audioEnabled ? .applyLastUserChangeForAllFutureStories : .applyChangeForSingleStory,
defaultState: config.audioEnabled ? .unmuted : .muted
)
storifyMeWidget.setWidgetAudioOptions(options: audioOptions)
storifyMeWidget.setGifPosterEnabled(config.gifPostersEnabled)
storifyMeWidget.setVideoPosterEnabled(config.videoPostersEnabled)
if let theme = config.customTheme {
applyCustomTheme(storifyMeWidget, theme: theme)
}
}
private func applyCustomTheme(_ storifyMeWidget: StorifyMeWidget, theme: String) {
switch theme {
case "dark":
applyDarkTheme(storifyMeWidget)
case "brand":
applyBrandTheme(storifyMeWidget)
default:
break
}
}
private func applyDarkTheme(_ storifyMeWidget: StorifyMeWidget) {
// Apply dark theme customizations
storifyMeWidget.backgroundColor = .black
}
private func applyBrandTheme(_ storifyMeWidget: StorifyMeWidget) {
// Apply brand-specific customizations
storifyMeWidget.backgroundColor = UIColor(named: "BrandColor")
}
}
Navigation Integration
UINavigationController Integration
class StoriesNavigationController: UINavigationController {
private var deepLinkQueue: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupDeepLinkHandling()
}
private func setupDeepLinkHandling() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDeepLink(_:)),
name: NSNotification.Name("StorifyMeDeepLink"),
object: nil
)
}
@objc private func handleDeepLink(_ notification: Notification) {
guard let handle = notification.object as? String else { return }
if let storiesVC = topViewController as? StoriesViewController {
storiesVC.openStory(by: handle)
} else {
// Queue for later
deepLinkQueue.append(handle)
navigateToStories()
}
}
private func navigateToStories() {
let storiesVC = StoriesViewController()
// Process queued deep links
storiesVC.onViewDidAppear = { [weak self] in
self?.processQueuedDeepLinks(in: storiesVC)
}
pushViewController(storiesVC, animated: true)
}
private func processQueuedDeepLinks(in viewController: StoriesViewController) {
for handle in deepLinkQueue {
viewController.openStory(by: handle)
break // Only open the first one
}
deepLinkQueue.removeAll()
}
}
class StoriesViewController: UIViewController {
@IBOutlet weak var storifyMeWidget: StorifyMeWidget!
var onViewDidAppear: (() -> Void)?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
onViewDidAppear?()
}
func openStory(by handle: String) {
storifyMeWidget.openWidgetStoryByHandle(handle) { result in
// Handle result
}
}
}
Testing Patterns
Unit Testing with Mocks
import XCTest
class MockStorifyMeEventHandler: StorifyMeStoryEventProtocol {
var didCallOnLoad = false
var didCallOnFail = false
var lastLoadedStories: [StorifyMeStory] = []
var lastError: String?
func onLoad(widgetId: Int, storyList: [StorifyMeStory]) {
didCallOnLoad = true
lastLoadedStories = storyList
}
func onFail(widgetId: Int, error: String) {
didCallOnFail = true
lastError = error
}
}
class StoriesViewModelTests: XCTestCase {
private var viewModel: StoriesViewModel!
private var mockEventHandler: MockStorifyMeEventHandler!
override func setUp() {
super.setUp()
viewModel = StoriesViewModel()
mockEventHandler = MockStorifyMeEventHandler()
}
func testWidgetInitialization() {
// Given
let widgetId = 123
// When
viewModel.initializeWidget(widgetId: widgetId)
// Then
XCTAssertEqual(viewModel.widgetState, .loading)
}
func testSuccessfulWidgetLoad() {
// Given
let mockStories = createMockStories()
// When
mockEventHandler.onLoad(widgetId: 123, storyList: mockStories)
// Then
XCTAssertTrue(mockEventHandler.didCallOnLoad)
XCTAssertEqual(mockEventHandler.lastLoadedStories.count, mockStories.count)
}
private func createMockStories() -> [StorifyMeStory] {
// Create mock story objects for testing
return []
}
}
UI Testing
import XCTest
class StoriesUITests: XCTestCase {
private var app: XCUIApplication!
override func setUp() {
super.setUp()
app = XCUIApplication()
app.launchArguments.append("--uitesting")
app.launch()
}
func testStoriesWidgetDisplays() {
// Wait for stories widget to appear
let storiesWidget = app.otherElements["StoriesWidget"]
XCTAssertTrue(storiesWidget.waitForExistence(timeout: 10))
}
func testStoryOpensOnTap() {
// Given
let storiesWidget = app.otherElements["StoriesWidget"]
XCTAssertTrue(storiesWidget.waitForExistence(timeout: 10))
// When
storiesWidget.tap()
// Then
let storyViewer = app.otherElements["StoryViewer"]
XCTAssertTrue(storyViewer.waitForExistence(timeout: 5))
}
}
Memory Management Patterns
Lifecycle-Aware Widget Manager
class StoriesLifecycleManager {
private var activeWidgets: Set<StorifyMeWidget> = []
private var observers: [NSObjectProtocol] = []
init() {
setupNotificationObservers()
}
private func setupNotificationObservers() {
let didEnterBackgroundObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.pauseAllWidgets()
}
let willEnterForegroundObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.resumeAllWidgets()
}
let didReceiveMemoryWarningObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.optimizeAllWidgetsForMemory()
}
observers = [didEnterBackgroundObserver, willEnterForegroundObserver, didReceiveMemoryWarningObserver]
}
func registerWidget(_ widget: StorifyMeWidget) {
activeWidgets.insert(widget)
}
func unregisterWidget(_ widget: StorifyMeWidget) {
activeWidgets.remove(widget)
}
private func pauseAllWidgets() {
activeWidgets.forEach { widget in
// Pause widget if SDK provides method
// widget.pause()
}
}
private func resumeAllWidgets() {
activeWidgets.forEach { widget in
// Resume widget if SDK provides method
// widget.resume()
}
}
private func optimizeAllWidgetsForMemory() {
activeWidgets.forEach { widget in
widget.setGifPosterEnabled(false)
widget.setVideoPosterEnabled(false)
}
}
deinit {
observers.forEach { NotificationCenter.default.removeObserver($0) }
}
}
These patterns provide a comprehensive foundation for integrating StorifyMe into complex iOS applications while maintaining clean architecture, testability, and performance.