Как сделать UIScrollView масштабировать только в одном направлении при использовании автоматического макета

Я пытаюсь сделать UIScrollView только для масштабирования в горизонтальном направлении. Вид прокрутки настраивается с использованием чистого подхода к автоопределению. Обычный подход, как было предложено в ряде ответов (например, этот) и other. Я успешно использовал это в приложениях до того, как существует автоопределение. Этот подход выглядит следующим образом: в представлении содержимого просмотра прокрутки я переопределяю setTransform() и изменяю переданный в преобразовании по шкале в направлении x:

override var transform: CGAffineTransform {
    get { return super.transform }
    set {
        var t = newValue
        t.d = 1.0
        super.transform = t
    }
}

Я также reset смещение содержимого прокрутки, чтобы оно не прокручивалось по вертикали во время масштабирования:

func scrollViewDidZoom(scrollView: UIScrollView) {
    scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}

Это работает очень хорошо, если вы не используете автозапуск:

Масштабирование без автозапуска

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

Масштабирование с автоопределением

Я привел пример проекта в GitHub (используемый для создания видео в этом вопросе). Он содержит две раскадровки, Main.storyboard и NoAutolayout.storyboard, которые идентичны, за исключением того, что Main.storyboard включает автозапуск, а NoAutolayout - нет. Переключайтесь между ними, изменяя настройку проекта основного интерфейса, и вы можете видеть поведение в обоих случаях.

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

ИЗМЕНИТЬ: Я добавил третий образец раскадровки AutolayoutVariableColumns.storyboard к образцу проекта. Это добавляет текстовое поле для изменения количества столбцов в GridView, что изменяет его собственный размер содержимого. Это более подробно показывает поведение, которое мне нужно в реальном приложении, которое вызвало этот вопрос.

Ответ 1

Думаю, я понял это. Вам необходимо применить перевод в преобразовании для смещения центрирования, который UIScrollView пытается делать во время масштабирования.

Добавьте это в свой GridView:

var unzoomedViewHeight: CGFloat?
override func layoutSubviews() {
    super.layoutSubviews()
    unzoomedViewHeight = frame.size.height
}

override var transform: CGAffineTransform {
    get { return super.transform }
    set {
        if let unzoomedViewHeight = unzoomedViewHeight {
            var t = newValue
            t.d = 1.0
            t.ty = (1.0 - t.a) * unzoomedViewHeight/2
            super.transform = t
        }
    }
}

Чтобы вычислить преобразование, нам нужно знать неограниченную высоту представления. Здесь я просто хватаю размер кадра во время layoutSubviews() и предполагаю, что он содержит необработанную высоту. Вероятно, есть некоторые крайние случаи, когда это неверно; может быть лучшим местом в цикле обновления представления для вычисления высоты, но это основная идея.

Ответ 2

Попробуйте установить translatesAutoresizingMaskIntoConstraints

func scrollViewDidZoom(scrollView: UIScrollView) {
    gridView.translatesAutoresizingMaskIntoConstraints = true
}

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

Кроме того, если это не поможет, обновите образец проекта, чтобы мы могли воспроизвести проблему с переменной intrinsicContentSize()

Эта статья Ole Begemann мне очень помогла
Как я научился прекращать волнение и любовь Cocoa Автомакет

WWDC 2015 видео
Тайны автоматического макета, часть 1
Тайны автоматического макета, часть 2

И, к счастью, для этого есть флаг. Он называется translatesAutoResizingMask IntoConstraints [без пробела].

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

Ответ 3

Несмотря на то, что решение @Anna работает, UIScrollView обеспечивает способ работы с автоматической компоновкой. Но поскольку представления прокрутки работают несколько иначе, чем другие представления, ограничения интерпретируются по-разному:

  • Ограничения между ребрами/полями прокрутки и его содержанием прикрепляются к экрану прокрутки область содержимого.
  • Ограничения между высотой, шириной или центрами прикрепляются к просмотру прокрутки фрейм.
  • Ограничения между просмотром прокрутки и представлениями вне прокрутки работают как обычный вид.

Итак, когда вы добавляете subview в представление прокрутки, прикрепленное к его краям/полям, это подвью становится областью содержимого прокрутки или представлением содержимого.

Apple предлагает следующий подход в Работа со списками прокрутки:

  • Добавить представление прокрутки в сцену.
  • Ограничения рисования для определения размера и положения прокрутки, как обычно.
  • Добавить представление в список прокрутки. Установите метку вида Xcode для представления содержимого.
  • Соедините представления содержимого сверху, снизу, переднего и заднего краев с прокручивающимися представлениями соответствующих ребер. Теперь представление содержимого определяет область содержимого прокрутки.
  • (...)
  • (...)
  • Разложите содержимое прокрутки в пределах содержимого. Используйте ограничения для размещения содержимого внутри представления содержимого как обычно.

В вашем случае GridView должен находиться внутри представления содержимого:

Interface Builder

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

Ответ 4

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

Ваш ViewController.swift должен выглядеть следующим образом:

// Add an outlet for the scrollView in interface builder.
@IBOutlet weak var scrollView: UIScrollView!

// Remove the outlet and view for gridView.
//@IBOutlet var gridView: GridView!

// Create the gridView here:
var gridView: GridView!

// Setup some views
override func viewDidLoad() {
    super.viewDidLoad()
    // The width for the gridView is set to 1000 here, change if needed.
    gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
    scrollView.addSubview(gridView)
    scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
    gridView.contentMode = .ScaleAspectFill
}

func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
    return gridView
}

func scrollViewDidZoom(scrollView: UIScrollView) {
    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
    scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}