├── preview.png ├── nice_components.png ├── textAndButtonStyles.jpg ├── Sources ├── NiceComponents │ ├── Helper │ │ ├── Colors.xcassets │ │ │ ├── Contents.json │ │ │ ├── error.colorset │ │ │ │ └── Contents.json │ │ │ ├── onError.colorset │ │ │ │ └── Contents.json │ │ │ ├── onPrimary.colorset │ │ │ │ └── Contents.json │ │ │ ├── onSurface.colorset │ │ │ │ └── Contents.json │ │ │ ├── primary.colorset │ │ │ │ └── Contents.json │ │ │ ├── secondary.colorset │ │ │ │ └── Contents.json │ │ │ ├── surface.colorset │ │ │ │ └── Contents.json │ │ │ ├── background.colorset │ │ │ │ └── Contents.json │ │ │ ├── onBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── onSecondary.colorset │ │ │ │ └── Contents.json │ │ │ ├── primaryVariant.colorset │ │ │ │ └── Contents.json │ │ │ ├── shadow.colorset │ │ │ │ └── Contents.json │ │ │ └── secondaryVariant.colorset │ │ │ │ └── Contents.json │ │ ├── Font+NiceTextStyle.swift │ │ ├── NiceSpacing.swift │ │ ├── Color+Hex.swift │ │ ├── DynamicTypeSize+Max.swift │ │ ├── NiceBorderStyle.swift │ │ └── ScaledFont.swift │ ├── Components │ │ ├── Divider │ │ │ ├── NiceDividerStyle.swift │ │ │ └── NiceDivider.swift │ │ ├── ErrorView.swift │ │ ├── NiceShadowModifier.swift │ │ ├── NiceTextField.swift │ │ └── NiceImage.swift │ ├── Button │ │ ├── Style │ │ │ ├── NiceButtonColorStyle.swift │ │ │ ├── ButtonStyle+Styles.swift │ │ │ └── NiceButtonStyle.swift │ │ ├── NiceButtonImage.swift │ │ └── NiceButton.swift │ ├── Text │ │ ├── NiceText+Styles.swift │ │ ├── NiceTextStyle.swift │ │ ├── NiceText+ViewModifier.swift │ │ └── NiceText.swift │ ├── Color │ │ ├── NiceColorTheme.swift │ │ └── NiceColorStyle.swift │ └── Config.swift ├── NiceInit │ └── NiceInit.swift └── NiceInitMacros │ └── NiceInitMacro.swift ├── NiceComponentsExample ├── NiceComponentsExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── olivecat.imageset │ │ │ ├── olivecat.png │ │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Resources │ │ ├── NotoSerif-Bold.ttf │ │ ├── NotoSerif-Regular.ttf │ │ └── Theme.swift │ ├── NiceComponentsExampleApp.swift │ ├── ContentView.swift │ ├── View │ │ ├── SampleSignInView.swift │ │ ├── AllComponentsView.swift │ │ └── CustomizingComponentsView.swift │ └── Info.plist └── NiceComponentsExample.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CONTRIBUTING.md ├── Package.resolved ├── LICENSE ├── Package.swift ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steamclock/niceComponents/HEAD/preview.png -------------------------------------------------------------------------------- /nice_components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steamclock/niceComponents/HEAD/nice_components.png -------------------------------------------------------------------------------- /textAndButtonStyles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steamclock/niceComponents/HEAD/textAndButtonStyles.jpg -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Resources/NotoSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steamclock/niceComponents/HEAD/NiceComponentsExample/NiceComponentsExample/Resources/NotoSerif-Bold.ttf -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Resources/NotoSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steamclock/niceComponents/HEAD/NiceComponentsExample/NiceComponentsExample/Resources/NotoSerif-Regular.ttf -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Assets.xcassets/olivecat.imageset/olivecat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steamclock/niceComponents/HEAD/NiceComponentsExample/NiceComponentsExample/Assets.xcassets/olivecat.imageset/olivecat.png -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Assets.xcassets/olivecat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "olivecat.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Welcome! 2 | 3 | Our project is small, so we're happy to receive feedback and bug reports via Github issues. @brendanlensink will work to triage and respond. You can also email us, contact@steamclock.com. 4 | 5 | If you'd like to submit a pull request that doesn't fix something there's already an open issue for, it's probably best to start with filing an issue about what change you'd like to make. 6 | -------------------------------------------------------------------------------- /Sources/NiceInit/NiceInit.swift: -------------------------------------------------------------------------------- 1 | @attached(member, names: named(init), named(with)) 2 | public macro NiceInit() = #externalMacro(module: "NiceInitMacros", type: "NiceInitMacro") 3 | 4 | @attached(accessor, names: named(willSet)) 5 | public macro NiceDefault(_ stringLiteral: String) = 6 | #externalMacro(module: "NiceInitMacros", type: "DefaultMacro") 7 | 8 | @attached(accessor, names: named(willSet)) 9 | public macro NiceAsset() = 10 | #externalMacro(module: "NiceInitMacros", type: "AssetMacro") 11 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher.git", 7 | "state" : { 8 | "revision" : "c1f60c63f356d364f4284ba82961acbe7de79bcc", 9 | "version" : "7.8.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-syntax", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-syntax.git", 16 | "state" : { 17 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 18 | "version" : "509.1.1" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/NiceComponentsExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceComponentsExampleApp.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceComponents 10 | import SwiftUI 11 | 12 | @main 13 | struct NiceComponentsExampleApp: App { 14 | init() { 15 | Config.current = Theme.config 16 | } 17 | 18 | var body: some Scene { 19 | WindowGroup { 20 | ContentView() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher.git", 7 | "state" : { 8 | "revision" : "c1f60c63f356d364f4284ba82961acbe7de79bcc", 9 | "version" : "7.8.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-syntax", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-syntax.git", 16 | "state" : { 17 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 18 | "version" : "509.1.1" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Components/Divider/NiceDividerStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceDividerStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceInit 10 | import SwiftUI 11 | 12 | /// Defines the style for a divider, including its color, height, and opacity. 13 | @NiceInit public struct NiceDividerStyle { 14 | /// The color of the divider. 15 | public var color: Color? 16 | 17 | /// The thickness (height) of the divider. 18 | public var height: CGFloat = 1 19 | 20 | /// The opacity of the divider, determining its transparency. 21 | public var opacity: CGFloat = 0.6 22 | } 23 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/error.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x26", 9 | "green" : "0x26", 10 | "red" : "0xDC" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x26", 27 | "green" : "0x26", 28 | "red" : "0xDC" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/onError.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFB", 9 | "green" : "0xFA", 10 | "red" : "0xF9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFB", 27 | "green" : "0xFA", 28 | "red" : "0xF9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/onPrimary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFB", 9 | "green" : "0xFA", 10 | "red" : "0xF9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFB", 27 | "green" : "0xFA", 28 | "red" : "0xF9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/onSurface.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x27", 9 | "green" : "0x18", 10 | "red" : "0x11" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFB", 27 | "green" : "0xFA", 28 | "red" : "0xF9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/primary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE5", 9 | "green" : "0x46", 10 | "red" : "0x4F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xE5", 27 | "green" : "0x46", 28 | "red" : "0x4F" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/secondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x69", 9 | "green" : "0x96", 10 | "red" : "0x05" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x69", 27 | "green" : "0x96", 28 | "red" : "0x05" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/surface.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF6", 9 | "green" : "0xF4", 10 | "red" : "0xF3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x37", 27 | "green" : "0x29", 28 | "red" : "0x1F" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEB", 9 | "green" : "0xE7", 10 | "red" : "0xE5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x27", 27 | "green" : "0x18", 28 | "red" : "0x11" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/onBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x27", 9 | "green" : "0x18", 10 | "red" : "0x11" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xEB", 27 | "green" : "0xE7", 28 | "red" : "0xE5" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/onSecondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFB", 9 | "green" : "0xFA", 10 | "red" : "0xF9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFB", 27 | "green" : "0xFA", 28 | "red" : "0xF9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/primaryVariant.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xCA", 9 | "green" : "0x38", 10 | "red" : "0x43" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xCA", 27 | "green" : "0x38", 28 | "red" : "0x43" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/shadow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.150", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.150", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Colors.xcassets/secondaryVariant.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x57", 9 | "green" : "0x78", 10 | "red" : "0x04" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x57", 27 | "green" : "0x78", 28 | "red" : "0x04" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Font+NiceTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font+NiceTextStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension Font { 12 | /// Create a custom Font from a given TextTheme 13 | /// - Parameter fontStyle: The styling to use when creating a Font. 14 | /// - Returns: A Font using the size and weight specified in your NiceTextStyle 15 | static func custom(_ style: NiceTextStyle) -> Font { 16 | if let fontName = style.font { 17 | return .custom(fontName, size: style.size) 18 | } else { 19 | return .system(size: style.size, weight: style.weight) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/NiceSpacing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceSpacing.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A collection of common spacing values to ensure consistent spacing across components. 12 | public enum NiceSpacing { 13 | /// xLarge: 64 14 | public static let xLarge: CGFloat = 64 15 | /// large: 32 16 | public static let large: CGFloat = 32 17 | /// medium: 24 18 | public static let medium: CGFloat = 24 19 | /// standard: 16 20 | public static let standard: CGFloat = 16 21 | /// small: 8 22 | public static let small: CGFloat = 8 23 | /// xSmall: 4 24 | public static let xSmall: CGFloat = 4 25 | } 26 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Resources/Theme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Theme.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceComponents 10 | import SwiftUI 11 | 12 | enum Theme { 13 | static var config: Config { 14 | var newConfig = Config( 15 | colorTheme: NiceColorTheme() 16 | .with( 17 | primary: Color(hex: "FFA71A") 18 | ) 19 | ) 20 | 21 | newConfig.bodyTextStyle = NiceTextStyle( 22 | color: Color(hex: "FFA71A"), 23 | size: 16 24 | ) 25 | 26 | return newConfig 27 | } 28 | } 29 | 30 | extension NiceTextStyle { 31 | static var bodyBold: NiceTextStyle { 32 | Config.current.bodyTextStyle 33 | .with(weight: .bold) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Button/Style/NiceButtonColorStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceButtonColorStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceInit 10 | import SwiftUI 11 | 12 | /// Defines the color style for a `NiceButton`, including states for active and inactive. 13 | @NiceInit public struct NiceButtonColorStyle { 14 | /// The button's background color in its active state. 15 | public var surface: Color 16 | 17 | /// The button's text or icon color in its active state. 18 | public var onSurface: Color 19 | 20 | /// The button's background color in its inactive state. 21 | @NiceDefault("surface") public var inactiveSurface: Color 22 | 23 | /// The button's text or icon color in its inactive state. 24 | @NiceDefault("onSurface") public var inactiveOnSurface: Color 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Steamclock Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceComponents 10 | import SwiftUI 11 | 12 | struct ContentView: View { 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | NavigationLink(destination: AllComponentsView()) { 17 | Text("All Components") 18 | } 19 | NavigationLink(destination: CustomizingComponentsView()) { 20 | Text("Customizing Components") 21 | } 22 | NavigationLink(destination: SampleSignInView()) { 23 | Text("Sign In") 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | struct ContentView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | ContentView() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Text/NiceText+Styles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceText+Styles.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension NiceTextStyle { 12 | /// The text style for screen titles. 13 | static var screenTitle: NiceTextStyle { 14 | Config.current.screenTitleStyle 15 | } 16 | 17 | /// The text style for section titles. 18 | static var sectionTitle: NiceTextStyle { 19 | Config.current.sectionTitleStyle 20 | } 21 | 22 | /// The text style for item titles. 23 | static var itemTitle: NiceTextStyle { 24 | Config.current.itemTitleStyle 25 | } 26 | 27 | /// The text style for body text. 28 | static var body: NiceTextStyle { 29 | Config.current.bodyTextStyle 30 | } 31 | 32 | /// The text style for detailed text. 33 | static var detail: NiceTextStyle { 34 | Config.current.detailTextStyle 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Components/Divider/NiceDivider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceDivider.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A view that creates a line divider with customizable style. 12 | public struct NiceDivider: View { 13 | /// The style applied to the divider. 14 | let style: NiceDividerStyle 15 | 16 | /// Initializes the divider with an optional custom style. 17 | /// - Parameter style: The style to apply to the divider. Uses a default style if not specified. 18 | public init(style: NiceDividerStyle? = nil) { 19 | self.style = style ?? NiceDividerStyle() 20 | } 21 | 22 | /// The body of the `NiceDivider`, defining its appearance based on the specified style. 23 | public var body: some View { 24 | Divider() 25 | .background(style.color ?? Config.current.colorStyle.divider) 26 | .opacity(style.opacity) 27 | .frame(height: style.height) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | import CompilerPluginSupport 4 | 5 | let package = Package( 6 | name: "NiceComponents", 7 | platforms: [ 8 | .iOS(.v15), 9 | .macOS(.v10_15) 10 | ], 11 | products: [ 12 | .library( 13 | name: "NiceComponents", 14 | targets: ["NiceComponents"]), 15 | .library( 16 | name: "NiceInit", 17 | targets: ["NiceInit"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package( 22 | url: "https://github.com/onevcat/Kingfisher.git", 23 | from: "7.6.1" 24 | ), 25 | .package( 26 | url: "https://github.com/apple/swift-syntax.git", 27 | from: "509.0.0" 28 | ), 29 | ], 30 | targets: [ 31 | .macro( 32 | name: "NiceInitMacros", 33 | dependencies: [ 34 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 35 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 36 | ] 37 | ), 38 | 39 | .target(name: "NiceInit", dependencies: ["NiceInitMacros"]), 40 | 41 | .target( 42 | name: "NiceComponents", 43 | dependencies: ["Kingfisher", "NiceInit"]), 44 | 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Text/NiceTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceTextStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceInit 10 | import SwiftUI 11 | 12 | /// A struct representing a style for text elements in SwiftUI. 13 | @NiceInit public struct NiceTextStyle { 14 | /// The color of the text. 15 | public var color: Color 16 | 17 | /// The font of the text. 18 | public var font: String? 19 | 20 | /// The size of the text. Default is 16. 21 | public var size: CGFloat = 16 22 | 23 | /// The weight of the text. Default is .regular. 24 | public var weight: Font.Weight = .regular 25 | 26 | /// The tracking value of the text. Only applied if running on iOS 16+. Default is 0. 27 | public var tracking: CGFloat = 0 28 | 29 | /// The maximum size for dynamic type scaling. 30 | public var dynamicTypeMaxSize: DynamicTypeSize? 31 | 32 | /// The maximum number of lines for the text. 33 | public var lineLimit: Int? 34 | 35 | /// The spacing between lines of text. 36 | public var lineSpacing: CGFloat? 37 | } 38 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/View/SampleSignInView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleSignInView.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceComponents 10 | import SwiftUI 11 | 12 | public struct SampleSignInView: View { 13 | @State private var emailField: String = "" 14 | @State private var passwordField: String = "" 15 | 16 | public var body: some View { 17 | VStack(alignment: .leading, spacing: NiceSpacing.standard) { 18 | NiceText("Sign In", style: .screenTitle) 19 | 20 | NiceText("Email", style: .detail) 21 | TextField("", text: $emailField) 22 | .textFieldStyle(RoundedBorderTextFieldStyle()) 23 | 24 | NiceText("Password", style: .detail) 25 | TextField("", text: $passwordField) 26 | .textFieldStyle(RoundedBorderTextFieldStyle()) 27 | 28 | NiceButton("Sign In", style: .primary) {} 29 | 30 | NiceButton("Create an Account", style: .secondary) {} 31 | 32 | Spacer() 33 | }.padding(NiceSpacing.standard) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Components/ErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorView.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A view for displaying an error message. 12 | public struct ErrorView: View { 13 | /// The error to be displayed. 14 | private let error: Error 15 | 16 | /// Initializes the view with an error. 17 | /// - Parameter error: The error to display. 18 | public init(error: Error) { 19 | self.error = error 20 | } 21 | 22 | /// The body of the `ErrorView`. Contains the error message. 23 | public var body: some View { 24 | VStack(alignment: .center) { 25 | NiceText("Error:", style: .body) 26 | NiceText(error.localizedDescription, style: .body) 27 | } 28 | } 29 | } 30 | 31 | struct ErrorView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | ErrorView(error: CustomError()) 34 | } 35 | } 36 | 37 | private struct CustomError: Error { 38 | var description: String { 39 | "Something's gone wrong. Try again later." 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Button/Style/ButtonStyle+Styles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonStyle+Styles.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Extends `NiceButtonStyle` with predefined styles for convenience. 12 | public extension NiceButtonStyle { 13 | /// The primary style for buttons, typically used for the most important action in a view. 14 | static var primary: NiceButtonStyle { 15 | Config.current.primaryButtonStyle 16 | } 17 | 18 | /// The secondary style for buttons, used for actions of lesser importance than the primary action. 19 | static var secondary: NiceButtonStyle { 20 | Config.current.secondaryButtonStyle 21 | } 22 | 23 | /// A borderless style for buttons, often used for minimalistic or less prominent actions. 24 | static var borderless: NiceButtonStyle { 25 | Config.current.borderlessButtonStyle 26 | } 27 | 28 | /// A style for buttons that perform destructive actions, such as deleting or removing. 29 | static var destructive: NiceButtonStyle { 30 | Config.current.destructiveButtonStyle 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Text/NiceText+ViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceText+ViewModifier.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A view modifier for applying a specific text style to a view. 12 | struct NiceTextModifier: ViewModifier { 13 | /// The text style to apply. 14 | let style: NiceTextStyle 15 | 16 | /// Applies the specified text style to the content view. 17 | /// - Parameter content: The content view to modify. 18 | /// - Returns: A modified view with the specified text style. 19 | func body(content: Content) -> some View { 20 | content 21 | .foregroundStyle(style.color) 22 | .scaledFont( 23 | name: style.font, 24 | size: style.size, 25 | weight: style.weight, 26 | maxSize: style.dynamicTypeMaxSize 27 | ) 28 | .fixedSize(horizontal: false, vertical: true) 29 | } 30 | } 31 | 32 | public extension View { 33 | /// Applies the specified text style to the view. 34 | /// - Parameter style: The text style to apply. 35 | /// - Returns: A modified view with the specified text style. 36 | func niceText(_ style: NiceTextStyle) -> some View { 37 | modifier(NiceTextModifier(style: style)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/Color+Hex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Hex.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Color { 12 | /// Create a new color from a hex string 13 | /// From https://stackoverflow.com/a/56874327 14 | /// - Parameter hex: The hex string to create a color from. Can be passed with or without #. 15 | public init(hex: String) { 16 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 17 | var int: UInt64 = 0 18 | Scanner(string: hex).scanHexInt64(&int) 19 | let a, r, g, b: UInt64 20 | switch hex.count { 21 | case 3: // RGB (12-bit) 22 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 23 | case 6: // RGB (24-bit) 24 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 25 | case 8: // ARGB (32-bit) 26 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 27 | default: 28 | (a, r, g, b) = (1, 1, 1, 0) 29 | } 30 | 31 | self.init( 32 | .sRGB, 33 | red: Double(r) / 255, 34 | green: Double(g) / 255, 35 | blue: Double(b) / 255, 36 | opacity: Double(a) / 255 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Components/NiceShadowModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceShadowModifier.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceInit 10 | import SwiftUI 11 | 12 | @NiceInit public struct NiceShadowStyle { 13 | /// The color of the shadow. Defaults to black. 14 | public var color: Color? 15 | 16 | /// The blur radius of the shadow. Defaults to 4. 17 | public var radius: CGFloat = 4 18 | 19 | /// The horizontal offset of the shadow. Defaults to 0. 20 | public var x: CGFloat = 0 21 | 22 | /// The vertical offset of the shadow. Defaults to 4. 23 | public var y: CGFloat = 4 24 | } 25 | 26 | /// Attach a drop shadow to the given View. 27 | public struct NiceShadowModifier: ViewModifier { 28 | let style: NiceShadowStyle 29 | 30 | public func body(content: Content) -> some View { 31 | content 32 | .shadow( 33 | color: style.color ?? Config.current.colorStyle.shadow, 34 | radius: style.radius, 35 | x: style.x, 36 | y: style.y 37 | ) 38 | } 39 | } 40 | 41 | public extension View { 42 | /// Attach a drop shadow to the provided View. 43 | /// - Parameter style: The style to use for the drop shadow. Defaults to your config's `shadowStyle`. 44 | func shadow(_ style: NiceShadowStyle = Config.current.shadowStyle) -> some View { 45 | modifier(NiceShadowModifier(style: style)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIAppFonts 24 | 25 | NotoSerif-Bold.ttf 26 | NotoSerif-Regular.ttf 27 | 28 | UIApplicationSceneManifest 29 | 30 | UIApplicationSupportsMultipleScenes 31 | 32 | 33 | UIApplicationSupportsIndirectInputEvents 34 | 35 | UILaunchScreen 36 | 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/DynamicTypeSize+Max.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicTypeSize.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension DynamicTypeSize { 12 | /// Gets the max font size for a given base size, based on the given dynamic type size. 13 | /// Max size was determined based off the iOS scaling logic given [here](https://developer.apple.com/design/human-interface-guidelines/foundations/typography/#dynamic-type-sizes) 14 | /// - Parameter baseSize: The original size of the font to be scaled 15 | /// - Returns: The new scaled font size. 16 | func getMaxFontSize(for baseSize: CGFloat) -> CGFloat? { 17 | var resultSize: CGFloat = baseSize 18 | 19 | switch self { 20 | case .xSmall: 21 | resultSize = baseSize - 3.0 22 | case .small: 23 | resultSize = baseSize - 2.0 24 | case .medium: 25 | resultSize = baseSize - 1.0 26 | case .large: 27 | resultSize = baseSize 28 | case .xLarge: 29 | resultSize = baseSize + 2.0 30 | case .xxLarge: 31 | resultSize = baseSize + 4.0 32 | case .xxxLarge: 33 | resultSize = baseSize + 6.0 34 | case .accessibility1: 35 | resultSize = baseSize + 10.0 36 | case .accessibility2: 37 | resultSize = baseSize + 14.0 38 | case .accessibility3: 39 | resultSize = baseSize + 18.0 40 | case .accessibility4: 41 | resultSize = baseSize + 22.0 42 | case .accessibility5: 43 | resultSize = baseSize + 26.0 44 | @unknown default: 45 | return nil 46 | } 47 | 48 | if resultSize < 11.0 { 49 | return 11.0 50 | } 51 | 52 | return resultSize 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/NiceBorderStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceBorderStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Defines the border style for a component. 12 | public enum NiceBorderStyle { 13 | /// No border is shown. 14 | case none 15 | 16 | /// A standard, square border. 17 | case border(color: Color, width: CGFloat) 18 | 19 | /// A rounded, pill-style, border. 20 | case capsule(color: Color? = nil, width: CGFloat? = nil) 21 | 22 | /// A rounded border with customizable corner radius. 23 | case rounded(color: Color? = nil, cornerRadius: CGFloat, width: CGFloat? = nil) 24 | 25 | /// Set a custom border with the built-in StrokeStyle. 26 | case stroke(strokeStyle: StrokeStyle) 27 | 28 | var color: Color { 29 | switch self { 30 | case .border(let color, _): return color 31 | case .capsule(let color, _), .rounded(let color, _, _): 32 | if let color = color { 33 | return color 34 | } 35 | 36 | fallthrough 37 | default: 38 | return .clear 39 | } 40 | } 41 | 42 | var width: CGFloat { 43 | switch self { 44 | case .border(_, let width): return width 45 | case .capsule(_, let width), .rounded(_, _, let width): 46 | if let width = width { 47 | return width 48 | } 49 | 50 | fallthrough 51 | default: 52 | return 0.0 53 | } 54 | } 55 | 56 | var cornerRadius: CGFloat { 57 | switch self { 58 | case .rounded(_, let radius, _): return radius 59 | default: return 0.0 60 | } 61 | } 62 | 63 | var strokeStyle: StrokeStyle? { 64 | switch self { 65 | case .stroke(let strokeStyle): return strokeStyle 66 | default: return nil 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Color/NiceColorTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceColorTheme.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | import NiceInit 11 | 12 | /// A collection of styling settings for colors, used across components 13 | /// The language and structure used here is heavily influenced by the [Material Design color system](https://m2.material.io/design/color/the-color-system.html). 14 | @NiceInit public struct NiceColorTheme { 15 | /// The primary brand or theme color for your components. 16 | @NiceAsset public var primary: Color 17 | 18 | /// An optional variant, or shade, of your primary color. 19 | @NiceAsset public var primaryVariant: Color 20 | 21 | /// The color elements presented on top of primary colors should use. 22 | @NiceAsset public var onPrimary: Color 23 | 24 | /// An alternate theme color, complimentary to the primary color. 25 | @NiceAsset public var secondary: Color 26 | 27 | /// An optional variant of the secondary theme color. 28 | @NiceAsset public var secondaryVariant: Color 29 | 30 | /// The color elements presented on top of secondary colors should use. 31 | @NiceAsset public var onSecondary: Color 32 | 33 | /// The color that should appear behind scrollable content within the app. 34 | @NiceAsset public var background: Color 35 | 36 | /// The color elements presented on top of a background should use. 37 | @NiceAsset public var onBackground: Color 38 | 39 | /// The color used to indicate errors in components. 40 | @NiceAsset public var error: Color 41 | 42 | /// The color elements presented on top of errors should use. 43 | @NiceAsset public var onError: Color 44 | 45 | /// The color used for background colors in components, such as sheets, cards and menus. 46 | @NiceAsset public var surface: Color 47 | 48 | /// The color elements presented on top of a surfaces should use. 49 | @NiceAsset public var onSurface: Color 50 | 51 | /// The color used for drop shadows. 52 | @NiceAsset public var shadow: Color 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Helper/ScaledFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaledFont.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Set the Font for a view while respecting Dynamic Type sizing and styling. 12 | /// https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-dynamic-type-with-a-custom-font 13 | public struct ScaledFont: ViewModifier { 14 | @Environment(\.sizeCategory) var sizeCategory 15 | var name: String? 16 | var weight: Font.Weight 17 | var size: CGFloat 18 | var maxSize: DynamicTypeSize? 19 | 20 | public func body(content: Content) -> some View { 21 | return content.font(.scaledFont(name: name, size: size, weight: weight, maxSize: maxSize)) 22 | } 23 | } 24 | 25 | extension View { 26 | /// Set a view's font property using the provided scaled font. 27 | /// - Parameters 28 | /// - name: The name of the font to use. 29 | /// - size: The size of the font you'd like to use as a base 30 | /// - weight: The weight of the font to use. Default is `nil`. 31 | /// - maxSize: The max DynamicTypeSize to scale to. Default is `nil`. 32 | /// - Returns: The font, scaled to the correct size 33 | public func scaledFont(name: String?, size: CGFloat, weight: Font.Weight?, maxSize: DynamicTypeSize? = nil) -> some View { 34 | return self.modifier(ScaledFont(name: name, weight: weight ?? .regular, size: size, maxSize: maxSize)) 35 | } 36 | } 37 | 38 | public extension Font { 39 | /// Create a new scaled font, given a base font, size and weight. 40 | /// - Parameters 41 | /// - name: The name of the font to use. 42 | /// - size: The size of the font you'd like to use as a base 43 | /// - weight: The weight of the font to use. Default is `nil`. 44 | /// - maxSize: The max DynamicTypeSize to scale to. Default is `nil`. 45 | /// - Returns: The font, scaled to the correct size 46 | static func scaledFont(name: String?, size: CGFloat, weight: Font.Weight? = nil, maxSize: DynamicTypeSize? = nil) -> Font { 47 | var scaledSize = UIFontMetrics.default.scaledValue(for: size) 48 | 49 | if let maxFontSize = maxSize?.getMaxFontSize(for: size) { 50 | scaledSize = min(maxFontSize, scaledSize) 51 | } 52 | if let name = name { 53 | return Self.custom(name, fixedSize: scaledSize).weight(weight ?? .regular) 54 | } 55 | 56 | return Font.system(size: scaledSize, weight: weight ?? .regular) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.1.1] 8 | - Fixed custom fonts not applying `NiceTextStyle.dynamicTypeMaxSize`. 9 | 10 | ## [2.1.0] 11 | - Added `horizontalContentPadding` param to NiceButton constructor to allow for setting buttons to size to fit. 12 | - Fixed an issue that was causing system icons to not work in buttons. 13 | - Changed default button image sizing from undefined to width: 16, height: 14. 14 | - Changed default button image padding from 0 to 8. 15 | 16 | ## [2.0.4] 17 | - Added a NiceImage constructor that accepts an `ImageResource` for iOS 17.0 and beyond. 18 | 19 | ## [2.0.3] 20 | - Fixed scaled fonts not applying weight correctly to custom fonts 21 | 22 | ## [2.0.1] 23 | - Fixed some text styling not being properly applied to text objects 24 | 25 | ## [2.0.0] 26 | - Removed individual component Views, replaced with NiceButton & NiceText 27 | - Reworked color theming, added ColorTheme and ColorStyle 28 | 29 | ## [1.1.2] 30 | - Fixed padding of NiceButton with image. 31 | 32 | ## [1.1.1] 33 | - Updated cornerRadius handling for buttons 34 | 35 | ## [1.1.0] 36 | - Added lineSpacing to NiceTextStyle. 37 | - Added tracking to FontStyle. 38 | 39 | ## [1.0.0] 40 | - Lots of prep for initial public release! 41 | - Added a bunch of documentation, comments and clarification. 42 | - Removed some unused, or outdated components that have SwiftUI equivalents now. 43 | - Renamed a handful of components and helpers to be more clear or avoid potential collisions. 44 | 45 | ## [0.6.0] 46 | - Removed stateful view and view+if helpers, to be moved into a separate utils library. 47 | 48 | ## [0.5.0] 49 | - Add functionality to NiceText to allow setting a maximum dynamic type font size 50 | - Add two helper functions to `View`, `if` and `iflet` that allow for optional view modifying 51 | 52 | ## [0.4.0] - 2022-07-22 53 | - Reworked stateful view 54 | - ResizeableImage now supports loading / fallbackImage 55 | 56 | 57 | ## [0.3.0] - 2022-07-19 58 | - Min iOS version incremented to iOS 15 59 | - NiceButton introduced 60 | - All Button components are now NiceButtons 61 | - ResizableImage supports systemIcons 62 | - NiceButtonStyle replaced ButtonStyle 63 | - NiceBorderStyle replaced BorderStyle 64 | - AttributedString support for Text components 65 | - Update to StatefulView to support opaque view types 66 | - LoadingView improvement 67 | 68 | 69 | ## [0.2.0] 70 | - Added shadowStyle 71 | - Text components implement NiceText 72 | - Layout documentation 73 | - ContentLoadState is now equatable 74 | - ResizableImage handles both bundle string and URL 75 | - InactiveButton removed onClick modifier 76 | 77 | 78 | ## [0.1.0] 79 | - Initial release! Adds a basic set of components and a first pass at config options to customize them. 80 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Color/NiceColorStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceColorStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceInit 10 | import SwiftUI 11 | 12 | /// While a ColorTheme describes _what_ colors you'll use in an app, 13 | /// a ColorStyle should describe _how_ they're used. 14 | @NiceInit public struct NiceColorStyle { 15 | /// Color style for primary buttons. 16 | public var primaryButton: NiceButtonColorStyle 17 | 18 | /// Color style for secondary buttons. 19 | public var secondaryButton: NiceButtonColorStyle 20 | 21 | /// Color style for buttons that perform destructive actions. 22 | public var destructiveButton: NiceButtonColorStyle 23 | 24 | /// Color style for buttons without a border. 25 | public var borderlessButton: NiceButtonColorStyle 26 | 27 | /// Color style applied to text fields. 28 | public var textField: NiceButtonColorStyle 29 | 30 | /// Color used for screen titles. 31 | public var screenTitle: Color 32 | 33 | /// Color used for section titles within a screen. 34 | public var sectionTitle: Color 35 | 36 | /// Color used for item titles in lists or tables. 37 | public var itemTitle: Color 38 | 39 | /// Color used for body text. 40 | public var bodyText: Color 41 | 42 | /// Color used for detail text, such as captions or annotations. 43 | public var detailText: Color 44 | 45 | /// Color used for dividers. 46 | public var divider: Color 47 | 48 | /// Color used for shadows. 49 | public var shadow: Color 50 | 51 | /// Initializes a `NiceColorStyle` using a `NiceColorTheme` to derive color styles. 52 | /// - Parameter theme: The `NiceColorTheme` instance to derive color styles from. 53 | init( 54 | theme: NiceColorTheme 55 | ) { 56 | self.primaryButton = NiceButtonColorStyle(surface: theme.primary, onSurface: theme.onPrimary) 57 | self.secondaryButton = NiceButtonColorStyle(surface: theme.secondary, onSurface: theme.onSecondary) 58 | self.destructiveButton = NiceButtonColorStyle(surface: theme.error, onSurface: theme.onPrimary) 59 | self.borderlessButton = NiceButtonColorStyle(surface: .clear, onSurface: theme.primary) 60 | 61 | self.textField = NiceButtonColorStyle(surface: theme.surface, onSurface: theme.onSurface) // TODO: Review if correct 62 | 63 | self.screenTitle = theme.onBackground 64 | self.sectionTitle = theme.onBackground 65 | self.itemTitle = theme.onBackground 66 | 67 | self.bodyText = theme.onSurface 68 | self.detailText = theme.onSurface 69 | 70 | self.divider = theme.onSurface 71 | self.shadow = theme.shadow 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This Code of Conduct is adapted from the [Contributor Covenant, version 1.4](http://contributor-covenant.org/version/1/4). Hopefully this is also common sense. 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to creating a positive environment include: 12 | 13 | * Using welcoming and inclusive language 14 | * Being respectful of differing viewpoints and experiences 15 | * Gracefully accepting constructive criticism 16 | * Focusing on what is best for the community 17 | * Showing empathy towards other community members 18 | 19 | Examples of unacceptable behavior by participants include: 20 | 21 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 22 | * Trolling, insulting/derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 25 | * Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Our Responsibilities 28 | 29 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 30 | 31 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team, and Allen Pike specifically, at contact@steamclock.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 40 | 41 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 42 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Text/NiceText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceText.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A SwiftUI view for displaying styled text. 12 | public struct NiceText: View { 13 | /// The text style to apply. 14 | let style: NiceTextStyle 15 | 16 | /// The attributed string to display. 17 | let text: AttributedString 18 | 19 | /// Initializes a new `NiceText` view with the specified attributed text and style. 20 | /// - Parameters: 21 | /// - text: The attributed string to display. 22 | /// - style: The style to apply to the text. 23 | public init( _ text: AttributedString, style: NiceTextStyle) { 24 | self.style = style 25 | self.text = text 26 | } 27 | 28 | /// The body of the `NiceText` view, configuring the text appearance based on the provided style. 29 | public var body: some View { 30 | if #available(iOS 16.0, *) { 31 | Text(text) 32 | .lineLimit(style.lineLimit) 33 | .lineSpacing(style.lineSpacing ?? 0) 34 | .tracking(style.tracking) 35 | .foregroundStyle(style.color) 36 | .scaledFont( 37 | name: style.font, 38 | size: style.size, 39 | weight: style.weight, 40 | maxSize: style.dynamicTypeMaxSize 41 | ) 42 | .fixedSize(horizontal: false, vertical: true) 43 | } else { 44 | Text(text) 45 | .lineLimit(style.lineLimit) 46 | .lineSpacing(style.lineSpacing ?? 0) 47 | .foregroundStyle(style.color) 48 | .scaledFont( 49 | name: style.font, 50 | size: style.size, 51 | weight: style.weight, 52 | maxSize: style.dynamicTypeMaxSize 53 | ) 54 | .fixedSize(horizontal: false, vertical: true) 55 | } 56 | } 57 | } 58 | 59 | public extension NiceText { 60 | /// Initializes a new `NiceText` view with a plain string and style. 61 | /// - Parameters: 62 | /// - text: The string to display. 63 | /// - style: The style to apply to the text. 64 | init(_ text: String, style: NiceTextStyle) { 65 | self.init(AttributedString(text), style: style) 66 | } 67 | 68 | /// Initializes a new `NiceText` view with a plain string, style, and custom configuration for the attributed string. 69 | /// - Parameters: 70 | /// - text: The string to display. 71 | /// - style: The style to apply to the text. 72 | /// - configure: A closure to customize the attributed string before displaying. 73 | init( 74 | _ text: String, 75 | style: NiceTextStyle, 76 | configure: (inout AttributedString) -> Void 77 | ) { 78 | var attributedString = AttributedString(text) 79 | configure(&attributedString) 80 | self.init(attributedString, style: style) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/View/AllComponentsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllComponentsView.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceComponents 10 | import SwiftUI 11 | 12 | struct AllComponentsView: View { 13 | @State var emailFieldText = "test@example.com" 14 | @State var email2FieldText = "" 15 | @State var nameFieldText = "" 16 | @State var passwordFieldText = "" 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack(alignment: .leading, spacing: 5) { 21 | VStack(alignment: .leading, spacing: 2) { 22 | NiceText("Screen Title", style: .screenTitle) 23 | 24 | NiceText("Section Title", style: .sectionTitle) 25 | 26 | NiceText("Item Title", style: .itemTitle) 27 | 28 | NiceText("Body Text", style: .body) 29 | 30 | NiceText("Detail Text", style: .detail) 31 | 32 | NiceText("Attributed Item Title", style: .itemTitle) { string in 33 | if let range = string.range(of: "Attributed") { 34 | string[range].foregroundColor = .red 35 | string[range].underlineStyle = .single 36 | } 37 | } 38 | } 39 | 40 | NiceDivider() 41 | 42 | VStack(alignment: .leading, spacing: 2) { 43 | NiceText("Body Text", style: .body) 44 | 45 | NiceText("Detail Text", style: .detail) 46 | } 47 | 48 | NiceDivider() 49 | 50 | VStack(alignment: .leading, spacing: 2) { 51 | NiceTextField($emailFieldText, placeholder: "Normal Text Field") 52 | 53 | NiceTextField( 54 | $passwordFieldText, 55 | isSecure: true, 56 | placeholder: "Secure text field" 57 | ) 58 | } 59 | 60 | NiceDivider() 61 | 62 | VStack(alignment: .leading, spacing: 4) { 63 | NiceButton("Primary Button", style: .primary, horizontalContentPadding: 20) {} 64 | 65 | NiceButton("Secondary Button", style: .secondary) {} 66 | 67 | NiceButton("Borderless Button", style: .borderless) {} 68 | 69 | NiceButton("Destructive Button", style: .destructive) {} 70 | } 71 | 72 | NiceDivider() 73 | 74 | NiceImage( 75 | URL(string: "https://placekitten.com/200/300"), 76 | width: 200, 77 | height: 300 78 | ) 79 | 80 | NiceImage( 81 | URL(string: "https://placekitten.com/100/150"), 82 | height: 200, 83 | contentMode: .fill 84 | ) 85 | 86 | NiceImage( 87 | URL(string: "https://placekitten.com/100/150"), 88 | height: 200, 89 | contentMode: .fit 90 | ) 91 | 92 | NiceImage( 93 | resource: .olivecat, 94 | height: 200, 95 | contentMode: .fit 96 | ) 97 | 98 | VStack { 99 | NiceImage(URL(string: "https://placekitten.com/100/150")) 100 | }.background(Color.red) 101 | .frame(width: 200, height: 200) 102 | } 103 | }.padding(NiceSpacing.standard) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Button/Style/NiceButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceButtonStyle.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceInit 10 | import SwiftUI 11 | 12 | /// A customizable style for a Nice button. 13 | @NiceInit public struct NiceButtonStyle { 14 | /// The text style to be applied to the button's label. 15 | public var textStyle: NiceTextStyle 16 | 17 | /// The fixed height of the button. Default is 44. 18 | public var height: CGFloat = 44 19 | 20 | /// The color style defining the button's background and foreground colors. 21 | public var colorStyle: NiceButtonColorStyle 22 | 23 | /// The border style applied to the button, including width, color, and corner radius. Default is none. 24 | public var border: NiceBorderStyle = .none 25 | 26 | public func with( 27 | textStyle: NiceTextStyle? = nil, 28 | height: CGFloat? = nil, 29 | colorStyle: NiceButtonColorStyle? = nil, 30 | border: NiceBorderStyle? = nil, 31 | surface: Color? = nil, 32 | onSurface: Color? = nil, 33 | inactiveSurface: Color? = nil, 34 | inactiveOnSurface: Color? = nil, 35 | font: String? = nil, 36 | size: CGFloat? = nil, 37 | weight: Font.Weight? = nil, 38 | tracking: CGFloat? = nil, 39 | dynamicTypeMaxSize: DynamicTypeSize? = nil, 40 | lineLimit: Int? = nil, 41 | lineSpacing: CGFloat? = nil 42 | ) -> NiceButtonStyle { 43 | let textStyle = textStyle ?? self.textStyle 44 | let colorStyle = colorStyle ?? self.colorStyle 45 | 46 | return NiceButtonStyle( 47 | textStyle: NiceTextStyle( 48 | color: onSurface ?? colorStyle.onSurface, 49 | font: font ?? textStyle.font, 50 | size: size ?? textStyle.size, 51 | weight: weight ?? textStyle.weight, 52 | tracking: tracking ?? textStyle.tracking, 53 | dynamicTypeMaxSize: dynamicTypeMaxSize ?? textStyle.dynamicTypeMaxSize, 54 | lineLimit: lineLimit ?? textStyle.lineLimit, 55 | lineSpacing: lineSpacing ?? textStyle.lineSpacing 56 | ), 57 | height: height ?? self.height, 58 | colorStyle: NiceButtonColorStyle( 59 | surface: surface ?? colorStyle.surface, 60 | onSurface: onSurface ?? colorStyle.onSurface, 61 | inactiveSurface: inactiveSurface ?? colorStyle.inactiveSurface, 62 | inactiveOnSurface: inactiveOnSurface ?? colorStyle.inactiveOnSurface 63 | ), 64 | border: border ?? self.border) 65 | } 66 | 67 | } 68 | 69 | internal extension NiceButtonStyle { 70 | /// Calculates the additional padding needed based on the button's border width. 71 | var paddingToAdd: CGFloat { 72 | if let strokeWidth = border.strokeStyle?.lineWidth, strokeWidth > 0.0 { 73 | return strokeWidth / 2 74 | } else if border.width > 0.0 { 75 | return border.width / 2 76 | } 77 | return 0.0 78 | } 79 | 80 | /// Provides a view modifier for creating the button's border overlay. 81 | @ViewBuilder 82 | var borderOverlay: some View { 83 | if let strokeStyle = border.strokeStyle { 84 | RoundedRectangle(cornerRadius: cornerRadius) 85 | .stroke(style: strokeStyle) 86 | } else { 87 | RoundedRectangle(cornerRadius: cornerRadius) 88 | .stroke(border.color, lineWidth: border.width) 89 | } 90 | } 91 | 92 | /// Calculates the corner radius for the button's border, supporting `.capsule` style. 93 | var cornerRadius: CGFloat { 94 | if case .capsule = border { 95 | return height / 2 96 | } 97 | 98 | return border.cornerRadius 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample/View/CustomizingComponentsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomizingComponentsView.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import NiceComponents 10 | import SwiftUI 11 | 12 | struct CustomizingComponentsView: View { 13 | @State var emailFieldText = "test@example.com" 14 | @State var email2FieldText = "" 15 | @State var nameFieldText = "" 16 | @State var passwordFieldText = "" 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack(alignment: .leading, spacing: NiceSpacing.small) { 21 | VStack(alignment: .leading, spacing: NiceSpacing.xSmall) { 22 | 23 | NiceText("Use a default style", style: .body) 24 | 25 | NiceText("Customize it with `with`", style: .body.with(weight: .semibold)) 26 | 27 | Text("Use a view modifier") 28 | .niceText(.itemTitle) 29 | 30 | NiceText("Style attributed text", style: .body) { string in 31 | if let range = string.range(of: "attributed") { 32 | string[range].foregroundColor = .red 33 | string[range].underlineStyle = .single 34 | } 35 | } 36 | 37 | NiceText("Or make your own resusable style", style: .customBodyText) 38 | 39 | NiceText( 40 | "Or make it inline", 41 | style: NiceTextStyle( 42 | color: .green, 43 | font: "Impact" 44 | ) 45 | ) 46 | 47 | NiceText("maybe even add a Nice little shadow", style: .body) 48 | .shadow() 49 | } 50 | 51 | NiceDivider() 52 | 53 | VStack(alignment: .leading, spacing: 2) { 54 | NiceButton("Buttons too!", style: .primary) {} 55 | 56 | NiceButton("Buttons too!", style: .primary.with(surface: .red)) {} 57 | 58 | NiceButton("System icons on the left", style: .secondary, leftImage: NiceButtonImage(systemIcon: "heart.fill")) {} 59 | 60 | NiceButton("And the right", style: .secondary, rightImage: NiceButtonImage(systemIcon: "heart")) {} 61 | 62 | NiceButton("Smaller ones too", style: .secondary, leftImage: NiceButtonImage(systemIcon: "heart.fill", offset: 8), horizontalContentPadding: 20) {} 63 | 64 | NiceButton("Over here as well", style: .secondary, rightImage: NiceButtonImage(systemIcon: "heart"), horizontalContentPadding: 20) {} 65 | 66 | 67 | NiceButton("and buttons with images", style: .primary, balanceImages: false) {} 68 | .withLeftImage( 69 | NiceImage(systemIcon: "fireworks", width: 25, height: 25), 70 | offset: 25 71 | ) 72 | 73 | NiceButton("and images that don't offset your text", style: .primary) {} 74 | .withLeftImage( 75 | NiceImage(systemIcon: "fireworks", width: 25, height: 25), 76 | offset: 25 77 | ) 78 | 79 | NiceButton( 80 | "or fun premade borders", 81 | style: .primary.with( 82 | border: .capsule(color: .black, width: 2) 83 | ) 84 | ) {} 85 | 86 | NiceButton( 87 | "or go full custom", 88 | style: .primary.with( 89 | height: 55, 90 | border: .stroke(strokeStyle: StrokeStyle(lineWidth: 1, lineCap: .butt, lineJoin: .bevel, miterLimit: 1, dash: [CGFloat](), dashPhase: 1)) 91 | ) 92 | ) {} 93 | } 94 | 95 | NiceDivider() 96 | 97 | } 98 | }.padding(NiceSpacing.standard) 99 | } 100 | } 101 | 102 | 103 | extension NiceTextStyle { 104 | static var customBodyText = NiceTextStyle(color: .orange) 105 | } 106 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Button/NiceButtonImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceButtonImage.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A struct that encapsulates the image and its offset used in a `NiceButton`. 12 | public struct NiceButtonImage { 13 | /// The image to be displayed within a button. 14 | let image: NiceImage 15 | 16 | /// The horizontal offset for the image within the button. 17 | let offset: CGFloat 18 | 19 | /// Initializes a `NiceButtonImage` with the specified image and offset. 20 | /// - Parameters: 21 | /// - image: The `NiceImage` to be displayed. 22 | /// - offset: The horizontal offset for the image. Defaults to 8. 23 | public init(_ image: NiceImage, offset: CGFloat = 8) { 24 | self.image = image 25 | self.offset = offset 26 | } 27 | 28 | /// Initializes a `NiceButtonImage` with an image from a bundle. 29 | /// - Parameters: 30 | /// - bundleString: The string identifier for the image in the bundle. 31 | /// - width: Width for the icon. Default is 16. 32 | /// - height: Height for the icon. Default is 14. 33 | /// - tintColor: The tint color to apply to the image. Optional. 34 | /// - contentMode: The mode that determines how the UI fits the image within its bounds. Defaults to `.fill`. 35 | /// - imageAlignment: The alignment of the image within its frame. Defaults to `.center`. 36 | /// - offset: The horizontal offset for the image. Defaults to 8. 37 | public init(_ bundleString: String, 38 | width: CGFloat = 16, 39 | height: CGFloat = 14, 40 | tintColor: Color? = nil, 41 | contentMode: SwiftUI.ContentMode = .fill, 42 | imageAlignment: Alignment = .center, 43 | offset: CGFloat = 8 44 | ) { 45 | self.init(NiceImage(bundleString, width: width, height: height, tintColor: tintColor, contentMode: contentMode, imageAlignment: imageAlignment), offset: offset) 46 | } 47 | 48 | /// Initializes a `NiceButtonImage` with a system icon. 49 | /// - Parameters: 50 | /// - systemIcon: The system icon name. 51 | /// - width: Width for the icon. Default is 16. 52 | /// - height: Height for the icon. Default is 14. 53 | /// - tintColor: Optional tint color for the icon. 54 | /// - contentMode: How the UI fits the icon within its bounds. Defaults to `.fill`. 55 | /// - imageAlignment: The alignment of the icon within its frame. Defaults to `.center`. 56 | /// - offset: The horizontal offset for the icon. Defaults to 8. 57 | public init( 58 | systemIcon: String, 59 | width: CGFloat = 16, 60 | height: CGFloat = 14, 61 | tintColor: Color? = nil, 62 | contentMode: SwiftUI.ContentMode = .fill, 63 | imageAlignment: Alignment = .center, 64 | offset: CGFloat = 8 65 | ) { 66 | self.init(NiceImage(systemIcon: systemIcon, width: width, height: height, tintColor: tintColor, contentMode: contentMode, imageAlignment: imageAlignment), offset: offset) 67 | } 68 | 69 | /// Initializes a `NiceButtonImage` with an image from an URL. 70 | /// - Parameters: 71 | /// - url: The URL of the image. Optional. 72 | /// - width: Width for the icon. Default is 16. 73 | /// - height: Height for the icon. Default is 14. 74 | /// - tintColor: Optional tint color for the image. 75 | /// - fallbackImage: A fallback image string identifier to use if the URL image cannot be loaded. Optional. 76 | /// - contentMode: How the UI fits the image within its bounds. Defaults to `.fill`. 77 | /// - loadingStyle: The style for the activity indicator shown while the image is loading. Optional. 78 | /// - imageAlignment: The alignment of the image within its frame. Defaults to `.center`. 79 | /// - offset: The horizontal offset for the image. Defaults to 8. 80 | public init( 81 | _ url: URL?, 82 | width: CGFloat = 16, 83 | height: CGFloat = 14, 84 | tintColor: Color? = nil, 85 | fallbackImage: String? = nil, 86 | contentMode: SwiftUI.ContentMode = .fill, 87 | loadingStyle: UIActivityIndicatorView.Style? = nil, 88 | imageAlignment: Alignment = .center, 89 | offset: CGFloat = 8 90 | ) { 91 | self.init(NiceImage(url, width: width, height: height, tintColor: tintColor, contentMode: contentMode, imageAlignment: imageAlignment), offset: offset) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Components/NiceTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceTextField.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Create a nice looking text entry field. 12 | public struct NiceTextField: View { 13 | /// The text entered by the user, bound to a variable. 14 | @Binding public var text: String 15 | 16 | /// Identifies the semantic meaning of the text content, aiding in content-specific keyboard layout. Defaults to `nil`. 17 | public let contentType: UITextContentType? 18 | 19 | /// Specifies the keyboard type that appears when the text field is in focus. Defaults to `.default`. 20 | public let keyboardType: UIKeyboardType 21 | 22 | /// Determines whether the text field is for secure text entry. Defaults to `false`. 23 | public let isSecure: Bool 24 | 25 | /// Placeholder text displayed when there is no other text in the text field. 26 | public let placeholder: String 27 | 28 | /// The style applied to the placeholder text. 29 | public let placeholderStyle: NiceTextStyle 30 | 31 | /// The overall style applied to the text field, affecting its visual appearance. 32 | public let style: NiceButtonStyle 33 | 34 | /// An optional image displayed to the left of the text field. Defaults to `nil`. 35 | var leftImage: NiceButtonImage? 36 | 37 | /// Initializes a `NiceTextField` with customizable properties. 38 | /// - Parameters: 39 | /// - text: A binding to the text entered by the user. 40 | /// - style: The overall style of the text field. 41 | /// - contentType: The content type for the text field, affecting keyboard type. 42 | /// - keyboardType: The keyboard type for the text field. 43 | /// - isSecure: Indicates whether the text field is for secure text entry. 44 | /// - placeholder: The placeholder text for the text field. 45 | /// - placeholderStyle: The style for the placeholder text. 46 | /// - leftImage: An optional image to display on the left side of the text field. 47 | public init( 48 | _ text: Binding, 49 | style: NiceButtonStyle? = nil, 50 | contentType: UITextContentType? = nil, 51 | isSecure: Bool = false, 52 | keyboardType: UIKeyboardType = .default, 53 | placeholder: String = "", 54 | placeholderStyle: NiceTextStyle? = nil, 55 | leftImage: NiceButtonImage? = nil 56 | ) { 57 | self._text = text 58 | self.style = style ?? Config.current.textFieldStyle 59 | self.contentType = contentType 60 | self.isSecure = isSecure 61 | self.keyboardType = keyboardType 62 | self.placeholder = placeholder 63 | self.placeholderStyle = placeholderStyle ?? Config.current.textFieldPlaceholderStyle 64 | self.leftImage = leftImage 65 | } 66 | 67 | public var body: some View { 68 | VStack { 69 | VStack(alignment: .leading, spacing: 0) { 70 | if !text.isEmpty { 71 | Text(placeholder) 72 | .foregroundColor(placeholderStyle.color) 73 | .scaledFont( 74 | name: placeholderStyle.font, 75 | size: placeholderStyle.size, 76 | weight: placeholderStyle.weight, 77 | maxSize: placeholderStyle.dynamicTypeMaxSize 78 | ) 79 | } 80 | 81 | HStack(spacing: 0) { 82 | if let leftImage = leftImage { 83 | leftImage.image 84 | .padding(.trailing, leftImage.offset) 85 | } 86 | 87 | if isSecure { 88 | SecureField(placeholder, text: $text) 89 | .foregroundColor(text.isEmpty ? style.colorStyle.inactiveOnSurface : style.colorStyle.onSurface) 90 | .keyboardType(keyboardType) 91 | .textContentType(contentType) 92 | } else { 93 | TextField(placeholder, text: $text) 94 | .foregroundColor(text.isEmpty ? style.colorStyle.inactiveOnSurface : style.colorStyle.onSurface) 95 | .keyboardType(keyboardType) 96 | .textContentType(contentType) 97 | } 98 | } 99 | }.padding(.horizontal, 16) 100 | .padding(.vertical, text.isEmpty ? 16 : 8) 101 | }.frame(maxWidth: .infinity) 102 | .frame(height: style.height) 103 | .fixedSize(horizontal: false, vertical: true) 104 | .background(style.colorStyle.surface) 105 | .cornerRadius(style.border.cornerRadius) 106 | .overlay( 107 | style.borderOverlay 108 | ) 109 | .padding(style.paddingToAdd) 110 | } 111 | } 112 | 113 | struct NiceTextField_Previews: PreviewProvider { 114 | static var previews: some View { 115 | NiceTextField(.constant("Nice!")) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![NiceComponents](nice_components.png) 2 | 3 | NiceComponents is a simple library with some nice looking SwiftUI components to get your next project started. 🚀 4 | 5 | Jumpstart your prototypes with some sensible default components, then come back later and customize the look and feel of your app exactly how you want – all in one place. 6 | 7 | ![](preview.png) 8 | 9 | ## Usage 10 | 11 | ### Example Project 12 | 13 | You can clone and run the example project to see examples of all the default components, plus a little sample of a more customized sign in screen, and demos of how to customize each component. 14 | 15 | ### Prototyping 16 | 17 | When you're just starting out with your project, you should be able to get some reasonable results by just dropping in our components straight out of the box. 18 | 19 | NiceComponents are made up of a couple fundamental pieces: 20 | - **Components** are the Views you'll construct and the bits that your users will see, like `NiceButton` or `NiceText`. 21 | - **Styles** are the set of colors, fonts, etc that describe a specific components, like a Primary button or some Detail text. 22 | - **Themes** are interfaces that describe a set of colors, fonts, etc needed to describe a component. 23 | 24 | ```swift 25 | import NiceComponents 26 | 27 | struct DemoView: View { 28 | var body: some View { 29 | NiceText("I'm a nice big title!", style: .screenTitle) 30 | 31 | NiceButton("And I'm a nice little button", style: .primary) { 32 | doTheThing() 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | We provide the following text and button styles to get you started: 39 | 40 | ![](textAndButtonStyles.jpg) 41 | 42 | ### Customizing Components 43 | 44 | Once you're ready to start putting your own touch on components, you've got a couple options, based on how much you'd like to change. 45 | 46 | #### Setting a Global Config at Startup 47 | 48 | If you'd like to change _all_ instances of a component, or change global variables like your ColorTheme or ColorStyle we recommend creating a custom config that you can set when your app first starts. Note that you once you've set this config once, you'll be unable to update it. 49 | 50 | In the case of multiple customizations applying to the same component, the _most specific_ one will take precedence. 51 | 52 | ```swift 53 | import NiceComponents 54 | 55 | @main struct ExampleApp: App { 56 | init() { 57 | var newConfig = Config() 58 | 59 | newConfig.bodyTextStyle = NiceTextStyle( 60 | color: Color(hex: "FFA71A"), 61 | size: 16 62 | ) 63 | Config.current = newConfig 64 | } 65 | } 66 | ``` 67 | 68 | #### Extending an Existing Component 69 | 70 | If you want to create a new component, or extend an existing one, all you need to do is add a new Style: 71 | 72 | ```swift 73 | extension NiceTextStyle { 74 | static var bodyBold: NiceTextStyle { 75 | Config.current.bodyTextStyle 76 | .with(weight: .bold) 77 | } 78 | } 79 | 80 | ``` 81 | 82 | #### Customizing a Single Instance of a Component 83 | 84 | If you just need to change something for a one-off UI element, each Style comes with a handy mutator function to allow you to customize it. 85 | 86 | ```swift 87 | NiceText( 88 | "Customize it with `with`", 89 | style: .body.with(weight: .semibold) 90 | ) 91 | 92 | NiceText( 93 | "Customize it with `with`", 94 | style: NiceTextStyle( 95 | color: Color(hex: "FFA71A") 96 | with: .body, 97 | font: "Impact", 98 | size: 20 99 | ) 100 | ) 101 | } 102 | 103 | ``` 104 | 105 | ### Setting a Color Palette 106 | 107 | In addition to being able to customize or extend each of the pre-set styles provided by NiceComponents, we provide two ways to change your color palette. 108 | 109 | You can set a ColorTheme and/or ColorStyle when you create your custom Config by passing them into the constructor. 110 | 111 | 112 | #### ColorTheme 113 | 114 | Of the two options, ColorTheme is the more general option, allowing you to change colors that are applied to a variety of different components and places at once. The naming and usage here is indluenced heavily by the [wonderful Material Design palettes](https://m2.material.io/design/color/the-color-system.html). 115 | 116 | ColorTheme takes the colors declared in your asset catalog and gives them a semantic meaning not tied to specific UI elements. 117 | 118 | We recommend declaring your colors in an Asset Catalog and pulling them in from there to make supporting light and dark mode a breeze. 119 | 120 | #### ColorStyle 121 | 122 | ColorStyle describes the colors applied to all components by default semantically, changes here will take precedence over changes made to a ColorTheme, though will default to your ColorTheme if not specified. 123 | 124 | In most cases, you'll probably be fine just changing your ColorTheme and allowing those changes to cascade into the different UI elements, but if you need a little more control over what colors things like specific buttons are, this is your place to do it. 125 | 126 | #### Customizing your TextStyle 127 | 128 | If you want to globally change the font in your app, you can change the Config's textStyle, and all the preset styles will respect your new styling. 129 | 130 | Note that only `bodyText` will use the default `textStyle` size and weight. 131 | 132 | 133 | ### Installation 134 | 135 | NiceComponents is available through **[Swift Package Manager](https://swift.org/package-manager/)**. To install it, follow these steps: 136 | 137 | 1. In Xcode, click **File**, then **Swift Package Manager**, then **Add Package Dependency** 138 | 2. Choose your project 139 | 3. Enter this URL in the search bar `git@github.com:steamclock/niceComponents.git` 140 | 141 | #### Building with CI 142 | 143 | Since NiceComponents uses some macros to automatically generate initializers for some classes, you may need to add `-skipMacroValidation` to your `xcodebuild` call to make it work. 144 | 145 | ### Migrating from Nice Components 1.0 146 | 147 | Given the size and scope of changes from Nice Components 1 to 2, migrating may be a somewhat big process. The good news, a lot of that work can be done with good ol' Find and Replace. 148 | 149 | To migrate, you'll want to: 150 | 1. Change any references to Components like PrimaryButton, BodyText, etc to use NiceText and NiceButton 151 | 2. Change any custom Components you've created into custom styles, extending either NiceTextStyle or NiceButtonStyle 152 | 3. Update your Config to use the new NiceButtonStyle and NiceTextStyles. 153 | 154 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Button/NiceButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceButton.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A SwiftUI view that represents a customizable button component. 12 | public struct NiceButton: View { 13 | /// The text displayed on the button. 14 | let text: String 15 | 16 | /// The style configuration for the button. 17 | let style: NiceButtonStyle 18 | 19 | /// Padding between the button text/images and edges of the button. Default is 8. 20 | /// Set this to `nil` to have the button fill it's available space, like you'd set `maxWidth: .infinity`. 21 | var horizontalContentPadding: CGFloat? 22 | 23 | /// An optional image to display on the left side of the button. 24 | var leftImage: NiceButtonImage? 25 | 26 | /// An optional image to display on the right side of the button. 27 | var rightImage: NiceButtonImage? 28 | 29 | /// A Boolean value indicating whether the images should be balanced or 30 | /// if adding an image should push your text off center. 31 | var balanceImages: Bool 32 | 33 | /// The closure executed when the button is tapped. 34 | let action: () -> Void 35 | 36 | /// Indicates whether the button is in an inactive state. 37 | var inactive: Bool 38 | 39 | /// Initializes a new button with the provided parameters. 40 | /// - Parameters: 41 | /// - text: The text to display on the button. 42 | /// - style: The style configuration for the button. 43 | /// - inactive: A Boolean value that determines whether the button is inactive. Defaults to `false`. 44 | /// - balanceImages: A Boolean value indicating whether the images should be balanced. Defaults to `true`. 45 | /// - leftImage: An optional image to display on the left side of the button. 46 | /// - rightImage: An optional image to display on the right side of the button. 47 | /// - horizontalContentPadding: Padding between the button content and edges of the button. Default is nil, causing the button to expand to fill all available space. 48 | /// - action: The closure to execute when the button is tapped. 49 | public init( 50 | _ text: String, 51 | style: NiceButtonStyle, 52 | inactive: Bool = false, 53 | balanceImages: Bool = true, 54 | leftImage: NiceButtonImage? = nil, 55 | rightImage: NiceButtonImage? = nil, 56 | horizontalContentPadding: CGFloat? = nil, 57 | action: @escaping () -> Void 58 | ) { 59 | self.text = text 60 | self.style = style 61 | self.inactive = inactive 62 | self.balanceImages = balanceImages 63 | self.leftImage = leftImage 64 | self.rightImage = rightImage 65 | self.horizontalContentPadding = horizontalContentPadding 66 | self.action = action 67 | } 68 | 69 | // TODO: I think it may be worth writing a bunch of convenience functions here 70 | // to make it easier for folks to get in and customize style options, 71 | // but want to hold off until APIs are settled. 72 | 73 | public var body: some View { 74 | Button(action: action) { 75 | HStack(spacing: 0) { 76 | if let leftImage = leftImage { 77 | leftImage.image 78 | .padding(.leading, leftImage.offset) 79 | } 80 | 81 | Text(text) 82 | .foregroundColor(inactive ? style.colorStyle.inactiveOnSurface : style.colorStyle.onSurface) 83 | .scaledFont( 84 | name: style.textStyle.font, 85 | size: style.textStyle.size, 86 | weight: style.textStyle.weight, 87 | maxSize: style.textStyle.dynamicTypeMaxSize 88 | ).padding(.leading, leftImage?.offset ?? horizontalContentPadding ?? 0) 89 | .padding(.trailing, rightImage?.offset ?? horizontalContentPadding ?? 0) 90 | 91 | if let rightImage = rightImage { 92 | rightImage.image 93 | .padding(.trailing, rightImage.offset) 94 | } 95 | } 96 | .frame(maxWidth: horizontalContentPadding == nil ? .infinity : nil) 97 | } 98 | .disabled(inactive) 99 | .frame(height: style.height) 100 | .fixedSize(horizontal: false, vertical: true) 101 | .background(inactive ? style.colorStyle.inactiveSurface : style.colorStyle.surface) 102 | .cornerRadius(style.cornerRadius) 103 | .overlay( 104 | style.borderOverlay 105 | ) 106 | .padding(style.paddingToAdd) 107 | } 108 | } 109 | 110 | public extension NiceButton { 111 | /// Adds an image to the left side of the button. 112 | /// - Parameters: 113 | /// - image: The `NiceImage` to be displayed on the left. 114 | /// - offset: The horizontal offset for the image. Defaults to a small predefined spacing. 115 | mutating func addLeftImage(_ image: NiceImage, offset: CGFloat = NiceSpacing.small) { 116 | self.leftImage = NiceButtonImage(image, offset: offset) 117 | } 118 | 119 | /// Adds an image to the right side of the button. 120 | /// - Parameters: 121 | /// - image: The `NiceImage` to be displayed on the right. 122 | /// - offset: The horizontal offset for the image. Defaults to a small predefined spacing. 123 | mutating func addRightImage(_ image: NiceImage, offset: CGFloat = NiceSpacing.small) { 124 | self.rightImage = NiceButtonImage(image, offset: offset) 125 | } 126 | 127 | /// Returns a new `NiceButton` instance with an image added to the left side. 128 | /// - Parameters: 129 | /// - image: The `NiceImage` to be displayed on the left. 130 | /// - offset: The horizontal offset for the image. Defaults to 8.0. 131 | func withLeftImage(_ image: NiceImage, offset: CGFloat = 8.0) -> Self { 132 | var copy = self 133 | copy.addLeftImage(image, offset: offset) 134 | return copy 135 | } 136 | 137 | /// Returns a new `NiceButton` instance with an image added to the right side. 138 | /// - Parameters: 139 | /// - image: The `NiceImage` to be displayed on the right. 140 | /// - offset: The horizontal offset for the image. Defaults to 8.0. 141 | func withRightImage(_ image: NiceImage, offset: CGFloat = 8.0) -> Self { 142 | var copy = self 143 | copy.addRightImage(image, offset: offset) 144 | return copy 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import os 10 | import SwiftUI 11 | 12 | /// Global settings for all components. 13 | /// Styles defined here will be applied to any components that don't define their own. 14 | public struct Config { 15 | /// Your current component configuration. 16 | /// Note that you can only set this configuration once, ideally during app startup. Subsequent updates will be ignored. 17 | public static var current: Config { 18 | get { 19 | return _current 20 | } 21 | 22 | set { 23 | guard !hasSetConfig else { 24 | os_log("Error! Config has already been set once, at startup. Ignoring config update. ") 25 | return 26 | } 27 | 28 | hasSetConfig = true 29 | _current = newValue 30 | } 31 | } 32 | private static var _current = Config() 33 | private static var hasSetConfig: Bool = false 34 | 35 | /// The color theme for the application, influencing all components. 36 | public var colorTheme: NiceColorTheme 37 | 38 | /// Defines the primary color style for components. 39 | public var colorStyle: NiceColorStyle 40 | 41 | /// The default font styling for your app 42 | /// Note that the preset styles other than bodyText will ignore the size and weight set here, but will respect the font name. 43 | public var textStyle: NiceTextStyle? 44 | 45 | /// Style for primary buttons in the app. 46 | public var primaryButtonStyle: NiceButtonStyle 47 | 48 | /// Style for secondary buttons in the app. 49 | public var secondaryButtonStyle: NiceButtonStyle 50 | 51 | /// Style for buttons without borders. 52 | public var borderlessButtonStyle: NiceButtonStyle 53 | 54 | /// Style for buttons that perform destructive actions. 55 | public var destructiveButtonStyle: NiceButtonStyle 56 | 57 | /// Style for text fields. Despite not being a button, shares many styling options with buttons. 58 | public var textFieldStyle: NiceButtonStyle 59 | 60 | /// Style for placeholder text in text fields. 61 | public var textFieldPlaceholderStyle: NiceTextStyle 62 | 63 | /// Style for screen titles. 64 | public var screenTitleStyle: NiceTextStyle 65 | 66 | /// Style for section titles. 67 | public var sectionTitleStyle: NiceTextStyle 68 | 69 | /// Style for item titles. 70 | public var itemTitleStyle: NiceTextStyle 71 | 72 | /// Style for general body text. 73 | public var bodyTextStyle: NiceTextStyle 74 | 75 | /// Style for detailed text, possibly smaller or less emphasized. 76 | public var detailTextStyle: NiceTextStyle 77 | 78 | /// Global shadow style for components requiring depth or elevation. 79 | public var shadowStyle: NiceShadowStyle 80 | 81 | /// Create a new component configuration to use for all components in your project. 82 | /// - Parameters: 83 | /// - colorTheme: The color theme to apply to all the components. See the README for more info on how this works. 84 | /// - colorStyle: The color style to apply to specific components. Will fall back to colorTheme if unspecified. 85 | public init( 86 | colorTheme: NiceColorTheme? = nil, 87 | colorStyle: NiceColorStyle? = nil, 88 | textStyle: NiceTextStyle? = nil 89 | ) { 90 | self.colorTheme = colorTheme ?? NiceColorTheme() 91 | self.colorStyle = colorStyle ?? NiceColorStyle(theme: self.colorTheme) 92 | self.textStyle = textStyle 93 | 94 | // Set Text styles 95 | 96 | screenTitleStyle = NiceTextStyle( 97 | color: self.colorStyle.screenTitle, 98 | font: textStyle?.font, 99 | size: 48, 100 | weight: .semibold 101 | ) 102 | 103 | sectionTitleStyle = NiceTextStyle( 104 | color: self.colorStyle.sectionTitle, 105 | font: textStyle?.font, 106 | size: 34, 107 | weight: .semibold 108 | ) 109 | 110 | itemTitleStyle = NiceTextStyle( 111 | color: self.colorStyle.itemTitle, 112 | font: textStyle?.font, 113 | size: 20, 114 | weight: .semibold 115 | ) 116 | 117 | bodyTextStyle = NiceTextStyle( 118 | color: self.colorStyle.bodyText, 119 | font: textStyle?.font, 120 | size: textStyle?.size 121 | ) 122 | 123 | detailTextStyle = NiceTextStyle( 124 | color: self.colorStyle.detailText, 125 | font: textStyle?.font, 126 | size: 14 127 | ) 128 | 129 | // Set Button styles 130 | 131 | primaryButtonStyle = NiceButtonStyle( 132 | textStyle: self.bodyTextStyle, 133 | height: 44, 134 | colorStyle: NiceButtonColorStyle( 135 | surface: self.colorStyle.primaryButton.surface, 136 | onSurface: self.colorStyle.primaryButton.onSurface 137 | ), 138 | border: .rounded(cornerRadius: NiceSpacing.small) 139 | ) 140 | 141 | secondaryButtonStyle = NiceButtonStyle( 142 | textStyle: self.bodyTextStyle, 143 | height: 44, 144 | colorStyle: NiceButtonColorStyle( 145 | surface: self.colorStyle.secondaryButton.surface, 146 | onSurface: self.colorStyle.secondaryButton.onSurface 147 | ), 148 | border: .rounded(cornerRadius: NiceSpacing.small) 149 | ) 150 | 151 | borderlessButtonStyle = NiceButtonStyle( 152 | textStyle: self.bodyTextStyle, 153 | height: 44, 154 | colorStyle: NiceButtonColorStyle( 155 | surface: self.colorStyle.borderlessButton.surface, 156 | onSurface: self.colorStyle.borderlessButton.onSurface 157 | ) 158 | ) 159 | 160 | destructiveButtonStyle = NiceButtonStyle( 161 | textStyle: self.bodyTextStyle, 162 | height: 44, 163 | colorStyle: NiceButtonColorStyle( 164 | surface: self.colorStyle.destructiveButton.surface, 165 | onSurface: self.colorStyle.destructiveButton.onSurface 166 | ), 167 | border: .rounded(cornerRadius: NiceSpacing.small) 168 | ) 169 | 170 | textFieldStyle = NiceButtonStyle( 171 | textStyle: self.bodyTextStyle, 172 | height: 44, 173 | colorStyle: NiceButtonColorStyle( 174 | surface: self.colorStyle.textField.surface, 175 | onSurface: self.colorStyle.textField.onSurface 176 | ), 177 | border: .rounded(color: self.colorTheme.background, cornerRadius: 8, width: 2) 178 | ) 179 | 180 | textFieldPlaceholderStyle = NiceTextStyle( 181 | color: self.colorStyle.textField.onSurface, 182 | size: 10 183 | ) 184 | 185 | shadowStyle = NiceShadowStyle() 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Sources/NiceInitMacros/NiceInitMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceInitMacro 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftCompilerPlugin 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | import SwiftSyntaxMacros 13 | 14 | struct MessageError: Error, CustomStringConvertible { 15 | var description: String 16 | 17 | init(_ description: String) { 18 | self.description = description 19 | } 20 | } 21 | 22 | struct Property { 23 | var identifier: String 24 | var type: TypeSyntax 25 | var isOptional: Bool 26 | var hasDefault: Bool 27 | var attributeDefault: String? 28 | var isColorAsset: Bool 29 | 30 | var optType: String { 31 | return isOptional ? type.description : type.description + "?" 32 | } 33 | } 34 | 35 | public struct NiceInitMacro: MemberMacro { 36 | public static func expansion( 37 | of node: AttributeSyntax, 38 | providingMembersOf declaration: some DeclGroupSyntax, 39 | in context: some MacroExpansionContext 40 | ) throws -> [DeclSyntax] { 41 | guard let type = declaration.as(StructDeclSyntax.self)?.name.trimmed.description else { 42 | throw(MessageError("@NiceInit can only be attached to a struct")) 43 | } 44 | 45 | // TODO: this should be filtering out computed properties 46 | let memberList = declaration.memberBlock.members 47 | 48 | guard !memberList.isEmpty else { 49 | throw(MessageError("@NiceInit can only be attached to a type with some stored properties")) 50 | } 51 | 52 | // Extract the list of properties to initialize 53 | let properties = try memberList.compactMap { member -> Property? in 54 | guard 55 | let decl = member.decl.as(VariableDeclSyntax.self), 56 | let binding = decl.bindings.first, 57 | let propertyName = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text 58 | else { 59 | return nil 60 | } 61 | 62 | // TODO: wont work for declarations without explicit type i.e. ("var foo: Foo = Foo()" will work, but "var foo = Foo()" won't 63 | guard let propertyType = binding.typeAnnotation?.type.trimmed else { 64 | throw(MessageError("@NiceInit currently only supports properties with explicit type annotations")) 65 | } 66 | 67 | let defaultValue = binding.initializer?.value 68 | var hasDefault = defaultValue != nil 69 | let optional = propertyType.description.last == "?" // TODO: should handle Optional as well 70 | var isColorAsset = false 71 | 72 | var attributeDefault: String? = nil 73 | for element in decl.attributes { 74 | if case .attribute(let attribute) = element { 75 | if attribute.attributeName.trimmedDescription == "NiceDefault" { 76 | hasDefault = true 77 | let arg = attribute.arguments?.as(LabeledExprListSyntax.self)?.first?.as(LabeledExprSyntax.self)?.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue 78 | 79 | attributeDefault = arg 80 | } else if attribute.attributeName.trimmedDescription == "NiceAsset" { 81 | if propertyType.description != "Color" { 82 | throw(MessageError("@Asset can only be attached to a property of type Color (\(propertyName))")) 83 | } 84 | isColorAsset = true 85 | } 86 | } 87 | } 88 | return Property(identifier: propertyName, type: propertyType, isOptional: optional, hasDefault: hasDefault, attributeDefault: attributeDefault, isColorAsset: isColorAsset) 89 | } 90 | 91 | var generated: [DeclSyntax] = [] 92 | 93 | // generate the basic memberwise initializer 94 | do { 95 | var baseInit: String = "public init(\n" 96 | 97 | baseInit += properties.map { 98 | if $0.isColorAsset || $0.hasDefault || $0.isOptional { 99 | " \($0.identifier): \($0.optType) = nil" 100 | } else { 101 | " \($0.identifier): \($0.type)" 102 | } 103 | }.joined(separator: ",\n") 104 | 105 | baseInit += "\n) {\n" 106 | 107 | baseInit += properties.map { 108 | if $0.isColorAsset { 109 | "self.\($0.identifier) = \($0.identifier) ?? Color(\"\($0.identifier)\", bundle: Bundle.module)" 110 | } else if $0.hasDefault, let attributeDefault = $0.attributeDefault { 111 | "self.\($0.identifier) = \($0.identifier) ?? \(attributeDefault)\n" 112 | } else if $0.hasDefault { 113 | "if let _\($0.identifier) = \($0.identifier) { self.\($0.identifier) = _\($0.identifier) }\n" 114 | } else { 115 | "self.\($0.identifier) = \($0.identifier)\n" 116 | } 117 | }.joined() 118 | 119 | baseInit += "}" 120 | generated.append(DeclSyntax(stringLiteral: baseInit)) 121 | } 122 | 123 | // generate the copy-and-modify initializer 124 | do { 125 | var copyInit: String = "public init(\nwith: \(type)" 126 | 127 | copyInit += properties.map { 128 | ",\n\($0.identifier): \($0.optType) = nil" 129 | }.joined() 130 | 131 | copyInit += "\n) {\n" 132 | 133 | copyInit += properties.map { 134 | "self.\($0.identifier) = \($0.identifier) ?? with.\($0.identifier)\n" 135 | }.joined() 136 | 137 | copyInit += "}" 138 | 139 | generated.append(DeclSyntax(stringLiteral: copyInit)) 140 | } 141 | 142 | // generate the "create from template" with function" 143 | do { 144 | var with: String = "public func with(\n" 145 | 146 | with += properties.map { 147 | "\($0.identifier): \($0.optType) = nil" 148 | }.joined(separator: ",\n") 149 | 150 | with += "\n) -> \(type) {\n\(type)(" 151 | 152 | with += properties.map { 153 | "\($0.identifier):\($0.identifier) ?? self.\($0.identifier)" 154 | }.joined(separator: ",\n") 155 | 156 | with += ")\n}" 157 | generated.append(DeclSyntax(stringLiteral: with)) 158 | } 159 | 160 | return generated 161 | } 162 | } 163 | 164 | // Empty marker macro, doesn't actually generate any syntax, just there so it can be read by the main NiceInitMacro 165 | public struct DefaultMacro: AccessorMacro { 166 | public static func expansion< 167 | Context: MacroExpansionContext, 168 | Declaration: DeclSyntaxProtocol 169 | >( 170 | of node: AttributeSyntax, 171 | providingAccessorsOf declaration: Declaration, 172 | in context: Context 173 | ) throws -> [AccessorDeclSyntax] { 174 | return [] 175 | } 176 | } 177 | 178 | // Empty marker macro, doesn't actually generate any syntax, just there so it can be read by the main NiceInitMacro 179 | public struct AssetMacro: AccessorMacro { 180 | public static func expansion< 181 | Context: MacroExpansionContext, 182 | Declaration: DeclSyntaxProtocol 183 | >( 184 | of node: AttributeSyntax, 185 | providingAccessorsOf declaration: Declaration, 186 | in context: Context 187 | ) throws -> [AccessorDeclSyntax] { 188 | return [] 189 | } 190 | } 191 | 192 | 193 | @main 194 | struct NiceInitPlugin: CompilerPlugin { 195 | let providingMacros: [Macro.Type] = [ 196 | NiceInitMacro.self, 197 | DefaultMacro.self, 198 | AssetMacro.self 199 | ] 200 | } 201 | -------------------------------------------------------------------------------- /Sources/NiceComponents/Components/NiceImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiceImage.swift 3 | // NiceComponents: [https://github.com/steamclock/NiceComponents](https://github.com/steamclock/NiceComponents) 4 | // 5 | // Copyright © 2024, Steamclock Software. 6 | // Some rights reserved: [https://github.com/steamclock/NiceComponents/blob/main/LICENSE](https://github.com/steamclock/NiceComponents/blob/main/LICENSE) 7 | // 8 | 9 | import SwiftUI 10 | import UIKit 11 | import Kingfisher 12 | 13 | /// Image View that allows for creating an image through a variety of sources, 14 | /// including bundleString, systemIcon, or URL. 15 | public struct NiceImage: View { 16 | /// The local image to display 17 | public let image: Image? 18 | 19 | /// Optional: URL of an image. 20 | public let url: URL? 21 | 22 | /// Optional: Custom width of the image. 23 | public let width: CGFloat? 24 | 25 | /// Optional: Custom height of the image. 26 | public let height: CGFloat? 27 | 28 | /// Optional: Tint color for the image. 29 | public let tintColor: Color? 30 | 31 | /// The content mode for displaying the image. Defaults to .fill. 32 | public let contentMode: SwiftUI.ContentMode 33 | 34 | /// Optional: Style of the loading indicator (for URL images). 35 | public let loadingStyle: UIActivityIndicatorView.Style? 36 | 37 | /// Optional: A fallback image to display in case of an error or while loading. 38 | public let fallbackImage: UIImage? 39 | 40 | /// The alignment of the image within its frame. Defaults to .center. 41 | public let imageAlignment: Alignment 42 | 43 | @State private var didErrorWithNoFallback: Bool = false 44 | 45 | /// Create a new image from an asset located in the bundle. 46 | /// - Parameters: 47 | /// - bundleString: The name of the image asset. 48 | /// - width: The width of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 49 | /// - height: The height of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 50 | /// - tintColor: Optional color to tint the image. Default is `nil`. 51 | /// - contentMode: Content mode for the image. Default is `.fill`. 52 | /// - imageAlignment: Image's frame alignment. Default is `.center`. 53 | public init( 54 | _ bundleString: String, 55 | width: CGFloat? = nil, 56 | height: CGFloat? = nil, 57 | tintColor: Color? = nil, 58 | contentMode: SwiftUI.ContentMode = .fill, 59 | imageAlignment: Alignment = .center 60 | ) { 61 | self.init( 62 | image: Image(bundleString), 63 | url: nil, 64 | width: width, 65 | height: height, 66 | tintColor: tintColor, 67 | fallbackImage: nil, 68 | contentMode: contentMode, 69 | loadingStyle: nil, 70 | imageAlignment: imageAlignment 71 | ) 72 | } 73 | 74 | /// Create a new image from a system icon. 75 | /// - Parameters: 76 | /// - systemIcon: The name of the icon to use. 77 | /// - width: The width of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 78 | /// - height: The height of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 79 | /// - tintColor: Optional color to tint the image. Default is `nil`. 80 | /// - contentMode: Content mode for the image. Default is `.fill`. 81 | /// - imageAlignment: Image's frame alignment. Default is `.center`. 82 | public init( 83 | systemIcon: String, 84 | width: CGFloat? = nil, 85 | height: CGFloat? = nil, 86 | tintColor: Color? = nil, 87 | contentMode: SwiftUI.ContentMode = .fill, 88 | imageAlignment: Alignment = .center 89 | ) { 90 | self.init( 91 | image: Image(systemName: systemIcon), 92 | url: nil, 93 | width: width, 94 | height: height, 95 | tintColor: tintColor, 96 | fallbackImage: nil, 97 | contentMode: contentMode, 98 | loadingStyle: nil, 99 | imageAlignment: imageAlignment 100 | ) 101 | } 102 | 103 | /// Create a new image from an ImageResource. 104 | /// - Parameters: 105 | /// - resource: The resource to use. 106 | /// - width: The width of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 107 | /// - height: The height of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 108 | /// - tintColor: Optional color to tint the image. Default is `nil`. 109 | /// - contentMode: Content mode for the image. Default is `.fill`. 110 | /// - imageAlignment: Image's frame alignment. Default is `.center`. 111 | @available(iOS 17.0, *) 112 | public init( 113 | resource: SwiftUI.ImageResource, 114 | width: CGFloat? = nil, 115 | height: CGFloat? = nil, 116 | tintColor: Color? = nil, 117 | contentMode: SwiftUI.ContentMode = .fill, 118 | imageAlignment: Alignment = .center 119 | ) { 120 | self.init( 121 | image: Image(resource), 122 | url: nil, 123 | width: width, 124 | height: height, 125 | tintColor: tintColor, 126 | fallbackImage: nil, 127 | contentMode: contentMode, 128 | loadingStyle: nil, 129 | imageAlignment: imageAlignment 130 | ) 131 | } 132 | 133 | /// Create a new image from an URL. 134 | /// Under the hood, we use Kingfisher to fetch and cache the image. 135 | /// Parameters: 136 | /// - url: The URL of the image to fetch. 137 | /// - width: The width of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 138 | /// - height: The height of the image. Note that `.infinity` will be converted to `nil` to avoid invalid frame dimensions. Default is `nil`. 139 | /// - tintColor: Optional color to tint the image. Default is `nil`. 140 | /// - fallbackImage: The bundle string for a fallback image to show if something goes wrong. Default is `nil`. 141 | /// - contentMode: Content mode for the image. Default is `.fill`. 142 | /// - loadingStyle: The UIActivityIndicatorView.Style to use while loading. Default is `nil`. 143 | /// - imageAlignment: Image's frame alignment. Default is `.center`. 144 | public init( 145 | _ url: URL?, 146 | width: CGFloat? = nil, 147 | height: CGFloat? = nil, 148 | tintColor: Color? = nil, 149 | fallbackImage: String? = nil, 150 | contentMode: SwiftUI.ContentMode = .fill, 151 | loadingStyle: UIActivityIndicatorView.Style? = nil, 152 | imageAlignment: Alignment = .center 153 | ) { 154 | self.init( 155 | image: nil, 156 | url: url, 157 | width: width, 158 | height: height, 159 | tintColor: tintColor, 160 | fallbackImage: fallbackImage, 161 | contentMode: contentMode, 162 | loadingStyle: loadingStyle, 163 | imageAlignment: imageAlignment 164 | ) 165 | } 166 | 167 | private init( 168 | image: Image?, 169 | url: URL?, 170 | width: CGFloat?, 171 | height: CGFloat?, 172 | tintColor: Color? = nil, 173 | fallbackImage: String? = nil, 174 | contentMode: SwiftUI.ContentMode, 175 | loadingStyle: UIActivityIndicatorView.Style?, 176 | imageAlignment: Alignment 177 | ) { 178 | self.image = image 179 | self.url = url 180 | self.width = width == .infinity ? nil : width 181 | self.height = height == .infinity ? nil : height 182 | self.tintColor = tintColor 183 | self.contentMode = contentMode 184 | self.loadingStyle = loadingStyle 185 | self.imageAlignment = imageAlignment 186 | 187 | if let imageName = fallbackImage { 188 | self.fallbackImage = UIImage(named: imageName) 189 | } else { 190 | self.fallbackImage = nil 191 | } 192 | } 193 | 194 | public var body: some View { 195 | if let url = url { 196 | if didErrorWithNoFallback { 197 | Color.clear 198 | .frame(width: width, height: height, alignment: imageAlignment) 199 | } else { 200 | KFImage(url) 201 | .renderingMode(tintColor == nil ? .original : .template) 202 | .resizable() 203 | .placeholder { 204 | ProgressView() 205 | } 206 | .onFailure { _ in 207 | if fallbackImage == nil { 208 | didErrorWithNoFallback = true 209 | } 210 | } 211 | .onFailureImage(fallbackImage) 212 | .foregroundColor(tintColor) 213 | .aspectRatio(contentMode: contentMode) 214 | .frame(width: width, height: height, alignment: imageAlignment) 215 | .clipped() 216 | } 217 | } else if let image = image { 218 | image 219 | .renderingMode(tintColor == nil ? .original : .template) 220 | .resizable() 221 | .aspectRatio(contentMode: contentMode) 222 | .frame(width: width, height: height, alignment: imageAlignment) 223 | .foregroundColor(tintColor) 224 | .clipped() 225 | } else { 226 | EmptyView() 227 | } 228 | } 229 | } 230 | 231 | struct NiceImage_Previews: PreviewProvider { 232 | static var previews: some View { 233 | NiceImage("gear", width: 44, height: 44) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /NiceComponentsExample/NiceComponentsExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B36D8F802B71A89D0095B13B /* CustomizingComponentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D8F7F2B71A89D0095B13B /* CustomizingComponentsView.swift */; }; 11 | C614E74226E12DAB00F7F87C /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C614E74126E12DAB00F7F87C /* Theme.swift */; }; 12 | C628F0D325C4A08B001331AB /* NotoSerif-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C628F0D125C4A08B001331AB /* NotoSerif-Bold.ttf */; }; 13 | C628F0D425C4A08B001331AB /* NotoSerif-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C628F0D225C4A08B001331AB /* NotoSerif-Regular.ttf */; }; 14 | C671309E25C4948800F75E44 /* NiceComponentsExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C671309D25C4948800F75E44 /* NiceComponentsExampleApp.swift */; }; 15 | C67130A025C4948800F75E44 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C671309F25C4948800F75E44 /* ContentView.swift */; }; 16 | C67130A225C4948900F75E44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C67130A125C4948900F75E44 /* Assets.xcassets */; }; 17 | C67130A525C4948900F75E44 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C67130A425C4948900F75E44 /* Preview Assets.xcassets */; }; 18 | C67130B625C4966400F75E44 /* NiceComponents in Frameworks */ = {isa = PBXBuildFile; productRef = C67130B525C4966400F75E44 /* NiceComponents */; }; 19 | C6CD255825D6F01C008026D5 /* AllComponentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CD255725D6F01C008026D5 /* AllComponentsView.swift */; }; 20 | C6CD255D25D6F0B1008026D5 /* SampleSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CD255C25D6F0B1008026D5 /* SampleSignInView.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | B36D8F7F2B71A89D0095B13B /* CustomizingComponentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizingComponentsView.swift; sourceTree = ""; }; 25 | C614E74126E12DAB00F7F87C /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 26 | C628F0D125C4A08B001331AB /* NotoSerif-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSerif-Bold.ttf"; sourceTree = ""; }; 27 | C628F0D225C4A08B001331AB /* NotoSerif-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSerif-Regular.ttf"; sourceTree = ""; }; 28 | C671309A25C4948800F75E44 /* NiceComponentsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NiceComponentsExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | C671309D25C4948800F75E44 /* NiceComponentsExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NiceComponentsExampleApp.swift; sourceTree = ""; }; 30 | C671309F25C4948800F75E44 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 31 | C67130A125C4948900F75E44 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 32 | C67130A425C4948900F75E44 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 33 | C67130A625C4948900F75E44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | C67130AD25C4949100F75E44 /* NiceComponents */ = {isa = PBXFileReference; lastKnownFileType = folder; name = NiceComponents; path = ..; sourceTree = ""; }; 35 | C6CD255725D6F01C008026D5 /* AllComponentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllComponentsView.swift; sourceTree = ""; }; 36 | C6CD255C25D6F0B1008026D5 /* SampleSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleSignInView.swift; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | C671309725C4948800F75E44 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | C67130B625C4966400F75E44 /* NiceComponents in Frameworks */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | C628F0D025C49F9B001331AB /* Resources */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | C628F0D125C4A08B001331AB /* NotoSerif-Bold.ttf */, 55 | C628F0D225C4A08B001331AB /* NotoSerif-Regular.ttf */, 56 | C614E74126E12DAB00F7F87C /* Theme.swift */, 57 | ); 58 | path = Resources; 59 | sourceTree = ""; 60 | }; 61 | C671309125C4948800F75E44 = { 62 | isa = PBXGroup; 63 | children = ( 64 | C67130AD25C4949100F75E44 /* NiceComponents */, 65 | C671309C25C4948800F75E44 /* NiceComponentsExample */, 66 | C671309B25C4948800F75E44 /* Products */, 67 | C67130AF25C495C200F75E44 /* Frameworks */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | C671309B25C4948800F75E44 /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | C671309A25C4948800F75E44 /* NiceComponentsExample.app */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | C671309C25C4948800F75E44 /* NiceComponentsExample */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | C6CD255625D6F006008026D5 /* View */, 83 | C628F0D025C49F9B001331AB /* Resources */, 84 | C671309D25C4948800F75E44 /* NiceComponentsExampleApp.swift */, 85 | C671309F25C4948800F75E44 /* ContentView.swift */, 86 | C67130A125C4948900F75E44 /* Assets.xcassets */, 87 | C67130A625C4948900F75E44 /* Info.plist */, 88 | C67130A325C4948900F75E44 /* Preview Content */, 89 | ); 90 | path = NiceComponentsExample; 91 | sourceTree = ""; 92 | }; 93 | C67130A325C4948900F75E44 /* Preview Content */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | C67130A425C4948900F75E44 /* Preview Assets.xcassets */, 97 | ); 98 | path = "Preview Content"; 99 | sourceTree = ""; 100 | }; 101 | C67130AF25C495C200F75E44 /* Frameworks */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | ); 105 | name = Frameworks; 106 | sourceTree = ""; 107 | }; 108 | C6CD255625D6F006008026D5 /* View */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | C6CD255725D6F01C008026D5 /* AllComponentsView.swift */, 112 | C6CD255C25D6F0B1008026D5 /* SampleSignInView.swift */, 113 | B36D8F7F2B71A89D0095B13B /* CustomizingComponentsView.swift */, 114 | ); 115 | path = View; 116 | sourceTree = ""; 117 | }; 118 | /* End PBXGroup section */ 119 | 120 | /* Begin PBXNativeTarget section */ 121 | C671309925C4948800F75E44 /* NiceComponentsExample */ = { 122 | isa = PBXNativeTarget; 123 | buildConfigurationList = C67130A925C4948900F75E44 /* Build configuration list for PBXNativeTarget "NiceComponentsExample" */; 124 | buildPhases = ( 125 | C671309625C4948800F75E44 /* Sources */, 126 | C671309725C4948800F75E44 /* Frameworks */, 127 | C671309825C4948800F75E44 /* Resources */, 128 | ); 129 | buildRules = ( 130 | ); 131 | dependencies = ( 132 | ); 133 | name = NiceComponentsExample; 134 | packageProductDependencies = ( 135 | C67130B525C4966400F75E44 /* NiceComponents */, 136 | ); 137 | productName = NiceComponentsExample; 138 | productReference = C671309A25C4948800F75E44 /* NiceComponentsExample.app */; 139 | productType = "com.apple.product-type.application"; 140 | }; 141 | /* End PBXNativeTarget section */ 142 | 143 | /* Begin PBXProject section */ 144 | C671309225C4948800F75E44 /* Project object */ = { 145 | isa = PBXProject; 146 | attributes = { 147 | LastSwiftUpdateCheck = 1240; 148 | LastUpgradeCheck = 1240; 149 | TargetAttributes = { 150 | C671309925C4948800F75E44 = { 151 | CreatedOnToolsVersion = 12.4; 152 | }; 153 | }; 154 | }; 155 | buildConfigurationList = C671309525C4948800F75E44 /* Build configuration list for PBXProject "NiceComponentsExample" */; 156 | compatibilityVersion = "Xcode 9.3"; 157 | developmentRegion = en; 158 | hasScannedForEncodings = 0; 159 | knownRegions = ( 160 | en, 161 | Base, 162 | ); 163 | mainGroup = C671309125C4948800F75E44; 164 | productRefGroup = C671309B25C4948800F75E44 /* Products */; 165 | projectDirPath = ""; 166 | projectRoot = ""; 167 | targets = ( 168 | C671309925C4948800F75E44 /* NiceComponentsExample */, 169 | ); 170 | }; 171 | /* End PBXProject section */ 172 | 173 | /* Begin PBXResourcesBuildPhase section */ 174 | C671309825C4948800F75E44 /* Resources */ = { 175 | isa = PBXResourcesBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | C67130A525C4948900F75E44 /* Preview Assets.xcassets in Resources */, 179 | C628F0D425C4A08B001331AB /* NotoSerif-Regular.ttf in Resources */, 180 | C628F0D325C4A08B001331AB /* NotoSerif-Bold.ttf in Resources */, 181 | C67130A225C4948900F75E44 /* Assets.xcassets in Resources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXResourcesBuildPhase section */ 186 | 187 | /* Begin PBXSourcesBuildPhase section */ 188 | C671309625C4948800F75E44 /* Sources */ = { 189 | isa = PBXSourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | C67130A025C4948800F75E44 /* ContentView.swift in Sources */, 193 | C671309E25C4948800F75E44 /* NiceComponentsExampleApp.swift in Sources */, 194 | C614E74226E12DAB00F7F87C /* Theme.swift in Sources */, 195 | C6CD255D25D6F0B1008026D5 /* SampleSignInView.swift in Sources */, 196 | C6CD255825D6F01C008026D5 /* AllComponentsView.swift in Sources */, 197 | B36D8F802B71A89D0095B13B /* CustomizingComponentsView.swift in Sources */, 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | /* End PBXSourcesBuildPhase section */ 202 | 203 | /* Begin XCBuildConfiguration section */ 204 | C67130A725C4948900F75E44 /* Debug */ = { 205 | isa = XCBuildConfiguration; 206 | buildSettings = { 207 | ALWAYS_SEARCH_USER_PATHS = NO; 208 | CLANG_ANALYZER_NONNULL = YES; 209 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 210 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 211 | CLANG_CXX_LIBRARY = "libc++"; 212 | CLANG_ENABLE_MODULES = YES; 213 | CLANG_ENABLE_OBJC_ARC = YES; 214 | CLANG_ENABLE_OBJC_WEAK = YES; 215 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 216 | CLANG_WARN_BOOL_CONVERSION = YES; 217 | CLANG_WARN_COMMA = YES; 218 | CLANG_WARN_CONSTANT_CONVERSION = YES; 219 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 220 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 221 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 222 | CLANG_WARN_EMPTY_BODY = YES; 223 | CLANG_WARN_ENUM_CONVERSION = YES; 224 | CLANG_WARN_INFINITE_RECURSION = YES; 225 | CLANG_WARN_INT_CONVERSION = YES; 226 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 227 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 228 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 229 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 230 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 231 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 232 | CLANG_WARN_STRICT_PROTOTYPES = YES; 233 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 234 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 235 | CLANG_WARN_UNREACHABLE_CODE = YES; 236 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 237 | COPY_PHASE_STRIP = NO; 238 | DEBUG_INFORMATION_FORMAT = dwarf; 239 | ENABLE_STRICT_OBJC_MSGSEND = YES; 240 | ENABLE_TESTABILITY = YES; 241 | GCC_C_LANGUAGE_STANDARD = gnu11; 242 | GCC_DYNAMIC_NO_PIC = NO; 243 | GCC_NO_COMMON_BLOCKS = YES; 244 | GCC_OPTIMIZATION_LEVEL = 0; 245 | GCC_PREPROCESSOR_DEFINITIONS = ( 246 | "DEBUG=1", 247 | "$(inherited)", 248 | ); 249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 251 | GCC_WARN_UNDECLARED_SELECTOR = YES; 252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 253 | GCC_WARN_UNUSED_FUNCTION = YES; 254 | GCC_WARN_UNUSED_VARIABLE = YES; 255 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 256 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 257 | MTL_FAST_MATH = YES; 258 | ONLY_ACTIVE_ARCH = YES; 259 | SDKROOT = iphoneos; 260 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 261 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 262 | }; 263 | name = Debug; 264 | }; 265 | C67130A825C4948900F75E44 /* Release */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_ANALYZER_NONNULL = YES; 270 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 272 | CLANG_CXX_LIBRARY = "libc++"; 273 | CLANG_ENABLE_MODULES = YES; 274 | CLANG_ENABLE_OBJC_ARC = YES; 275 | CLANG_ENABLE_OBJC_WEAK = YES; 276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 277 | CLANG_WARN_BOOL_CONVERSION = YES; 278 | CLANG_WARN_COMMA = YES; 279 | CLANG_WARN_CONSTANT_CONVERSION = YES; 280 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 281 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 282 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 283 | CLANG_WARN_EMPTY_BODY = YES; 284 | CLANG_WARN_ENUM_CONVERSION = YES; 285 | CLANG_WARN_INFINITE_RECURSION = YES; 286 | CLANG_WARN_INT_CONVERSION = YES; 287 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 288 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 289 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 290 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 291 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 292 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 293 | CLANG_WARN_STRICT_PROTOTYPES = YES; 294 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 295 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 296 | CLANG_WARN_UNREACHABLE_CODE = YES; 297 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 298 | COPY_PHASE_STRIP = NO; 299 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 300 | ENABLE_NS_ASSERTIONS = NO; 301 | ENABLE_STRICT_OBJC_MSGSEND = YES; 302 | GCC_C_LANGUAGE_STANDARD = gnu11; 303 | GCC_NO_COMMON_BLOCKS = YES; 304 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 305 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 306 | GCC_WARN_UNDECLARED_SELECTOR = YES; 307 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 308 | GCC_WARN_UNUSED_FUNCTION = YES; 309 | GCC_WARN_UNUSED_VARIABLE = YES; 310 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 311 | MTL_ENABLE_DEBUG_INFO = NO; 312 | MTL_FAST_MATH = YES; 313 | SDKROOT = iphoneos; 314 | SWIFT_COMPILATION_MODE = wholemodule; 315 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 316 | VALIDATE_PRODUCT = YES; 317 | }; 318 | name = Release; 319 | }; 320 | C67130AA25C4948900F75E44 /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 325 | CODE_SIGN_STYLE = Automatic; 326 | DEVELOPMENT_ASSET_PATHS = "\"NiceComponentsExample/Preview Content\""; 327 | DEVELOPMENT_TEAM = GH868RP95T; 328 | ENABLE_PREVIEWS = YES; 329 | INFOPLIST_FILE = NiceComponentsExample/Info.plist; 330 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 331 | LD_RUNPATH_SEARCH_PATHS = ( 332 | "$(inherited)", 333 | "@executable_path/Frameworks", 334 | ); 335 | MARKETING_VERSION = 0.5; 336 | PRODUCT_BUNDLE_IDENTIFIER = com.steamclock.NiceComponentsExample; 337 | PRODUCT_NAME = "$(TARGET_NAME)"; 338 | SWIFT_VERSION = 5.0; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | C67130AB25C4948900F75E44 /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 348 | CODE_SIGN_STYLE = Automatic; 349 | DEVELOPMENT_ASSET_PATHS = "\"NiceComponentsExample/Preview Content\""; 350 | DEVELOPMENT_TEAM = GH868RP95T; 351 | ENABLE_PREVIEWS = YES; 352 | INFOPLIST_FILE = NiceComponentsExample/Info.plist; 353 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 354 | LD_RUNPATH_SEARCH_PATHS = ( 355 | "$(inherited)", 356 | "@executable_path/Frameworks", 357 | ); 358 | MARKETING_VERSION = 0.5; 359 | PRODUCT_BUNDLE_IDENTIFIER = com.steamclock.NiceComponentsExample; 360 | PRODUCT_NAME = "$(TARGET_NAME)"; 361 | SWIFT_VERSION = 5.0; 362 | TARGETED_DEVICE_FAMILY = "1,2"; 363 | }; 364 | name = Release; 365 | }; 366 | /* End XCBuildConfiguration section */ 367 | 368 | /* Begin XCConfigurationList section */ 369 | C671309525C4948800F75E44 /* Build configuration list for PBXProject "NiceComponentsExample" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | C67130A725C4948900F75E44 /* Debug */, 373 | C67130A825C4948900F75E44 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | C67130A925C4948900F75E44 /* Build configuration list for PBXNativeTarget "NiceComponentsExample" */ = { 379 | isa = XCConfigurationList; 380 | buildConfigurations = ( 381 | C67130AA25C4948900F75E44 /* Debug */, 382 | C67130AB25C4948900F75E44 /* Release */, 383 | ); 384 | defaultConfigurationIsVisible = 0; 385 | defaultConfigurationName = Release; 386 | }; 387 | /* End XCConfigurationList section */ 388 | 389 | /* Begin XCSwiftPackageProductDependency section */ 390 | C67130B525C4966400F75E44 /* NiceComponents */ = { 391 | isa = XCSwiftPackageProductDependency; 392 | productName = NiceComponents; 393 | }; 394 | /* End XCSwiftPackageProductDependency section */ 395 | }; 396 | rootObject = C671309225C4948800F75E44 /* Project object */; 397 | } 398 | --------------------------------------------------------------------------------