Как сделать так, чтобы мои объекты в маске VStack отличались от градиентов полноэкранного размера в swiftUI? - PullRequest
0 голосов
/ 23 февраля 2020

Внутри ZStack : я знаю, что вы можете установить VStack внутри .mask(), но затем мне нужно добавить смещения для каждого объекта, чтобы они не перекрывались .

Есть ли способ изменить ваш объект так, чтобы он маскировал определенный c вид внутри одного VStack?

multi fixed gradients, VStack objects masking to specific gradient view

1 Ответ

1 голос
/ 24 февраля 2020

Таким образом, вы, вероятно, захотите либо левый столбец, либо правый столбец этой демонстрации:

animated demo of scroll views with gradients spanning vertically

В левом столбце градиент охватывает все пузыри, в том числе пузыри с экрана. Таким образом, кажется, что градиент прокручивается вместе с пузырьками.

В правом столбце градиент охватывает только видимую рамку вида прокрутки, поэтому фоны пузырьков изменяются при движении пузырьков вверх и вниз.

В любом случае, это сложная проблема! Я решил это до для UIKit. Вот решение для SwiftUI.

Вот что мы будем делать:

  1. Нарисуйте каждый «пузырь» (прямоугольник с закругленными углами) с прозрачным (прозрачным) фоном .
  2. Запишите фрейм (в глобальных координатах) каждого пузырька в «предпочтении». preference - это API-интерфейс SwiftUI для передачи значений из дочерних представлений в представления предков.
  3. Добавьте фон к некоторому общему предку всех пузырьковых представлений. Фон 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()
    }
}
...