Skip to content

Instantly share code, notes, and snippets.

@fabiogiolito
Last active September 13, 2022 14:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save fabiogiolito/67c0fe45972685b5101594e75bb7f35b to your computer and use it in GitHub Desktop.
Save fabiogiolito/67c0fe45972685b5101594e75bb7f35b to your computer and use it in GitHub Desktop.
Instagram Stories in SwiftUI. Demo video: https://twitter.com/fabiogiolito/status/1142924688525529088
//
// InstaStories.swift
// SwiftUITests
//
// Created by Fabio Giolito on 23/06/2019.
// Copyright © 2019 Fabio Giolito. All rights reserved.
//
import Combine
import SwiftUI
struct Story: Identifiable {
var id: Int
var user: String
var videos: [Int]
}
struct InstaStories : View {
var stories = [
Story(id: 0, user: "cat", videos: [0, 1, 2]),
Story(id: 1, user: "hand", videos: [0, 1, 2, 3]),
Story(id: 2, user: "man", videos: [0, 1])
]
@State var currentStory: Int = 0
@State var currentVideo: Int = 0
var body: some View {
ZStack {
ForEach(stories.reversed()) { story in
ZStack {
ForEach(story.videos.identified(by: \.self)) { video in
Image("Story\(story.id)Video\(video)").resizable()
.opacity(self.isVisible(story: story.id, video: video) ? 1 : 0)
}
// Overlay UI
VStack {
HStack {
ForEach(story.videos.identified(by: \.self)) { video in
ZStack(alignment: .leading) {
if (self.isVisible(story: story.id, video: video)) {
Rectangle()
}
Rectangle().opacity(0.3)
}.frame(height: 4).cornerRadius(2)
}
}
AvatarBar(userAvatar: story.user, userName: story.user)
Spacer()
CommentsBar()
}
.foregroundColor(.white)
.padding()
}.offset(x: CGFloat(story.id - self.currentStory) * UIScreen.main.bounds.width)
.animation(.fluidSpring())
}
// Navigation buttons
HStack {
Rectangle().opacity(0).tapAction { self.prevVideo() }
Rectangle().opacity(0).tapAction { self.nextVideo() }
}
}.cornerRadius(20).clipped()
}
func isVisible(story: Int, video: Int) -> Bool {
if (video == 0) { return true }
if (story < currentStory || (story == currentStory && video <= currentVideo)) { return true }
return false
}
func prevVideo() {
if (currentVideo > 0) {
currentVideo -= 1
} else if (currentStory > 0) {
currentStory -= 1
currentVideo = stories[currentStory].videos.count - 1
print("prev story")
} else {
print("at lower limit")
}
}
func nextVideo() {
if (currentVideo < stories[currentStory].videos.count - 1) {
currentVideo += 1
} else if (currentStory < stories.count - 1) {
currentStory += 1
currentVideo = 0
print("next story")
} else {
print("at top limit")
}
}
}
struct AvatarBar : View {
var userAvatar: String
var userName: String
var body: some View {
HStack {
Image(userAvatar).resizable().frame(width: 40, height: 40)
Text(userName)
Spacer()
Image(systemName: "xmark")
}
}
}
struct CommentsBar : View {
var body: some View {
HStack {
HStack {
Image(systemName: "camera.fill")
}
.frame(width: 40, height: 40)
.border(Color.white, width: 1, cornerRadius: 20)
HStack {
Text("Send message")
Spacer()
Image(systemName: "ellipsis")
}
.padding(.horizontal)
.frame(height: 40)
.border(Color.white, width: 1, cornerRadius: 20)
Image(systemName: "paperplane").imageScale(.large)
}
}
}
#if DEBUG
struct InstaStories_Previews : PreviewProvider {
static var previews: some View {
InstaStories()
}
}
#endif
@fabiogiolito
Copy link
Author

fabiogiolito commented Jun 24, 2019

Two things I wasn't able to do yet:

  • Timer
  • Progress animation

For Timer I tried

@State var isRunning = false

var body: some View {
    if !isRunning {
        isRunning = true
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { (timer) in
            self.nextVideo()
        }
    }
    return ZStack { … }
}

But this keeps setting multiple timers.


For progress animation I tried replacing lines 45 to 47 with:

Rectangle()
    .relativeWidth(self.isVisible(story: story.id, video: video) ? 1 : 0)
    .animation(.basic(duration: 3, curve: .linear))

But bars on next stories keep growing shrinking and doesn't work correctly when you go back one step.

@davidahouse
Copy link

Try using .onAppear { }

.onAppear {
                Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { (timer) in
                    self.nextVideo()
                }
        }

Put at the end of the main body

@fabiogiolito
Copy link
Author

Try using .onAppear { }

@davidahouse Thank you. Can't use it on body, but works as expected when added to the first child.

Still some ways to go, I need to move it to a variable so I can restart it when user taps to move forward/backwards. And call .fire() from that .onAppear method. I'll get back to this later and update the gist.

Thanks again!

@davidahouse
Copy link

Yeah I'm still working on how to describe where to put stuff in SwiftUI. I should have said added to the last child of body, that would have been more clear. And yeah those updates sound good so you have more control over the timer. Then you could put a button or something to toggle the timer on/off too. Good work!

@fabiogiolito
Copy link
Author

Also check you Facebook reactions in SwiftUI: https://gist.github.com/fabiogiolito/424e1d68766fc054ad4bf6fd3506e9c3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment