UICollectionView Представление украшения

Кто-нибудь реализовал представление оформления для iOS 6 UICollectionView? Невозможно найти какое-либо руководство по реализации декоративного представления в Интернете. По сути, в моем приложении у меня есть несколько разделов, и я просто хотел отобразить оформление за каждым разделом. Это должно быть просто реализовать, но мне не повезло. Это сводит меня с ума... Спасибо.


person vtruong    schedule 10.10.2012    source источник
comment
извините, но можете ли вы объяснить, что вы подразумеваете под видом украшения?   -  person geraldWilliam    schedule 10.10.2012
comment
@geraldWilliam, проверьте ссылку выше в моем комментарии. У них есть хороший учебник, который объясняет.   -  person iDev    schedule 10.10.2012
comment
@ACB В этом уроке нет оформления, только заголовок и ячейки.   -  person Guido Hendriks    schedule 10.10.2012
comment
Да, в учебнике на raywnderlich нет представления оформления, хотя в используемой ими композиции показано представление оформления. Возможно, в последний момент решили не показывать декоративный вид в готовом проекте. Вау... декоративный вид - большая загадка. Я бы хотел, чтобы Apple предоставила пример кода по этому вопросу.   -  person vtruong    schedule 10.10.2012
comment
Возможно, вы захотите проверить сеансы WWDC. Я делал несколько месяцев назад и помню, что они говорили об этом, но не помню подробностей.   -  person Guido Hendriks    schedule 10.10.2012
comment
Обратите внимание, что стандартный класс макета потока поддерживает только представления заголовка и нижнего колонтитула раздела и не поддерживает представления оформления. Для поддержки декоративных представлений вам необходимо создать подкласс UICollectionViewFlowLayout.   -  person masam    schedule 12.11.2012


Ответы (4)


Вот руководство по оформлению макета представления коллекции в Swift (это Swift 3, Xcode 8 seed 6).


Представления оформления не являются функцией UICollectionView; они по существу принадлежат UICollectionViewLayout. Никакие методы UICollectionView (или методы делегата или источника данных) не упоминают представления оформления. UICollectionView ничего о них не знает; он просто делает то, что ему говорят.

Для предоставления любых декоративных представлений вам понадобится подкласс UICollectionViewLayout; этот подкласс может свободно определять свои собственные свойства и делегировать методы протокола, которые настраивают то, как настраиваются его представления оформления, но это полностью зависит от вас.

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

Существует четыре шага для реализации декоративного представления в подклассе макета:

  1. Определите подкласс UICollectionReusableView.

  2. Зарегистрируйте подкласс UICollectionReusableView с макетом (не представление коллекции), вызвав register(_:forDecorationViewOfKind:). Инициализатор макета — хорошее место для этого.

  3. Реализуйте layoutAttributesForDecorationView(ofKind:at:) для возврата атрибутов макета, которые позиционируют UICollectionReusableView. Чтобы создать атрибуты макета, вызовите init(forDecorationViewOfKind:with:) и настройте атрибуты.

  4. Переопределите layoutAttributesForElements(in:), чтобы результат layoutAttributesForDecorationView(ofKind:at:) был включен в возвращаемый массив.

Последний шаг — это то, что приводит к тому, что представление украшения появляется в представлении коллекции. Когда представление набора вызывает layoutAttributesForElements(in:), оно обнаруживает, что результирующий массив включает атрибуты макета для представления оформления определенного типа. Представление коллекции ничего не знает о представлениях оформления, поэтому оно возвращается к макету, запрашивая фактический экземпляр такого вида представления оформления. Вы зарегистрировали этот вид декоративного представления, чтобы он соответствовал вашему подклассу UICollectionReusableView, поэтому создается экземпляр вашего подкласса UICollectionReusableView, и этот экземпляр возвращается, а представление коллекции позиционирует его в соответствии с атрибутами макета.

Итак, давайте следовать шагам. Определите подкласс UICollectionReusableView:

class MyTitleView : UICollectionReusableView {
    weak var lab : UILabel!
    override init(frame: CGRect) {
        super.init(frame:frame)
        let lab = UILabel(frame:self.bounds)
        self.addSubview(lab)
        lab.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        lab.font = UIFont(name: "GillSans-Bold", size: 40)
        lab.text = "Testing"
        self.lab = lab
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Теперь обратимся к нашему подклассу UICollectionViewLayout, который я назову MyFlowLayout. Мы регистрируем MyTitleView в инициализаторе макета; Я также определил некоторые частные свойства, которые мне понадобятся для оставшихся шагов:

private let titleKind = "title"
private let titleHeight : CGFloat = 50
private var titleRect : CGRect {
    return CGRect(10,0,200,self.titleHeight)
}
override init() {
    super.init()
    self.register(MyTitleView.self, forDecorationViewOfKind:self.titleKind)
}

Орудие layoutAttributesForDecorationView(ofKind:at:):

override func layoutAttributesForDecorationView(
    ofKind elementKind: String, at indexPath: IndexPath) 
    -> UICollectionViewLayoutAttributes? {
        if elementKind == self.titleKind {
            let atts = UICollectionViewLayoutAttributes(
                forDecorationViewOfKind:self.titleKind, with:indexPath)
            atts.frame = self.titleRect
            return atts
        }
        return nil
}

Переопределить layoutAttributesForElements(in:); путь индекса здесь произвольный (я проигнорировал его в предыдущем коде):

override func layoutAttributesForElements(in rect: CGRect) 
    -> [UICollectionViewLayoutAttributes]? {
        var arr = super.layoutAttributesForElements(in: rect)!
        if let decatts = self.layoutAttributesForDecorationView(
            ofKind:self.titleKind, at: IndexPath(item: 0, section: 0)) {
                if rect.intersects(decatts.frame) {
                    arr.append(decatts)
                }
        }
        return arr
}

Это работает! Метка заголовка с надписью «Тестирование» появляется в верхней части представления коллекции.


Теперь я покажу, как сделать метку настраиваемой. Вместо заголовка «Тестирование» мы позволим клиенту установить свойство, определяющее заголовок. Я дам моему подклассу макета общедоступное свойство title:

class MyFlowLayout : UICollectionViewFlowLayout {
    var title = ""
    // ...
}

Тот, кто использует этот макет, должен установить это свойство. Например, предположим, что это представление коллекции отображает 50 штатов США:

func setUpFlowLayout(_ flow:UICollectionViewFlowLayout) {
    flow.headerReferenceSize = CGSize(50,50)
    flow.sectionInset = UIEdgeInsetsMake(0, 10, 10, 10)
    (flow as? MyFlowLayout)?.title = "States" // *
}

Теперь мы подходим к любопытной загадке. Наш макет имеет свойство title, значение которого необходимо каким-то образом передать нашему экземпляру MyTitleView. Но когда это может произойти? Мы не отвечаем за создание экземпляра MyTitleView; это происходит автоматически, когда представление коллекции запрашивает экземпляр за кулисами. Нет момента, когда экземпляр MyFlowLayout и экземпляр MyTitleView встречаются.

Решение заключается в использовании атрибутов макета в качестве мессенджера. MyFlowLayout никогда не встречается с MyTitleView, но создает объект атрибутов макета, который передается в представление коллекции для настройки MyFlowLayout. Таким образом, объект атрибутов макета подобен конверту. Создав подкласс UICollectionViewLayoutAttributes, мы можем включить в этот конверт любую информацию, которая нам нравится, например заголовок:

class MyTitleViewLayoutAttributes : UICollectionViewLayoutAttributes {
    var title = ""
}

Вот наш конверт! Теперь мы перепишем нашу реализацию layoutAttributesForDecorationView. Когда мы создаем экземпляр объекта атрибутов макета, мы создаем экземпляр нашего подкласса и устанавливаем его свойство title:

override func layoutAttributesForDecorationView(
    ofKind elementKind: String, at indexPath: IndexPath) -> 
    UICollectionViewLayoutAttributes? {
        if elementKind == self.titleKind {
            let atts = MyTitleViewLayoutAttributes( // *
                forDecorationViewOfKind:self.titleKind, with:indexPath)
            atts.title = self.title // *
            atts.frame = self.titleRect
            return atts
        }
        return nil
}

Наконец, в MyTitleView мы реализуем метод apply(_:). Это будет вызываться, когда представление коллекции настраивает представление украшения — с объектом атрибутов макета в качестве параметра! Поэтому мы вытаскиваем title и используем его как текст нашей метки:

class MyTitleView : UICollectionReusableView {
    weak var lab : UILabel!
    // ... the rest as before ...
    override func apply(_ atts: UICollectionViewLayoutAttributes) {
        if let atts = atts as? MyTitleViewLayoutAttributes {
            self.lab.text = atts.title
        }
    }
}

Легко увидеть, как вы можете расширить пример, чтобы сделать такие функции этикетки, как шрифт и высоту, настраиваемыми. Поскольку мы являемся подклассом UICollectionViewFlowLayout, могут также потребоваться некоторые дополнительные модификации, чтобы освободить место для декоративного представления, сдвинув другие элементы. Кроме того, технически мы должны переопределить isEqual(_:) в MyTitleView, чтобы различать разные заголовки. Все это остается в качестве упражнения для читателя.

person matt    schedule 30.08.2016
comment
@matt Большое спасибо за раздел об использовании атрибутов макета в качестве мессенджера для связи с представлением оформления! Это интересный API - вы не создаете напрямую экземпляр или не имеете ссылки на представление оформления. - person s.ka; 02.03.2017
comment
Означает ли это, что представление украшения нельзя добавить через раскадровку? - person hzxu; 29.05.2017
comment
Я делаю для SupplementaryViews действие, поскольку заголовок - это каждый раздел. Я использую UICollectionViewLayout. Все работает нормально. Но не удалось добавить заголовок. viewForSupplementaryElementOfKind этот метод находится в UICollectionViewcontroller. Этот метод не вызывается, когда мы переходим к CustomLayout. Когда мы собираемся использовать FlowLayout, заголовок раздела виден. Пожалуйста, помогите мне в этом. Я много искал. Но, не смог найти. - person McDonal_11; 09.02.2018
comment
Теперь обратимся к нашему подклассу UICollectionViewLayout, который я назову MyFlowLayout. Но класс MyFlowLayout : UICollectionViewFlowLayout . Немного запутался. UICollectionViewFlowLayout или UICollectionViewLayout?? - person McDonal_11; 09.02.2018
comment
@McDonal_11 Оба - person matt; 09.02.2018
comment
Спасибо! Я так разочарован документами Apple, которые часто слишком неясны или даже неполны, что это сводит меня с ума. - person surfrider; 05.07.2018
comment
Я нашел проблему. Вместо if rect.contains(decatts.frame) вы должны использовать if rect.intersects(decatts.frame). - person Marián Černý; 13.08.2018
comment
@MarianČerný Спасибо, посмотрю! - person matt; 13.08.2018
comment
@matt Во-первых, большое спасибо за пост. Я выполнил все шаги, которые вы описали. Я узнал, что ни layoutAttributesForDecorationView, ни layoutAttributesForItem никогда не звонят. Вызывается только layoutAttributesForElements. Что может быть причиной? Уже несколько часов бьюсь с этим, но бесполезно. - person Adeel; 12.09.2018
comment
Есть ошибка: переопределить func layoutAttributesForElements(..): никогда не используйте строку: 0 и раздел: 0 для вашего декоративного представления. В противном случае рано или поздно произойдет сбой, поскольку одна из ваших ячеек будет иметь тот же индексный путь. Я использую «строку», начиная с 1000, чтобы избежать этого. - person yoooriii; 24.03.2019
comment
@yooooriii Я не понимаю, почему наличие ячейки с одним и тем же путем индекса может привести к сбою. Конечно, у меня есть ячейка с индексным путем 0,0; это первая ячейка. Если бы я собирался разбиться из-за этого, разве я не разбился бы сразу? - person matt; 24.03.2019
comment
Вылетает не сразу, а вылетает время от времени, иногда при прокрутке. Дело в том, что представление коллекции не может иметь 2 разных элемента (представление ячейки и представление украшения) с одним и тем же путем индекса. Я обнаружил это буквально пару дней назад, до этого мое приложение работало хорошо, и я не знал о таком. Поскольку я установил свой индексный путь (item: 1000, section: 0), я не могу воспроизвести сбой. Вот и все. Просто опыт. - person yoooriii; 26.03.2019
comment
@yooooriii Очевидно, мне очень интересно! Я хочу сделать это правильно. Я благодарен за информацию. Я просто хотел бы сам увидеть такой сбой или журнал сбоя, чтобы понять проблему (и почему я с ней не сталкивался). - person matt; 26.03.2019
comment
Если вы создаете подкласс и реализуете какие-либо настраиваемые атрибуты макета, вы также должны переопределить унаследованный метод isEqual: для сравнения значений ваших свойств. В iOS 7 и более поздних версиях представление коллекции не применяет атрибуты макета, если эти атрибуты не изменились. Он определяет, изменились ли атрибуты, сравнивая старые и новые объекты атрибутов с помощью метода isEqual: - person Igor Vasilev; 25.01.2021
comment
@IgorVasilev Чрезвычайно полезный момент, спасибо. - person matt; 25.01.2021

Я получил эту работу с пользовательским макетом со следующим:

Создайте подкласс UICollectionReusableView и, например, добавьте к нему UIImageView:

@implementation AULYFloorPlanDecorationViewCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        UIImage *backgroundImage = [UIImage imageNamed:@"Layout.png"];
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame];
        imageView.image = backgroundImage;
        [self addSubview:imageView];
    }
    return self;
}

@end

Затем в вашем контроллере в viewDidLoad зарегистрируйте этот подкласс со следующим кодом (замените код на свой собственный макет)

AULYAutomationObjectLayout *automationLayout = (AULYAutomationObjectLayout *)self.collectionView.collectionViewLayout;
[automationLayout registerClass:[AULYFloorPlanDecorationViewCell class]  forDecorationViewOfKind:@"FloorPlan"];

Затем в своем пользовательском макете реализуйте следующие методы (или аналогичные):

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
    layoutAttributes.frame = CGRectMake(0.0, 0.0, self.collectionViewContentSize.width, self.collectionViewContentSize.height);
    layoutAttributes.zIndex = -1;
    return layoutAttributes;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *allAttributes = [[NSMutableArray alloc] initWithCapacity:4];

    [allAttributes addObject:[self layoutAttributesForDecorationViewOfKind:@"FloorPlan" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]];

    for (NSInteger i = 0; i < [self.collectionView numberOfItemsInSection:0]; i++)
    {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        [allAttributes addObject:layoutAttributes];
    }
    return allAttributes;
}

Кажется, для него нет документации, но следующий документ привел меня на правильный путь: Руководство по программированию представления коллекции для iOS

ОБНОВЛЕНИЕ: вероятно, лучше создать подкласс UICollectionReusableView для декоративного представления вместо UICollectionViewCell.

person dominikk    schedule 10.10.2012
comment
Спасибо, это поставило меня на правильный путь. Похоже, вы просто отображаете один вид украшения для раздела 0. В моем представлении коллекции есть несколько разделов. Мне все еще нужно выяснить, как отобразить один и тот же вид оформления для каждого раздела с разным количеством элементов. - person vtruong; 11.10.2012
comment
Да, я показываю только один вид оформления, но вы можете отобразить дополнительные виды оформления, добавив дополнительные строки в layoutAttributesForElementsInRect: [allAttributes addObject:[self layoutAttributesForDecorationViewOfKind:@"FloorPlan" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]]; (и вы можете добавить дополнительные для разделов 2, 3, 4 и т. д. Затем вам нужно обязательно установить атрибуты макета, чтобы границы были правильными для этого раздела. Чтобы узнать количество разделов и количество элементов, которые они содержат, вы должны иметь доступ к self.collectionView.dataSource - person dominikk; 11.10.2012
comment
Нужно ли создавать подкласс UICollectionViewLayout для использования декоративных представлений? Можно ли это сделать из FlowLayout? - person Drew C; 26.03.2013
comment
@DrewC вы можете создать подкласс UICollectionViewFlowLayout и переопределить методы, упомянутые выше. - person SimpsOff; 29.05.2013
comment
Почему этот ответ не принят? Оно полное и правильное. Единственное, что он не объясняет, — это то, как передать пользовательские атрибуты макета из макета в представление оформления, и это достаточно легко выяснить. - person matt; 29.08.2016

Вот как это сделать в MonoTouch:

public class DecorationView : UICollectionReusableView
{
    private static NSString classId = new NSString ("DecorationView");
    public static NSString ClassId { get { return classId; } }

    UIImageView blueMarble;

    [Export("initWithFrame:")]
    public DecorationView (RectangleF frame) : base(frame)
    {
        blueMarble = new UIImageView (UIImage.FromBundle ("bluemarble.png"));

        AddSubview (blueMarble);    
    }
}


public class SimpleCollectionViewController : UICollectionViewController
{
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();
        //Register the cell class (code for AnimalCell snipped)
        CollectionView.RegisterClassForCell (typeof(AnimalCell), AnimalCell.ClassId);
        //Register the supplementary view class (code for SideSupplement snipped)
        CollectionView.RegisterClassForSupplementaryView (typeof(SideSupplement), UICollectionElementKindSection.Header, SideSupplement.ClassId);

        //Register the decoration view
        CollectionView.CollectionViewLayout.RegisterClassForDecorationView (typeof(DecorationView), DecorationView.ClassId);
    }

 //...snip...
}

public class LineLayout : UICollectionViewFlowLayout
{
    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect (RectangleF rect)
    {
        var array = base.LayoutAttributesForElementsInRect (rect);
        /* 
        ...snip content relating to cell layout...
        */
        //Add decoration view
        var attributesWithDecoration = new List<UICollectionViewLayoutAttributes> (array.Length + 1);
        attributesWithDecoration.AddRange (array);
        var decorationIndexPath = NSIndexPath.FromIndex (0);
        var decorationAttributes = LayoutAttributesForDecorationView (DecorationView.ClassId, decorationIndexPath);
        attributesWithDecoration.Add (decorationAttributes);

        var extended = attributesWithDecoration.ToArray<UICollectionViewLayoutAttributes> ();

        return extended;
    }

    public override UICollectionViewLayoutAttributes LayoutAttributesForDecorationView (NSString kind, NSIndexPath indexPath)
    {
        var layoutAttributes = UICollectionViewLayoutAttributes.CreateForDecorationView (kind, indexPath);
        layoutAttributes.Frame = new RectangleF (0, 0, CollectionView.ContentSize.Width, CollectionView.ContentSize.Height);
        layoutAttributes.ZIndex = -1;
        return layoutAttributes;
    }
//...snip...
}

С конечным результатом, похожим на: введите здесь описание изображения

person Larry OBrien    schedule 03.01.2013

В моем случае: я хотел перейти с UITableView на UICollectionView.

разделы uitableview >>> дополнительные виды

uitableview headerView >>> декоративный вид

В моем случае я чувствовал макет подкласса и делал другие вещи, это «слишком много» для простого «headerView» (украшение)

Поэтому мое решение состояло в том, чтобы просто создать заголовок (не раздел) в качестве первой ячейки и раздел 1 в качестве первого раздела (раздел 0 имел нулевой размер)

person user1105951    schedule 19.06.2018