Как сделать просмотр размера другого представления в SwiftUI - PullRequest
8 голосов
/ 08 июня 2019

Я пытаюсь воссоздать часть приложения Twitter для iOS, чтобы изучить SwiftUI, и мне интересно, как динамически изменить ширину одного представления, чтобы оно стало шириной другого представления.В моем случае, чтобы подчеркивание было такой же ширины, как текстовое представление.

Я приложил скриншот, чтобы попытаться лучше объяснить, о чем я говорю.Любая помощь будет принята с благодарностью, спасибо!

Также вот код, который у меня есть:

import SwiftUI

struct GridViewHeader : View {

    @State var leftPadding: Length = 0.0
    @State var underLineWidth: Length = 100

    var body: some View {
        return VStack {
            HStack {
                Text("Tweets")
                    .tapAction {
                        self.leftPadding = 0

                }
                Spacer()
                Text("Tweets & Replies")
                    .tapAction {
                        self.leftPadding = 100
                    }
                Spacer()
                Text("Media")
                    .tapAction {
                        self.leftPadding = 200
                }
                Spacer()
                Text("Likes")
            }
            .frame(height: 50)
            .padding(.horizontal, 10)
            HStack {
                Rectangle()
                    .frame(width: self.underLineWidth, height: 2, alignment: .bottom)
                    .padding(.leading, leftPadding)
                    .animation(.basic())
                Spacer()
            }
        }
    }
}

Ответы [ 2 ]

6 голосов
/ 19 июня 2019

Я написал подробное объяснение об использовании GeometryReader, настройках просмотра и привязках. Код ниже использует эти понятия. Для получения дополнительной информации о том, как они работают, проверьте эту статью, которую я разместил: https://swiftui -lab.com / Communication-with-the-View-Tree-Part-1 /

Решение, приведенное ниже, будет правильно анимировать подчеркивание:

enter image description here

Я изо всех сил пытался заставить это работать, и я согласен с вами. Иногда вам просто нужно пройти вверх или вниз по иерархии, некоторую информацию о кадрировании. Фактически, сеанс 237 WWDC2019 (Создание пользовательских представлений с помощью SwiftUI) объясняет, что представления непрерывно сообщают свои размеры. Это в основном говорит, что Родитель предлагает размер ребенку, дети решают, как они хотят расположить себя и общаться с родителем. Как они это делают? Я подозреваю, что anchorPreference как-то связан с этим. Однако это очень неясно и пока не документировано. API выставлен, но он понимает, как работают эти прототипы длинных функций ... это ад, на который у меня сейчас нет времени.

Я думаю, что Apple оставила это недокументированным, чтобы заставить нас переосмыслить всю структуру и забыть о «старых» привычках UIKit и начать мыслить декларативно. Однако бывают времена, когда это необходимо. Вы когда-нибудь задумывались, как работает модификатор фона? Я хотел бы увидеть эту реализацию. Это многое бы объяснило! Я надеюсь, что Apple документирует предпочтения в ближайшем будущем. Я экспериментировал с пользовательским PreferenceKey, и он выглядит интересно.

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

Положение x текста Я решил это путем создания собственного горизонтального выравнивания. Больше информации о том сеансе проверки 237 (в минуту 19:00). Хотя я рекомендую вам посмотреть все это, оно проливает много света на то, как работает процесс верстки.

Однако шириной я не очень горжусь ... ;-) Требуется DispatchQueue, чтобы избежать обновления вида во время отображения. ОБНОВЛЕНИЕ: я исправил это во второй реализации ниже

Первая реализация

import SwiftUI

extension HorizontalAlignment {
    private enum UnderlineLeading: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length {
            return d[.leading]
        }
    }

    static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}


struct GridViewHeader : View {

    @State private var activeIdx: Int = 0
    @State private var w: [Length] = [0, 0, 0, 0]

    var body: some View {
        return VStack(alignment: .underlineLeading) {
            HStack {
                Text("Tweets").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 0))
                Spacer()
                Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))
                Spacer()
                Text("Media").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 2))
                Spacer()
                Text("Likes").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 3))
                }
                .frame(height: 50)
                .padding(.horizontal, 10)
            Rectangle()
                .alignmentGuide(.underlineLeading) { d in d[.leading]  }
                .frame(width: w[activeIdx],  height: 2)
                .animation(.basic())
        }
    }
}

struct MagicStuff: ViewModifier {
    @Binding var activeIdx: Int
    @Binding var widths: [Length]
    let idx: Int

    func body(content: Content) -> some View {
        Group {
            if activeIdx == idx {
                content.alignmentGuide(.underlineLeading) { d in
                    DispatchQueue.main.async { self.widths[self.idx] = d.width }

                    return d[.leading]
                }.tapAction { self.activeIdx = self.idx }

            } else {
                content.tapAction { self.activeIdx = self.idx }
            }
        }
    }
}

Обновление: лучшая реализация без использования DispatchQueue

Мое первое решение работает, но я не слишком гордился тем, как ширина передается в подчеркивание.

Я нашел лучший способ достичь того же. Оказывается, модификатор background очень мощный. Это гораздо больше, чем модификатор, который позволяет вам украшать фон вида.

Основные шаги:

  1. Используйте Text("text").background(TextGeometry()). TextGeometry - это пользовательское представление, родительский элемент которого имеет тот же размер, что и текстовое представление. Это то, что делает .background (). Очень мощный.
  2. В моей реализации TextGeometry Я использую GeometryReader, чтобы получить геометрию родителя, что означает, что я получаю геометрию текстового представления, что означает, что теперь у меня есть ширина.
  3. Теперь, чтобы передать ширину, я использую Предпочтения . О них нет никакой документации, но после небольшого эксперимента, я думаю, предпочтения - это что-то вроде «атрибутов вида», если хотите. Я создал свой пользовательский PreferenceKey , который называется WidthPreferenceKey , и я использую его в TextGeometry, чтобы "прикрепить" ширину к представлению, чтобы его можно было прочитать выше в иерархии.
  4. Вернувшись к предку, я использую onPreferenceChange для обнаружения изменений ширины и соответственно устанавливаю массив widths.

Возможно, все это звучит слишком сложно, но код лучше всего это иллюстрирует. Вот новая реализация:

import SwiftUI

extension HorizontalAlignment {
    private enum UnderlineLeading: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length {
            return d[.leading]
        }
    }

    static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}

struct WidthPreferenceKey: PreferenceKey {
    static var defaultValue = CGFloat(0)

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }

    typealias Value = CGFloat
}


struct GridViewHeader : View {

    @State private var activeIdx: Int = 0
    @State private var w: [Length] = [0, 0, 0, 0]

    var body: some View {
        return VStack(alignment: .underlineLeading) {
            HStack {
                Text("Tweets")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 0))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[0] = $0 })

                Spacer()

                Text("Tweets & Replies")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = $0 })

                Spacer()

                Text("Media")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = $0 })

                Spacer()

                Text("Likes")
                    .modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
                    .background(TextGeometry())
                    .onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = $0 })

                }
                .frame(height: 50)
                .padding(.horizontal, 10)
            Rectangle()
                .alignmentGuide(.underlineLeading) { d in d[.leading]  }
                .frame(width: w[activeIdx],  height: 2)
                .animation(.basic())
        }
    }
}

struct TextGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle().preference(key: WidthPreferenceKey.self, value: geometry.size.width)
        }
    }
}

struct MagicStuff: ViewModifier {
    @Binding var activeIdx: Int
    let idx: Int

    func body(content: Content) -> some View {
        Group {
            if activeIdx == idx {
                content.alignmentGuide(.underlineLeading) { d in
                    return d[.leading]
                }.tapAction { self.activeIdx = self.idx }

            } else {
                content.tapAction { self.activeIdx = self.idx }
            }
        }
    }
}
1 голос
/ 19 июня 2019

Попробуйте:

import SwiftUI

var titles = ["Tweets", "Tweets & Replies", "Media", "Likes"]

struct GridViewHeader : View {

    @State var selectedItem: String = "Tweets"

    var body: some View {
        HStack(spacing: 20) {
            ForEach(titles.identified(by: \.self)) { title in
                HeaderTabButton(title: title, selectedItem: self.$selectedItem)
                }
                .frame(height: 50)
        }.padding(.horizontal, 10)

    }
}

struct HeaderTabButton : View {
    var title: String

    @Binding var selectedItem: String

    var isSelected: Bool {
        selectedItem == title
    }

    var body: some View {
        VStack {
            Button(action: { self.selectedItem = self.title }) {
                Text(title).fixedSize(horizontal: true, vertical: false)

                Rectangle()
                    .frame(height: 2, alignment: .bottom)
                    .relativeWidth(1)
                    .foregroundColor(isSelected ? Color.accentColor : Color.clear)

            }
        }
    }
}

А вот как это выглядит в превью: Preview screen

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...