├── 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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------