react-native-theme-reveal 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +734 -2
  3. package/android/build.gradle +32 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/baseline-prof.txt +4 -0
  6. package/android/src/main/java/expo/modules/themereveal/CapturePlan.kt +199 -0
  7. package/android/src/main/java/expo/modules/themereveal/CaptureRules.kt +50 -0
  8. package/android/src/main/java/expo/modules/themereveal/GpuSnapshot.kt +367 -0
  9. package/android/src/main/java/expo/modules/themereveal/KeptBitmaps.kt +102 -0
  10. package/android/src/main/java/expo/modules/themereveal/KeptTarget.kt +103 -0
  11. package/android/src/main/java/expo/modules/themereveal/Motion.kt +79 -0
  12. package/android/src/main/java/expo/modules/themereveal/Once.kt +13 -0
  13. package/android/src/main/java/expo/modules/themereveal/RenderThreadReveal.kt +107 -0
  14. package/android/src/main/java/expo/modules/themereveal/RevealOverlayView.kt +192 -0
  15. package/android/src/main/java/expo/modules/themereveal/SecondSnapshotWait.kt +112 -0
  16. package/android/src/main/java/expo/modules/themereveal/ShapePath.kt +47 -0
  17. package/android/src/main/java/expo/modules/themereveal/ThemeRevealModule.kt +904 -0
  18. package/android/src/main/java/expo/modules/themereveal/TransitionArgs.kt +50 -0
  19. package/android/src/main/java/expo/modules/themereveal/WindowSnapshot.kt +536 -0
  20. package/build/ThemeRevealModule.d.ts +11 -0
  21. package/build/ThemeRevealModule.d.ts.map +1 -0
  22. package/build/ThemeRevealModule.js +11 -0
  23. package/build/ThemeRevealModule.js.map +1 -0
  24. package/build/createThemeReveal.d.ts +39 -0
  25. package/build/createThemeReveal.d.ts.map +1 -0
  26. package/build/createThemeReveal.js +186 -0
  27. package/build/createThemeReveal.js.map +1 -0
  28. package/build/index.d.ts +9 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +5 -0
  31. package/build/index.js.map +1 -0
  32. package/build/originFromPress.d.ts +12 -0
  33. package/build/originFromPress.d.ts.map +1 -0
  34. package/build/originFromPress.js +26 -0
  35. package/build/originFromPress.js.map +1 -0
  36. package/build/resolveTransition.d.ts +13 -0
  37. package/build/resolveTransition.d.ts.map +1 -0
  38. package/build/resolveTransition.js +43 -0
  39. package/build/resolveTransition.js.map +1 -0
  40. package/build/store.d.ts +8 -0
  41. package/build/store.d.ts.map +1 -0
  42. package/build/store.js +23 -0
  43. package/build/store.js.map +1 -0
  44. package/build/types.d.ts +31 -0
  45. package/build/types.d.ts.map +1 -0
  46. package/build/types.js +2 -0
  47. package/build/types.js.map +1 -0
  48. package/expo-module.config.json +9 -0
  49. package/ios/Motion.swift +171 -0
  50. package/ios/RevealOverlay.swift +115 -0
  51. package/ios/SVGPath.swift +373 -0
  52. package/ios/ThemeReveal.podspec +26 -0
  53. package/ios/ThemeRevealModule.swift +264 -0
  54. package/mocks/ThemeReveal.js +10 -0
  55. package/package.json +67 -4
  56. package/src/ThemeRevealModule.ts +24 -0
  57. package/src/createThemeReveal.ts +262 -0
  58. package/src/index.ts +14 -0
  59. package/src/originFromPress.ts +40 -0
  60. package/src/resolveTransition.ts +63 -0
  61. package/src/store.ts +30 -0
  62. package/src/types.ts +17 -0
@@ -0,0 +1,115 @@
1
+ import UIKit
2
+
3
+ // Window snapshot on top of the app content, animated away to reveal the new theme.
4
+ final class RevealOverlay {
5
+ let view: UIView
6
+ let size: CGSize
7
+ private var playing = 0
8
+
9
+ convenience init?(window: UIWindow) {
10
+ guard let snapshot = Self.snapshot(of: window) else { return nil }
11
+ self.init(snapshot: snapshot, window: window)
12
+ }
13
+
14
+ // Puts a snapshot from snapshot(of:) on top of the window.
15
+ init(snapshot: UIView, window: UIWindow) {
16
+ window.addSubview(snapshot)
17
+ view = snapshot
18
+ size = snapshot.frame.size
19
+ }
20
+
21
+ // Window's content right now, as a view that isn't in the window yet.
22
+ static func snapshot(of window: UIWindow) -> UIView? {
23
+ guard let snapshot = window.snapshotView(afterScreenUpdates: false) else { return nil }
24
+ snapshot.frame = window.bounds
25
+ snapshot.isUserInteractionEnabled = false
26
+ snapshot.isAccessibilityElement = false
27
+ snapshot.accessibilityElementsHidden = true
28
+ return snapshot
29
+ }
30
+
31
+ func reveal(_ shape: RevealShape, origin: CGPoint, inverse: Bool, duration: Double, onEnd: @escaping () -> Void) {
32
+ let bounds = view.bounds
33
+ let from = Double(Motion.startScale(for: shape))
34
+ let to = Double(Motion.coverScale(
35
+ originX: origin.x, originY: origin.y, width: bounds.width, height: bounds.height,
36
+ inscribedRadius: shape.inscribedRadius))
37
+ let steps = Motion.sampleCount(duration: duration)
38
+ let paths = Motion.scales(from: from, to: to, steps: steps, inverse: inverse).map { scale in
39
+ Motion.maskPath(shape, origin: origin, scale: CGFloat(scale), bounds: bounds, inverse: inverse)
40
+ }
41
+ play(onEnd) {
42
+ let mask = CAShapeLayer()
43
+ mask.frame = bounds
44
+ mask.fillRule = inverse ? .nonZero : .evenOdd
45
+ mask.path = paths.last
46
+ view.layer.mask = mask
47
+ let animation = CAKeyframeAnimation(keyPath: "path")
48
+ animation.values = paths
49
+ animation.calculationMode = .linear
50
+ animation.duration = duration
51
+ mask.add(animation, forKey: "reveal")
52
+ }
53
+ }
54
+
55
+ func wipe(_ direction: WipeDirection, duration: Double, onEnd: @escaping () -> Void) {
56
+ let bounds = view.bounds
57
+ let offset = Motion.wipeOffset(direction, size: bounds.size)
58
+ play(onEnd) {
59
+ let mask = CALayer()
60
+ mask.frame = bounds
61
+ mask.backgroundColor = UIColor.black.cgColor
62
+ let start = mask.position
63
+ let end = CGPoint(x: start.x + offset.x, y: start.y + offset.y)
64
+ mask.position = end
65
+ view.layer.mask = mask
66
+ let animation = CABasicAnimation(keyPath: "position")
67
+ animation.fromValue = NSValue(cgPoint: start)
68
+ animation.toValue = NSValue(cgPoint: end)
69
+ animation.duration = duration
70
+ animation.timingFunction = Self.easing
71
+ mask.add(animation, forKey: "wipe")
72
+ }
73
+ }
74
+
75
+ func crossfade(duration: Double, onEnd: @escaping () -> Void) {
76
+ play(onEnd) {
77
+ view.layer.opacity = 0
78
+ let animation = CABasicAnimation(keyPath: "opacity")
79
+ animation.fromValue = 1
80
+ animation.toValue = 0
81
+ animation.duration = duration
82
+ animation.timingFunction = Self.easing
83
+ view.layer.add(animation, forKey: "crossfade")
84
+ }
85
+ }
86
+
87
+ func remove() {
88
+ playing += 1
89
+ view.layer.mask?.removeAllAnimations()
90
+ view.layer.removeAllAnimations()
91
+ view.removeFromSuperview()
92
+ }
93
+
94
+ // Calls onEnd when the animation added in body ends. An earlier animation that body
95
+ // replaces never calls its onEnd.
96
+ private func play(_ onEnd: @escaping () -> Void, _ body: () -> Void) {
97
+ playing += 1
98
+ let ticket = playing
99
+ view.layer.mask?.removeAllAnimations()
100
+ view.layer.removeAllAnimations()
101
+ view.layer.mask = nil
102
+ view.layer.opacity = 1
103
+ CATransaction.begin()
104
+ CATransaction.setDisableActions(true)
105
+ CATransaction.setCompletionBlock { [weak self] in
106
+ guard let self, self.playing == ticket else { return }
107
+ onEnd()
108
+ }
109
+ body()
110
+ CATransaction.commit()
111
+ }
112
+
113
+ private static let easing = CAMediaTimingFunction(
114
+ controlPoints: Float(Motion.easeX1), Float(Motion.easeY1), Float(Motion.easeX2), Float(Motion.easeY2))
115
+ }
@@ -0,0 +1,373 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ /// Parses SVG path data (the `d` attribute) into a `CGPath`.
5
+ ///
6
+ /// Supports `M L H V C S Q T A Z` in absolute and relative forms, implicit
7
+ /// repeated commands, and the SVG number syntax (`1e-3`, `.5.5`, `-1-2`).
8
+ /// Returns `nil` for malformed input.
9
+ enum SVGPath {
10
+ static func parse(_ d: String) -> CGPath? {
11
+ var parser = SVGPathParser(Array(d.utf8))
12
+ return parser.parse()
13
+ }
14
+ }
15
+
16
+ private struct SVGPathParser {
17
+ private let bytes: [UInt8]
18
+ private var index = 0
19
+ private let path = CGMutablePath()
20
+ private var current = CGPoint.zero
21
+ private var subpathStart = CGPoint.zero
22
+ private var lastCubicControl: CGPoint?
23
+ private var lastQuadControl: CGPoint?
24
+ private var needsMove = false
25
+
26
+ init(_ bytes: [UInt8]) {
27
+ self.bytes = bytes
28
+ }
29
+
30
+ mutating func parse() -> CGPath? {
31
+ skipSeparators()
32
+ guard let first = peek(), first == ascii("M") || first == ascii("m") else { return nil }
33
+
34
+ var command = first
35
+ while true {
36
+ skipSeparators()
37
+ guard let byte = peek() else { break }
38
+ if Self.commands.contains(byte) {
39
+ index += 1
40
+ command = byte
41
+ } else if isNumberStart(byte) {
42
+ // A number with no command letter repeats the previous command. Extra coordinate
43
+ // pairs after M or m are L or l.
44
+ switch command {
45
+ case ascii("M"): command = ascii("L")
46
+ case ascii("m"): command = ascii("l")
47
+ case ascii("Z"), ascii("z"): return nil
48
+ default: break
49
+ }
50
+ } else {
51
+ return nil
52
+ }
53
+ guard execute(command) else { return nil }
54
+ }
55
+ return path.copy()
56
+ }
57
+
58
+ // MARK: - Commands
59
+
60
+ private static let commands = Set("MmLlHhVvCcSsQqTtAaZz".utf8)
61
+
62
+ private mutating func execute(_ command: UInt8) -> Bool {
63
+ let relative = command >= ascii("a")
64
+ let origin = relative ? current : .zero
65
+ let previousCubicControl = lastCubicControl
66
+ let previousQuadControl = lastQuadControl
67
+ lastCubicControl = nil
68
+ lastQuadControl = nil
69
+
70
+ switch command | 0x20 {
71
+ case ascii("m"):
72
+ guard let point = point(from: origin) else { return false }
73
+ path.move(to: point)
74
+ subpathStart = point
75
+ current = point
76
+ needsMove = false
77
+
78
+ case ascii("l"):
79
+ guard let point = point(from: origin) else { return false }
80
+ line(to: point)
81
+
82
+ case ascii("h"):
83
+ guard let x = number(), isFinite(origin.x + x) else { return false }
84
+ line(to: CGPoint(x: origin.x + x, y: current.y))
85
+
86
+ case ascii("v"):
87
+ guard let y = number(), isFinite(origin.y + y) else { return false }
88
+ line(to: CGPoint(x: current.x, y: origin.y + y))
89
+
90
+ case ascii("c"):
91
+ guard let control1 = point(from: origin),
92
+ let control2 = point(from: origin),
93
+ let end = point(from: origin)
94
+ else { return false }
95
+ cubic(to: end, control1: control1, control2: control2)
96
+
97
+ case ascii("s"):
98
+ guard let control2 = point(from: origin), let end = point(from: origin) else { return false }
99
+ let control1 = previousCubicControl.map(reflect) ?? current
100
+ guard isFinite(control1) else { return false }
101
+ cubic(to: end, control1: control1, control2: control2)
102
+
103
+ case ascii("q"):
104
+ guard let control = point(from: origin), let end = point(from: origin) else { return false }
105
+ quad(to: end, control: control)
106
+
107
+ case ascii("t"):
108
+ guard let end = point(from: origin) else { return false }
109
+ let control = previousQuadControl.map(reflect) ?? current
110
+ guard isFinite(control) else { return false }
111
+ quad(to: end, control: control)
112
+
113
+ case ascii("a"):
114
+ guard let rx = number(),
115
+ let ry = number(),
116
+ let rotation = number(),
117
+ let largeArc = flag(),
118
+ let sweep = flag(),
119
+ let end = point(from: origin)
120
+ else { return false }
121
+ guard arc(to: end, rx: rx, ry: ry, rotation: rotation, largeArc: largeArc, sweep: sweep) else {
122
+ return false
123
+ }
124
+
125
+ case ascii("z"):
126
+ path.closeSubpath()
127
+ current = subpathStart
128
+ needsMove = true
129
+
130
+ default:
131
+ return false
132
+ }
133
+ return true
134
+ }
135
+
136
+ /// After Z, the next drawing command starts at the closed subpath's start point.
137
+ private mutating func beginSegment() {
138
+ if needsMove {
139
+ path.move(to: current)
140
+ needsMove = false
141
+ }
142
+ }
143
+
144
+ private mutating func line(to point: CGPoint) {
145
+ beginSegment()
146
+ path.addLine(to: point)
147
+ current = point
148
+ }
149
+
150
+ private mutating func cubic(to end: CGPoint, control1: CGPoint, control2: CGPoint) {
151
+ beginSegment()
152
+ path.addCurve(to: end, control1: control1, control2: control2)
153
+ lastCubicControl = control2
154
+ current = end
155
+ }
156
+
157
+ private mutating func quad(to end: CGPoint, control: CGPoint) {
158
+ beginSegment()
159
+ path.addQuadCurve(to: end, control: control)
160
+ lastQuadControl = control
161
+ current = end
162
+ }
163
+
164
+ private func reflect(_ control: CGPoint) -> CGPoint {
165
+ CGPoint(x: 2 * current.x - control.x, y: 2 * current.y - control.y)
166
+ }
167
+
168
+ /// Elliptical arc per the SVG 1.1 implementation notes: endpoint to centre conversion
169
+ /// (F.6.5) with out-of-range radii correction (F.6.6), drawn as cubic segments of at most
170
+ /// 90 degrees each. Returns false if the arithmetic leaves the finite range, so the parse
171
+ /// fails instead of trapping.
172
+ private mutating func arc(
173
+ to end: CGPoint, rx: CGFloat, ry: CGFloat, rotation: CGFloat, largeArc: Bool, sweep: Bool
174
+ ) -> Bool {
175
+ let start = current
176
+ guard isFinite(start), isFinite(end) else { return false }
177
+ guard start != end else { return true }
178
+ var rx = abs(rx)
179
+ var ry = abs(ry)
180
+ guard rx > 0, ry > 0 else {
181
+ line(to: end)
182
+ return true
183
+ }
184
+
185
+ let phi = rotation * .pi / 180
186
+ let cosPhi = cos(phi)
187
+ let sinPhi = sin(phi)
188
+
189
+ // F.6.5.1: the start point in the ellipse's rotated frame.
190
+ let halfDx = (start.x - end.x) / 2
191
+ let halfDy = (start.y - end.y) / 2
192
+ let x1 = cosPhi * halfDx + sinPhi * halfDy
193
+ let y1 = -sinPhi * halfDx + cosPhi * halfDy
194
+
195
+ // F.6.6: scale the radii up if they cannot span the end points.
196
+ let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry)
197
+ if lambda > 1 {
198
+ let scale = lambda.squareRoot()
199
+ rx *= scale
200
+ ry *= scale
201
+ }
202
+
203
+ // F.6.5.2: the centre in the rotated frame.
204
+ let rx2 = rx * rx
205
+ let ry2 = ry * ry
206
+ let denominator = rx2 * y1 * y1 + ry2 * x1 * x1
207
+ let ratio = (rx2 * ry2 - denominator) / denominator
208
+ // End points too close for these radii to place a centre, so draw a line.
209
+ guard denominator != 0, ratio != .infinity else {
210
+ line(to: end)
211
+ return true
212
+ }
213
+ var coefficient = max(0, ratio).squareRoot()
214
+ if largeArc == sweep {
215
+ coefficient = -coefficient
216
+ }
217
+ let centreX1 = coefficient * rx * y1 / ry
218
+ let centreY1 = -coefficient * ry * x1 / rx
219
+
220
+ // F.6.5.3: the centre in user space.
221
+ let centreX = cosPhi * centreX1 - sinPhi * centreY1 + (start.x + end.x) / 2
222
+ let centreY = sinPhi * centreX1 + cosPhi * centreY1 + (start.y + end.y) / 2
223
+
224
+ // F.6.5.5 and F.6.5.6: the start angle and the sweep angle.
225
+ let startAngle = atan2((y1 - centreY1) / ry, (x1 - centreX1) / rx)
226
+ let endAngle = atan2((-y1 - centreY1) / ry, (-x1 - centreX1) / rx)
227
+ var sweepAngle = endAngle - startAngle
228
+ if sweep, sweepAngle < 0 {
229
+ sweepAngle += 2 * .pi
230
+ } else if !sweep, sweepAngle > 0 {
231
+ sweepAngle -= 2 * .pi
232
+ }
233
+ guard centreX.isFinite, centreY.isFinite, startAngle.isFinite, sweepAngle.isFinite else {
234
+ return false
235
+ }
236
+
237
+ func onEllipse(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
238
+ CGPoint(
239
+ x: centreX + rx * cosPhi * x - ry * sinPhi * y,
240
+ y: centreY + rx * sinPhi * x + ry * cosPhi * y
241
+ )
242
+ }
243
+
244
+ let segments = max(1, Int((abs(sweepAngle) / (.pi / 2) - 1e-7).rounded(.up)))
245
+ let step = sweepAngle / CGFloat(segments)
246
+ let handle = 4 / 3 * tan(step / 4)
247
+ var angle = startAngle
248
+ var curves: [(point: CGPoint, control1: CGPoint, control2: CGPoint)] = []
249
+ for segment in 0..<segments {
250
+ let next = angle + step
251
+ let cos1 = cos(angle)
252
+ let sin1 = sin(angle)
253
+ let cos2 = cos(next)
254
+ let sin2 = sin(next)
255
+ let control1 = onEllipse(cos1 - handle * sin1, sin1 + handle * cos1)
256
+ let control2 = onEllipse(cos2 + handle * sin2, sin2 - handle * cos2)
257
+ let point = segment == segments - 1 ? end : onEllipse(cos2, sin2)
258
+ guard isFinite(control1), isFinite(control2), isFinite(point) else { return false }
259
+ curves.append((point, control1, control2))
260
+ angle = next
261
+ }
262
+ beginSegment()
263
+ for curve in curves {
264
+ path.addCurve(to: curve.point, control1: curve.control1, control2: curve.control2)
265
+ }
266
+ current = end
267
+ return true
268
+ }
269
+
270
+ // MARK: - Tokens
271
+
272
+ private func peek() -> UInt8? {
273
+ index < bytes.count ? bytes[index] : nil
274
+ }
275
+
276
+ private mutating func skipSeparators() {
277
+ while let byte = peek(), Self.separators.contains(byte) {
278
+ index += 1
279
+ }
280
+ }
281
+
282
+ private static let separators = Set(" \t\n\r\u{0C},".utf8)
283
+
284
+ private func isDigit(_ byte: UInt8) -> Bool {
285
+ byte >= ascii("0") && byte <= ascii("9")
286
+ }
287
+
288
+ private func isNumberStart(_ byte: UInt8) -> Bool {
289
+ isDigit(byte) || byte == ascii(".") || byte == ascii("-") || byte == ascii("+")
290
+ }
291
+
292
+ private mutating func point(from origin: CGPoint) -> CGPoint? {
293
+ guard let x = number(), let y = number() else { return nil }
294
+ let point = CGPoint(x: origin.x + x, y: origin.y + y)
295
+ return isFinite(point) ? point : nil
296
+ }
297
+
298
+ /// Scans `[sign] digits [. digits] [e [sign] digits]`. A sign or a second
299
+ /// dot ends the number, so `-1-2` and `.5.5` are two numbers each.
300
+ private mutating func number() -> CGFloat? {
301
+ skipSeparators()
302
+ let start = index
303
+ if let byte = peek(), byte == ascii("-") || byte == ascii("+") {
304
+ index += 1
305
+ }
306
+ var digits = 0
307
+ while let byte = peek(), isDigit(byte) {
308
+ index += 1
309
+ digits += 1
310
+ }
311
+ // SVG allows `5.` but Double(String) rejects it, so drop that trailing dot from the
312
+ // text.
313
+ var bareDot: Int?
314
+ if peek() == ascii(".") {
315
+ bareDot = index
316
+ index += 1
317
+ while let byte = peek(), isDigit(byte) {
318
+ index += 1
319
+ digits += 1
320
+ bareDot = nil
321
+ }
322
+ }
323
+ guard digits > 0 else {
324
+ index = start
325
+ return nil
326
+ }
327
+ if let byte = peek(), byte == ascii("e") || byte == ascii("E") {
328
+ var exponent = index + 1
329
+ if exponent < bytes.count, bytes[exponent] == ascii("-") || bytes[exponent] == ascii("+") {
330
+ exponent += 1
331
+ }
332
+ if exponent < bytes.count, isDigit(bytes[exponent]) {
333
+ while exponent < bytes.count, isDigit(bytes[exponent]) {
334
+ exponent += 1
335
+ }
336
+ index = exponent
337
+ }
338
+ }
339
+ var text = bytes[start..<index]
340
+ if let bareDot {
341
+ text.remove(at: bareDot)
342
+ }
343
+ guard let value = Double(String(decoding: text, as: UTF8.self)), value.isFinite else { return nil }
344
+ return CGFloat(value)
345
+ }
346
+
347
+ /// Arc flags are a single `0` or `1` and need no separator after them.
348
+ private mutating func flag() -> Bool? {
349
+ skipSeparators()
350
+ switch peek() {
351
+ case ascii("0"):
352
+ index += 1
353
+ return false
354
+ case ascii("1"):
355
+ index += 1
356
+ return true
357
+ default:
358
+ return nil
359
+ }
360
+ }
361
+ }
362
+
363
+ private func isFinite(_ value: CGFloat) -> Bool {
364
+ value.isFinite
365
+ }
366
+
367
+ private func isFinite(_ point: CGPoint) -> Bool {
368
+ point.x.isFinite && point.y.isFinite
369
+ }
370
+
371
+ private func ascii(_ scalar: Unicode.Scalar) -> UInt8 {
372
+ UInt8(ascii: scalar)
373
+ }
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ThemeReveal'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = 'Bohdan Pomohaibo'
12
+ s.homepage = package['homepage']
13
+ s.platforms = { :ios => '16.4' }
14
+ s.swift_version = '5.9'
15
+ s.source = { git: 'https://github.com/chocky335/react-native-theme-reveal.git', tag: "v#{s.version}" }
16
+ s.static_framework = true
17
+
18
+ s.dependency 'ExpoModulesCore'
19
+
20
+ s.pod_target_xcconfig = {
21
+ 'DEFINES_MODULE' => 'YES',
22
+ }
23
+
24
+ s.source_files = '**/*.{h,m,mm,swift}'
25
+ s.exclude_files = 'Tests/**'
26
+ end