Таким образом, вы, вероятно, захотите либо левый столбец, либо правый столбец этой демонстрации:
В левом столбце градиент охватывает все пузыри, в том числе пузыри с экрана. Таким образом, кажется, что градиент прокручивается вместе с пузырьками.
В правом столбце градиент охватывает только видимую рамку вида прокрутки, поэтому фоны пузырьков изменяются при движении пузырьков вверх и вниз.
В любом случае, это сложная проблема! Я решил это до для UIKit. Вот решение для SwiftUI.
Вот что мы будем делать:
- Нарисуйте каждый «пузырь» (прямоугольник с закругленными углами) с прозрачным (прозрачным) фоном .
- Запишите фрейм (в глобальных координатах) каждого пузырька в «предпочтении». preference - это API-интерфейс SwiftUI для передачи значений из дочерних представлений в представления предков.
- Добавьте фон к некоторому общему предку всех пузырьковых представлений. Фон dr aws градиент достаточно большой, чтобы покрыть всего общего предка, но замаскированный, чтобы быть видимым только в скругленном прямоугольнике под каждым пузырем.
Мы соберем рамки пузырьков в эта структура данных:
struct BubbleFramesValue {
var framesForKey: [AnyHashable: [CGRect]] = [:]
var gradientFrame: CGRect? = nil
}
Мы соберем кадры пузырьков в свойстве framesForKey
. Поскольку мы хотим нарисовать два градиента (золотой и бирюзовый), нам нужно хранить отдельные коллекции пузырьковых рамок. Так что framesForKey[gold]
собирает рамки золотых пузырьков, а framesForKey[teal]
собирает рамки пузырьков бирюзового цвета.
Нам также понадобится рамка общего предка, поэтому мы сохраним это в gradientFrame
свойство.
Мы будем собирать эти кадры, используя API предпочтений, что означает, что нам нужно определить тип, соответствующий протоколу PreferenceKey
:
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 }
}
}
Теперь мы можно определить два новых метода для View
. Первый метод объявляет, что представление должно быть пузырем, что означает, что у него должен быть закругленный прямоугольный фон, который показывает градиент. Этот метод использует модификатор preference
и GeometryReader
для предоставления своего собственного кадра (в глобальных координатах) как BubbleFramesValue
:
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))
})
}
}
Другой метод объявляет, что представление является общим предком пузыри, и поэтому он должен определять свойство gradientFrame
. Он также вставляет фоновый градиент позади себя, с соответствующей маской, сделанной из RoundedRectangle
s:
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))
}
}
}
}
}
}
Мы установили градиент, чтобы иметь правильный размер, положение и маску в методе masked(toBubbleFrames:inGradientFrame:readerFrame:)
:
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)
}
}
Вот так! Теперь мы готовы попробовать это. Давайте напишем ContentView
, который я использовал для демонстрации в верхней части этого ответа. Мы начнем с определения градиента золота и градиента бирюзового цвета:
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)
}
}
Теперь мы можем определить body
из ContentView
. Мы рисуем VStack
s пузырьков, каждый внутри вида прокрутки. С левой стороны я поместил модификатор bubbleFrame
на VStack
. На правой стороне я поместил модификатор bubbleFrame
на ScrollView
.
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)
}
}
}
А вот PreviewProvider
, чтобы мы могли увидеть его на холсте Xcode:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}