Skip to content

Instantly share code, notes, and snippets.

@mayoff
Created February 26, 2020 16:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mayoff/481fe99f88e13dafa50d6248efcee00e to your computer and use it in GitHub Desktop.
Save mayoff/481fe99f88e13dafa50d6248efcee00e to your computer and use it in GitHub Desktop.
SwiftUI gradients spanning multiple views
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