diff --git a/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj b/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj index c5ff6cdc..21971b1d 100644 --- a/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj +++ b/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj @@ -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 */; }; @@ -158,6 +160,7 @@ /* Begin PBXFileReference section */ 639039862A13A496004576FF /* ContentListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentListView.swift; sourceTree = ""; }; 8F6D944D2AE98ED800AF0D15 /* Versioning.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versioning.xcconfig; sourceTree = ""; }; + E797E2612DB6E484008AB610 /* SkeletonLoaderCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonLoaderCatalog.swift; sourceTree = ""; }; 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 = ""; }; F905151E29F03B0700F9B66D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -255,6 +258,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + E797E2602DB6E456008AB610 /* Skeleton Loader */ = { + isa = PBXGroup; + children = ( + E797E2612DB6E484008AB610 /* SkeletonLoaderCatalog.swift */, + ); + path = "Skeleton Loader"; + sourceTree = ""; + }; F905151929F03B0700F9B66D /* PlaybookShowcase */ = { isa = PBXGroup; children = ( @@ -891,6 +902,7 @@ F94895452D6CFDD3001B0122 /* Section Separator */, F948954F2D6CFDD3001B0122 /* Select */, F94895512D6CFDD3001B0122 /* Selectable Card */, + E797E2602DB6E456008AB610 /* Skeleton Loader */, F948955B2D6CFDD3001B0122 /* Tab Bar */, F94895642D6CFDD3001B0122 /* Text Area */, F948956F2D6CFDD3001B0122 /* Text Input */, @@ -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 */, @@ -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 */, diff --git a/PlaybookShowcase/PlaybookShowcase/Components/Skeleton Loader/SkeletonLoaderCatalog.swift b/PlaybookShowcase/PlaybookShowcase/Components/Skeleton Loader/SkeletonLoaderCatalog.swift new file mode 100644 index 00000000..167da49c --- /dev/null +++ b/PlaybookShowcase/PlaybookShowcase/Components/Skeleton Loader/SkeletonLoaderCatalog.swift @@ -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@example.com") + + } + + 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) + +} diff --git a/PlaybookShowcase/PlaybookShowcase/ComponentsView.swift b/PlaybookShowcase/PlaybookShowcase/ComponentsView.swift index f902d220..a45bf0e8 100644 --- a/PlaybookShowcase/PlaybookShowcase/ComponentsView.swift +++ b/PlaybookShowcase/PlaybookShowcase/ComponentsView.swift @@ -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" @@ -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() diff --git a/Sources/Playbook/Components/Icon/PBIcon.swift b/Sources/Playbook/Components/Icon/PBIcon.swift index ba4f125d..a9dad686 100644 --- a/Sources/Playbook/Components/Icon/PBIcon.swift +++ b/Sources/Playbook/Components/Icon/PBIcon.swift @@ -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")] } } diff --git a/Sources/Playbook/Components/Skeleton Loader/PBSkeletonLoader.swift b/Sources/Playbook/Components/Skeleton Loader/PBSkeletonLoader.swift new file mode 100644 index 00000000..f6821957 --- /dev/null +++ b/Sources/Playbook/Components/Skeleton Loader/PBSkeletonLoader.swift @@ -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: 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 = .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) + } + } + } + } +}