Для небольшого ряда меток в приложении для Mac используйте ViewThatFits, чтобы выбрать самый длинный полный префикс, который помещается вместе с числом скрытых элементов. В этом руководстве создаётся такой ряд без усечения меток и обновления состояния во время компоновки.

Конкретная задача — создать изменяемое по размеру окно macOS с шестью синтетическими метками. Сохраняйте их порядок, показывайте только целые метки и отображайте значок, например ещё 3, для пропущенного суффикса. Решение предполагает, что для значка с числом скрытых элементов достаточно места по горизонтали, а по вертикали хватает места для одной строки.

Выберите полные варианты строки

Apple описывает ViewThatFits как средство выбора первого дочернего элемента, чьего идеального размера хватает в рамках предложенного размера по ограниченным осям. Он доступен в macOS 13 и более поздних версиях. Здесь используйте .horizontal, поскольку задача связана с шириной строки; окружающий интерфейс должен предоставить строке нужную высоту. ViewThatFits

Для шести меток задайте семь вариантов в таком порядке: все шесть меток без значка, пять меток плюс ещё 1, четыре плюс ещё 2 и так далее, заканчивая вариантом только с ещё 6. Каждый вариант представляет собой полную строку. Значок участвует в проверке того, помещается ли строка, поэтому добавление числа не вытеснит неожиданно последнюю видимую метку.

Так реализуется политика префикса: из-за длинной первой метки могут быть пропущены все метки, даже если более поздняя короткая метка поместилась бы. Это сделано намеренно. Если задача допускает пропуск отдельных меток, сначала задайте другую политику выбора.

Примените fixedSize() к каждой строке-кандидату, чтобы метки и значок сохраняли свои идеальные размеры. Apple поясняет, что этот модификатор может позволить представлению выйти за границы родителя; сам по себе он не гарантирует, что представление поместится. Здесь решение о выборе подходящего варианта принимает окружающий контейнер. fixedSize()

Создайте полный пример для macOS

Создайте приложение SwiftUI для macOS в Xcode и задайте целевую версию macOS 13 или новее. Замените сгенерированные объявление приложения и представление содержимого приведённой ниже программой в одном файле. В целевом проекте оставьте ровно одно объявление приложения с атрибутом @main.

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

import SwiftUI

struct DemoItem: Identifiable {
    let id: Int
    let title: String
}

struct ItemChip: View {
    let title: String

    var body: some View {
        Text(title)
            .lineLimit(1)
            .padding(.horizontal, 10)
            .padding(.vertical, 6)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(Color.blue.opacity(0.12))
            )
    }
}

struct PrefixCandidate: View {
    let items: [DemoItem]
    let visibleCount: Int

    var body: some View {
        HStack(spacing: 8) {
            ForEach(items.prefix(visibleCount)) { item in
                ItemChip(title: item.title)
            }

            if visibleCount < items.count {
                Text("\(items.count - visibleCount) more")
                    .lineLimit(1)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.15))
                    )
            }
        }
        .fixedSize()
    }
}

struct WholeItemRow: View {
    let items: [DemoItem]

    var body: some View {
        if items.isEmpty {
            Text("No items")
                .fixedSize()
        } else {
            ViewThatFits(in: .horizontal) {
                ForEach(
                    Array((0...items.count).reversed()),
                    id: \.self
                ) { visibleCount in
                    PrefixCandidate(
                        items: items,
                        visibleCount: visibleCount
                    )
                }
            }
        }
    }
}

struct ContentView: View {
    let items: [DemoItem] = [
        DemoItem(id: 1, title: "Sketch"),
        DemoItem(id: 2, title: "Color samples"),
        DemoItem(id: 3, title: "Shapes"),
        DemoItem(id: 4, title: "Paper textures"),
        DemoItem(id: 5, title: "Layout notes"),
        DemoItem(id: 6, title: "Preview")
    ]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Sample labels")
                .font(.headline)

            WholeItemRow(items: items)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("Resize the window to change how many labels fit.")
                .foregroundStyle(.secondary)
        }
        .font(.system(size: 14))
        .padding(24)
        .frame(minWidth: 320, idealWidth: 680, minHeight: 160)
    }
}

@main
struct WholeItemsDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Идентификаторы вариантов-кандидатов обозначают возможное количество видимых элементов и идут по убыванию — от общего количества элементов до нуля. В каждом варианте идентификаторы элементов определяют метки. Каждая итерация создаёт один PrefixCandidate, который содержит одну горизонтальную строку.

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

Это полный пример исходного кода, но здесь он не компилировался и не запускался, поскольку компилятор Swift был недоступен. Приведённая в следующем разделе арифметическая иллюстрация была проверена отдельно; фактическую ширину текста необходимо проверить в приложении для Mac.

Учитывайте значок при расчёте ширины

Полезное правило выбора: выбирайте наибольшее количество видимых элементов, для которого целиком помещается весь вариант. Количество скрытых элементов всегда равно общему числу элементов минус число видимых элементов в этом варианте. Отдельная оценка, которую нужно синхронизировать, не требуется.

Рассмотрим иллюстративный ряд из четырёх элементов шириной 72, 96, 64 и 88 пунктов с учётом внутренних отступов. Предположим, что интервал между элементами равен 8 пунктам, а значок для любого ненулевого количества скрытых элементов имеет ширину 60 пунктов. Это вымышленные исходные данные для расчёта, а не результаты измерений тестового приложения.

Когда видны все четыре элемента, требуемая ширина равна 72 + 96 + 64 + 88 + 3 × 8 = 344 пунктам. Если видны три элемента, значок заменяет пропущенный суффикс, поэтому ширина составляет 72 + 96 + 64 + 3 × 8 + 60 = 316 пунктов. Нужны три интервала, поскольку вариант содержит три элемента и один значок.

Доступная ширина Видимые элементы Скрытые элементы Ширина выбранного варианта
344 пункта 4 0 344 пункта
343 пункта 3 1 316 пунктов
300 пунктов 2 2 244 пункта
220 пунктов 1 3 140 пунктов
60 пунктов 0 4 60 пунктов

В программе в каждом варианте используется фактический текст значка. Поэтому она учитывает различия между строками значков и не полагается на предположение о постоянной ширине из примера. Ширина вариантов не обязана изменяться равномерно; порядок по убыванию числа видимых элементов выражает предпочтение при выборе.

Вариант только со счётчиком задаёт минимальную полезную ширину. Если даже этот значок не помещается, то в рамках этих правил показать полное количество в доступной ширине невозможно. Задайте достаточную минимальную ширину, разрешите другое расположение или перенесите число в другое место. Обрезание не означает, что элемент показан целиком.

В примере задана минимальная ширина содержимого и умеренный явный размер шрифта для короткого тестового значка. Если изменить текст, шрифт, отступы или окружающую компоновку, ещё раз проверьте вариант только со счётчиком. Минимальная ширина, выбранная для этого примера, не является универсальной мерой ширины значка.

Отделите выбор компоновки от жизненного цикла представления

Количество скрытых элементов входит в неизменяемые входные данные каждого варианта-кандидата. Программа не записывает значение в привязку из метода измерения, не планирует асинхронное обновление количества и не определяет видимость по обратным вызовам появления. При выборе строки одновременно выбирается соответствующий ей значок.

Старайтесь, чтобы повторяющееся содержимое вариантов оставалось простым. Повторение метки в нескольких вариантах не означает, что у неё один общий идентификатор во всех вариантах. Apple объясняет, что хранилище состояния связано с идентичностью представления, а при изменении идентичности состояние может замениться. Demystify SwiftUI

Если позже вы замените метки редактируемыми элементами управления, храните общие значения в общем предке или модели и передавайте необходимые входные данные или привязки каждому варианту отображения. Apple рекомендует хранить общее состояние в наименее общем предке тех представлений, которым оно требуется. Не рассчитывайте, что локальное состояние варианта сохранится после смены отображения. Managing user interface state

Это решение отображает количество скрытых элементов в выбранной строке. Оно не передаёт выбранное количество соседнему представлению или модели приложения. В документированном Apple инициализаторе ViewThatFits принимаются ограниченные оси и содержимое-кандидат; привязки к выбранному количеству он не предоставляет. Рассматривайте передачу этого значения наружу как отдельное требование к проектированию. init(in:content:)

Когда для пользовательской компоновки нужен другой подход

Пользовательская Layout подходит, когда требуется особая геометрия. Apple определяет её основные задачи как вычисление общего размера и задание позиций подставлений через прокси компоновки. Прокси предоставляют операции измерения и размещения, а не прямой доступ к лежащим в основе представлениям. Layout

Не реализуйте пропуск элементов, просто прекращая их размещение после последнего поместившегося элемента. В документации Apple для LayoutSubview указано, что необходимо разместить каждое подставление ровно один раз. Пропуск размещения не является документированным механизмом скрытия, а прокси компоновки не предоставляет универсальной операции изменения содержимого, которая превращает другое подставление в новую метку со счётчиком. LayoutSubview

Если вы создаёте пользовательскую компоновку, обеспечьте согласованность вычислений размера и размещения, включая интервалы и предложение размера. Apple прямо требует, чтобы эти вычисления были согласованы. Это требование сохраняется и в том случае, если компоновка проверяет несколько возможных вариантов размещения. placeSubviews(in:proposal:subviews:cache:)

Подход с вариантами-кандидатами намеренно ограничен. Для шести элементов он задаёт семь строк-кандидатов и 21 появление элемента в этих строках. В общем случае количество появлений элементов растёт как n × (n + 1) / 2. Это структурный подсчёт, а не бенчмарк производительности и не утверждение, что при каждом обновлении отображается каждый вариант. Используйте этот подход для небольшой коллекции меток; прежде чем распространять его на большую коллекцию или ресурсоёмкое содержимое, измерьте производительность.

Проверьте строку на граничных значениях

Запустите приложение и медленно меняйте размер его окна. Перед адаптацией компонента проверьте следующее:

  • При большой ширине видны все метки и нет значка ещё 0.
  • При сужении окна метки исчезают с конца без обрезания и многоточий.
  • Сумма количества видимых элементов и скрытых элементов на значке всегда равна шести.
  • Если первая метка исключительно длинная, доступен вариант только со счётчиком.
  • Если массив пуст, строка показывает Нет элементов.
  • При одинаковых заголовках и разных идентификаторах оба элемента учитываются в общем числе.
  • После изменения шрифтов или текста значка вариант только со счётчиком по-прежнему помещается в наименьшую поддерживаемую ширину.

Для небольшого упорядоченного ряда полные варианты упрощают проверку правила выбора: каждый вариант содержит метки, интервалы и точное количество скрытых элементов. Если явно задать условие минимальной ширины и оставить повторяющееся содержимое лёгким, строка сможет адаптироваться к изменению размера окна без частичных элементов и отдельного цикла обновления счётчика.

Источники