├── .gitignore
├── Assets
├── Docs
│ ├── Components
│ │ ├── Containers
│ │ │ └── GridView
│ │ │ │ ├── grid-view-example-1.png
│ │ │ │ ├── grid-view-example-2.png
│ │ │ │ ├── grid-view-example-3.png
│ │ │ │ └── grid-view.md
│ │ ├── Extensions
│ │ │ └── InnerShadows
│ │ │ │ ├── a-little-depth-with-gradient.png
│ │ │ │ ├── a-little-depth.png
│ │ │ │ ├── a-lot-of-depth.png
│ │ │ │ ├── animated-shadows.gif
│ │ │ │ ├── caveat-stroke-clipped.png
│ │ │ │ ├── caveat-stroke-inset.png
│ │ │ │ ├── caveat-stroke-rounded.png
│ │ │ │ ├── green-text-with-shadow-incorrect.png
│ │ │ │ ├── green-text-with-shadow.png
│ │ │ │ ├── inner-shadows.md
│ │ │ │ ├── lighting.png
│ │ │ │ ├── navigation-animation.gif
│ │ │ │ ├── pause-button.png
│ │ │ │ ├── profile-pics-1-artifacts-zoomed.png
│ │ │ │ ├── profile-pics-1-artifacts.png
│ │ │ │ ├── profile-pics-1.png
│ │ │ │ ├── profile-pics-2-artifacts-zoomed.png
│ │ │ │ ├── profile-pics-2-artifacts.png
│ │ │ │ ├── profile-pics-2.png
│ │ │ │ ├── rating.png
│ │ │ │ ├── sf-symbol-background-scaled.png
│ │ │ │ ├── sf-symbol-cropped-background-bounding-region.png
│ │ │ │ ├── sf-symbol-cropped-background.png
│ │ │ │ ├── shape-fill.png
│ │ │ │ ├── shape-stroke.png
│ │ │ │ └── text-depth.png
│ │ └── Model
│ │ │ └── Color
│ │ │ ├── gradient-map.md
│ │ │ ├── gradient-severity-picker-animation.gif
│ │ │ └── gradient-size-based-color-animation.gif
│ └── LICENCE.md
└── Images
│ └── pure-swift-ui-tools-logo.png
├── Package.swift
├── README.md
├── Sources
└── PureSwiftUITools
│ ├── ExportedModules.swift
│ ├── Extensions
│ └── InnerShadows
│ │ ├── PS_InnerShadowExtensions.swift
│ │ ├── PS_InnerShadowModel.swift
│ │ └── PS_InnerShadowViewModifiers.swift
│ ├── Model
│ ├── GradientMap.swift
│ └── RGBA.swift
│ └── Views
│ └── Containers
│ └── GridView.swift
└── Tests
└── PureSwiftUIToolsTests
└── Model
└── Color
└── RGBGradientMapTests.swift
/.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 |
92 | # project specific
93 | Resources/
94 | Package.resolved
95 |
--------------------------------------------------------------------------------
/Assets/Docs/Components/Containers/GridView/grid-view-example-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Containers/GridView/grid-view-example-1.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Containers/GridView/grid-view-example-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Containers/GridView/grid-view-example-2.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Containers/GridView/grid-view-example-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Containers/GridView/grid-view-example-3.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Containers/GridView/grid-view.md:
--------------------------------------------------------------------------------
1 | # GridView
2 |
3 | It's common in `SwiftUI` to create layouts in grids. This rather repetitive boiler-plate of nested `ForEach` views distracts from the intent of the code. `GridView` abstracts this logic into a container view and - as with the case with `ForEach` provides the context of each cell's column and row allowing the calling code to style each component view accordingly.
4 |
5 | A simple 3x3 grid is created like so:
6 |
7 | ```swift
8 | GridView(5, spacing: 8) { column, row in
9 | TitleText("\(column),\(row)", .white)
10 | .greedyFrame()
11 | .clipRoundedRectangleWithStroke(10, .black, lineWidth: 2, fill: Color.blue)
12 | .shadow(5)
13 | }
14 | .frame(300, 300)
15 | ```
16 |
17 | Which produces the following output:
18 |
19 |
20 |
21 |
22 |
23 | You can control the individual size of the cells by specifying the exact size of the components. Perhaps based on the column and row, for example:
24 |
25 | ```swift
26 | GridView(3, 5, spacing: 10) { column, row in
27 | Frame(50 * (column + 1), 30 * (row + 1), .blue)
28 | .clipRoundedRectangleWithStroke(10, .black, lineWidth: 2)
29 | .shadow(5)
30 | }
31 | ```
32 |
33 | Resulting in this grid:
34 |
35 |
36 |
37 |
38 |
39 | Of course grids within grids are just as easy. The following output is produced with the gist found [here][gist-nested-grids]:
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 | [gist-nested-grids]: https://gist.github.com/CodeSlicing/9266dc4cd23d58e81d4cc52999de2def
50 |
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/a-little-depth-with-gradient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/a-little-depth-with-gradient.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/a-little-depth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/a-little-depth.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/a-lot-of-depth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/a-lot-of-depth.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/animated-shadows.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/animated-shadows.gif
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/caveat-stroke-clipped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/caveat-stroke-clipped.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/caveat-stroke-inset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/caveat-stroke-inset.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/caveat-stroke-rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/caveat-stroke-rounded.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/green-text-with-shadow-incorrect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/green-text-with-shadow-incorrect.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/green-text-with-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/green-text-with-shadow.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/inner-shadows.md:
--------------------------------------------------------------------------------
1 | # Inner Shadows
2 |
3 | With inner shadows in [PureSwiftUITools][pure-swift-ui-tools] you can easily create interesting looking UIs with a little depth to them:
4 |
5 |
6 |
7 |
8 |
9 | Or maybe a lot?
10 |
11 |
12 |
13 |
14 |
15 | Clearly this is an area where a less-is-more approach would perhaps be a wise one, but I'm not here to tell you how to write fantastic and engaging user interfaces, I'm just giving you some tools; use them as you see fit.
16 |
17 | There are various ways in which inner shadows can be applied in [PureSwiftUI][pure-swift-ui] and this section will go through each in turn. It is important to note that the available options depend on the type on which the modifier is directly applied. So for `Shape` the arguments will be different to those for an `Image`, `SFSymbol`, `Text`, or a `View`.
18 |
19 | - [API](#api)
20 | - [Shapes](#shapes)
21 | - [SF Symbols and Alpha](#sf-symbols-and-alpha)
22 | - [Views](#views)
23 | - [Text](#text)
24 | - [Configuring the Shadow](#configuring-the-shadow)
25 | - [Animating the Shadow](#animating-the-shadow)
26 | - [Known Problems and Workarounds](#known-problems-and-workarounds)
27 | - [Foreground Color on Text](#foreground-color-on-text)
28 | - [Scaling Contents](#scaling-contents)
29 | - [Clipping of Stroked Shapes](#clipping-of-stroked-shapes)
30 | - [Error on `clipShape` for Masked Inner Shadows](#error-on-clipshape-for-masked-inner-shadows)
31 |
32 | ## API
33 |
34 | Regardless of the type on which the inner shadow is being applied, the general structure of the modifier is the same. You pass in the *content*
35 | and the *configuration* for the shadow:
36 |
37 | ```swift
38 | let content = Color.white
39 | let config = PSInnerShadowConfig(radius: 3, offset: .point(1))
40 | Something()
41 | .ps_innerShadow(content, config)
42 | // which is equivalent to:
43 | .ps_innerShadow(content, radius: 3, offset: .point(1))
44 | // or if you want a transparent background with no shadow offset
45 | .ps_innerShadow(radius: 3)
46 | ```
47 |
48 | The content is a `View` so you are not restricted to `ShapeStyle` as you would be with a normal shape fill. This defines the background that is going to have the inner shadow applied to it. If you want a transparent background, you can either provide `Color.clear` as the content, or omit the argument altogether.
49 |
50 | In the case of placing an inner shadow on a `View`, the `content` parameter should consist of a shape as well as the content. I will go into more detail when talking specifically about [Views](#views).
51 |
52 | ## Shapes
53 |
54 | There are two ways of decorating shapes with inner shadows: You can either decorate the fill or the stroke. However, since a stroke on a shape is actually just another shape, they are *ultimately* the same thing. The inner shadow modifier on a `Shape` takes either a `FillAndContent` or a `StrokeAndContent` which determine whether or not the inner shadow is created on the stroke of the shape or the fill.
55 |
56 | Let's put an inner shadow on the stroke of a `Circle`:
57 |
58 | ```swift
59 | let gradient = LinearGradient([.red, .yellow], to: .topTrailing)
60 | ...
61 | Circle().inset(by: 10)
62 | .ps_innerShadow(.stroke(gradient, lineWidth: 20), radius: 4)
63 | .frame(100)
64 | ```
65 |
66 | which gives us:
67 |
68 |
69 |
70 |
71 |
72 | I'll go into more detail as to why we need to inset this shape in the [caveats](#known-problems-and-workarounds) section.
73 |
74 | If we wanted to put an inner shadow with a fill, we would do it like this:
75 |
76 | ```swift
77 | Circle()
78 | .ps_innerShadow(.fill(gradient), radius: 4)
79 | .frame(100)
80 | ```
81 |
82 | and we get:
83 |
84 |
85 |
86 |
87 |
88 | As you can see, the API relies on either passing a stroke or a fill configuration to the inner shadow modifier.
89 |
90 | There are a couple of issues when specifically working with inner shadows on strokes, so please refer to the [known problems and workarounds](#known-problems-and-workarounds) section so you are aware of them.
91 |
92 | ## Views
93 |
94 | When decorating a `View` with an inner shadow, you must specify the shape for the shadow. This shape will fill the view which is why the inner shadow modifier takes a content argument as well as a shape so it can provide the background at the same time. The advantage of doing it this way is that it will clip the background to the shape in question. So let's take the example at the top of the page and give a little bit of depth a bit of pizzazz with a gradient background. You can achieve this by passing a gradient to the capsule shape configuration as follows:
95 |
96 | ```swift
97 | private let gradient = LinearGradient([.red, .yellow], to: .trailing)
98 | ...
99 | Text("A little depth")
100 | .padding()
101 | .ps_innerShadow(.capsule(gradient), radius: 2)
102 | ```
103 |
104 | and you get this simply spectacular result:
105 |
106 |
107 |
108 |
109 |
110 | This is just an example of how *any* `View` can be modified with an inner shadow, including `VStack`s, and of course `Images` which opens up a big door of creativity through which to walk. Let's say for example you want to use inner shadows for profile pictures or something.
111 |
112 | There are two main ways of doing this - one has a small advantage discussed in the section on [known problems](#known-problems-and-workarounds) - the first way is to add an inner shadow directly to the image `View`:
113 |
114 | ```swift
115 | Image("profile-pic-\(index)")
116 | .resizedToFill(90)
117 | .ps_innerShadow(.roundedRectangle(10), radius: 5, intensity: 0.6)
118 | .clipShape(RoundedRectangle(10))
119 | ```
120 |
121 | Giving us:
122 |
123 |
124 |
125 |
126 |
127 | Note that in this case, since there is some contrast in the images, I have increased the intensity slightly to 0.6 from a default of 0.56. Now, looking at the code you can see that I have to manually clip the shape to a rounded rectangle. This is due to the way inner shadows are implemented and stems from a bug in the `SwiftUI` framework at the time of writing meaning I'm forced to code it in such a way that I cannot clip the overall `View` internally. Not a huge hardship I guess, but you can achieve the same result by applying an inner shadow to a `Frame` and using the content argument of the inner shadow itself to be the image in question like so:
128 |
129 | ```swift
130 | Frame(90)
131 | .ps_innerShadow(.roundedRectangle(10, Image("profile-pic-\(index)").resizedToFill()), radius: 5, intensity: 0.6)
132 | ```
133 |
134 | Which has the advantage of being somewhat more concise and ends up with the same output. The benefit I mentioned also pertains to this technique having fewer masking artifacts as discussed [here](#known-problems-and-workarounds).
135 |
136 | ## SF Symbols and Alpha
137 |
138 | Although inner shadows can be added to an `Image` in the same way as can be added to an `SFSymbol`, this usage is really meant just for `SFSymbols` or other images that have transparency. It's the alpha that's the key here (literally), so let's take a look at what that means.
139 |
140 | `SFSymbols` are essentially vector shapes consisting of opaque and transparent areas. This can be exploited for use with inner shadows and is used as follows:
141 |
142 | ```swift
143 | private struct RatingView: View {
144 | var body: some View {
145 | HStack(spacing: 20) {
146 | ForEach(0..<5) { index in
147 | SFSymbol(.star_fill)
148 | .ps_innerShadow(gradient.scale(2).opacityIf(index > 3, 0), radius: 2, offset: .point(1))
149 | .fontSize(45, weight: .bold)
150 | }
151 | }
152 | }
153 | }
154 | ```
155 |
156 | to generate the following rating view:
157 |
158 |
159 |
160 |
161 |
162 | A couple of takeaways from this code: I'm offsetting the shadow in this example to give the shadow a sense of direction. If you wanted to explore a directions of light a little more, you could create a background with a gradient to give the illusion of a light source:
163 |
164 | ```swift
165 | let backgroundGradient = LinearGradient([Color(white: 0.9), Color(white: 0.5)], from: .topLeading)
166 | private struct MicButton2View: View {
167 | var body: some View {
168 | SFSymbol(.star_fill)
169 | .ps_innerShadow(gradient.scale(2), radius: 2, offset: 1, angle: .bottomTrailing)
170 | .fontSize(45, weight: .bold)
171 | .frame(90)
172 | .ps_innerShadow(.circle(backgroundGradient), radius: 3, offset: 1, angle: .bottomTrailing)
173 | }
174 | }
175 | ```
176 |
177 | and gives you:
178 |
179 |
180 |
181 |
182 |
183 | The other thing worth mentioning is the fact that I'm scaling up the gradient to be used as the background for the `SFSymbol`. The reason this is necessary is covered in the [caveats](#known-problems-and-workarounds) section so take a look.
184 |
185 | You could also build some kind of navigation, perhaps, where a filled background indicates the selected state:
186 |
187 |
188 |
189 |
190 |
191 | which is achieved thus:
192 |
193 | ```swift
194 | private struct NavigationView: View {
195 | @State private var selectedIndex = 0
196 | ...
197 | let symbols: [SFSymbolName] = [.envelope_fill, .paperclip, .person_2_fill]
198 | var body: some View {
199 | HStack(spacing: 50) {
200 | ForEach(0..
227 |
228 |
229 |
230 | ## Configuring the Shadow
231 |
232 | The inner shadow can be configured with a `radius`, an `offset`, and an `intensity` or `shadowColor`. An important difference in the way inner shadows are implemented and native shadows in `SwiftUI` is that inner shadows can only be used in a way that darkens the result, even when specifying the `shadowColor`. This is because, for various reasons, the effect is ultimately implemented using the `multiply` blend-mode under the covers.
233 |
234 | The `radius` parameter is equivalent to the `radius` parameter for the native shadow effect. In reality this is the `radius` of the blur that's applied to the mask that supplies the inner shadow.
235 |
236 | The `offset` can be defined in two ways. You can either directly specify an offset defined by a `CGPoint`, or you can specify an offset length and an angle. The following are equivalent:
237 |
238 | ```swift
239 | .ps_innerShadow(radius: 3, offset: 1, angle: .trailing) // or angle: 90.degrees
240 | // is equivalent to
241 | .ps_innerShadow(radius: 3, offset: .x(1))
242 | // since .x(v) is a static constructor for CGPoint resulting in CGPoint(x: v, y: 0)
243 | ```
244 |
245 | The inner shadow `intensity` determines the darkness of the shadow from 0 to 1. This is useful in situations where the color or contrast surrounding the component is higher meaning the shadow effect needs a little more oomph to be noticeable.
246 |
247 | As hinted at previously, you can also tint the shadow by specifying the `shadowColor`. This will only tint the shadow, however, since as explained earlier the blend is a multiplication. Darker colors will have more of an impact for this same reason.
248 |
249 | ## Animating the Shadow
250 |
251 | Unlike native shadows in `SwiftUI`, inner shadows can be animated. You can animate the radius of the shadow and the offset and / or angle in which the shadow falls.
252 |
253 |
254 |
255 |
256 |
257 | I'm not sure how often animated shadows would be desirable in a UI, but it certainly looks cool.
258 |
259 | ## Known Problems and Workarounds
260 |
261 | This section will discuss a number of issues associated with inner shadows. These are all the result of current limitations of the `SwiftUI` framework, or actual bugs. There are workarounds for all but one of them at this time.
262 |
263 | ### Foreground Color on Text
264 |
265 | If you are applying an inner shadow to a `Text` component, you must not specify a foreground color on it. This is because the result of applying a `foregroundColor` modifier to a `Text` component is another `Text` component with a foreground color. This is important because once a `Text` element has had a foreground color applied to it, you cannot change it.
266 |
267 | Internally, the implementation for inner shadows on text requires that I'm able to use various foreground colors on versions of the `Text` component being modified. Therefore the effect simply won't work if a foreground color has already been applied.
268 |
269 | So if you want green text with an inner shadow, the following will not work:
270 |
271 | ```swift
272 | // using PureSwiftUI convenience constructor:
273 | CustomText("Green Text", 60, .green, .bold)
274 | .ps_innerShadow(radius: 2, offset: .point(1))
275 |
276 | // natively in SwiftUI
277 | Text("Green Text")
278 | .foregroundColor(.green)
279 | .ps_innerShadow(radius: 2, offset: .point(1))
280 | .font(Font.system(size: 60).weight(.bold))
281 | ```
282 |
283 | The [PureSwiftUI][pure-swift-ui] convenience constructor internally sets the foreground color of the Text and in the native example we are explicitly setting it and it cannot work as expected. The result will be:
284 |
285 |
286 |
287 |
288 |
289 | Not only are the inner shadows not working, but you can see that the color is darker than it should be as a result of the multiplication blend-mode going on under the covers.
290 |
291 | Instead we should pass in the `Color` as the content argument to the inner shadow modifier and avoid setting the foreground color even implicitly, like so:
292 |
293 | ```swift
294 | Text("Green Text")
295 | .ps_innerShadow(Color.green, radius: 2, offset: .point(1))
296 | .fontSize(60, weight: .bold)
297 | ```
298 |
299 | And it works!
300 |
301 |
302 |
303 |
304 |
305 | ### Scaling Contents
306 |
307 | As I mentioned in the section on [SF Symbols](#sf-symbols-and-alpha), I had to scale up the content in order for it to work correctly. I'll flesh that out a bit here.
308 |
309 | The problem is that the content backing the symbol is going to be restricted to the bounding rectangle for that `View`. When you use SF Symbols they will extend *beyond this boundary*. So, if you do not scale up the content, you will run into problems.
310 |
311 | So let's give this a try with the following code (using the gradient defined in an earlier example):
312 |
313 | ```swift
314 | SFSymbol(.music_house_fill)
315 | .ps_innerShadow(gradient, radius: 2, offset: .point(1))
316 | .fontSize(60, weight: .bold)
317 | ```
318 |
319 | Seems legit, but the output would be:
320 |
321 |
322 |
323 |
324 |
325 | So you can see that the content does not extend to cover the full extent of the symbol. It's clearer to see what's going on if I draw a line around the border of the symbol by adding:
326 |
327 | ```swift
328 | .border(Color.black, width: 1)
329 | ```
330 |
331 | to get this:
332 |
333 |
334 |
335 |
336 |
337 | So you can see that the content needs to be scaled up in order to cover the full extent of the symbol, and we do that like this:
338 |
339 | ```swift
340 | SFSymbol(.music_house_fill)
341 | .ps_innerShadow(gradient.scale(1.8), radius: 2, offset: .point(1))
342 | .fontSize(60, weight: .bold)
343 | ```
344 |
345 | Much better:
346 |
347 |
348 |
349 |
350 |
351 | Remember that you only have to scale the content as much as needed for the symbol in question, so that might be more or less than the 1.8 factor I've used in this example so play around with it to see what works for you.
352 |
353 | ### Clipping of Stroked Shapes
354 |
355 | There are a couple of caveats when working with inner shadows on strokes of a `Shape` so let's talk about them now.
356 |
357 | The first is what happens when you draw a shape that reaches the bounds of the canvas since adding a stroke will result in a shape that extends beyond that canvas. This presents problems in the current implementation of `SwiftUI` because inner shadows for shapes rely on using a `.drawingGroup()` modifier. Unfortunately when you apply this modifier to a `View` it restricts the contents to the bounding box for that `View`. So what does that mean for us in a practical sense?
358 |
359 | This next example uses a `Shape` defined like so:
360 |
361 | ```swift
362 | private struct DiamondShape: Shape {
363 | let inset: CGFloat
364 | func path(in rect: CGRect) -> Path {
365 | var path = Path()
366 | var layout = LayoutGuide.polar(rect.inset(inset), rings: 1, segments: 4)
367 | path.move(layout.top)
368 | for index in 1...layout.yCount {
369 | path.line(layout[1, index])
370 | }
371 | path.closeSubpath()
372 | return path
373 | }
374 | }
375 | ```
376 |
377 | which is essentially a diamond that extends to the edges of the canvas. Note the `inset` property of the `Shape` because that's going to be important going forward. Now I'm going to take that shape and apply an inner shadow to the stroke like this:
378 |
379 | ```swift
380 | let shadowConfig = PSInnerShadowConfig(radius: 1.5, offset: .point(0.5))
381 | ...
382 | DiamondShape(inset: 0)
383 | .ps_innerShadow(.stroke(gradient, lineWidth: 20), shadowConfig)
384 | ```
385 |
386 | Since we're using a stroke with this `Shape` we're going to run into problems since this will push the resulting `Shape` beyond the boundary. You can see the trouble here:
387 |
388 |
389 |
390 |
391 |
392 | Oh dear; not what we wanted. The parts of the shape that extend beyond the bounds of the `View` have been clipped! There is currently no way to fix this without making some adjustments. One thing you can do, and is what I would recommend, is to inset the `CGRect` being used to draw the original shape. Fortunately this is made easy in [PureSwiftUI][pure-swift-ui] as you can see in the `Shape` implementation above. So using an inset and a value for stroke that roughly maintains the original proportions and then scaling it up a bit to match the original:
393 |
394 | ```swift
395 | DiamondShape(inset: 13)
396 | .ps_innerShadow(.stroke(gradient, lineWidth: 16), shadowConfig)
397 | .scale(1.4)
398 | ```
399 |
400 | you can achieve the desired effect:
401 |
402 |
403 |
404 |
405 |
406 | Almost... This is good, but can you see the glitch at the top of the diamond? This is *also* due to the fact that a `.drawingGroup()` modifier is being used internally.
407 |
408 | Unfortunately, there's no workaround for this if you want to use sharp corners, but you can avoid the ugly artifact by using a rounded line join with the following stroke style:
409 |
410 | ```swift
411 | DiamondShape(inset: 13)
412 | .ps_innerShadow(.stroke(gradient, style: .init(lineWidth: 16, lineJoin: .round)), shadowConfig)
413 | .scale(1.4)
414 | ```
415 |
416 | So although it's not a workaround as such, it's still prettier:
417 |
418 |
419 |
420 |
421 |
422 | Before you get too down in the dumps about this last problem, it's actually a bit of an outlier when constructing shapes and I picked this example specifically to show off the issue. It also only happens at the join point when closing a sub-path and may be fixed in a future release of `SwiftUI` - if that happens I'll gladly update this page.
423 |
424 | ### Error on `clipShape` for Masked Inner Shadows
425 |
426 | Another issue of which I'm aware is due to another bug in the current `SwiftUI` implementation. It occurs when you apply a clipping shape to a `View` hierarchy to which a mask has been applied somewhere. Since `Text` and `Image` inner shadows are implemented using masks, you can potentially run into this problem when using them.
427 |
428 | It doesn't happen for *all* clip shapes either so I'll tell you what to look out for. To demonstrate we can look at an example that could potentially come up. So maybe we want to have an inner shadow on an SF symbol and then clip a `Circle` around that view and raise it with a shadow to create a pause button like this:
429 |
430 |
431 |
432 |
433 |
434 | The natural instinct would be to code it like this:
435 |
436 | ```swift
437 | private let gradient = LinearGradient([.red, .yellow], to: .topTrailing)
438 | private let backgroundGradient = LinearGradient([Color(white: 0.8), Color(white: 0.4)], to: .bottomTrailing)
439 | ...
440 | SFSymbol(.pause_fill)
441 | .ps_innerShadow(gradient.scale(1.2), radius: 2, offset: .point(1))
442 | .fontSize(50)
443 | .frame(100)
444 | .background(backgroundGradient)
445 | .clipCircle() // PureSwiftUI equivalent to .clipShape(Circle())
446 | .shadow(3, x: 1, y: 1)
447 | ```
448 |
449 | This will not work and will crash! It does not work for the same reason the following code does not work in the current implementation of `SwiftUI`:
450 |
451 | ```swift
452 | Color.red.mask(Circle())
453 | .clipCircle()
454 | ```
455 |
456 | which seems so simple yet does not run. Internally, inner shadows on `Image`s such as SF symbols are implemented using masks. So when you try to clip them you may run into problems like this and the insidious part is that there absolutely *zero* indication as to what's causing the problem. At least in Xcode. All you get is the "...may have crashed" error. You get more of a clue as to the root of the problem when you run it in Playgrounds on the iPad where it says "Could not cast value of type 'CALayer' to 'SwiftUI.MaskLayer' so you can be sure this is a problem with the framework that may or may not be fixed down the road.
457 |
458 | Fortunately there are a couple of workarounds for this once you know what's causing the problem. The easiest way is to simply use a mask instead of a clip shape:
459 |
460 | ```swift
461 | SFSymbol(.pause_fill)
462 | .ps_innerShadow(gradient.scale(1.2), radius: 2, offset: .point(1))
463 | .fontSize(50)
464 | .frame(100)
465 | .background(backgroundGradient)
466 | .mask(Circle()) // replaces .clipCircle()
467 | .shadow(3, x: 1, y: 1)
468 | ```
469 |
470 | or you can use a clip shape for the background and stick it in a `ZStack`:
471 |
472 | ```swift
473 | ZStack {
474 | Frame(100)
475 | .background(backgroundGradient)
476 | .clipCircle()
477 | .shadow(3, x: 1, y: 1)
478 | SFSymbol(.pause_fill)
479 | .ps_innerShadow(gradient.scale(1.2), radius: 2, offset: .point(1))
480 | .fontSize(50)
481 | }
482 | ```
483 |
484 | If performance is your main concern, the evidence is that clipping with a shape performs somewhat better than a mask although functionally they are the same. Not a difference that you'll notice if you're only talking about a handful of views so for the most part it really comes down to personal preference here.
485 |
486 | ### Masking Artifacts
487 |
488 | When we were designing some fancy profile picture thumbnails above I mentioned something about masking artifacts so let's look at that.
489 |
490 | In the [example](#views) everything looked just fine because it was on a white background. But what if we did the same thing on a more interesting background like a gray ramp like this:
491 |
492 |
493 |
494 |
495 |
496 | It may not be obvious so I'll zoom in a bit:
497 |
498 |
499 |
500 |
501 |
502 | So there you can see some aliasing artifacts that look a bit messy. I haven't found a way to address this internally yet but I mentioned that the second technique for creating these profile pics suffered less from this problem by using this code:
503 |
504 | ```swift
505 | Frame(90)
506 | .ps_innerShadow(.roundedRectangle(10, Image("profile-pic-\(index)").resizedToFill()), radius: 5, intensity: 0.8)
507 | ```
508 |
509 | And the result is:
510 |
511 |
512 |
513 |
514 |
515 | and zoomed in looks much better:
516 |
517 |
518 |
519 |
520 |
521 | Oh, when using a darker background you might need to bump up the intensity of the shadows to bring them out as I've done here using a value of 0.8.
522 |
523 | So that's it for inner shadows. If you have any questions, feel free to [ask][codeslice-twitter].
524 |
525 |
526 | **Image Credits:**
527 |
528 | [Mubariz Mehdizadeh](https://unsplash.com/@mehdizadeh?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
529 | [Isaiah McClean](https://unsplash.com/@isaiahmcclean?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
530 | [Štefan Štefančík](https://unsplash.com/@cikstefan?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
531 | [Candice Picard](https://unsplash.com/@candice_picard?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
532 | [J'Waye Covington](https://unsplash.com/@jwayecovington?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
533 | [Guilherme Stecanella](https://unsplash.com/@guilhermestecanella?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
534 | [Cody Black](https://unsplash.com/@cblack09?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
535 | [Tyler Nix](https://unsplash.com/@jtylernix?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
536 |
537 |
540 |
541 | [pure-swift-ui]: https://github.com/CodeSlicing/pure-swift-ui
542 | [pure-swift-ui-tools]: https://github.com/CodeSlicing/pure-swift-ui-tools
543 | [swift-ui]: https://developer.apple.com/xcode/swiftui/
544 | [codeslice-twitter]: https://twitter.com/CodeSlice
545 |
546 |
549 |
550 | [gist-shield]: https://gist.github.com/CodeSlicing/af02bd37dd60252fd39acaf95d28a7d0
551 |
552 |
555 |
556 | [docs-layout-guides]: ../LayoutGuides/layout-guides.md
557 |
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/lighting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/lighting.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/navigation-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/navigation-animation.gif
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/pause-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/pause-button.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-1-artifacts-zoomed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-1-artifacts-zoomed.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-1-artifacts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-1-artifacts.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-1.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-2-artifacts-zoomed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-2-artifacts-zoomed.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-2-artifacts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-2-artifacts.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/profile-pics-2.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/rating.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/rating.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/sf-symbol-background-scaled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/sf-symbol-background-scaled.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/sf-symbol-cropped-background-bounding-region.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/sf-symbol-cropped-background-bounding-region.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/sf-symbol-cropped-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/sf-symbol-cropped-background.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/shape-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/shape-fill.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/shape-stroke.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/shape-stroke.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Extensions/InnerShadows/text-depth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Extensions/InnerShadows/text-depth.png
--------------------------------------------------------------------------------
/Assets/Docs/Components/Model/Color/gradient-map.md:
--------------------------------------------------------------------------------
1 | # GradientMap
2 |
3 | While the gradients we have at our disposal in `SwiftUI` are numerous, we don't have a way of extracting colours from those gradients for use in situations where we might want to represent state based on a color *from* those gradients.
4 |
5 | In other words if we have a selection of options, for example, where each option represents a particular severity, unfortunately we can't create a [LinearGradient][linear-gradient] and then pick color from it at a specific location. `GradientMap` addresses this issue by providing a map of interpolated colors based on the array of colors with which it is initialized.
6 |
7 | A picture paints a thousand words, so here's an example. Let's say you have a picker where each value corresponds a more or less severe option. The severity might be represented by a gradient of colors from green, to yellow, to red, like so:
8 |
9 |
10 |
11 |
12 |
13 | Using `GradientMap` could produce this in the following way:
14 |
15 | ```swift
16 | private let gradientMap = GradientMap([.green, .yellow, .red], withResolution: 10)
17 |
18 | struct PickerSeverityDemo: View {
19 | var choices = 0..<10
20 | @State private var choice = 5
21 | var body: some View {
22 | VStack {
23 | Picker("Severity Picker", selection: self.$choice) {
24 | ForEach(choices) { choice in
25 | Text((choice + 1).asString)
26 | .width(50)
27 | .backgroundColor(gradientMap.colorAt(choice, default: .white))
28 | .clipRoundedRectangleWithStroke(5, .black, lineWidth: 2)
29 | }
30 | }
31 | .labelsHidden()
32 | }
33 | }
34 | }
35 | ```
36 |
37 | You are not, of course, restricted to representing a certain contextual *level*; you can pull a color out of the map to represent anything you like. For example, you could base the color of a view on its size. This next demonstration shows each block being colored depending on the individual heights.
38 |
39 |
40 |
41 |
42 |
43 | You can find the gist [here][gist-rgb-gradient-map-based-on-size] but the code driving the color is as follows:
44 |
45 | ```swift
46 | ...
47 | let randomValue = 1.0.random()
48 | return Rectangle()
49 | .fillColor(gradientMap.colorAt(randomValue))
50 | ...
51 | ```
52 |
53 | As you can see, the flexibility here is only limited by your imagination.
54 |
55 | You can access the range of colors in `GradientMap` in various ways. You can access `Color` values by using the subscript on `GradientMap` like so:
56 |
57 | ```swift
58 | // subscript returns an optional Color so you can use nil coalescing
59 | let color1 = gradientMap[index] ?? .white
60 |
61 | // or provide a default
62 | let color2 = gradientMap[index, default: .white]
63 | ```
64 |
65 | Normal usage, though, would be by accessing the `Color` of the `GradientMap` to be used directly in a view as with the previous example as follows:
66 |
67 | ```swift
68 | ...
69 | // fraction is a Double from 0 to 1
70 | .backgroundColor(gradientMap.colorAt(fraction, default: .white))
71 | ```
72 |
73 |
76 |
77 | [linear-gradient]: https://developer.apple.com/documentation/swiftui/lineargradient
78 |
79 |
82 |
83 | [gist-rgb-gradient-map-based-on-size]: https://gist.github.com/CodeSlicing/c4d59618d66117a502c8ed8dcfb98c4b
84 |
--------------------------------------------------------------------------------
/Assets/Docs/Components/Model/Color/gradient-severity-picker-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Model/Color/gradient-severity-picker-animation.gif
--------------------------------------------------------------------------------
/Assets/Docs/Components/Model/Color/gradient-size-based-color-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Docs/Components/Model/Color/gradient-size-based-color-animation.gif
--------------------------------------------------------------------------------
/Assets/Docs/LICENCE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright © 2019 Adam Fordyce
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Assets/Images/pure-swift-ui-tools-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-tools/69506a53b1a2cf3bc35a7b050f7977d3840d1aa8/Assets/Images/pure-swift-ui-tools-logo.png
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "PureSwiftUITools",
7 | platforms: [
8 | .iOS(.v13),
9 | .watchOS(.v6),
10 | .tvOS(.v13),
11 | .macOS(.v10_15)
12 | ],
13 | products: [
14 | .library(
15 | name: "PureSwiftUITools",
16 | targets: ["PureSwiftUITools"]),
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/CodeSlicing/pure-swift-ui.git", .upToNextMajor(from: "3.0.0")),
20 | ],
21 | targets: [
22 | .target(
23 | name: "PureSwiftUITools",
24 | dependencies: ["PureSwiftUI"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | [PureSwiftUITools][pure-swift-ui-tools] is a companion package to [PureSwiftUI][pure-swift-ui] which is designed to provide useful implementation of various concepts written for [SwiftUI][swift-ui].
9 |
10 | - [Motivation](#motivation)
11 | - [Documentation](#documentation)
12 | - [Containers](#containers)
13 | - [GridView](#gridview)
14 | - [Utilities](#utilities)
15 | - [GradientMap](#gradientmap)
16 | - [Extensions](#extensions)
17 | - [Inner Shadows](#inner-shadows)
18 | - [Caveats](#caveats)
19 | - [Installation](#installation)
20 | - [Versioning](#versioning)
21 | - [Version History](#version-history)
22 | - [Licensing](#licensing)
23 | - [Contact](#contact)
24 |
25 | ## Motivation
26 |
27 | Since `SwiftUI` is still relatively new, there are many use-cases that are not addressed either directly by Apple or by the community in general. [PureSwiftUITools][pure-swift-ui-tools] is a way of formulating various ideas into tools which can be used directly in projects, or used as a basis for learning, extending for bespoke purposes, or as a foundation for building a more robust approach. I see this package as a educational platform as much as anything else. As various utilities are introduced they will be accompanied by appropriate demos and gists that demonstrate usage.
28 |
29 | ## Documentation
30 |
31 | ### Containers
32 |
33 | #### GridView
34 |
35 | [GridView][containers-grid-view] facilitates the easy creation and manipulation of grids with a specified number of columns of rows.
36 |
37 | ### Utilities
38 |
39 | #### GradientMap
40 |
41 | [GradientMap][gradient-map] allows the extraction of colors along a gradient for use where context sensitive colors are a requirement of the UX.
42 |
43 | ### Extensions
44 |
45 | #### Inner Shadows
46 |
47 | These extensions bring inner shadows to all major component types in `SwiftUI`. You can read about them [here][inner-shadows].
48 |
49 | ## Caveats
50 |
51 | Although [PureSwiftUITools][pure-swift-ui-tools] exports `SwiftUI` - meaning you don't need to import `SwiftUI` at the top of your views for compilation - unfortunately at the time of writing previews do not work if you are not explicitly importing `SwiftUI`. Hopefully this will be addressed in a future release.
52 |
53 | ## Installation
54 |
55 | The `pure-swift-ui-tools` package can be found at:
56 |
57 |
58 |
59 | Instructions for installing swift packages can be found [here][swift-package-installation].
60 |
61 | ## Versioning
62 |
63 | This project adheres to a [semantic versioning](https://semver.org) paradigm.
64 |
65 | ## Version History
66 |
67 | - [1.0.0][tag-1.0.0] Commit initial code with GridView
68 | - [1.1.0][tag-1.1.0] Add GradientMap and RGBA with appropriate supporting extensions
69 | - [1.2.0][tag-1.2.0] Add inner shadows
70 | - [1.3.0][tag-1.3.0] Fix segmentation fault in Xcode 11.4
71 | - [1.3.1][tag-1.3.1] Updating dependency on PureSwiftUI to 1.20.0
72 | - [2.0.0][tag-2.0.0] Updating dependency on PureSwiftUI to 2.0.0. Breaking changes in PureSwiftUI 2.0.0 (although only on the type system) so be careful if using this library to pull in PureSwiftUI.
73 | - [2.0.1][tag-2.0.1] Updating dependency on PureSwiftUI to 2.0.1.
74 | - [2.0.2][tag-2.0.2] Updating dependency on PureSwiftUI to 2.0.1 to next major version.
75 | - [3.0.0][tag-3.0.0] Updating dependency on PureSwiftUI to 3.0.0 to next major version.
76 |
77 | ## Licensing
78 |
79 | This project is licensed under the MIT License - see [here][mit-licence] for details.
80 |
81 | ## Contact
82 |
83 | You can contact me on Twitter [@CodeSlice][codeslice-twitter]. Happy to hear suggestions for improving the package, or feature suggestions. I've probably made a few boo boos along the way, so I'm open to course correction. I won't be open-sourcing the project for the moment since I simply don't have time to administer PRs at this point, though I do intend to do so in the future if there's enough interest.
84 |
85 |
88 |
89 | [pure-swift-ui]: https://github.com/CodeSlicing/pure-swift-ui
90 | [pure-swift-ui-tools]: https://github.com/CodeSlicing/pure-swift-ui-tools
91 | [codeslice-twitter]: https://twitter.com/CodeSlice
92 | [swift-ui]: https://developer.apple.com/xcode/swiftui/
93 | [swift-functions]: https://docs.swift.org/swift-book/LanguageGuide/Functions.html
94 | [swift-package-installation]: https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app
95 |
96 |
99 |
100 | [gist-offset-to-position-demo]: https://gist.github.com/CodeSlicing/2c5376552fa8c27456925370403caa46
101 | [gist-relative-offset-demo]: https://gist.github.com/CodeSlicing/6873695fd0113c27d5cdd8591eca9d1d
102 |
103 |
106 |
107 | [tag-1.0.0]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/1.0.0
108 | [tag-1.1.0]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/1.1.0
109 | [tag-1.2.0]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/1.2.0
110 | [tag-1.3.0]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/1.3.0
111 | [tag-1.3.1]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/1.3.1
112 | [tag-2.0.0]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/2.0.0
113 | [tag-2.0.1]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/2.0.1
114 | [tag-2.0.2]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/2.0.2
115 | [tag-3.0.0]: https://github.com/CodeSlicing/pure-swift-ui-rools/tree/3.0.0
116 |
117 |
118 |
121 |
122 | [mit-licence]: ./Assets/Docs/LICENCE.md
123 | [containers-grid-view]: ./Assets/Docs/Components/Containers/GridView/grid-view.md
124 | [gradient-map]: ./Assets/Docs/Components/Model/Color/gradient-map.md
125 | [inner-shadows]: ./Assets/Docs/Components/Extensions/InnerShadows/inner-shadows.md
126 |
127 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/ExportedModules.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportedModules.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 20/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | @_exported import SwiftUI
10 | @_exported import PureSwiftUI
11 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/Extensions/InnerShadows/PS_InnerShadowExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PS_InnerShadowExtensions.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/03/2020.
6 | //
7 |
8 | import PureSwiftUI
9 |
10 | private struct InnerShadowOnViewViewModifier: ViewModifier {
11 |
12 | let shapeContent: ShapeAndContent
13 | let config: PS_InnerShadowConfig
14 |
15 | func body(content: Content) -> some View {
16 | let decoration = Color.clear.background(self.shapeContent.shape.ps_innerShadow(.fill(self.shapeContent.content), self.config))
17 | .clipShape(self.shapeContent.shape)
18 | let color = self.shapeContent.content as? Color
19 | return RenderIf(color != nil && color == .clear) {
20 | content.overlay(decoration)
21 | }.elseRender {
22 | content.background(decoration)
23 | }
24 | }
25 | }
26 |
27 | public extension View {
28 |
29 | // @inlinable
30 | // func ps_innerShadow(_ config: PS_InnerShadowConfig) -> some View {
31 | // ps_innerShadow(ShapeAndContent(Rectangle(), Color.clear), config)
32 | // }
33 |
34 | @inlinable
35 | func ps_innerShadow(radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
36 | ps_innerShadow(ShapeAndContent(Rectangle(), Color.clear), radius: radius, offset: offset, color: color)
37 | }
38 |
39 | @inlinable
40 | func ps_innerShadow(radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
41 | ps_innerShadow(ShapeAndContent(Rectangle(), Color.clear), .config(radius: radius, offset: offset, intensity: intensity))
42 | }
43 |
44 | func ps_innerShadow(_ shapeContent: ShapeAndContent, _ config: PS_InnerShadowConfig) -> some View {
45 | modifier(InnerShadowOnViewViewModifier(shapeContent: shapeContent, config: config))
46 | }
47 |
48 | @inlinable
49 | func ps_innerShadow(_ shapeContent: ShapeAndContent, radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
50 | ps_innerShadow(shapeContent, .config(radius: radius, offset: offset, color: color))
51 | }
52 |
53 | @inlinable
54 | func ps_innerShadow(_ shapeContent: ShapeAndContent, radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
55 | ps_innerShadow(shapeContent, .config(radius: radius, offset: offset, intensity: intensity))
56 | }
57 |
58 | }
59 | // MARK: ----- INNER SHADOW OFFSET AND ANGLE
60 |
61 | public extension View {
62 |
63 | @inlinable
64 | func ps_innerShadow(_ config: PS_InnerShadowConfig) -> some View {
65 | ps_innerShadow(Color.clear, config)
66 | }
67 |
68 | @inlinable
69 | func ps_innerShadow(radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
70 | ps_innerShadow(Color.clear, radius: radius, offset: offset, angle: angle, intensity: intensity)
71 | }
72 |
73 | @inlinable
74 | func ps_innerShadow(radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
75 | ps_innerShadow(Color.clear, radius: radius, offset: offset, angle: angle, color: color)
76 | }
77 |
78 | @inlinable
79 | func ps_innerShadow(_ content: V, _ config: PS_InnerShadowConfig) -> some View {
80 | ps_innerShadow(.rectangle(content), config)
81 | }
82 |
83 | @inlinable
84 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
85 | ps_innerShadow(.rectangle(content), radius: radius, offset: offset, angle: angle, intensity: intensity)
86 | }
87 |
88 | @inlinable
89 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
90 | ps_innerShadow(.rectangle(content), radius: radius, offset: offset, angle: angle, color: color)
91 | }
92 |
93 | @inlinable
94 | func ps_innerShadow(_ shapeContent: ShapeAndContent, radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
95 | ps_innerShadow(shapeContent, .config(radius: radius, offset: offset, angle: angle, intensity: intensity))
96 | }
97 |
98 | @inlinable
99 | func ps_innerShadow(_ shapeContent: ShapeAndContent, radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
100 | ps_innerShadow(shapeContent, .config(radius: radius, offset: offset, angle: angle, color: color))
101 | }
102 | }
103 |
104 | private struct InnerShadowOnImageViewModifier: ViewModifier {
105 |
106 | let image: Image
107 | let shadowContent: V
108 | let config: PS_InnerShadowConfig
109 |
110 | func body(content: Content) -> some View {
111 | RenderIf(config.hasAngle) {
112 | content.modifier(PSInnerShadowImageOffsetWithAngleViewModifier(image: self.image, content: self.shadowContent, radius: self.config.radius, offset: self.config.offsetLength, angle: self.config.angle, color: self.config.color))
113 | }.elseRender {
114 | content.modifier(PSInnerShadowImageOffsetViewModifier(image: self.image, content: self.shadowContent, radius: self.config.radius, offset: self.config.offset, color: self.config.color))
115 | }
116 | }
117 | }
118 |
119 | public extension Image {
120 |
121 |
122 | // func ps_innerShadow(_ content: V, _ config: PS_InnerShadowConfig) -> some View {
123 | // modifier(InnerShadowOnImageViewModifier(image: self, shadowContent: content, config: config))
124 | // }
125 |
126 | @inlinable
127 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
128 | ps_innerShadow(content, .config(radius: radius, offset: offset, intensity: intensity))
129 | }
130 |
131 | @inlinable
132 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
133 | ps_innerShadow(content, .config(radius: radius, offset: offset, color: color))
134 | }
135 |
136 | @inlinable
137 | func ps_innerShadow(_ config: PS_InnerShadowConfig) -> some View {
138 | ps_innerShadow(Color.clear, config)
139 | }
140 |
141 | @inlinable
142 | func ps_innerShadow(radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
143 | ps_innerShadow(Color.clear, .config(radius: radius, offset: offset, intensity: intensity))
144 | }
145 |
146 | @inlinable
147 | func ps_innerShadow(radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
148 | ps_innerShadow(Color.clear, .config(radius: radius, offset: offset, color: color))
149 | }
150 |
151 | @inlinable
152 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
153 | ps_innerShadow(content, .config(radius: radius, offset: offset, angle: angle, intensity: intensity))
154 | }
155 |
156 | @inlinable
157 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
158 | ps_innerShadow(content, .config(radius: radius, offset: offset, angle: angle, color: color))
159 | }
160 |
161 | @inlinable
162 | func ps_innerShadow(radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
163 | ps_innerShadow(.config(radius: radius, offset: offset, angle: angle, intensity: intensity))
164 | }
165 |
166 | @inlinable
167 | func ps_innerShadow(radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
168 | ps_innerShadow(.config(radius: radius, offset: offset, angle: angle, color: color))
169 | }
170 | }
171 |
172 | // MARK: ----- GENERIC ON SHAPE
173 |
174 | private struct InnerShadowOnShapeViewModifier: ViewModifier {
175 |
176 | let shape: S
177 | let fillAndContent: FillAndContent
178 | let config: PS_InnerShadowConfig
179 |
180 | func body(content: Content) -> some View {
181 | RenderIf(config.hasAngle) {
182 | self.fillAndContent.content.clipShape(self.shape, style: self.fillAndContent.fillStyle)
183 | .modifier(PSInnerShadowShapeOffsetWithAngleViewModifier(shape: self.shape, fillStyle: self.fillAndContent.fillStyle, radius: self.config.radius, offset: self.config.offsetLength, angle: self.config.angle, color: self.config.color))
184 | }.elseRender {
185 | self.fillAndContent.content.clipShape(self.shape, style: self.fillAndContent.fillStyle)
186 | .modifier(PSInnerShadowShapeOffsetViewModifier(shape: self.shape, fillStyle: self.fillAndContent.fillStyle, radius: self.config.radius, offset: self.config.offset, color: self.config.color))
187 | }
188 | }
189 | }
190 |
191 | public extension Shape {
192 |
193 | // internal func addPSInnerShadow(_ shape: S, _ fillAndContent: FillAndContent, _ config: PS_InnerShadowConfig) -> some View {
194 | // modifier(InnerShadowOnShapeViewModifier(shape: shape, fillAndContent: fillAndContent, config: config))
195 | // }
196 | }
197 |
198 | // MARK: ----- GENERIC ON SHAPE STROKE
199 |
200 | public extension Shape {
201 |
202 | // @inlinable
203 | func ps_innerShadow(_ strokeAndContent: StrokeAndContent, _ config: PS_InnerShadowConfig) -> some View {
204 | let strokedShape = stroke(style: strokeAndContent.style)
205 | return strokedShape.modifier(InnerShadowOnShapeViewModifier(shape: strokedShape, fillAndContent: strokeAndContent.fillAndContent, config: config))
206 | // .addPSInnerShadow(strokedShape, strokeAndContent.fillAndContent, config)
207 | }
208 |
209 | @inlinable
210 | func ps_innerShadow(_ strokeAndContent: StrokeAndContent, radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
211 | ps_innerShadow(strokeAndContent, .config(radius: radius, offset: offset, intensity: intensity))
212 | }
213 |
214 | @inlinable
215 | func ps_innerShadow(_ strokeAndContent: StrokeAndContent, radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
216 | ps_innerShadow(strokeAndContent, .config(radius: radius, offset: offset, color: color))
217 | }
218 |
219 | @inlinable
220 | func ps_innerShadow(_ strokeAndContent: StrokeAndContent, radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
221 | ps_innerShadow(strokeAndContent, .config(radius: radius, offset: offset, angle: angle, intensity: intensity))
222 | }
223 |
224 | @inlinable
225 | func ps_innerShadow(_ strokeAndContent: StrokeAndContent, radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
226 | ps_innerShadow(strokeAndContent, .config(radius: radius, offset: offset, angle: angle, color: color))
227 | }
228 | }
229 |
230 | public extension Shape {
231 |
232 |
233 | @inlinable
234 | func ps_innerShadow(_ fillAndContent: FillAndContent, radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
235 | ps_innerShadow(fillAndContent, .config(radius: radius, offset: offset, intensity: intensity))
236 | }
237 |
238 | @inlinable
239 | func ps_innerShadow(_ fillAndContent: FillAndContent, radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
240 | ps_innerShadow(fillAndContent, .config(radius: radius, offset: offset, color: color))
241 | }
242 |
243 | @inlinable
244 | func ps_innerShadow(_ fillAndContent: FillAndContent, radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
245 | ps_innerShadow(fillAndContent, .config(radius: radius, offset: offset, angle: angle, intensity: intensity))
246 | }
247 |
248 | @inlinable
249 | func ps_innerShadow(_ fillAndContent: FillAndContent, radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
250 | ps_innerShadow(fillAndContent, .config(radius: radius, offset: offset, angle: angle, color: color))
251 | }
252 |
253 | // @inlinable
254 | func ps_innerShadow(_ fillAndContent: FillAndContent, _ config: PS_InnerShadowConfig) -> some View {
255 | modifier(InnerShadowOnShapeViewModifier(shape: self, fillAndContent: fillAndContent, config: config))
256 | // self.addPSInnerShadow(self, fillAndContent, config)
257 | }
258 | }
259 |
260 | // MARK: ----- TEXT INNER SHADOW WITH OFFSET
261 |
262 | private struct InnerShadowOnTextViewModifier: ViewModifier {
263 |
264 | let text: Text
265 | let shadowContent: V
266 | let config: PS_InnerShadowConfig
267 |
268 | func body(content: Content) -> some View {
269 | RenderIf(config.hasAngle) {
270 | content.modifier(PSInnerShadowTextOffsetWithAngleViewModifier(text: self.text, content: self.shadowContent, radius: self.config.radius, offset: self.config.offsetLength, angle: self.config.angle, color: self.config.color))
271 | }.elseRender {
272 | content.modifier(PSInnerShadowTextOffsetViewModifier(text: self.text, content: self.shadowContent, radius: self.config.radius, offset: self.config.offset, color: self.config.color))
273 | }
274 | }
275 | }
276 |
277 | public extension Text {
278 |
279 | // internal func addPSInnerShadow(_ content: V, _ config: PS_InnerShadowConfig) -> some View {
280 | // modifier(InnerShadowOnTextViewModifier(text: self, shadowContent: content, config: config))
281 | // }
282 |
283 | @inlinable
284 | func ps_innerShadow(radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
285 | ps_innerShadow(Color.clear, .config(radius: radius, offset: offset, intensity: intensity))
286 | }
287 |
288 | @inlinable
289 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGPoint = .zero, intensity: CGFloat) -> some View {
290 | ps_innerShadow(content, .config(radius: radius, offset: offset, intensity: intensity))
291 | }
292 |
293 | @inlinable
294 | func ps_innerShadow(radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
295 | ps_innerShadow(Color.clear, .config(radius: radius, offset: offset, color: color))
296 | }
297 |
298 | @inlinable
299 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGPoint = .zero, color: Color = ps_defaultInnerShadowColor) -> some View {
300 | ps_innerShadow(content, .config(radius: radius, offset: offset, color: color))
301 | }
302 |
303 | @inlinable
304 | func ps_innerShadow(_ config: PS_InnerShadowConfig) -> some View {
305 | ps_innerShadow(Color.clear, config)
306 | }
307 |
308 | // @inlinable
309 | func ps_innerShadow(_ content: V, _ config: PS_InnerShadowConfig) -> some View {
310 | // addPSInnerShadow(content, config)
311 | modifier(InnerShadowOnTextViewModifier(text: self, shadowContent: content, config: config))
312 | }
313 | }
314 |
315 | // MARK: ----- TEXT INNER SHADOW WITH OFFSET AND ANGLE
316 |
317 | public extension Text {
318 |
319 | @inlinable
320 | func ps_innerShadow(radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
321 | ps_innerShadow(Color.clear, .config(radius: radius, offset: offset, angle: angle, intensity: intensity))
322 | }
323 |
324 | @inlinable
325 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> some View {
326 | ps_innerShadow(content, .config(radius: radius, offset: offset, angle: angle, intensity: intensity))
327 | }
328 |
329 | @inlinable
330 | func ps_innerShadow(radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
331 | ps_innerShadow(Color.clear, .config(radius: radius, offset: offset, angle: angle, color: color))
332 | }
333 |
334 | @inlinable
335 | func ps_innerShadow(_ content: V, radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> some View {
336 | ps_innerShadow(content, .config(radius: radius, offset: offset, angle: angle, color: color))
337 | }
338 |
339 | }
340 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/Extensions/InnerShadows/PS_InnerShadowModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PS_InnerShadowModel.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/03/2020.
6 | //
7 |
8 | import PureSwiftUI
9 |
10 | public let ps_defaultInnerShadowColor = Color(white: 0.56)
11 |
12 | private func convertIntensityToColor(_ intensity: CGFloat) -> Color {
13 | return Color(white: 1 - intensity.asDouble.clamped(max: 1, abs: false))
14 | }
15 |
16 | public struct PS_InnerShadowConfig {
17 |
18 | let radius: CGFloat
19 | let offsetLength: CGFloat!
20 | let offset: CGPoint!
21 | let angle: Angle!
22 | let color: Color!
23 |
24 | private init(radiusInternal: CGFloat, offsetLength: CGFloat? = nil, offset: CGPoint? = nil, angle: Angle? = nil, color: Color = ps_defaultInnerShadowColor) {
25 | self.radius = radiusInternal
26 | self.offsetLength = offsetLength
27 | self.offset = offset
28 | self.angle = angle
29 | self.color = color
30 | }
31 |
32 | public init(radius: CGFloat, offsetLength: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) {
33 | self.init(radiusInternal: radius, offsetLength: offsetLength, angle: angle, color: color)
34 | }
35 |
36 | public init(radius: CGFloat, offsetLength: CGFloat, angle: Angle, intensity: CGFloat) {
37 | self.init(radius: radius, offsetLength: offsetLength, angle: angle, color: convertIntensityToColor(intensity))
38 | }
39 |
40 | public init(radius: CGFloat, offset: CGPoint, color: Color = ps_defaultInnerShadowColor) {
41 | self.init(radiusInternal: radius, offset: offset, color: color)
42 | }
43 |
44 | public init(radius: CGFloat, offset: CGPoint, intensity: CGFloat) {
45 | self.init(radius: radius, offset: offset, color: convertIntensityToColor(intensity))
46 | }
47 |
48 | public init(radius: CGFloat, color: Color = ps_defaultInnerShadowColor) {
49 | self.init(radiusInternal: radius, offset: .zero, color: color)
50 | }
51 |
52 | public init(radius: CGFloat, intensity: CGFloat) {
53 | self.init(radius: radius, offset: .zero, intensity: intensity)
54 | }
55 |
56 | var hasAngle: Bool {
57 | angle != nil
58 | }
59 | }
60 |
61 | public extension PS_InnerShadowConfig {
62 |
63 | static func config(radius: CGFloat, intensity: CGFloat) -> PS_InnerShadowConfig {
64 | config(radius: radius, offset: .zero, intensity: intensity)
65 | }
66 |
67 | static func config(radius: CGFloat, color: Color = ps_defaultInnerShadowColor) -> PS_InnerShadowConfig {
68 | config(radius: radius, offset: .zero, color: color)
69 | }
70 |
71 | static func config(radius: CGFloat, offset: CGFloat, angle: Angle, intensity: CGFloat) -> PS_InnerShadowConfig {
72 | config(radius: radius, offset: offset, angle: angle, color: convertIntensityToColor(intensity))
73 | }
74 |
75 | static func config(radius: CGFloat, offset: CGFloat, angle: Angle, color: Color = ps_defaultInnerShadowColor) -> PS_InnerShadowConfig {
76 | PS_InnerShadowConfig(radius: radius, offsetLength: offset, angle: angle, color: color)
77 | }
78 |
79 | static func config(radius: CGFloat, offset: CGPoint, intensity: CGFloat) -> PS_InnerShadowConfig {
80 | config(radius: radius, offset: offset, color: convertIntensityToColor(intensity))
81 | }
82 |
83 | static func config(radius: CGFloat, offset: CGPoint, color: Color = ps_defaultInnerShadowColor) -> PS_InnerShadowConfig {
84 | PS_InnerShadowConfig(radius: radius, offset: offset, color: color)
85 | }
86 | }
87 |
88 | public struct FillAndContent {
89 | let fillStyle: FillStyle
90 | let content: V?
91 |
92 | public init(_ fillStyle: FillStyle, _ content: V?) {
93 | self.fillStyle = fillStyle
94 | self.content = content
95 | }
96 | }
97 |
98 | public extension FillAndContent {
99 |
100 | static var fill: FillAndContent {
101 | fill(Color.clear)
102 | }
103 |
104 | static func fill(eoFill: Bool = false, antialiased: Bool = true) -> FillAndContent {
105 | fill(Color.clear, eoFill: eoFill, antialiased: antialiased)
106 | }
107 |
108 | static func fill(_ content: V, eoFill: Bool = false, antialiased: Bool = true) -> FillAndContent {
109 | FillAndContent(FillStyle(eoFill: eoFill, antialiased: antialiased), content)
110 | }
111 | }
112 |
113 | public struct StrokeAndContent {
114 | let style: StrokeStyle
115 | let fillAndContent: FillAndContent
116 |
117 | public init(_ style: StrokeStyle, _ fillAndContent: FillAndContent) {
118 | self.style = style
119 | self.fillAndContent = fillAndContent
120 | }
121 | }
122 |
123 | public extension StrokeAndContent {
124 | static func stroke(lineWidth: CGFloat) -> StrokeAndContent {
125 | stroke(Color.clear, lineWidth: lineWidth)
126 | }
127 |
128 | static func stroke(_ content: V, lineWidth:CGFloat) -> StrokeAndContent {
129 | StrokeAndContent(StrokeStyle(lineWidth: lineWidth), .fill(content))
130 | }
131 |
132 | static func stroke(style: StrokeStyle) -> StrokeAndContent {
133 | stroke(Color.clear, style: style)
134 | }
135 |
136 | static func stroke(_ content: V, style: StrokeStyle) -> StrokeAndContent {
137 | StrokeAndContent(style, .fill(content))
138 | }
139 | }
140 |
141 | public struct ShapeAndContent {
142 | let shape: S
143 | let content: V?
144 |
145 | public init(_ shape: S, _ content: V? = nil) {
146 | self.shape = shape
147 | self.content = content
148 | }
149 | }
150 |
151 | public extension ShapeAndContent {
152 |
153 | static var circle: ShapeAndContent {
154 | circle(Color.clear)
155 | }
156 |
157 | static func circle(_ content: V? = nil) -> ShapeAndContent {
158 | ShapeAndContent(Circle(), content)
159 | }
160 |
161 | static var ellipse: ShapeAndContent {
162 | ellipse(Color.clear)
163 | }
164 |
165 | static func ellipse(_ content: V? = nil) -> ShapeAndContent {
166 | ShapeAndContent(Ellipse(), content)
167 | }
168 |
169 | static var capsule: ShapeAndContent {
170 | capsule(Color.clear)
171 | }
172 |
173 | static func capsule(_ content: V? = nil) -> ShapeAndContent {
174 | ShapeAndContent(Capsule(), content)
175 | }
176 |
177 | static var rectangle: ShapeAndContent {
178 | rectangle(Color.clear)
179 | }
180 |
181 | static func rectangle(_ content: V? = nil) -> ShapeAndContent {
182 | ShapeAndContent(Rectangle(), content)
183 | }
184 |
185 | static func roundedRectangle(_ cornerRadius: CGFloat) -> ShapeAndContent {
186 | roundedRectangle(cornerRadius, Color.clear)
187 | }
188 |
189 | static func roundedRectangle(_ cornerRadius: CGFloat, _ content: V? = nil) -> ShapeAndContent {
190 | ShapeAndContent(RoundedRectangle(cornerRadius), content)
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/Extensions/InnerShadows/PS_InnerShadowViewModifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PS_InnerShadowViewModifiers.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/03/2020.
6 | //
7 |
8 | import PureSwiftUI
9 |
10 | internal extension CGPoint {
11 |
12 | var asAnimatablePair: AnimatablePair {
13 | AnimatablePair(x, y)
14 | }
15 | }
16 |
17 | internal extension AnimatablePair where First == CGFloat, Second == CGFloat {
18 |
19 | var asCGPoint: CGPoint {
20 | CGPoint(first, second)
21 | }
22 | }
23 |
24 | // MARK: ----- INTERIOR SHADOW SHAPE VIEW MODIFIERS
25 |
26 | internal struct PSInnerShadowShapeOffsetViewModifier: AnimatableModifier {
27 | let shape: S
28 | let fillStyle: FillStyle
29 | var offset: CGPoint
30 | var radius: CGFloat
31 | var color: Color
32 |
33 |
34 | var animatableData: AnimatablePair, CGFloat> {
35 | get {
36 | AnimatablePair(AnimatablePair(offset.x, offset.y), radius)
37 | }
38 | set {
39 | offset = newValue.first.asCGPoint
40 | radius = newValue.second
41 | }
42 | }
43 |
44 | init(shape: S, fillStyle: FillStyle, radius: CGFloat, offset: CGPoint = .zero, color: Color) {
45 | self.shape = shape
46 | self.fillStyle = fillStyle
47 | self.radius = radius
48 | self.offset = offset
49 | self.color = color
50 | }
51 |
52 | func body(content: Content) -> some View {
53 | content.modifier(PSInnerShadowForShapeViewModifier(shape: shape, fillStyle: fillStyle, offset: offset, radius: radius, color: color))
54 | }
55 | }
56 |
57 | internal struct PSInnerShadowShapeOffsetWithAngleViewModifier: AnimatableModifier {
58 | let shape: S
59 | let fillStyle: FillStyle
60 | var offset: CGFloat
61 | var angle: Angle
62 | var radius: CGFloat
63 | var color: Color
64 |
65 | var animatableData: AnimatablePair, CGFloat> {
66 | get {
67 | AnimatablePair(AnimatablePair(offset, angle.degrees), radius)
68 | }
69 | set {
70 | offset = newValue.first.first
71 | angle = newValue.first.second.degrees
72 | radius = newValue.second
73 | }
74 | }
75 |
76 | init(shape: S, fillStyle: FillStyle, radius: CGFloat, offset: CGFloat = 0, angle: Angle = .top, color: Color) {
77 | self.shape = shape
78 | self.fillStyle = fillStyle
79 | self.radius = radius
80 | self.offset = offset
81 | self.angle = angle
82 | self.color = color
83 | }
84 |
85 | func body(content: Content) -> some View {
86 | content.modifier(PSInnerShadowForShapeViewModifier(shape: shape, fillStyle: fillStyle, offset: .point(offset, angle), radius: radius, color: color))
87 | }
88 | }
89 |
90 | private struct PSInnerShadowForShapeViewModifier: ViewModifier {
91 |
92 | let shape: S
93 | let fillStyle: FillStyle
94 | var offset: CGPoint
95 | var radius: CGFloat
96 | var color: Color
97 |
98 | func body(content: Content) -> some View {
99 | content.overlay(
100 | ZStack {
101 | shape.fill(self.color, style: fillStyle)
102 | shape.fill(Color.white, style: fillStyle)
103 | .blur(self.radius)
104 | .offset(self.offset)
105 | }
106 | .clipShape(shape)
107 | .drawingGroup()
108 | .blendMode(.multiply)
109 | )
110 | }
111 | }
112 |
113 | // MARK: ----- IMAGE VIEW MODIFIERS
114 |
115 | internal struct PSInnerShadowImageOffsetViewModifier: AnimatableModifier {
116 | let image: Image
117 | let content: V
118 | var offset: CGPoint
119 | var radius: CGFloat
120 | var color: Color
121 |
122 | var animatableData: AnimatablePair, CGFloat> {
123 | get {
124 | AnimatablePair(AnimatablePair(offset.x, offset.y), radius)
125 | }
126 | set {
127 | offset = newValue.first.asCGPoint
128 | radius = newValue.second
129 | }
130 | }
131 |
132 | init(image: Image, content: V, radius: CGFloat, offset: CGPoint = .zero, color: Color) {
133 | self.image = image
134 | self.content = content
135 | self.radius = radius
136 | self.offset = offset
137 | self.color = color
138 | }
139 |
140 | func body(content: Content) -> some View {
141 | content.modifier(PSInnerShadowForImageViewModifier(image: image, content: self.content, radius: radius, offset: offset, color: color))
142 | }
143 | }
144 |
145 | internal struct PSInnerShadowImageOffsetWithAngleViewModifier: AnimatableModifier {
146 |
147 | let image: Image
148 | let content: V
149 | var offset: CGFloat
150 | var angle: Angle
151 | var radius: CGFloat
152 | var color: Color
153 |
154 | var animatableData: AnimatablePair, CGFloat> {
155 | get {
156 | AnimatablePair(AnimatablePair(offset, angle.degrees), radius)
157 | }
158 | set {
159 | offset = newValue.first.first
160 | angle = newValue.first.second.degrees
161 | radius = newValue.second
162 | }
163 | }
164 |
165 | init(image: Image, content: V, radius: CGFloat, offset: CGFloat = 0, angle: Angle = .top, color: Color) {
166 | self.image = image
167 | self.content = content
168 | self.radius = radius
169 | self.offset = offset
170 | self.angle = angle
171 | self.color = color
172 | }
173 |
174 | func body(content: Content) -> some View {
175 | content.modifier(PSInnerShadowForImageViewModifier(image: image, content: self.content, radius: radius, offset: .point(offset, angle), color: color))
176 | }
177 | }
178 |
179 | private struct PSInnerShadowForImageViewModifier: ViewModifier {
180 |
181 | let image: Image
182 | let content: V
183 | var offset: CGPoint
184 | var radius: CGFloat
185 | var color: Color
186 |
187 | init(image: Image, content: V, radius: CGFloat, offset: CGPoint, color: Color) {
188 | self.image = image
189 | self.content = content
190 | self.radius = radius
191 | self.offset = offset
192 | self.color = color
193 | }
194 |
195 | func body(content: Content) -> some View {
196 |
197 | let templateImage = image.renderingMode(.template)
198 | let whiteTemplate = templateImage.foregroundColor(.white)
199 | return image
200 | .background(self.content.mask(whiteTemplate))
201 | .overlay(
202 | ZStack {
203 | templateImage.foregroundColor(color)
204 | templateImage.foregroundColor(.white).blur(radius).offset(self.offset)
205 | }
206 | .mask(whiteTemplate)
207 | .blendMode(.multiply)
208 | )
209 | .foregroundColor(Color.clear)
210 | }
211 | }
212 |
213 | // MARK: ----- TEXT INTERIOR SHADOW
214 |
215 | internal struct PSInnerShadowTextOffsetViewModifier: AnimatableModifier {
216 | let text: Text
217 | let content: V
218 | var offset: CGPoint
219 | var radius: CGFloat
220 | var color: Color
221 |
222 | var animatableData: AnimatablePair, CGFloat> {
223 | get {
224 | AnimatablePair(AnimatablePair(offset.x, offset.y), radius)
225 | }
226 | set {
227 | offset = newValue.first.asCGPoint
228 | radius = newValue.second
229 | }
230 | }
231 |
232 | init(text: Text, content: V, radius: CGFloat, offset: CGPoint = .zero, color: Color) {
233 | self.text = text
234 | self.content = content
235 | self.radius = radius
236 | self.offset = offset
237 | self.color = color
238 | }
239 |
240 | func body(content: Content) -> some View {
241 | content.modifier(PSInnerShadowForTextViewModifier(text: text, content: self.content, radius: radius, offset: offset, color: color))
242 | }
243 | }
244 |
245 | internal struct PSInnerShadowTextOffsetWithAngleViewModifier: AnimatableModifier {
246 |
247 | let text: Text
248 | let content: V
249 | var offset: CGFloat
250 | var angle: Angle
251 | var radius: CGFloat
252 | var color: Color
253 |
254 | var animatableData: AnimatablePair, CGFloat> {
255 | get {
256 | AnimatablePair(AnimatablePair(offset, angle.degrees), radius)
257 | }
258 | set {
259 | offset = newValue.first.first
260 | angle = newValue.first.second.degrees
261 | radius = newValue.second
262 | }
263 | }
264 |
265 | init(text: Text, content: V, radius: CGFloat, offset: CGFloat = 0, angle: Angle = .top, color: Color) {
266 | self.text = text
267 | self.content = content
268 | self.radius = radius
269 | self.offset = offset
270 | self.angle = angle
271 | self.color = color
272 | }
273 |
274 | func body(content: Content) -> some View {
275 | content.modifier(PSInnerShadowForTextViewModifier(text: text, content: self.content, radius: radius, offset: .point(offset, angle), color: color))
276 | }
277 | }
278 |
279 | private struct PSInnerShadowForTextViewModifier: ViewModifier {
280 |
281 | let text: Text
282 | let content: V
283 | var offset: CGPoint
284 | var radius: CGFloat
285 | var color: Color
286 |
287 | init(text: Text, content: V, radius: CGFloat, offset: CGPoint, color: Color) {
288 | self.text = text
289 | self.content = content
290 | self.radius = radius
291 | self.offset = offset
292 | self.color = color
293 | }
294 |
295 | func body(content: Content) -> some View {
296 | let whiteContent = text.foregroundColor(.white)
297 |
298 | return text
299 | .overlay(self.content.mask(whiteContent))
300 | .overlay(
301 | ZStack {
302 | text.foregroundColor(color)
303 | text.foregroundColor(.white).blur(radius).offset(offset)
304 | }
305 | .mask(whiteContent)
306 | .blendMode(.multiply)
307 | )
308 | .foregroundColor(Color.clear)
309 |
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/Model/GradientMap.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GradientMap.swift
3 | //
4 | // Created by Adam Fordyce on 22/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | public struct GradientMap {
9 |
10 | private var internalGradientMap: [(color: Color, rgba: RGBA)]
11 |
12 | public var count: Int {
13 | self.internalGradientMap.count
14 | }
15 |
16 | public init(_ colors: [RGBA], withResolution resolution: Int) {
17 | self.internalGradientMap = GradientMap.calculateGradientMap(colors: colors, resolution: resolution)
18 | }
19 |
20 | public subscript(index: Int) -> Color? {
21 | get {
22 | return internalMapValue(at: index)?.color
23 | }
24 | }
25 |
26 | public subscript(index: Int, default defaultColor: Color) -> Color {
27 | get {
28 | self[index] ?? defaultColor
29 | }
30 | }
31 |
32 | private func internalMapValue(at index: Int) -> (color: Color, rgba: RGBA)? {
33 | if index > internalGradientMap.count - 1 || index < 0 {
34 | return nil
35 | } else {
36 | return internalGradientMap[index]
37 | }
38 | }
39 |
40 | public static func calculateGradientMap(colors: [RGBA], resolution: Int) -> [(color: Color, rgba: RGBA)] {
41 |
42 | var toReturn = [(color: Color, rgba: RGBA)]()
43 |
44 | let stops = GradientMap.calculateStops(colors: colors)
45 |
46 | toReturn.append((colors.first!.asColor, colors.first!))
47 |
48 | let deltaPerPoint = 1 / resolution.asDouble
49 |
50 | for point in 1..<(resolution - 1) {
51 |
52 | let fraction = deltaPerPoint * point.asDouble
53 | let colorToInterpolate: (from: RGBA, to: RGBA, fraction: Double) = calculateInterpolationColors(colors: colors, stops: stops, fraction: fraction)
54 |
55 | let rgba = colorToInterpolate.from.interpolate(to: colorToInterpolate.to, fraction: colorToInterpolate.fraction)
56 |
57 | toReturn.append((rgba.asColor, rgba))
58 | }
59 | toReturn.append((colors.last!.asColor, colors.last!))
60 |
61 | return toReturn
62 | }
63 |
64 | public static func calculateStops(colors: [RGBA]) -> [Double] {
65 |
66 | let fractionPerStop = 1 / (colors.count.asDouble - 1)
67 |
68 | var stops = [Double]()
69 |
70 | stops.append(0)
71 | for i in 1..<(colors.count - 1) {
72 | stops.append(fractionPerStop * i.asDouble)
73 | }
74 | stops.append(1)
75 |
76 | return stops
77 | }
78 |
79 | public static func calculateInterpolationColors(colors: [RGBA], stops: [Double], fraction: Double) -> (RGBA, RGBA, Double) {
80 |
81 | var startIndex = 0
82 | var endIndex = stops.count - 1
83 |
84 | for i in 1.. Int {
106 | (fraction * (count - 1).asDouble).asInt
107 | }
108 |
109 | func colorAt(_ fraction: Double, default defaultColor: Color = .clear) -> Color {
110 | self[fractionAsIndex(fraction)] ?? defaultColor
111 | }
112 |
113 | func colorAt(index: Int, default defaultColor: Color = .clear) -> Color {
114 | self[index] ?? defaultColor
115 | }
116 |
117 | func rgbaAt(_ fraction: Double, default defaultColor: RGBA = .clear) -> RGBA {
118 | internalMapValue(at: fractionAsIndex(fraction))?.rgba ?? defaultColor
119 | }
120 |
121 | func rgbaAt(index: Int, default defaultColor: RGBA = .clear) -> RGBA {
122 | internalMapValue(at: index)?.rgba ?? defaultColor
123 | }
124 |
125 | #if !os(watchOS)
126 | func ciColorAt(_ fraction: Double, default defaultColor: CIColor = .clear) -> CIColor {
127 | internalMapValue(at: fractionAsIndex(fraction))?.rgba.asCIColor ?? defaultColor
128 | }
129 |
130 | func ciColorAt(index: Int, default defaultColor: CIColor = .clear) -> CIColor {
131 | internalMapValue(at: index)?.rgba.asCIColor ?? defaultColor
132 | }
133 | #endif
134 |
135 | #if canImport(UIKit)
136 | func uiColorAt(_ fraction: Double, default defaultColor: UIColor = .clear) -> UIColor {
137 | internalMapValue(at: fractionAsIndex(fraction))?.rgba.asUIColor ?? defaultColor
138 | }
139 |
140 | func uiColorAt(index: Int, default defaultColor: UIColor = .clear) -> UIColor {
141 | internalMapValue(at: index)?.rgba.asUIColor ?? defaultColor
142 | }
143 | #endif
144 |
145 | #if !os(watchOS)
146 | func cgColorAt(_ fraction: Double, default defaultColor: CGColor = .clear) -> CGColor {
147 | internalMapValue(at: fractionAsIndex(fraction))?.rgba.asCGColor ?? defaultColor
148 | }
149 |
150 | func cgColorAt(index: Int, default defaultColor: CGColor = .clear) -> CGColor {
151 | internalMapValue(at: index)?.rgba.asCGColor ?? defaultColor
152 | }
153 | #else
154 | func cgColorAt(_ fraction: Double, default defaultColor: CGColor = UIColor.clear.cgColor) -> CGColor {
155 | internalMapValue(at: fractionAsIndex(fraction))?.rgba.asCGColor ?? defaultColor
156 | }
157 |
158 | func cgColorAt(index: Int, default defaultColor: CGColor = UIColor.clear.cgColor) -> CGColor {
159 | internalMapValue(at: index)?.rgba.asCGColor ?? defaultColor
160 | }
161 | #endif
162 | }
163 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/Model/RGBA.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RGBColor.swift
3 | //
4 | // Created by Adam Fordyce on 22/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import PureSwiftUI
9 |
10 | import SwiftUI
11 |
12 | public struct RGBA: Hashable {
13 | var red: Double
14 | var green: Double
15 | var blue: Double
16 | var alpha: Double
17 |
18 | public init(_ r: Double, _ g: Double, _ b: Double, _ a: Double = 1) {
19 | self.red = r
20 | self.green = g
21 | self.blue = b
22 | self.alpha = a
23 | }
24 |
25 | private static let equalsTolerance = 0.0001
26 |
27 | public static func == (lhs: RGBA, rhs: RGBA) -> Bool {
28 | return abs(lhs.red - rhs.red) <= equalsTolerance &&
29 | abs(lhs.green - rhs.green) <= equalsTolerance &&
30 | abs(lhs.blue - rhs.blue) <= equalsTolerance
31 | }
32 | }
33 |
34 | // MARK: ----- INTERPOLATION
35 |
36 | public extension RGBA {
37 |
38 | func interpolate(to targetColor: RGBA, fraction: Double) -> RGBA {
39 | Self.interpolateColor(from: self, to: targetColor, fraction: fraction)
40 | }
41 |
42 | static func interpolateColor(from: RGBA, to: RGBA, fraction: Double) -> RGBA {
43 |
44 | return RGBA(
45 | interpolateValue(from: from.red, to: to.red, fraction: fraction),
46 | interpolateValue(from: from.green, to: to.green, fraction: fraction),
47 | interpolateValue(from: from.blue, to: to.blue, fraction: fraction),
48 | interpolateValue(from: from.alpha, to: to.alpha, fraction: fraction)
49 | )
50 | }
51 |
52 | static func interpolateValue(from: Double, to: Double, fraction: Double) -> Double {
53 |
54 | let range = to - from
55 | let fractionalRange = range * fraction
56 | return from + fractionalRange
57 | }
58 | }
59 |
60 | // MARK: ----- SYSTEM COLORS
61 |
62 | public extension RGBA {
63 |
64 | // SwiftUI colors
65 | static let white = RGBA(1, 1, 1)
66 | static let black = RGBA(0, 0, 0)
67 | static let gray = RGBA(142 / 255, 142 / 255, 142 / 255)
68 | static let red = RGBA(1,58 / 255, 48 / 255)
69 | static let green = RGBA(52.0 / 255 , 199.0 / 255, 89.0 / 255)
70 | static let blue = RGBA(0, 122 / 255, 1)
71 | static let orange = RGBA(1, 149 / 255, 0)
72 | static let yellow = RGBA(1, 204 / 255, 1.0 / 255)
73 | static let pink = RGBA(1, 44 / 255, 85 / 255)
74 | static let purple = RGBA(175 / 255, 82 / 255, 222 / 255)
75 | static let clear = RGBA(0, 0, 0, 0)
76 |
77 | // pure colors
78 | static let pureRed = RGBA(1, 0, 0)
79 | static let pureGreen = RGBA(0, 1, 0)
80 | static let pureBlue = RGBA(0, 0, 1)
81 | static let pureYellow = RGBA(1, 1, 0)
82 | static let pureMagenta = RGBA(1, 0, 1)
83 | static let pureOrange = RGBA(1, 0.5, 0)
84 | static let purePurple = RGBA(0.5, 0, 0.5)
85 | }
86 |
87 | // MARK: ----- OPACITY
88 |
89 | public extension RGBA {
90 |
91 | func withRed(_ value: Double) -> RGBA {
92 | var newRGB = self
93 | newRGB.red = value
94 | return newRGB
95 | }
96 |
97 | func withGreen(_ value: Double) -> RGBA {
98 | var newRGB = self
99 | newRGB.green = value
100 | return newRGB
101 | }
102 |
103 | func withBlue(_ value: Double) -> RGBA {
104 | var newRGB = self
105 | newRGB.blue = value
106 | return newRGB
107 | }
108 |
109 | func withOpacity(_ value: Double) -> RGBA {
110 | var newRGB = self
111 | newRGB.alpha = value
112 | return newRGB
113 | }
114 | }
115 |
116 | // MARK: ----- TYPE COERCION
117 |
118 | public extension RGBA {
119 |
120 | var asColor: Color {
121 | Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
122 | }
123 |
124 | var asCGColor: CGColor {
125 | CGColor(srgbRed: red.asCGFloat, green: green.asCGFloat, blue: blue.asCGFloat, alpha: alpha.asCGFloat)
126 | }
127 |
128 | #if !os(watchOS)
129 | #if canImport(UIKit)
130 | var asUIColor: UIColor {
131 | UIColor(ciColor: asCIColor)
132 | }
133 | #endif
134 |
135 | var asCIColor: CIColor {
136 | CIColor(red: red.asCGFloat, green: green.asCGFloat, blue: blue.asCGFloat, alpha: alpha.asCGFloat)
137 | }
138 | #else
139 | var asUIColor: UIColor {
140 | UIColor(cgColor: asCGColor)
141 | }
142 | #endif
143 | }
144 |
145 | // MARK: ----- FOUNDATION EXTENSIONS
146 |
147 | #if !os(watchOS)
148 | public extension CIColor {
149 |
150 | var asRGBA: RGBA {
151 | return RGBA(self.red.asDouble, self.green.asDouble, self.blue.asDouble, self.alpha.asDouble)
152 | }
153 | }
154 |
155 | #if canImport(UIKit)
156 | public extension UIColor {
157 |
158 | var asRGBA: RGBA {
159 | CIColor(color: self).asRGBA
160 | }
161 | }
162 | #endif
163 |
164 | public extension CGColor {
165 |
166 | static let clear = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 0)
167 |
168 | var asRGBA: RGBA {
169 | CIColor(cgColor: self).asRGBA
170 | }
171 | }
172 | #endif
173 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUITools/Views/Containers/GridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridView.swift
3 | // PureSwiftUIToolsProvingGround
4 | //
5 | // Created by Adam Fordyce on 18/01/2020.
6 | // Copyright © 2020 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import PureSwiftUI
11 |
12 | public struct GridView: View {
13 | let columns: Int
14 | let rows: Int
15 | let spacing: CGFloat
16 | let content: (Int, Int) -> Content
17 |
18 | public init(_ columns: Int, _ rows: Int, @ViewBuilder content: @escaping (Int, Int) -> Content) {
19 | self.init(columns, rows, spacing: 0, content: content)
20 | }
21 |
22 | public init(_ size: Int, @ViewBuilder content: @escaping (Int, Int) -> Content) {
23 | self.init(size, size, spacing: 0, content: content)
24 | }
25 |
26 | public init(_ size: Int, spacing: CGFloat, @ViewBuilder content: @escaping (Int, Int) -> Content) {
27 | self.init(size, size, spacing: spacing, content: content)
28 | }
29 |
30 | public init(_ columns: Int, _ rows: Int, spacing: CGFloat, @ViewBuilder content: @escaping (Int, Int) -> Content) {
31 | self.columns = columns
32 | self.rows = rows
33 | self.spacing = spacing
34 | self.content = content
35 | }
36 |
37 | public var body: some View {
38 | VStack(spacing: self.spacing) {
39 | ForEach(0..