Skip to main content

Performance Optimization

This guide covers performance optimization techniques you can implement to ensure smooth StorifyMe integration in your iOS application.

Poster Configuration

Optimizing Poster Types

The most impactful performance optimization you can control is managing poster types:

// ✅ Recommended for performance-sensitive apps
storifyMeWidget.setGifPosterEnabled(false) // Reduces memory usage significantly
storifyMeWidget.setVideoPosterEnabled(false) // Reduces memory and CPU usage

// ✅ For apps with good performance headroom
storifyMeWidget.setGifPosterEnabled(true) // Better user experience
storifyMeWidget.setVideoPosterEnabled(false) // Still conservative on resources

// ⚠️ Use with caution - highest resource usage
storifyMeWidget.setGifPosterEnabled(true)
storifyMeWidget.setVideoPosterEnabled(true) // Maximum visual appeal but highest cost

Performance Impact:

  • GIF Posters: 2-5x more memory usage per story
  • Video Posters: 3-8x more memory usage + CPU for decoding
  • Static Images: Baseline memory usage

Dynamic Poster Configuration

class AdaptivePosterManager {
func configurePostersBasedOnDevice(_ storifyMeWidget: StorifyMeWidget) {
let deviceMemory = ProcessInfo.processInfo.physicalMemory
let memoryGB = deviceMemory / (1024 * 1024 * 1024)

// Check device model for performance classification
let deviceModel = UIDevice.current.model
let isOlderDevice = isLowPerformanceDevice()

switch (memoryGB, isOlderDevice) {
case (0..<3, true), (0..<4, false):
// Low-end device
storifyMeWidget.setGifPosterEnabled(false)
storifyMeWidget.setVideoPosterEnabled(false)

case (3..<6, _):
// Mid-range device
storifyMeWidget.setGifPosterEnabled(true)
storifyMeWidget.setVideoPosterEnabled(false)

default:
// High-end device
storifyMeWidget.setGifPosterEnabled(true)
storifyMeWidget.setVideoPosterEnabled(true)
}
}

private func isLowPerformanceDevice() -> Bool {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value))!)
}

// List of older device identifiers that should use conservative settings
let lowPerformanceDevices = [
"iPhone8,1", "iPhone8,2", "iPhone8,4", // iPhone 6s, 6s Plus, SE 1st gen
"iPhone9,1", "iPhone9,2", "iPhone9,3", "iPhone9,4", // iPhone 7, 7 Plus
"iPad6,7", "iPad6,8", "iPad6,11", "iPad6,12" // iPad Pro 1st gen, iPad 5th gen
]

return lowPerformanceDevices.contains(identifier)
}
}

Loading Strategies

Lazy Loading Pattern

class LazyStoriesManager {
private weak var storifyMeWidget: StorifyMeWidget?
private var isLoaded = false
private var observer: NSKeyValueObservation?

func loadWhenVisible(_ widget: StorifyMeWidget, in scrollView: UIScrollView) {
self.storifyMeWidget = widget

// Observe scroll view content offset
observer = scrollView.observe(\.contentOffset, options: [.new]) { [weak self] _, _ in
self?.checkVisibilityAndLoad(in: scrollView)
}
}

private func checkVisibilityAndLoad(in scrollView: UIScrollView) {
guard !isLoaded,
let widget = storifyMeWidget,
isViewVisible(widget, in: scrollView) else { return }

widget.load()
isLoaded = true
observer?.invalidate()
}

private func isViewVisible(_ view: UIView, in scrollView: UIScrollView) -> Bool {
let viewFrame = view.convert(view.bounds, to: scrollView)
let visibleRect = CGRect(
x: scrollView.contentOffset.x,
y: scrollView.contentOffset.y,
width: scrollView.frame.width,
height: scrollView.frame.height
)

return visibleRect.intersects(viewFrame) &&
viewFrame.intersection(visibleRect).height > view.frame.height / 2
}

deinit {
observer?.invalidate()
}
}

// Usage in UITableViewCell
class StoriesTableViewCell: UITableViewCell {
@IBOutlet weak var storifyMeWidget: StorifyMeWidget!
private let lazyManager = LazyStoriesManager()

func configure(widgetId: Int, tableView: UITableView) {
storifyMeWidget.setWidgetId(widgetId: widgetId)
lazyManager.loadWhenVisible(storifyMeWidget, in: tableView)
}
}

Preloading Strategy

class StoriesPreloader {
private var preloadedWidgets: [Int: [StorifyMeStory]] = [:]
private var preloadViews: [Int: StorifyMeWidget] = [:]

func preloadWidget(widgetId: Int) {
guard preloadedWidgets[widgetId] == nil else { return }

let preloadWidget = StorifyMeWidget()
preloadWidget.eventHandler = PreloadEventHandler(widgetId: widgetId, preloader: self)
preloadWidget.setWidgetId(widgetId: widgetId)

preloadViews[widgetId] = preloadWidget
preloadWidget.load()
}

func isPreloaded(widgetId: Int) -> Bool {
return preloadedWidgets[widgetId] != nil
}

fileprivate func didPreload(widgetId: Int, stories: [StorifyMeStory]) {
preloadedWidgets[widgetId] = stories
preloadViews[widgetId] = nil // Clean up preload view
}
}

private class PreloadEventHandler: NSObject, StorifyMeStoryEventProtocol {
let widgetId: Int
weak var preloader: StoriesPreloader?

init(widgetId: Int, preloader: StoriesPreloader) {
self.widgetId = widgetId
self.preloader = preloader
}

func onLoad(widgetId: Int, stories: [StorifyMeStoryModel]) {
preloader?.didPreload(widgetId: widgetId, stories: stories)
}

func onFail(error: StorifyMeError) {
print("Preload failed: \(error.message)")
}
}

Memory Management

SDK Resource Management

For significant memory optimization, use the shutdown() method to release all SDK resources including WebView pools:

class SDKResourceManager {
/// Shutdown SDK to release memory when stories are not needed
static func releaseSDKResources(completion: (() -> Void)? = nil) {
StorifyMeInstance.shared.shutdown {
print("SDK resources released")
completion?()
}
}

/// Re-initialize SDK when stories are needed again
static func reinitializeIfNeeded(accountId: String, apiKey: String, completion: ((Bool) -> Void)? = nil) {
guard !StorifyMeInstance.shared.isReady else {
completion?(true)
return
}

StorifyMeInstance.shared.initialize(
accountId: accountId,
apiKey: apiKey,
env: .EU
) { result in
switch result {
case .success:
completion?(true)
case .failure:
completion?(false)
}
}
}
}

// Usage in App Lifecycle
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
// Release SDK resources on memory warning if stories are not visible
if !isStoriesCurrentlyVisible() {
SDKResourceManager.releaseSDKResources()
}
}

private func isStoriesCurrentlyVisible() -> Bool {
// Check if any stories UI is currently visible
return false
}
}
Memory Impact

Calling shutdown() releases:

  • All pre-allocated WebViews (WebView pool)
  • Cached story data
  • Internal observers and handlers

This can free 50-100MB+ of memory depending on usage patterns.

GIF Cache Management

The SDK automatically manages GIF caching with sensible defaults. For most apps, no configuration is needed.

Default Behavior:

  • Cache expiration: 7 days
  • Automatic cleanup of expired cache entries
  • Memory cache cleared on memory warnings

Optional Configuration:

// Adjust cache expiration (default: 7 days)
StorifyMeInstance.shared.diskCacheMaxAge = 7 * 24 * 60 * 60

// Disable automatic cleanup if you want manual control
StorifyMeInstance.shared.autoCleanExpiredCache = false

Manual Cache Management:

// Check current cache size
if let size = StorifyMeInstance.shared.getGIFDiskCacheSize() {
let sizeMB = Double(size) / 1024 / 1024
print("GIF cache size: \(String(format: "%.2f", sizeMB)) MB")
}

// Clear all caches (memory + disk)
StorifyMeInstance.shared.clearAllGIFCaches()

When to Clear Caches:

ScenarioAction
Memory warningSDK handles automatically
User logoutCall clearAllGIFCaches()
App settings "Clear Cache" buttonCall clearAllGIFCaches()
Normal operationNo action needed
tip

The SDK's automatic cache management is sufficient for most apps. Only use manual cache clearing for specific scenarios like user logout or explicit user requests.

Widget Cleanup

class StoriesMemoryManager {
static func cleanupWidget(_ storifyMeWidget: StorifyMeWidget) {
// Clear event handler to prevent retain cycles
storifyMeWidget.eventHandler = nil

// Remove from superview if needed
if storifyMeWidget.superview != nil {
storifyMeWidget.removeFromSuperview()
}
}

static func optimizeForMemory(_ storifyMeWidget: StorifyMeWidget) {
storifyMeWidget.setGifPosterEnabled(false)
storifyMeWidget.setVideoPosterEnabled(false)
}
}

// In ViewController lifecycle
class StoriesViewController: UIViewController {
@IBOutlet weak var storifyMeWidget: StorifyMeWidget!

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)

if isMovingFromParent {
StoriesMemoryManager.cleanupWidget(storifyMeWidget)
}
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
StoriesMemoryManager.optimizeForMemory(storifyMeWidget)
}
}

Memory Monitoring

class MemoryMonitor {
private var timer: Timer?

func startMonitoring(callback: @escaping (MemoryStatus) -> Void) {
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
let memoryStatus = self.getCurrentMemoryStatus()
callback(memoryStatus)
}
}

func stopMonitoring() {
timer?.invalidate()
timer = nil
}

private func getCurrentMemoryStatus() -> MemoryStatus {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4

let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count)
}
}

guard kerr == KERN_SUCCESS else {
return MemoryStatus(usedMemory: 0, availableMemory: 0, maxMemory: 0, usagePercentage: 0)
}

let usedMemory = Int64(info.resident_size)
let totalMemory = Int64(ProcessInfo.processInfo.physicalMemory)
let availableMemory = totalMemory - usedMemory
let usagePercentage = Float(usedMemory) / Float(totalMemory) * 100

return MemoryStatus(
usedMemory: usedMemory,
availableMemory: availableMemory,
maxMemory: totalMemory,
usagePercentage: usagePercentage
)
}
}

struct MemoryStatus {
let usedMemory: Int64
let availableMemory: Int64
let maxMemory: Int64
let usagePercentage: Float
}

// Usage
class StoriesViewController: UIViewController {
private let memoryMonitor = MemoryMonitor()

override func viewDidLoad() {
super.viewDidLoad()

memoryMonitor.startMonitoring { [weak self] status in
DispatchQueue.main.async {
if status.usagePercentage > 80 {
self?.optimizeForLowMemory()
}
}
}
}

private func optimizeForLowMemory() {
storifyMeWidget.setGifPosterEnabled(false)
storifyMeWidget.setVideoPosterEnabled(false)
}

deinit {
memoryMonitor.stopMonitoring()
}
}

Network Optimization

Connection-Aware Loading

import Network

class NetworkAwareStoriesManager {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")

func configureForCurrentNetwork(_ storifyMeWidget: StorifyMeWidget) {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.updateConfigurationForPath(path, widget: storifyMeWidget)
}
}
monitor.start(queue: queue)
}

private func updateConfigurationForPath(_ path: NWPath, widget: StorifyMeWidget) {
let connectionType = getConnectionType(from: path)

switch connectionType {
case .wifi:
// Full quality on Wi-Fi
widget.setGifPosterEnabled(true)
widget.setVideoPosterEnabled(true)

case .cellular:
// Check cellular type
if path.usesInterfaceType(.cellular) {
configureCellularOptimization(widget)
}

case .none:
handleOfflineMode(widget)
}
}

private func configureCellularOptimization(_ widget: StorifyMeWidget) {
// Conservative settings for cellular
widget.setGifPosterEnabled(true)
widget.setVideoPosterEnabled(false)
}

private func getConnectionType(from path: NWPath) -> ConnectionType {
if path.usesInterfaceType(.wifi) {
return .wifi
} else if path.usesInterfaceType(.cellular) {
return .cellular
} else {
return .none
}
}

private func handleOfflineMode(_ widget: StorifyMeWidget) {
// Handle offline state - widget will show cached content if available
widget.isHidden = false // Let widget handle offline state
}

deinit {
monitor.cancel()
}
}

enum ConnectionType {
case wifi, cellular, none
}

UICollectionView/UITableView Optimization

Efficient Cell Reuse

class StoriesCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var storifyMeWidget: StorifyMeWidget!
private var currentWidgetId: Int = 0

override func prepareForReuse() {
super.prepareForReuse()

// Clean up previous widget
if currentWidgetId != 0 {
StoriesMemoryManager.cleanupWidget(storifyMeWidget)
currentWidgetId = 0
}
}

func configure(with item: WidgetItem) {
guard currentWidgetId != item.widgetId else { return }

// Clean up previous widget if different
if currentWidgetId != 0 {
StoriesMemoryManager.cleanupWidget(storifyMeWidget)
}

currentWidgetId = item.widgetId

// Configure for performance
storifyMeWidget.setGifPosterEnabled(item.allowGifPosters)
storifyMeWidget.setVideoPosterEnabled(item.allowVideoPosters)

storifyMeWidget.eventHandler = self
storifyMeWidget.setWidgetId(widgetId: item.widgetId)
storifyMeWidget.load()
}
}

extension StoriesCollectionViewCell: StorifyMeStoryEventProtocol {
func onLoad(widgetId: Int, stories: [StorifyMeStoryModel]) {
// Handle load completion
}

func onFail(error: StorifyMeError) {
print("Widget failed to load: \(error.message)")
}
}

struct WidgetItem {
let widgetId: Int
let allowGifPosters: Bool
let allowVideoPosters: Bool

init(widgetId: Int, allowGifPosters: Bool = true, allowVideoPosters: Bool = false) {
self.widgetId = widgetId
self.allowGifPosters = allowGifPosters
self.allowVideoPosters = allowVideoPosters
}
}

Performance Monitoring

Performance Metrics Collection

class StoriesPerformanceMonitor {
private var loadStartTime: Date?
private var performanceMetrics: [String: TimeInterval] = [:]

func startLoadTimer() {
loadStartTime = Date()
}

func recordLoadTime(for widgetId: Int) {
guard let startTime = loadStartTime else { return }

let loadTime = Date().timeIntervalSince(startTime)
performanceMetrics["widget_\(widgetId)_load_time"] = loadTime

print("Widget \(widgetId) loaded in \(String(format: "%.2f", loadTime))s")

// Track slow loading for analytics
if loadTime > 3.0 {
trackSlowLoading(widgetId: widgetId, loadTime: loadTime)
}

loadStartTime = nil
}

func recordMemoryUsage(for widgetId: Int) {
let memoryUsage = getCurrentMemoryUsage()
performanceMetrics["widget_\(widgetId)_memory"] = TimeInterval(memoryUsage)

print("Widget \(widgetId) memory usage: \(memoryUsage / 1024 / 1024)MB")
}

private func getCurrentMemoryUsage() -> Int64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4

let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count)
}
}

return kerr == KERN_SUCCESS ? Int64(info.resident_size) : 0
}

private func trackSlowLoading(widgetId: Int, loadTime: TimeInterval) {
// Send to your analytics service
// Analytics.track("slow_widget_loading", properties: [
// "widget_id": widgetId,
// "load_time": loadTime
// ])
}
}

// Usage with event handler
class PerformanceTrackingEventHandler: NSObject, StorifyMeStoryEventProtocol {
private let performanceMonitor = StoriesPerformanceMonitor()

func startTracking() {
performanceMonitor.startLoadTimer()
}

func onLoad(widgetId: Int, stories: [StorifyMeStoryModel]) {
performanceMonitor.recordLoadTime(for: widgetId)
performanceMonitor.recordMemoryUsage(for: widgetId)
}

func onFail(error: StorifyMeError) {
print("Widget failed to load: \(error.message)")
}
}

Battery Optimization

Power-Efficient Configuration

class BatteryOptimizedStoriesManager {
func configureForBatteryMode(_ storifyMeWidget: StorifyMeWidget) {
let isLowPowerModeEnabled = ProcessInfo.processInfo.isLowPowerModeEnabled

if isLowPowerModeEnabled {
// Aggressive battery saving
storifyMeWidget.setGifPosterEnabled(false)
storifyMeWidget.setVideoPosterEnabled(false)
} else {
// Normal operation - still conservative
storifyMeWidget.setGifPosterEnabled(true)
storifyMeWidget.setVideoPosterEnabled(false)
}
}

func registerLowPowerModeObserver(callback: @escaping (Bool) -> Void) {
NotificationCenter.default.addObserver(
forName: .NSProcessInfoPowerStateDidChange,
object: nil,
queue: .main
) { _ in
let isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled
callback(isLowPowerMode)
}
}
}

// Usage
class StoriesViewController: UIViewController {
private let batteryManager = BatteryOptimizedStoriesManager()

override func viewDidLoad() {
super.viewDidLoad()

batteryManager.configureForBatteryMode(storifyMeWidget)

batteryManager.registerLowPowerModeObserver { [weak self] isLowPowerMode in
self?.handleLowPowerModeChange(isLowPowerMode)
}
}

private func handleLowPowerModeChange(_ isLowPowerMode: Bool) {
batteryManager.configureForBatteryMode(storifyMeWidget)
}
}

Performance Best Practices Summary

Configuration Recommendations

  1. Default Configuration (Balanced):

    storifyMeWidget.setGifPosterEnabled(true)
    storifyMeWidget.setVideoPosterEnabled(false)
  2. Performance-First Configuration:

    storifyMeWidget.setGifPosterEnabled(false)
    storifyMeWidget.setVideoPosterEnabled(false)
  3. Visual-First Configuration (use carefully):

    storifyMeWidget.setGifPosterEnabled(true)
    storifyMeWidget.setVideoPosterEnabled(true)

Monitoring Checklist

  • Monitor memory usage with Instruments
  • Test on older devices (iPhone 8, iPad Air 2)
  • Measure widget loading times
  • Test with different network conditions
  • Monitor battery usage during extended use
  • Test with multiple widgets in collection views
  • Verify performance in low power mode

Key Performance Indicators

MetricGoodAcceptablePoor
Load Time< 1s1-3s> 3s
Memory Usage< 50MB50-100MB> 100MB
Battery Impact< 5% per hour5-10% per hour> 10% per hour

iOS-Specific Considerations

  • Background App Refresh: Configure widgets to handle background state changes
  • Low Power Mode: Automatically reduce functionality when enabled
  • Memory Pressure: Respond to memory warnings by optimizing settings
  • Thermal State: Consider device thermal state for resource-intensive operations
  • Network Conditions: Adapt to cellular vs Wi-Fi automatically

By following these optimization strategies, you can ensure StorifyMe integrates smoothly into your iOS app while maintaining excellent performance across all device types and conditions.