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, storyList: [StorifyMeStory]) {
preloader?.didPreload(widgetId: widgetId, stories: storyList)
}
func onFail(widgetId: Int, error: String) {
print("Preload failed for widget \(widgetId): \(error)")
}
}
Memory Management
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, storyList: [StorifyMeStory]) {
// Handle load completion
}
func onFail(widgetId: Int, error: String) {
print("Widget \(widgetId) failed to load: \(error)")
}
}
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, storyList: [StorifyMeStory]) {
performanceMonitor.recordLoadTime(for: widgetId)
performanceMonitor.recordMemoryUsage(for: widgetId)
}
func onFail(widgetId: Int, error: String) {
print("Widget \(widgetId) failed to load: \(error)")
}
}
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
-
Default Configuration (Balanced):
storifyMeWidget.setGifPosterEnabled(true)
storifyMeWidget.setVideoPosterEnabled(false) -
Performance-First Configuration:
storifyMeWidget.setGifPosterEnabled(false)
storifyMeWidget.setVideoPosterEnabled(false) -
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
Metric | Good | Acceptable | Poor |
---|---|---|---|
Load Time | < 1s | 1-3s | > 3s |
Memory Usage | < 50MB | 50-100MB | > 100MB |
Battery Impact | < 5% per hour | 5-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.