StoriesKit is a modern Swift library for creating beautiful Instagram-style stories with support for both UIKit and SwiftUI. The library provides ready-to-use components for displaying stories with navigation, timers, and interactive elements.
β Like this project? Give it a star on GitHub! Your support helps me continue development and add new features.
π Want to see more? Follow me for updates and new releases!
- π¨ Beautiful Design β Modern UI in the style of popular social networks
- β‘ High Performance β Optimized architecture using SwiftUI and Combine
- πΌοΈ Media Support β Images and videos with smooth playback
- π₯ Video Playback β Advanced video player with preloading and state management
- β±οΈ Smart Timers β Configurable story duration with video synchronization
- π― Interactivity β Support for buttons, links, and gestures
- π± Responsive β Support for various screen sizes
- π Navigation β Smooth transitions between stories and groups
- ποΈ Flexible Customization β Rich customization options
- ποΈ Dual Platform Support β Works in both UIKit and SwiftUI
- πͺ Custom Content β Support for custom SwiftUI views in stories
- π¨ Theming β Centralized configuration with StoriesModel
Add StoriesKit to your project via Swift Package Manager:
dependencies: [
.package(url: "https://github.com/dimzfresh/StoriesKit.git", from: "2.0.0")
]import StoriesKit
// Create stories configuration
let storiesModel = StoriesModel(
groups: [
StoriesGroupModel(
id: "user1",
title: "User 1",
avatarImage: .url(URL(string: "https://example.com/avatar.jpg")!),
stories: [
StoriesPageModel(
title: AttributedString("Story Title"),
subtitle: AttributedString("Story Subtitle"),
backgroundColor: .systemBlue,
mediaSource: StoriesMediaModel(
media: .image(.url(URL(string: "https://example.com/story.jpg")!))
),
duration: 5.0
)
]
)
],
backgroundColor: .black,
progress: StoriesModel.Progress(
lineSize: 3.0,
gap: 2.0,
viewedColor: .gray,
unviewedColor: .green,
interItemSpacing: 4.0,
containerPadding: 16.0
),
avatar: StoriesModel.Avatar(
size: 60.0,
padding: 8.0,
progressPadding: 4.0
),
userName: StoriesModel.Text(
font: .systemFont(ofSize: 16, weight: .medium),
color: .white,
lineLimit: 1,
padding: 8.0,
spacingFromAvatar: 8.0,
multilineTextAlignment: .center
)
)
// Create stories for UIKit
let storiesViewController = Stories.build(
model: storiesModel,
delegate: self
)
// Present
present(storiesViewController, animated: true)import StoriesKit
import SwiftUI
struct ContentView: View {
var body: some View {
// Create pure SwiftUI View with StoriesModel
Stories.build(
model: StoriesModel(
groups: [
StoriesGroupModel(
id: "user1",
title: "User 1",
avatarImage: .url(URL(string: "https://example.com/avatar.jpg")!),
stories: [
StoriesPageModel(
title: AttributedString("Story Title"),
subtitle: AttributedString("Story Subtitle"),
backgroundColor: .blue,
mediaSource: StoriesMediaModel(
media: .image(.url(URL(string: "https://example.com/story.jpg")!))
),
duration: 5.0
)
]
)
],
backgroundColor: .black
)
)
}
}The new StoriesModel provides centralized configuration for all StoriesKit components:
let storiesModel = StoriesModel(
groups: [/* StoriesGroupModel array */],
backgroundColor: .black,
progress: StoriesModel.Progress(
lineSize: 3.0, // Progress bar thickness
gap: 2.0, // Gap between segments
viewedColor: .gray, // Color for viewed segments
unviewedColor: .green, // Color for unviewed segments
interItemSpacing: 4.0, // Spacing between progress bars
containerPadding: 16.0 // Container padding
),
avatar: StoriesModel.Avatar(
size: 60.0, // Avatar size
padding: 8.0, // Internal padding
progressPadding: 4.0 // Padding around progress circle
),
userName: StoriesModel.Text(
font: .systemFont(ofSize: 16, weight: .medium),
color: .white,
lineLimit: 1,
padding: 8.0,
spacingFromAvatar: 8.0,
multilineTextAlignment: .center
)
)Represents a group of stories (e.g., stories from one user):
StoriesGroupModel(
id: "unique_id",
title: "Group Title",
avatarImage: .url(URL(string: "avatar_url")!),
stories: [/* array of stories */]
)Individual story page with support for images, videos, and custom content:
// Image story
StoriesPageModel(
date: "Today",
mediaSource: StoriesMediaModel(
media: .image(.url(URL(string: "image_url")!))
),
duration: 4.0,
padding: EdgeInsets(top: 54, leading: 0, bottom: 44, trailing: 0),
cornerRadius: 12,
content: AnyView(
VStack(spacing: 0) {
Text("Story Title")
.font(.title)
.foregroundColor(.white)
.padding(.top, 32)
.padding(.horizontal, 16)
Text("Story Subtitle")
.font(.body)
.foregroundColor(.white.opacity(0.9))
.padding(.top, 8)
.padding(.horizontal, 16)
// Custom buttons in content
VStack(spacing: 12) {
Button("Next") {
// Handle next action
}
.frame(width: 148, height: 50)
.background(Color.blue)
.clipShape(RoundedRectangle(cornerRadius: 12))
Button("Learn More") {
// Handle link action
}
.frame(width: 148, height: 50)
.background(Color.green)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.padding(.bottom, 24)
}
)
)
// Video story
StoriesPageModel(
date: "Yesterday",
mediaSource: StoriesMediaModel(
media: .video(.url(URL(string: "video_url")!))
),
duration: 8.0,
padding: EdgeInsets(top: 54, leading: 0, bottom: 44, trailing: 0),
cornerRadius: 12
)Model for media (images and videos) with support for various sources:
// Image media
StoriesMediaModel(
media: .image(.url(URL(string: "image_url")!)) // or .image(UIImage)
)
// Video media
StoriesMediaModel(
media: .video(.url(URL(string: "video_url")!)) // or .video(AVAsset)
)
// Local video
StoriesMediaModel(
media: .video(.local(AVAsset(url: localVideoURL)))
)StoriesKit includes an advanced video player with:
- Preloading - Videos are preloaded to avoid black screen flickering
- State Management - Centralized video player state management
- Timer Synchronization - Video playback is synchronized with story timers
- Smooth Transitions - Seamless switching between videos
- Memory Efficient - Single player instance reused across all videos
Implement the IStoriesDelegate protocol to handle events:
extension YourViewController: IStoriesDelegate {
func didClose() {
// Story closed
}
func didOpenLink(url: URL) {
// Open link
UIApplication.shared.open(url)
}
func didOpenStory(storyId: String) {
// Open specific story
}
}// Next button
.actionType = .next
// Close button
.actionType = .close
// Link button
.actionType = .link(URL(string: "https://example.com")!)// No rounding
.corners = .none
// Circular button
.corners = .circle
// Custom rounding
.corners = .radius(12)The carousel supports both circular and rounded rectangle corner styles:
let configuration = StoriesCarouselConfiguration(
layout: StoriesCarouselConfiguration.Layout(
itemSpacing: 16,
horizontalPadding: 16,
corners: .radius(12) // Rounded rectangle with 12pt radius
),
avatar: StoriesCarouselConfiguration.Avatar(
size: 70,
progressPadding: 6
),
progress: StoriesCarouselConfiguration.Progress(
lineWidth: 3,
gap: 3,
viewedColor: .gray.opacity(0.6),
unviewedColor: .green.opacity(0.8)
)
)
StoriesCarouselView(
stateManager: stateManager,
avatarNamespace: avatarNamespace,
configuration: configuration
)// Circular carousel items (default)
corners: .circle
// Rounded rectangle with custom radius
corners: .radius(12) // 12pt corner radius
corners: .radius(8) // 8pt corner radiusThe corner style applies to both the avatar images and their progress indicator rings, ensuring visual consistency across all carousel items.
Buttons are now integrated directly into custom content views:
StoriesPageModel(
date: "Today",
mediaSource: StoriesMediaModel(
media: .image(.url(URL(string: "background_url")!))
),
content: AnyView(
VStack(spacing: 0) {
Text("Welcome to Stories")
.font(.title)
.foregroundColor(.white)
.padding(.top, 32)
.padding(.horizontal, 16)
Text("Discover amazing content")
.font(.body)
.foregroundColor(.white.opacity(0.9))
.padding(.top, 8)
.padding(.horizontal, 16)
// Custom buttons with actions
VStack(spacing: 12) {
Button("Get Started") {
// Handle button action
}
.frame(width: 148, height: 50)
.background(Color.blue)
.clipShape(RoundedRectangle(cornerRadius: 12))
Button("Learn More") {
// Handle link action
}
.frame(width: 148, height: 50)
.background(Color.green)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.padding(.bottom, 24)
}
),
duration: 6.0
)let videoStories = [
StoriesPageModel(
title: AttributedString("Amazing Video"),
subtitle: AttributedString("Check out this cool content"),
backgroundColor: .black,
mediaSource: StoriesMediaModel(
media: .video(.url(URL(string: "https://example.com/video.mp4")!))
),
content: AnyView(
VStack {
Text("π¬ Video Story")
.font(.title)
.foregroundColor(.white)
Text("Tap to interact")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
}
),
duration: 10.0
)
]let mixedStories = [
// Image story
StoriesPageModel(
title: AttributedString("Photo Story"),
mediaSource: StoriesMediaModel(
media: .image(.url(URL(string: "https://example.com/photo.jpg")!))
),
duration: 4.0
),
// Video story
StoriesPageModel(
title: AttributedString("Video Story"),
mediaSource: StoriesMediaModel(
media: .video(.url(URL(string: "https://example.com/video.mp4")!))
),
duration: 8.0
)
]import StoriesKit
import UIKit
class MainViewController: UIViewController {
private let storiesContainerView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupStories()
}
private func setupUI() {
view.addSubview(storiesContainerView)
storiesContainerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
storiesContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
storiesContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
storiesContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
storiesContainerView.heightAnchor.constraint(equalToConstant: 300)
])
}
private func setupStories() {
let stories: [StoriesPageModel] = [
.init(
title: pageTitle("Welcome to Stories"),
subtitle: pageSubtitle("Discover amazing content\nand share your moments\nwith the world!"),
backgroundColor: .systemBlue,
button: .init(
title: actionButtonTitle("Next"),
backgroundColor: .white,
corners: .radius(12),
actionType: .next
),
backgroundImage: .init(image: .image(UIImage(named: "story1")))
),
.init(
title: pageTitle("Interactive Features"),
subtitle: pageSubtitle("Tap to navigate, swipe to change\nstories, and enjoy smooth\ntransitions between content."),
backgroundColor: .systemPurple,
button: .init(
title: actionButtonTitle("Got it"),
backgroundColor: .white,
corners: .radius(12),
actionType: .close
),
backgroundImage: .init(image: .image(UIImage(named: "story2")))
)
]
let storiesViewController = Stories.build(
groups: [
.init(
id: UUID().uuidString,
title: "",
avatarImage: .image(nil),
stories: stories
)
],
delegate: self
)
// Add as child controller
addChild(storiesViewController)
storiesContainerView.addSubview(storiesViewController.view)
// Setup Auto Layout
storiesViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
storiesViewController.view.topAnchor.constraint(equalTo: storiesContainerView.topAnchor),
storiesViewController.view.leadingAnchor.constraint(equalTo: storiesContainerView.leadingAnchor),
storiesViewController.view.trailingAnchor.constraint(equalTo: storiesContainerView.trailingAnchor),
storiesViewController.view.bottomAnchor.constraint(equalTo: storiesContainerView.bottomAnchor)
])
storiesViewController.didMove(toParent: self)
}
// MARK: - Helper Methods
private func pageTitle(_ text: String) -> AttributedString {
var attributedString = AttributedString(text)
attributedString.font = .systemFont(ofSize: 24, weight: .bold)
attributedString.foregroundColor = .white
return attributedString
}
private func pageSubtitle(_ text: String) -> AttributedString {
var attributedString = AttributedString(text)
attributedString.font = .systemFont(ofSize: 16, weight: .medium)
attributedString.foregroundColor = .white.opacity(0.9)
return attributedString
}
private func actionButtonTitle(_ text: String) -> AttributedString {
var attributedString = AttributedString(text)
attributedString.font = .systemFont(ofSize: 16, weight: .semibold)
attributedString.foregroundColor = .black
return attributedString
}
}
// MARK: - IStoriesDelegate
extension MainViewController: IStoriesDelegate {
func didClose() {
print("Stories closed")
}
func didOpenLink(url: URL) {
UIApplication.shared.open(url)
}
func didOpenStory(storyId: String) {
print("Opened story: \(storyId)")
}
}- Configure
backgroundColorfor story backgrounds - Use
AttributedStringfor rich text formatting - Customize button colors and corner rounding
- Set
durationfor each story (default 4 seconds) - Timer automatically pauses on tap and resumes on release
- URL loading support with automatic caching
- Placeholder images for better UX
- Smooth transitions between images
StoriesKit is built on modern architecture using:
- SwiftUI β for UI components
- Combine β for reactive programming
- MVVM β architectural pattern
- Kingfisher β for image loading and caching
- AVFoundation β for video playback
Storiesβ main class for creating storiesStoriesModelβ centralized configuration modelStoriesStateManagerβ centralized state managementVideoPlayerStateManagerβ video player state managementContainerViewβ SwiftUI container for storiesContentViewβ main content with navigationPageViewβ individual story pageViewModelβ state management and logicViewControllerβ UIKit presentationProgressBarViewβ progress indicatorStoriesMediaViewβ universal media display (images/videos)VideoPlayerViewβ advanced video player component
ViewEventβ user events (taps, swipes, timers)ViewStateβ current state (groups, progress, indices)IStoriesDelegateβ protocol for event handlingVideoPlayerStateβ video player states (idle, playing, paused)
- Centralized State β All state managed through
StoriesStateManager - Video Synchronization β Video playback synchronized with story timers
- Memory Efficient β Single video player instance reused across all videos
- Reactive Updates β UI updates automatically when state changes
- iOS 15.0+
- Swift 5.9+
- Xcode 15.0+
- Kingfisher β for image loading
StoriesKit is distributed under the MIT license. See the LICENSE file for details.
We welcome contributions to StoriesKit! Please read our contributing guidelines.
- π₯ Video Support β Full video playback with preloading and state management
- π¨ StoriesModel β Centralized configuration for all components
- πͺ Custom Content β Support for custom SwiftUI views in stories with embedded buttons
- β‘ Performance β Optimized video player with single instance reuse
- π State Management β Centralized state management with StoriesStateManager
- π― Timer Sync β Video playback synchronized with story timers
- π¨ Theming β Rich customization options for all UI components
- π± Carousel Corners β Support for both circular and rounded rectangle carousel items
- ποΈ Custom Buttons β Buttons now integrated into custom content views
- π Flexible Layout β Custom padding and corner radius for story pages
If you're upgrading from version 1.x:
- Replace
StoriesImageModelwithStoriesMediaModel - Use
StoriesModelfor configuration instead of individual parameters - Update to new
StoriesPageModelstructure withmediaSource - Add video support using
.video()media type - Buttons in custom content β Move buttons from
StoriesPageModel.buttonto customcontentviews - Carousel configuration β Use
StoriesCarouselConfigurationfor carousel customization - Corner styles β Use
Layout.CornerStylefor carousel item shapes
If you have questions or suggestions, create an issue or contact us.
Made with β€οΈ for iOS developers
