Ящики для самостоятельной калибровки и управления динамическим размером для iOS

Определение проблемы

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

  • Оберните его контентом (например, UILabel с номеромOfLines = 0)
  • автоматически расширяет размер ячейки размером с размеру
  • обрабатывать поворот устройства.
  • не требуют специального кода в UITableCellView или ViewControll для реализации этой функции (для UILabel не требуется никакого специального кода).

Исследование

Первое, что я сделал, очень просто. Я решил посмотреть, как работает UILabel. Я сделал следующее:

  • создана таблица с ячейками для самостоятельного выбора.
  • создал пользовательскую ячейку, поместил UILabel (с numberOfLines=0) в ней
  • созданы ограничения, чтобы убедиться, что UILabel занимает целую ячейку
  • подклассы UILabel и переопределить множество методов, чтобы увидеть, как они себя ведут.

Я проверил следующие вещи

  • Запустите его в портрете (метка отображается правильно на нескольких строках), и высота ячейки верна.
  • Поверните его. Ширина и высота таблицы были обновлены, и они также верны.

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

Частичное решение

@Wingzero написал частичное решение ниже. Он создает ячейки правильного размера.

Однако его решение имеет две проблемы:

  • Он использует "self.superview.bounds.size.width". Это можно использовать, если ваш элемент управления занимает целую ячейку. Однако, если у вас есть что-то еще в ячейке, которая использует ограничения, то такой код не будет правильно вычислять ширину.

  • Он не обрабатывает вращение вообще. Я уверен, что он не обрабатывает другие события изменения размера (есть куча менее распространенных событий изменения размера - например, строка состояния становится больше по телефону и т.д.).

Вы знаете, как решить эти проблемы для этого случая? Я нашел кучу статей, в которых говорится о создании более статических пользовательских элементов управления и использовании предварительно встроенных элементов управления в ячейках для самостоятельной калибровки.

Однако я не нашел ничего, что составило бы решение для обработки этих двух.

Ответ 1

Здесь вы найдете решение, соответствующее вашим требованиям, а также IBDesignable, чтобы он просматривал live в интерфейсе Builder. Этот класс выложит серию квадратов (общее число равно значению IBInspectable count). По умолчанию он просто выложит все в одну длинную линию. Но если вы установите для свойства wrap IBInspectable значение On, оно будет обертывать квадраты и увеличивать их высоту на основе его ограниченной ширины (например, UILabel с номером 0fLines == 0). В ячейке просмотра таблицы размеров для самостоятельной калибровки это приведет к выталкиванию сверху и снизу, чтобы обеспечить обернутый внутренний размер пользовательского представления.

Код:

import Foundation
import UIKit


@IBDesignable class WrappingView : UIView {

    private class InnerWrappingView : UIView {

        private var lastPoint:CGPoint = CGPointZero
        private var wrap = false
        private var count:Int = 100
        private var size:Int = 8
        private var spacing:Int = 3

        private func calculatedSize() -> CGSize {
            lastPoint = CGPoint(x:-(size + spacing), y: 0)
            for _ in 0..<count {
                var nextPoint:CGPoint!
                if wrap {
                    nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing))
                } else {
                    nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y)
                }
                lastPoint = nextPoint
            }
            return CGSize(width: wrap ? bounds.width : lastPoint.x + CGFloat(size), height: lastPoint.y + CGFloat(size))
        }

        override func layoutSubviews() {
            super.layoutSubviews()
            guard bounds.size != calculatedSize() || subviews.count == 0 else {
                return
            }
            for subview in subviews {
                subview.removeFromSuperview()
            }
            lastPoint = CGPoint(x:-(size + spacing), y: 0)
            for _ in 0..<count {
                let square = createSquareView()
                var nextPoint:CGPoint!
                if wrap {
                    nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing))
                } else {
                    nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y)
                }
                square.frame = CGRect(origin: nextPoint, size: square.bounds.size)
                addSubview(square)
                lastPoint = nextPoint
            }
            let newframe = CGRect(origin: frame.origin, size: calculatedSize())
            frame = newframe
            invalidateIntrinsicContentSize()
            setNeedsLayout()
        }


        private func createSquareView() -> UIView {
            let square = UIView(frame: CGRect(x: 0, y: 0, width: size, height: size))
            square.backgroundColor = UIColor.blueColor()
            return square
        }

        override func intrinsicContentSize() -> CGSize {
            return calculatedSize()
        }
    }

    @IBInspectable var count:Int = 500 {
        didSet {
            innerView.count = count
            layoutSubviews()
        }
    }

    @IBInspectable var size:Int = 8 {
        didSet {
            innerView.size = size
            layoutSubviews()
        }
    }

    @IBInspectable var spacing:Int = 3 {
        didSet {
            innerView.spacing = spacing
            layoutSubviews()
        }
    }

    @IBInspectable var wrap:Bool = false {
        didSet {
            innerView.wrap = wrap
            layoutSubviews()
        }
    }

    private var _innerView:InnerWrappingView! {
        didSet {
            clipsToBounds = true
            addSubview(_innerView)
            _innerView.clipsToBounds = true
            _innerView.frame = bounds
            _innerView.wrap = wrap
            _innerView.translatesAutoresizingMaskIntoConstraints = false
            _innerView.leftAnchor.constraintEqualToAnchor(leftAnchor).active = true
            _innerView.rightAnchor.constraintEqualToAnchor(rightAnchor).active = true
            _innerView.topAnchor.constraintEqualToAnchor(topAnchor).active = true
            _innerView.bottomAnchor.constraintEqualToAnchor(bottomAnchor).active = true
            _innerView.setContentCompressionResistancePriority(750, forAxis: .Vertical)
            _innerView.setContentHuggingPriority(251, forAxis: .Vertical)
        }
    }

    private var innerView:InnerWrappingView! {
        if _innerView == nil {
            _innerView = InnerWrappingView()
        }
        return _innerView
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        if innerView.bounds.width != bounds.width {
            innerView.frame = CGRect(origin: CGPointZero, size: CGSize(width: bounds.width, height: 0))
        }
        innerView.layoutSubviews()
        if innerView.bounds.height != bounds.height {
            invalidateIntrinsicContentSize()
            superview?.layoutIfNeeded()
        }
    }

    override func intrinsicContentSize() -> CGSize {
        return innerView.calculatedSize()
    }
}

В моем примере приложения я установил представление таблицы для деактивации ячейки, содержащей это настраиваемое представление для каждой строки, и установил свойство count для пользовательского представления в 20 * строку indexPath. Пользовательский вид ограничен 50% ширины ячейки, поэтому его ширина автоматически изменяется при перемещении между пейзажем и портретом. Поскольку каждая последующая ячейка таблицы обертывает более длинную и длинную строку квадратов, каждая ячейка автоматически имеет размер, который должен быть выше и выше.

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

пользовательский просмотр в ячейках ячеек для самостоятельной калибровки

Ответ 2

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

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

позже я обнаружил, что maxPreferredWidth не используется полностью, поэтому игнорируйте его:

@interface LabelView : UIView

IB_DESIGNABLE
@property (nonatomic, copy) IBInspectable NSString *text;
@property (nonatomic, assign) IBInspectable CGFloat maxPreferredWidth;

@end

.m file:

#import "LabelView.h"

@implementation LabelView

-(void)setText:(NSString *)text {
    if (![_text isEqualToString:text]) {
        _text = text;
        [self invalidateIntrinsicContentSize];
    }
}

-(CGSize)intrinsicContentSize {
    CGRect boundingRect = [self.text boundingRectWithSize:CGSizeMake(self.superview.bounds.size.width, CGFLOAT_MAX)
                                           options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
                                        attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]}
                                           context:nil];
    return boundingRect.size;
}

@end

и UITableViewCell с xib:

заголовочный файл:

@interface LabelCell : UITableViewCell

@property (weak, nonatomic) IBOutlet LabelView *labelView;

@end

.m file:

@implementation LabelCell

- (void)awakeFromNib {
    [super awakeFromNib];
}

@end

xib, это простой, только верхний, нижний, ведущий, конечные ограничения: введите описание изображения здесь

Итак, запустив его, основываясь на прямоугольном прямоугольнике, высота ячейки отличается, в моем случае у меня есть два цикла: 1. "haha", 2. "asdf" {повторяйте много раз, чтобы создать длинный строка}

поэтому нечетная ячейка равна 19 по высоте, а четная ячейка - 58 height: введите описание изображения здесь

Это то, что вы ищете?

Мои идеи:

ширина ячейки UITableView всегда совпадает с табличным представлением, поэтому ширина. У UICollectionView может быть больше проблем, но дело в том, что мы вычислим его и просто вернем, что этого достаточно.

Демо-проект: https://github.com/liuxuan30/StackOverflow-DynamicSize
(Я изменился на основе моего старого проекта, который имеет некоторые изображения, игнорирует их.)

Ответ 3

Чтобы опираться на другой ответ от @Wingzero, макет - сложная проблема...

Указанный maxPreferredWidth важен и относится к preferredMaxLayoutWidth of UILabel. Точкой этого атрибута является указание метки не только одной длинной строки и вместо этого предпочитают обертывать, если ширина достигает этого значения. Поэтому при вычислении внутреннего размера вы должны использовать минимум preferredMaxLayoutWidth (если установлен) или ширину представления как максимальную ширину.

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

UILabel не обрабатывает вращение - он об этом не знает. Ответственность диспетчера представлений заключается в обнаружении и обработке вращения, как правило, путем отмены макета и обновления размера представления перед запуском нового запуска макета. Этикетки (и другие виды) находятся только там, чтобы обрабатывать результирующий макет. В качестве части вращения вы (т.е. Контроллер вида) можете изменить preferredMaxLayoutWidth, поскольку имеет смысл разрешить большую ширину в ландшафтном макете, например.

Ответ 4

введите описание изображения здесь

Вы ищете что-то вроде этого ^^? Ячейка имеет динамические высоты для облегчения содержимого UILabel, и нет кода для вычисления размера/ширины/высоты вообще - только некоторые ограничения.

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

И если вам нужно, чтобы метка была многострочной, настройте ее. Установите numberOfLines на 2, 3 или 0, до вас.

Вам не нужно вычислять высоту ячейки таблицы, автомакет будет вычисляться для вас; но вы должны сообщить ему об этом, указав на использование автоматического измерения: self.tableView.rowHeight = UITableViewAutomaticDimension или вернуть его в tableView:heightForRowAtIndexPath:. И вы также можете рассказать таблицу "приблизительную" оценку в tableView:estimatedHeightForRowAtIndexPath: для лучшей производительности.

Все еще не работает? Установите приоритет сопротивления сжатию содержимого - по вертикали до 1000/требуется для рассматриваемого UILabel, чтобы содержимое ярлыка постаралось максимально "сопротивляться сжатию", и конфигурация numberOfLines будет полностью подтверждена.

И он вращается? Попытайтесь наблюдать за поворотом (есть уведомления об изменении ориентации), а затем обновите макет (setNeedsLayout).

Все еще не работает? Подробнее читайте здесь: Использование автоматической компоновки в UITableView для динамических раскладок ячеек и высоты строк в строке