Skip to content

[PBIOS-714]Skeleton Loader Kit #531

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
639039872A13A496004576FF /* ContentListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639039862A13A496004576FF /* ContentListView.swift */; };
8F6D944E2AE98ED800AF0D15 /* Versioning.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 8F6D944D2AE98ED800AF0D15 /* Versioning.xcconfig */; };
8F6D944F2AE98ED800AF0D15 /* Versioning.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 8F6D944D2AE98ED800AF0D15 /* Versioning.xcconfig */; };
E797E2622DB6E484008AB610 /* SkeletonLoaderCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797E2612DB6E484008AB610 /* SkeletonLoaderCatalog.swift */; };
E797E2632DB6E484008AB610 /* SkeletonLoaderCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797E2612DB6E484008AB610 /* SkeletonLoaderCatalog.swift */; };
F905151B29F03B0700F9B66D /* PlaybookShowcaseApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F905151A29F03B0700F9B66D /* PlaybookShowcaseApp.swift */; };
F905151F29F03B0700F9B66D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F905151E29F03B0700F9B66D /* Assets.xcassets */; };
F905152229F03B0700F9B66D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F905152129F03B0700F9B66D /* Preview Assets.xcassets */; };
Expand Down Expand Up @@ -158,6 +160,7 @@
/* Begin PBXFileReference section */
639039862A13A496004576FF /* ContentListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentListView.swift; sourceTree = "<group>"; };
8F6D944D2AE98ED800AF0D15 /* Versioning.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versioning.xcconfig; sourceTree = "<group>"; };
E797E2612DB6E484008AB610 /* SkeletonLoaderCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonLoaderCatalog.swift; sourceTree = "<group>"; };
F905151829F03B0700F9B66D /* PlaybookShowcase-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PlaybookShowcase-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
F905151A29F03B0700F9B66D /* PlaybookShowcaseApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybookShowcaseApp.swift; sourceTree = "<group>"; };
F905151E29F03B0700F9B66D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -255,6 +258,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
E797E2602DB6E456008AB610 /* Skeleton Loader */ = {
isa = PBXGroup;
children = (
E797E2612DB6E484008AB610 /* SkeletonLoaderCatalog.swift */,
);
path = "Skeleton Loader";
sourceTree = "<group>";
};
F905151929F03B0700F9B66D /* PlaybookShowcase */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -891,6 +902,7 @@
F94895452D6CFDD3001B0122 /* Section Separator */,
F948954F2D6CFDD3001B0122 /* Select */,
F94895512D6CFDD3001B0122 /* Selectable Card */,
E797E2602DB6E456008AB610 /* Skeleton Loader */,
F948955B2D6CFDD3001B0122 /* Tab Bar */,
F94895642D6CFDD3001B0122 /* Text Area */,
F948956F2D6CFDD3001B0122 /* Text Input */,
Expand Down Expand Up @@ -1085,6 +1097,7 @@
F94898172D6CFDD3001B0122 /* TooltipCatalog.swift in Sources */,
F94898182D6CFDD3001B0122 /* LabelPillCatalog.swift in Sources */,
F94898192D6CFDD3001B0122 /* IconStatValueCatalog.swift in Sources */,
E797E2622DB6E484008AB610 /* SkeletonLoaderCatalog.swift in Sources */,
F948981A2D6CFDD3001B0122 /* ProgressSimpleCatalog.swift in Sources */,
F948981B2D6CFDD3001B0122 /* DateCatalog.swift in Sources */,
F91C448B2D6D0DB300E4E9C8 /* DesignElements.swift in Sources */,
Expand Down Expand Up @@ -1161,6 +1174,7 @@
F94895E52D6CFDD3001B0122 /* TooltipCatalog.swift in Sources */,
F94895E62D6CFDD3001B0122 /* LabelPillCatalog.swift in Sources */,
F94895E72D6CFDD3001B0122 /* IconStatValueCatalog.swift in Sources */,
E797E2632DB6E484008AB610 /* SkeletonLoaderCatalog.swift in Sources */,
F94895E82D6CFDD3001B0122 /* ProgressSimpleCatalog.swift in Sources */,
F94895E92D6CFDD3001B0122 /* DateCatalog.swift in Sources */,
F91C448A2D6D0DB300E4E9C8 /* DesignElements.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// Playbook Swift Design System
//
// Copyright © 2025 Power Home Remodeling Group
// This software is distributed under the ISC License
//
// SkeletonLoaderCatalog.swift
//

import SwiftUI
import Playbook

struct SkeletonLoaderCatalog: View {
@State var isLoading: Bool = true
@State var isLoading1: Bool = true
@State var isLoading2: Bool = true
@State var isLoading3: Bool = true
@State var isLoading4: Bool = true
@State var isLoading5: Bool = true
@State var isLoading6: Bool = true
@State var isLoading7: Bool = true
@State var isLoading8: Bool = true
var body: some View {
PBDocStack(title: "Skeleton Loader") {
PBDoc(title: "Member Card") {
memberCardView
}
}
}
}

extension SkeletonLoaderCatalog {
@ViewBuilder
var memberCardView: some View {
PBCard(padding: Spacing.xSmall, width: 300) {

PBSkeletonLoader(isLoading: $isLoading, shape: .rectangle(cornerRadius: 5), alignment: .center) {
Text("Member Info")
}

PBSectionSeparator()
.padding(.horizontal, -Spacing.xSmall)

PBSkeletonLoader(isLoading: $isLoading1, shape: .circle, alignment: .leading) {
PBAvatar(image: Image("andrew"), size: .large, status: .offline)
.transaction { transaction in
transaction.animation = nil
}
}

VStack(spacing: Spacing.xxSmall) {
PBSkeletonLoader(isLoading: $isLoading2, shape: .rectangle(cornerRadius: 5), alignment: .leading) {
Text("Kraig Schwerin")
.pbFont(.body, color: .text(.light))
}

PBSkeletonLoader(isLoading: $isLoading3, shape: .rectangle(cornerRadius: 5), alignment: .leading) {
Text("Director of Nitro Support Services")
.pbFont(.body, color: .text(.light))
}

PBSkeletonLoader(isLoading: $isLoading4, shape: .rectangle(cornerRadius: 5), alignment: .leading) {
Text("PHL \u{2022} Business Technology")
.pbFont(.subcaption)
}
}

VStack(spacing: Spacing.small) {
PBSkeletonLoader(isLoading: $isLoading5, shape: .rectangle(cornerRadius: 5), alignment: .leading) {
PBContact(type: .email, value: "[email protected]")

}

PBSkeletonLoader(isLoading: $isLoading6, shape: .rectangle(cornerRadius: 5), alignment: .leading) {
PBContact(type: .work, value: "3245627482")
}

PBSkeletonLoader(isLoading: $isLoading7, shape: .rectangle(cornerRadius: 5), alignment: .leading) {
PBContact(type: .cell, value: "3491859988")
}
}

Spacer(minLength: 50)
PBSkeletonLoader(isLoading: $isLoading8, shape: .rectangle(cornerRadius: 5), alignment: .center) {
PBButton(variant: .secondary, size: .small, shape: .primary, title: "Direct Message", icon: PBIcon(FontAwesome.messages), iconPosition: .left, iconColor: .pbPrimary) {}

}
.padding(.bottom, Spacing.xSmall)
}
}
}

#Preview {

SkeletonLoaderCatalog()
.frame(height: 800)

}
2 changes: 2 additions & 0 deletions PlaybookShowcase/PlaybookShowcase/ComponentsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum Components: String, CaseIterable {
case sectionSeparator = "Section Separator"
case select
case selectableCard = "Selectable Card"
case skeletonLoader = "Skeleton Loader"
case tabBar = "Tab Bar"
case textArea = "Textarea"
case textInput = "Text Input"
Expand Down Expand Up @@ -119,6 +120,7 @@ public enum Components: String, CaseIterable {
case .sectionSeparator: SectionSeparatorCatalog()
case .select: SelectCatalog()
case .selectableCard: SelectableCardCatalog()
case .skeletonLoader: SkeletonLoaderCatalog()
case .tabBar: TabBarCatalog()
case .textArea: TextAreaCatalog()
case .textInput: TextInputCatalog()
Expand Down
2 changes: 1 addition & 1 deletion Sources/Playbook/Components/Icon/PBIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public extension PBIcon {
}
}
public static var sizeArray: [(IconSize, String)] {
return [(.xSmall, "XSmall"), (.small, "Small"), (.large, "Large"), (.x1, "1x"), (.x2, "x2"), (.x3, "3x"), (.x4, "4x"), (.x5, "5x"), (.x6, "6x"), (.x7, "7x"), (.x8, "8x"), (.x9, "9x"), (.x10, "10x"), (.custom(170), "Custom")]
return [(.xSmall, "XSmall"), (.small, "Small"), (.large, "Large"), (.x1, "1x"), (.x2, "x2"), (.x3, "3x"), (.x4, "4x"), (.x5, "5x"), (.x6, "6x"), (.x7, "7x"), (.x8, "8x"), (.x9, "9x"), (.x10, "10x"), (.custom(170), "Custom")]
}
}

Expand Down
103 changes: 103 additions & 0 deletions Sources/Playbook/Components/Skeleton Loader/PBSkeletonLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// Playbook Swift Design System
//
// Copyright © 2025 Power Home Remodeling Group
// This software is distributed under the ISC License
//
// PBSkeletonLoader.swift
//

import SwiftUI

public struct PBSkeletonLoader<Content: View>: View {
@Binding var isLoading: Bool
let animation: Animation
let shape: SkeletonShape
let content: Content
var alignment: Alignment
@State private var startPoint: UnitPoint = UnitPoint(x: -1, y: 0.5)
@State private var endPoint: UnitPoint = UnitPoint(x: 0, y: 0.5)

public init(
isLoading: Binding<Bool> = .constant(false),
animation: Animation = .smooth(duration: 1.0),
shape: SkeletonShape = .rectangle(),
alignment: Alignment = .leading,
@ViewBuilder content: () -> Content
) {
self._isLoading = isLoading
self.animation = animation
self.shape = shape
self.alignment = alignment
self.content = content()
}

public var body: some View {

skeletonLoadingView

}
}

public extension PBSkeletonLoader {
enum SkeletonShape {
case rectangle(cornerRadius: CGFloat = 8)
case circle
case capsule
case custom(AnyShape)
}

@ViewBuilder
var skeletonShape: some View {
switch shape {
case .rectangle(let cornerRadius):
RoundedRectangle(cornerRadius: cornerRadius)
case .circle:
Circle()
case .capsule:
Capsule()
case .custom(let shape):
shape
}
}

var skeletonLoadingView: some View {
VStack {
if isLoading {
content
.hidden()
.overlay {
skeletonShapeView
}
} else {
content
}
}
.frame(maxWidth: .infinity, alignment: alignment)
}

var skeletonShapeView: some View {
skeletonShape
.foregroundStyle(
LinearGradient(
gradient: Gradient(colors: [
Color(hex: "#f3f7fb"),
Color(hex: "#f3f7fb"),
Color(hex: "#eef4f9"),
]),
startPoint: startPoint,
endPoint: endPoint
)
)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
isLoading = true

withAnimation(animation.repeatForever(autoreverses: false)) {
startPoint = UnitPoint(x: 1, y: 0.5)
endPoint = UnitPoint(x: 2, y: 0.5)
}
}
}
}
}