Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

End of Year: Epilogue Grand Finale Idea #567

Merged
merged 11 commits into from
Dec 5, 2022
4 changes: 4 additions & 0 deletions podcasts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,7 @@
C7C4CAEB28AB05A800CFC8CF /* AnalyticsLoggingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C4CAEA28AB05A800CFC8CF /* AnalyticsLoggingAdapter.swift */; };
C7C4CAEE28AB0BF200CFC8CF /* TracksSubscriptionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C4CAED28AB0BF200CFC8CF /* TracksSubscriptionData.swift */; };
C7C4CAF328ABFD0900CFC8CF /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C4CAF228ABFD0900CFC8CF /* Notifications.swift */; };
C7CA0559293E8918000E41BD /* HolographicEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CA0558293E8918000E41BD /* HolographicEffect.swift */; };
C7CE415A28CBCFC200AD063E /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7AF5790289D87CF0089E435 /* Analytics.swift */; };
C7CE415B28CBD00A00AD063E /* AnalyticsEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72CED2C289DA14F0017883A /* AnalyticsEvent.swift */; };
C7CE415C28CBD01F00AD063E /* String+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72CED2E289DA1650017883A /* String+Analytics.swift */; };
Expand Down Expand Up @@ -2971,6 +2972,7 @@
C7C4CAEA28AB05A800CFC8CF /* AnalyticsLoggingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsLoggingAdapter.swift; sourceTree = "<group>"; };
C7C4CAED28AB0BF200CFC8CF /* TracksSubscriptionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksSubscriptionData.swift; sourceTree = "<group>"; };
C7C4CAF228ABFD0900CFC8CF /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
C7CA0558293E8918000E41BD /* HolographicEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HolographicEffect.swift; sourceTree = "<group>"; };
C7D6551328E5153200AD7174 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
C7D854F228ADD98700877E87 /* AppLifecyleAnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLifecyleAnalyticsTests.swift; sourceTree = "<group>"; };
C7DA8D262923DA4500C1B08B /* PlusPricingInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusPricingInfoModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6071,6 +6073,7 @@
C7FF7716291D50880082A464 /* Motion.swift */,
C7A110F1291F1EC300887A90 /* HTMLTextView.swift */,
C7A110F8291F66C700887A90 /* Confetti.swift */,
C7CA0558293E8918000E41BD /* HolographicEffect.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
Expand Down Expand Up @@ -8057,6 +8060,7 @@
BDA028621C74466500476B28 /* GoogleCastPlayer.swift in Sources */,
BDB5F0CD20450FDC00437669 /* Enumerations.swift in Sources */,
BD7166971ECA880D007DD36E /* IndentedTextField.swift in Sources */,
C7CA0559293E8918000E41BD /* HolographicEffect.swift in Sources */,
BD0F6D7B24BD60FF00EDFB99 /* FilterDurationViewController.swift in Sources */,
40FFCDBD2304E7ED00395CA5 /* ThemeableImageView.swift in Sources */,
BD98C54F1BA8058800E85D3B /* PodcastHeaderListViewController.swift in Sources */,
Expand Down
68 changes: 65 additions & 3 deletions podcasts/End of Year/Stories/EpilogueStory.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import SwiftUI
import CoreHaptics

struct EpilogueStory: StoryView {
@Environment(\.renderForSharing) var renderForSharing: Bool
@ObservedObject private var visibility = Visiblity()
@State private var engine: CHHapticEngine?

var duration: TimeInterval = 5.seconds

var identifier: String = "epilogue"

var body: some View {
GeometryReader { geometry in
if visibility.isVisible {
WelcomeConfetti(type: .normal)
.onAppear(perform: playHaptics)
.allowsHitTesting(false)
.accessibilityHidden(true)
}

PodcastCoverContainer(geometry: geometry) {
Spacer()

StoryLabelContainer(topPadding: 0, geometry: geometry) {
Image("heart")
if visibility.isVisible {
HolographicEffect(geometry: geometry) {
Image("heart")
.renderingMode(.template)
}
} else {
Image("heart")
}

let pocketCasts = "Pocket Casts".nonBreakingSpaces()

Expand All @@ -31,15 +49,59 @@ struct EpilogueStory: StoryView {

Spacer()
}
}.background(Constants.backgroundColor.allowsHitTesting(false))
}.background(Constants.backgroundColor.allowsHitTesting(false).onAppear(perform: prepareHaptics))
}

func onAppear() {
self.visibility.isVisible = true
Analytics.track(.endOfYearStoryShown, story: identifier)
}

private enum Constants {
static let backgroundColor = Color(hex: "#1A1A1A")
static let backgroundColor = Color.black
}

private class Visiblity: ObservableObject {
@Published var isVisible = false
}

// MARK: - Haptics
private func prepareHaptics() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
engine = try? CHHapticEngine()
try? engine?.start()
}

private func playHaptics() {
guard let engine else { return }
var events = [CHHapticEvent]()

for i in stride(from: 0, to: 1, by: 0.1) {
let value = Float(i)

let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: value)
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: value)
let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: i)
events.append(event)
}

for i in stride(from: 0, to: 1, by: 0.1) {
let value = Float(1 - i)

let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: value)
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: value)
let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 1 + i)
events.append(event)
}

guard let pattern = try? CHHapticPattern(events: events, parameters: []), let player = try? engine.makePlayer(with: pattern) else {
return
}

// Make the haptics a little more in sync
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
try? player.start(atTime: 0)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions podcasts/Onboarding/Welcome/WelcomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ struct WelcomeView_Previews: PreviewProvider {
}

// MARK: - View Components
private struct WelcomeConfetti: View {
struct WelcomeConfetti: View {
let type: WelcomeConfettiEmitter.ConfettiType

var body: some View {
Expand Down Expand Up @@ -273,7 +273,7 @@ private struct SectionButton: ButtonStyle {

// MARK: - Confetti πŸŽ‰

private struct WelcomeConfettiEmitter: UIViewRepresentable {
struct WelcomeConfettiEmitter: UIViewRepresentable {
let type: ConfettiType
let frame: CGRect
let afterDelay: TimeInterval
Expand Down
54 changes: 54 additions & 0 deletions podcasts/SwiftUI/HolographicEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import SwiftUI

/// Applies a holographic/foil/rainbow effect to its contents that moves with the device motion
struct HolographicEffect<Content>: View where Content: View {
@StateObject var motion = MotionManager(options: .attitude)
private var content: () -> Content
private let geometry: GeometryProxy
private let multiplier = 4.0

init(geometry: GeometryProxy, @ViewBuilder _ content: @escaping () -> Content) {
self.content = content
self.geometry = geometry
}

var body: some View {
content()
.foregroundColor(.clear)
.background(gradientView)
.onAppear() {
motion.start()
}.onDisappear() {
motion.stop()
}
}

@ViewBuilder
private var gradientView: some View {
GeometryReader { proxy in
// make tighter rings
let colors = rainbowColors + rainbowColors + rainbowColors
RadialGradient(colors: colors, center: .center, startRadius: 0, endRadius: radius(proxy.size))
.scaleEffect(scale(proxy.size))
.offset(position)
.mask(content())
}
}

private var position: CGSize {
CGSize(width: (motion.roll / .pi * multiplier) * geometry.size.height,
height: (motion.pitch / .pi * multiplier) * geometry.size.width)
}

private func scale(_ size: CGSize) -> Double {
min(geometry.size.width, geometry.size.height) / radius(size) * multiplier
}

private func radius(_ size: CGSize) -> Double {
min(size.width, size.height) * 0.5
}

private let rainbowColors: [Color] = [
.red, .orange, .yellow, .green, .blue, .purple, .pink
]
}