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