Created
February 26, 2020 16:36
-
-
Save mayoff/481fe99f88e13dafa50d6248efcee00e to your computer and use it in GitHub Desktop.
SwiftUI gradients spanning multiple views
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct BubbleFramesValue { | |
var framesForKey: [AnyHashable: [CGRect]] = [:] | |
var gradientFrame: CGRect? = nil | |
} | |
struct BubbleFramesKey { } | |
extension BubbleFramesKey: PreferenceKey { | |
static let defaultValue: BubbleFramesValue = .init() | |
static func reduce(value: inout BubbleFramesValue, nextValue: () -> BubbleFramesValue) { | |
let next = nextValue() | |
switch (value.gradientFrame, next.gradientFrame) { | |
case (nil, .some(let frame)): value.gradientFrame = frame | |
case (_, nil): break | |
case (.some(_), .some(_)): fatalError("Two gradient frames defined!") | |
} | |
value.framesForKey.merge(next.framesForKey) { $0 + $1 } | |
} | |
} | |
extension View { | |
func bubble<Name: Hashable>(named name: Name) -> some View { | |
return self | |
.background(GeometryReader { proxy in | |
Color.clear | |
.preference( | |
key: BubbleFramesKey.self, | |
value: BubbleFramesValue( | |
framesForKey: [name: [proxy.frame(in: .global)]], | |
gradientFrame: nil)) | |
}) | |
} | |
} | |
extension View { | |
func bubbleFrame( | |
withGradientForKeyMap gradientForKey: [AnyHashable: LinearGradient] | |
) -> some View { | |
return self | |
.background(GeometryReader { proxy in | |
Color.clear | |
.preference( | |
key: BubbleFramesKey.self, | |
value: BubbleFramesValue( | |
framesForKey: [:], | |
gradientFrame: proxy.frame(in: .global))) | |
} // | |
.edgesIgnoringSafeArea(.all)) | |
.backgroundPreferenceValue(BubbleFramesKey.self) { | |
self.backgroundView(for: $0, gradientForKey: gradientForKey) } | |
} | |
private func backgroundView( | |
for bubbleDefs: BubbleFramesKey.Value, | |
gradientForKey: [AnyHashable: LinearGradient] | |
) -> some View { | |
return bubbleDefs.gradientFrame.map { gradientFrame in | |
GeometryReader { proxy in | |
ForEach(Array(gradientForKey.keys), id: \.self) { key in | |
bubbleDefs.framesForKey[key].map { bubbleFrames in | |
gradientForKey[key]!.masked( | |
toBubbleFrames: bubbleFrames, inGradientFrame: gradientFrame, | |
readerFrame: proxy.frame(in: .global)) | |
} | |
} | |
} | |
} | |
} | |
} | |
extension LinearGradient { | |
fileprivate func masked( | |
toBubbleFrames bubbleFrames: [CGRect], | |
inGradientFrame gradientFrame: CGRect, | |
readerFrame: CGRect | |
) -> some View { | |
let offset = CGSize( | |
width: gradientFrame.origin.x - readerFrame.origin.x, | |
height: gradientFrame.origin.y - readerFrame.origin.y) | |
let transform = CGAffineTransform.identity | |
.translatedBy(x: -readerFrame.origin.x, y: -readerFrame.origin.y) | |
var mask = Path() | |
for bubble in bubbleFrames { | |
mask.addRoundedRect( | |
in: bubble, | |
cornerSize: CGSize(width: 10, height: 10), | |
transform: transform) | |
} | |
return self | |
.frame( | |
width: gradientFrame.size.width, | |
height: gradientFrame.size.height) | |
.offset(offset) | |
.mask(mask) | |
} | |
} | |
struct ContentView { | |
init() { | |
self.gold = "gold" | |
self.teal = "teal" | |
gradientForKey = [ | |
gold: LinearGradient( | |
gradient: Gradient(stops: [ | |
.init(color: Color(#colorLiteral(red: 0.9823742509, green: 0.8662455082, blue: 0.4398147464, alpha: 1)), location: 0), | |
.init(color: Color(#colorLiteral(red: 0.3251565695, green: 0.2370383441, blue: 0.07140993327, alpha: 1)), location: 1), | |
]), | |
startPoint: UnitPoint(x: 0, y: 0), | |
endPoint: UnitPoint(x: 0, y: 1)), | |
teal: LinearGradient( | |
gradient: Gradient(stops: [ | |
.init(color: Color(#colorLiteral(red: 0, green: 0.8077999949, blue: 0.8187007308, alpha: 1)), location: 0), | |
.init(color: Color(#colorLiteral(red: 0.08204867691, green: 0.2874087095, blue: 0.4644176364, alpha: 1)), location: 1), | |
]), | |
startPoint: UnitPoint(x: 0, y: 0), | |
endPoint: UnitPoint(x: 0, y: 1)), | |
] | |
} | |
private let gold: String | |
private let teal: String | |
private let gradientForKey: [AnyHashable: LinearGradient] | |
} | |
extension ContentView { | |
private func bubbledItem(_ i: Int) -> some View { | |
Text("Bubble number \(i)") | |
.frame(height: 60 + CGFloat((i * 19) % 60)) | |
.frame(maxWidth: .infinity) | |
.bubble(named: i.isMultiple(of: 2) ? gold : teal) | |
.padding([.leading, .trailing], 20) | |
} | |
} | |
extension ContentView: View { | |
var body: some View { | |
HStack(spacing: 4) { | |
ScrollView { | |
VStack(spacing: 8) { | |
ForEach(Array(0 ..< 20), id: \.self) { i in | |
self.bubbledItem(i) | |
} | |
} // | |
.bubbleFrame(withGradientForKeyMap: gradientForKey) | |
} // | |
ScrollView { | |
VStack(spacing: 8) { | |
ForEach(Array(0 ..< 20), id: \.self) { i in | |
self.bubbledItem(i) | |
} | |
} | |
} // | |
.bubbleFrame(withGradientForKeyMap: gradientForKey) | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment