Archived
Push from command line
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
// Created by bryankeller on 10/16/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// Represents the layout information for a background in a section.
|
||||
struct BackgroundModel {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init() {
|
||||
id = NSUUID().uuidString
|
||||
originInSection = .zero
|
||||
size = .zero
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let id: String
|
||||
|
||||
var originInSection: CGPoint
|
||||
var size: CGSize
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Created by Roman Laitarenko on 1/31/19.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// Represents the layout information for a footer in a section.
|
||||
struct FooterModel {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(heightMode: MagazineLayoutFooterHeightMode, height: CGFloat, pinToVisibleBounds: Bool) {
|
||||
self.heightMode = heightMode
|
||||
self.pinToVisibleBounds = pinToVisibleBounds
|
||||
originInSection = .zero
|
||||
size = CGSize(width: 0, height: height)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
var heightMode: MagazineLayoutFooterHeightMode
|
||||
var pinToVisibleBounds: Bool
|
||||
var originInSection: CGPoint
|
||||
var size: CGSize
|
||||
var preferredHeight: CGFloat?
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Created by bryankeller on 10/16/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// Represents the layout information for a header in a section.
|
||||
struct HeaderModel {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(heightMode: MagazineLayoutHeaderHeightMode, height: CGFloat, pinToVisibleBounds: Bool) {
|
||||
self.heightMode = heightMode
|
||||
self.pinToVisibleBounds = pinToVisibleBounds
|
||||
originInSection = .zero
|
||||
size = CGSize(width: 0, height: height)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
var heightMode: MagazineLayoutHeaderHeightMode
|
||||
var pinToVisibleBounds: Bool
|
||||
var originInSection: CGPoint
|
||||
var size: CGSize
|
||||
var preferredHeight: CGFloat?
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Created by bryankeller on 7/9/17.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// Represents the layout information for an item in a section.
|
||||
struct ItemModel {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(sizeMode: MagazineLayoutItemSizeMode, height: CGFloat) {
|
||||
id = NSUUID().uuidString
|
||||
self.sizeMode = sizeMode
|
||||
originInSection = .zero
|
||||
size = CGSize(width: 0, height: height)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let id: String
|
||||
|
||||
var sizeMode: MagazineLayoutItemSizeMode
|
||||
var originInSection: CGPoint
|
||||
var size: CGSize
|
||||
var preferredHeight: CGFloat?
|
||||
|
||||
}
|
||||
+1085
File diff suppressed because it is too large
Load Diff
+640
@@ -0,0 +1,640 @@
|
||||
// Created by bryankeller on 7/9/17.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// Represents the layout information for a section.
|
||||
struct SectionModel {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(
|
||||
itemModels: [ItemModel],
|
||||
headerModel: HeaderModel?,
|
||||
footerModel: FooterModel?,
|
||||
backgroundModel: BackgroundModel?,
|
||||
metrics: MagazineLayoutSectionMetrics)
|
||||
{
|
||||
id = NSUUID().uuidString
|
||||
self.itemModels = itemModels
|
||||
self.headerModel = headerModel
|
||||
self.footerModel = footerModel
|
||||
self.backgroundModel = backgroundModel
|
||||
self.metrics = metrics
|
||||
calculatedHeight = 0
|
||||
numberOfRows = 0
|
||||
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: 0)
|
||||
calculateElementFramesIfNecessary()
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let id: String
|
||||
|
||||
private(set) var headerModel: HeaderModel?
|
||||
private(set) var footerModel: FooterModel?
|
||||
private(set) var backgroundModel: BackgroundModel?
|
||||
|
||||
var visibleBounds: CGRect?
|
||||
|
||||
var numberOfItems: Int {
|
||||
return itemModels.count
|
||||
}
|
||||
|
||||
func idForItemModel(atIndex index: Int) -> String {
|
||||
return itemModels[index].id
|
||||
}
|
||||
|
||||
func indexForItemModel(withID id: String) -> Int? {
|
||||
return itemModels.index { $0.id == id }
|
||||
}
|
||||
|
||||
func itemModel(atIndex index: Int) -> ItemModel {
|
||||
return itemModels[index]
|
||||
}
|
||||
|
||||
func preferredHeightForItemModel(atIndex index: Int) -> CGFloat? {
|
||||
return itemModels[index].preferredHeight
|
||||
}
|
||||
|
||||
mutating func calculateHeight() -> CGFloat {
|
||||
calculateElementFramesIfNecessary()
|
||||
|
||||
return calculatedHeight
|
||||
}
|
||||
|
||||
mutating func calculateFrameForItem(atIndex index: Int) -> CGRect {
|
||||
calculateElementFramesIfNecessary()
|
||||
|
||||
var origin = itemModels[index].originInSection
|
||||
if let rowIndex = rowIndicesForItemIndices[index] {
|
||||
origin.y += rowOffsetTracker?.offsetForRow(at: rowIndex) ?? 0
|
||||
} else {
|
||||
assertionFailure("Expected a row and a row height for item at \(index).")
|
||||
}
|
||||
|
||||
return CGRect(origin: origin, size: itemModels[index].size)
|
||||
}
|
||||
|
||||
mutating func calculateFrameForHeader(
|
||||
inSectionVisibleBounds sectionVisibleBounds: CGRect)
|
||||
-> CGRect?
|
||||
{
|
||||
guard headerModel != nil else { return nil }
|
||||
|
||||
calculateElementFramesIfNecessary()
|
||||
|
||||
// `headerModel` is a value type that might be mutated in `calculateElementFramesIfNecessary`,
|
||||
// so we can't use a copy made before that code executes (for example, in a
|
||||
// `guard let headerModel = headerModel else { ... }` at the top of this function).
|
||||
if let headerModel = headerModel {
|
||||
let originY: CGFloat
|
||||
if headerModel.pinToVisibleBounds {
|
||||
originY = max(
|
||||
min(
|
||||
sectionVisibleBounds.minY,
|
||||
calculateHeight() -
|
||||
metrics.sectionInsets.bottom -
|
||||
(footerModel?.size.height ?? 0) -
|
||||
headerModel.size.height),
|
||||
headerModel.originInSection.y)
|
||||
} else {
|
||||
originY = headerModel.originInSection.y
|
||||
}
|
||||
|
||||
return CGRect(
|
||||
origin: CGPoint(x: headerModel.originInSection.x, y: originY),
|
||||
size: headerModel.size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
mutating func calculateFrameForFooter(
|
||||
inSectionVisibleBounds sectionVisibleBounds: CGRect)
|
||||
-> CGRect?
|
||||
{
|
||||
guard footerModel != nil else { return nil }
|
||||
|
||||
calculateElementFramesIfNecessary()
|
||||
|
||||
var origin = footerModel?.originInSection
|
||||
if let rowIndex = indexOfFooterRow() {
|
||||
origin?.y += rowOffsetTracker?.offsetForRow(at: rowIndex) ?? 0
|
||||
} else {
|
||||
assertionFailure("Expected a row and a corresponding section footer.")
|
||||
}
|
||||
|
||||
// `footerModel` is a value type that might be mutated in `calculateElementFramesIfNecessary`,
|
||||
// so we can't use a copy made before that code executes (for example, in a
|
||||
// `guard let footerModel = footerModel else { ... }` at the top of this function).
|
||||
if let footerModel = footerModel, let origin = origin {
|
||||
let originY: CGFloat
|
||||
if footerModel.pinToVisibleBounds {
|
||||
originY = min(
|
||||
max(
|
||||
sectionVisibleBounds.maxY - footerModel.size.height,
|
||||
metrics.sectionInsets.top + (headerModel?.size.height ?? 0)),
|
||||
origin.y)
|
||||
} else {
|
||||
originY = origin.y
|
||||
}
|
||||
|
||||
return CGRect(
|
||||
origin: CGPoint(x: footerModel.originInSection.x, y: originY),
|
||||
size: footerModel.size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
mutating func calculateFrameForBackground() -> CGRect? {
|
||||
let calculatedHeight = calculateHeight()
|
||||
|
||||
backgroundModel?.originInSection = CGPoint(
|
||||
x: metrics.sectionInsets.left,
|
||||
y: metrics.sectionInsets.top)
|
||||
backgroundModel?.size.width = metrics.width
|
||||
backgroundModel?.size.height = calculatedHeight -
|
||||
metrics.sectionInsets.top -
|
||||
metrics.sectionInsets.bottom
|
||||
|
||||
if let backgroundModel = backgroundModel {
|
||||
return CGRect(
|
||||
origin: CGPoint(x: backgroundModel.originInSection.x, y: backgroundModel.originInSection.y),
|
||||
size: backgroundModel.size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func deleteItemModel(atIndex indexOfDeletion: Int) -> ItemModel {
|
||||
updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: indexOfDeletion)
|
||||
|
||||
return itemModels.remove(at: indexOfDeletion)
|
||||
}
|
||||
|
||||
mutating func insert(_ itemModel: ItemModel, atIndex indexOfInsertion: Int) {
|
||||
updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: indexOfInsertion)
|
||||
|
||||
itemModels.insert(itemModel, at: indexOfInsertion)
|
||||
}
|
||||
|
||||
mutating func updateMetrics(to metrics: MagazineLayoutSectionMetrics) {
|
||||
guard self.metrics != metrics else { return }
|
||||
|
||||
self.metrics = metrics
|
||||
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: 0)
|
||||
}
|
||||
|
||||
mutating func updateItemSizeMode(to sizeMode: MagazineLayoutItemSizeMode, atIndex index: Int) {
|
||||
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
|
||||
// and Swift retain / release calls.
|
||||
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
|
||||
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
|
||||
|
||||
directlyMutableItemModels[index].sizeMode = sizeMode
|
||||
|
||||
if case let .static(staticHeight) = sizeMode.heightMode {
|
||||
directlyMutableItemModels[index].size.height = staticHeight
|
||||
}
|
||||
|
||||
updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: index)
|
||||
}
|
||||
|
||||
mutating func setHeader(_ headerModel: HeaderModel) {
|
||||
let oldPreferredHeight = self.headerModel?.preferredHeight
|
||||
self.headerModel = headerModel
|
||||
|
||||
if case let .static(staticHeight) = headerModel.heightMode {
|
||||
self.headerModel?.size.height = staticHeight
|
||||
} else if case .dynamic = headerModel.heightMode {
|
||||
self.headerModel?.preferredHeight = oldPreferredHeight
|
||||
}
|
||||
|
||||
if let indexOfHeader = indexOfHeaderRow() {
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfHeader)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setFooter(_ footerModel: FooterModel) {
|
||||
let oldPreferredHeight = self.footerModel?.preferredHeight
|
||||
self.footerModel = footerModel
|
||||
|
||||
if case let .static(staticHeight) = footerModel.heightMode {
|
||||
self.footerModel?.size.height = staticHeight
|
||||
} else if case .dynamic = footerModel.heightMode {
|
||||
self.footerModel?.preferredHeight = oldPreferredHeight
|
||||
}
|
||||
|
||||
if let indexOfFooter = indexOfFooterRow() {
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfFooter)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func removeHeader() {
|
||||
if let indexOfHeader = indexOfHeaderRow() {
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfHeader)
|
||||
}
|
||||
|
||||
headerModel = nil
|
||||
}
|
||||
|
||||
mutating func removeFooter() {
|
||||
if let indexOfFooter = indexOfFooterRow() {
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfFooter)
|
||||
}
|
||||
|
||||
footerModel = nil
|
||||
}
|
||||
|
||||
mutating func updateItemHeight(toPreferredHeight preferredHeight: CGFloat, atIndex index: Int) {
|
||||
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
|
||||
// and Swift retain / release calls.
|
||||
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
|
||||
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
|
||||
|
||||
directlyMutableItemModels[index].preferredHeight = preferredHeight
|
||||
|
||||
if
|
||||
let rowIndex = rowIndicesForItemIndices[index],
|
||||
let rowHeight = itemRowHeightsForRowIndices[rowIndex]
|
||||
{
|
||||
let newRowHeight = updateHeightsForItemsInRow(at: rowIndex)
|
||||
let heightDelta = newRowHeight - rowHeight
|
||||
|
||||
calculatedHeight += heightDelta
|
||||
|
||||
let firstAffectedRowIndex = rowIndex + 1
|
||||
if firstAffectedRowIndex < numberOfRows {
|
||||
rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex)
|
||||
}
|
||||
} else {
|
||||
assertionFailure("Expected a row and a row height for item at \(index).")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mutating func updateHeaderHeight(toPreferredHeight preferredHeight: CGFloat) {
|
||||
headerModel?.preferredHeight = preferredHeight
|
||||
|
||||
if let indexOfHeaderRow = indexOfHeaderRow(), let headerModel = headerModel {
|
||||
let rowHeight = headerModel.size.height
|
||||
let newRowHeight = updateHeaderHeight(withMetricsFrom: headerModel)
|
||||
let heightDelta = newRowHeight - rowHeight
|
||||
|
||||
calculatedHeight += heightDelta
|
||||
|
||||
let firstAffectedRowIndex = indexOfHeaderRow + 1
|
||||
if firstAffectedRowIndex < numberOfRows {
|
||||
rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex)
|
||||
}
|
||||
} else {
|
||||
assertionFailure("Expected a row, a row height, and a corresponding section header.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mutating func updateFooterHeight(toPreferredHeight preferredHeight: CGFloat) {
|
||||
footerModel?.preferredHeight = preferredHeight
|
||||
|
||||
if let indexOfFooterRow = indexOfFooterRow(), let footerModel = footerModel {
|
||||
let rowHeight = footerModel.size.height
|
||||
let newRowHeight = updateFooterHeight(withMetricsFrom: footerModel)
|
||||
let heightDelta = newRowHeight - rowHeight
|
||||
|
||||
calculatedHeight += heightDelta
|
||||
|
||||
let firstAffectedRowIndex = indexOfFooterRow + 1
|
||||
if firstAffectedRowIndex < numberOfRows {
|
||||
rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex)
|
||||
}
|
||||
} else {
|
||||
assertionFailure("Expected a row, a row height, and a corresponding section footer.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setBackground(_ backgroundModel: BackgroundModel) {
|
||||
self.backgroundModel = backgroundModel
|
||||
// No need to invalidate since the background doesn't affect the layout.
|
||||
}
|
||||
|
||||
mutating func removeBackground() {
|
||||
backgroundModel = nil
|
||||
// No need to invalidate since the background doesn't affect the layout.
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var numberOfRows: Int
|
||||
private var itemModels: [ItemModel]
|
||||
private var metrics: MagazineLayoutSectionMetrics
|
||||
private var calculatedHeight: CGFloat
|
||||
|
||||
private var indexOfFirstInvalidatedRow: Int? {
|
||||
didSet {
|
||||
guard indexOfFirstInvalidatedRow != nil else { return }
|
||||
applyRowOffsetsIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
private var itemIndicesForRowIndices = [Int: [Int]]()
|
||||
private var rowIndicesForItemIndices = [Int: Int]()
|
||||
private var itemRowHeightsForRowIndices = [Int: CGFloat]()
|
||||
|
||||
private var rowOffsetTracker: RowOffsetTracker?
|
||||
|
||||
private func maxYForItemsRow(atIndex rowIndex: Int) -> CGFloat? {
|
||||
guard
|
||||
let itemIndices = itemIndicesForRowIndices[rowIndex],
|
||||
let itemY = itemIndices.first.flatMap({ itemModels[$0].originInSection.y }),
|
||||
let itemHeight = itemIndices.map({ itemModels[$0].size.height }).max() else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
|
||||
return itemY + itemHeight
|
||||
}
|
||||
|
||||
private func indexOfHeaderRow() -> Int? {
|
||||
guard headerModel != nil else { return nil }
|
||||
return 0
|
||||
}
|
||||
|
||||
private func indexOfFirstItemsRow() -> Int? {
|
||||
guard numberOfItems > 0 else { return nil }
|
||||
return headerModel == nil ? 0 : 1
|
||||
}
|
||||
|
||||
private func indexOfLastItemsRow() -> Int? {
|
||||
guard numberOfItems > 0 else { return nil }
|
||||
return rowIndicesForItemIndices[numberOfItems - 1]
|
||||
}
|
||||
|
||||
private func indexOfFooterRow() -> Int? {
|
||||
guard footerModel != nil else { return nil }
|
||||
return numberOfRows - 1
|
||||
}
|
||||
|
||||
private mutating func updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex changedIndex: Int) {
|
||||
guard
|
||||
let indexOfCurrentRow = rowIndicesForItemIndices[changedIndex],
|
||||
indexOfCurrentRow > 0 else
|
||||
{
|
||||
indexOfFirstInvalidatedRow = rowIndicesForItemIndices[0] ?? 0
|
||||
return
|
||||
}
|
||||
|
||||
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfCurrentRow - 1)
|
||||
}
|
||||
|
||||
private mutating func updateIndexOfFirstInvalidatedRowIfNecessary(
|
||||
toProposedIndex proposedIndex: Int)
|
||||
{
|
||||
indexOfFirstInvalidatedRow = min(proposedIndex, indexOfFirstInvalidatedRow ?? proposedIndex)
|
||||
}
|
||||
|
||||
private mutating func applyRowOffsetsIfNecessary() {
|
||||
guard let rowOffsetTracker = rowOffsetTracker else { return }
|
||||
|
||||
for rowIndex in 0..<numberOfRows {
|
||||
let rowOffset = rowOffsetTracker.offsetForRow(at: rowIndex)
|
||||
switch rowIndex {
|
||||
case indexOfHeaderRow(): headerModel?.originInSection.y += rowOffset
|
||||
case indexOfFooterRow(): footerModel?.originInSection.y += rowOffset
|
||||
default:
|
||||
for itemIndex in itemIndicesForRowIndices[rowIndex] ?? [] {
|
||||
itemModels[itemIndex].originInSection.y += rowOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.rowOffsetTracker = nil
|
||||
}
|
||||
|
||||
private mutating func calculateElementFramesIfNecessary() {
|
||||
guard var rowIndex = indexOfFirstInvalidatedRow else { return }
|
||||
guard rowIndex >= 0 else {
|
||||
assertionFailure("Invalid `rowIndex` / `indexOfFirstInvalidatedRow` (\(rowIndex)).")
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up item / row / height mappings starting at our `indexOfFirstInvalidatedRow`; we'll
|
||||
// make new mappings for those row indices as we do layout calculations below. Since all
|
||||
// item / row index mappings before `indexOfFirstInvalidatedRow` are still valid, we'll leave
|
||||
// those alone.
|
||||
for rowIndexKey in itemIndicesForRowIndices.keys {
|
||||
guard rowIndexKey >= rowIndex else { continue }
|
||||
|
||||
if let itemIndex = itemIndicesForRowIndices[rowIndexKey]?.first {
|
||||
rowIndicesForItemIndices[itemIndex] = nil
|
||||
}
|
||||
|
||||
itemIndicesForRowIndices[rowIndexKey] = nil
|
||||
itemRowHeightsForRowIndices[rowIndex] = nil
|
||||
}
|
||||
|
||||
// Header frame calculation
|
||||
if rowIndex == indexOfHeaderRow(), let existingHeaderModel = headerModel {
|
||||
rowIndex = 1
|
||||
|
||||
headerModel?.originInSection = CGPoint(
|
||||
x: metrics.sectionInsets.left,
|
||||
y: metrics.sectionInsets.top)
|
||||
headerModel?.size.width = metrics.width
|
||||
updateHeaderHeight(withMetricsFrom: existingHeaderModel)
|
||||
}
|
||||
|
||||
var currentY: CGFloat
|
||||
|
||||
// Item frame calculations
|
||||
|
||||
let startingItemIndex: Int
|
||||
if
|
||||
let indexOfLastItemInPreviousRow = itemIndicesForRowIndices[rowIndex - 1]?.last,
|
||||
indexOfLastItemInPreviousRow + 1 < numberOfItems,
|
||||
let maxYForPreviousRow = maxYForItemsRow(atIndex: rowIndex - 1)
|
||||
{
|
||||
// There's a previous row of items, so we'll use the max Y of that row as the starting place
|
||||
// for the current row of items.
|
||||
startingItemIndex = indexOfLastItemInPreviousRow + 1
|
||||
currentY = maxYForPreviousRow + metrics.verticalSpacing
|
||||
} else if (headerModel == nil && rowIndex == 0) || (headerModel != nil && rowIndex == 1) {
|
||||
// Our starting row doesn't exist yet, so we'll lay out our first row of items.
|
||||
startingItemIndex = 0
|
||||
currentY = (headerModel?.originInSection.y ?? metrics.sectionInsets.top) +
|
||||
(headerModel?.size.height ?? 0)
|
||||
} else {
|
||||
// Our starting row is after the last row of items, so we'll skip item layout.
|
||||
startingItemIndex = numberOfItems
|
||||
if
|
||||
let lastRowIndex = indexOfLastItemsRow(),
|
||||
rowIndex > lastRowIndex,
|
||||
let maxYOfLastRowOfItems = maxYForItemsRow(atIndex: lastRowIndex)
|
||||
{
|
||||
currentY = maxYOfLastRowOfItems
|
||||
} else {
|
||||
currentY = (headerModel?.originInSection.y ?? metrics.sectionInsets.top) +
|
||||
(headerModel?.size.height ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
|
||||
// and Swift retain / release calls.
|
||||
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
|
||||
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
|
||||
|
||||
var indexInCurrentRow = 0
|
||||
for itemIndex in startingItemIndex..<numberOfItems {
|
||||
// Create item / row index mappings
|
||||
itemIndicesForRowIndices[rowIndex] = itemIndicesForRowIndices[rowIndex] ?? []
|
||||
itemIndicesForRowIndices[rowIndex]?.append(itemIndex)
|
||||
rowIndicesForItemIndices[itemIndex] = rowIndex
|
||||
|
||||
let itemModel = itemModels[itemIndex]
|
||||
|
||||
if itemIndex == 0 {
|
||||
// Apply top item inset now that we're laying out items
|
||||
currentY += metrics.itemInsets.top
|
||||
}
|
||||
|
||||
let currentLeadingMargin: CGFloat
|
||||
let availableWidthForItems: CGFloat
|
||||
if itemModel.sizeMode.widthMode == .fullWidth(respectsHorizontalInsets: false) {
|
||||
currentLeadingMargin = metrics.sectionInsets.left
|
||||
availableWidthForItems = metrics.width
|
||||
} else {
|
||||
currentLeadingMargin = metrics.sectionInsets.left + metrics.itemInsets.left
|
||||
availableWidthForItems = metrics.width - metrics.itemInsets.left - metrics.itemInsets.right
|
||||
}
|
||||
|
||||
let totalSpacing = metrics.horizontalSpacing * (itemModel.sizeMode.widthMode.widthDivisor - 1)
|
||||
let itemWidth = round(
|
||||
(availableWidthForItems - totalSpacing) / itemModel.sizeMode.widthMode.widthDivisor)
|
||||
let itemX = CGFloat(indexInCurrentRow) *
|
||||
itemWidth + CGFloat(indexInCurrentRow) *
|
||||
metrics.horizontalSpacing + currentLeadingMargin
|
||||
let itemY = currentY
|
||||
|
||||
directlyMutableItemModels[itemIndex].originInSection = CGPoint(x: itemX, y: itemY)
|
||||
directlyMutableItemModels[itemIndex].size.width = itemWidth
|
||||
|
||||
if
|
||||
(indexInCurrentRow == Int(itemModel.sizeMode.widthMode.widthDivisor) - 1) ||
|
||||
(itemIndex == numberOfItems - 1) ||
|
||||
(itemIndex < numberOfItems - 1 && itemModels[itemIndex + 1].sizeMode.widthMode != itemModel.sizeMode.widthMode)
|
||||
{
|
||||
// We've reached the end of the current row, or there are no more items to lay out, or we're
|
||||
// about to lay out an item with a different width mode. In all cases, we're done laying out
|
||||
// the current row of items.
|
||||
let heightOfTallestItemInCurrentRow = updateHeightsForItemsInRow(at: rowIndex)
|
||||
currentY += heightOfTallestItemInCurrentRow
|
||||
indexInCurrentRow = 0
|
||||
|
||||
// If there are more items to layout, add vertical spacing and increment the row index
|
||||
if itemIndex < numberOfItems - 1 {
|
||||
currentY += metrics.verticalSpacing
|
||||
rowIndex += 1
|
||||
}
|
||||
} else {
|
||||
// We're still adding to the current row
|
||||
indexInCurrentRow += 1
|
||||
}
|
||||
}
|
||||
|
||||
if numberOfItems > 0 {
|
||||
// Apply bottom item inset now that we're done laying out items
|
||||
currentY += metrics.itemInsets.bottom
|
||||
}
|
||||
|
||||
// Footer frame calculations
|
||||
if let existingFooterModel = footerModel {
|
||||
rowIndex += 1
|
||||
|
||||
footerModel?.originInSection = CGPoint(x: metrics.sectionInsets.left, y: currentY)
|
||||
footerModel?.size.width = metrics.width
|
||||
updateFooterHeight(withMetricsFrom: existingFooterModel)
|
||||
}
|
||||
|
||||
numberOfRows = rowIndex + 1
|
||||
|
||||
// Final height calculation
|
||||
calculatedHeight = currentY + (footerModel?.size.height ?? 0) + metrics.sectionInsets.bottom
|
||||
|
||||
// The background frame is calculated just-in-time, since its value doesn't affect the layout.
|
||||
|
||||
// Create a row offset tracker now that we know how many rows we have
|
||||
rowOffsetTracker = RowOffsetTracker(numberOfRows: numberOfRows)
|
||||
|
||||
// Mark the layout as clean / no longer invalid
|
||||
indexOfFirstInvalidatedRow = nil
|
||||
}
|
||||
|
||||
private mutating func updateHeightsForItemsInRow(at rowIndex: Int) -> CGFloat {
|
||||
guard let indicesForItemsInRow = itemIndicesForRowIndices[rowIndex] else {
|
||||
assertionFailure("Expected item indices for row \(rowIndex).")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
|
||||
// and Swift retain / release calls.
|
||||
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
|
||||
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
|
||||
|
||||
var heightOfTallestItem = CGFloat(0)
|
||||
var stretchToTallestItemInRowItemIndices = Set<Int>()
|
||||
|
||||
for itemIndex in indicesForItemsInRow {
|
||||
let preferredHeight = itemModels[itemIndex].preferredHeight
|
||||
let height = itemModels[itemIndex].size.height
|
||||
directlyMutableItemModels[itemIndex].size.height = preferredHeight ?? height
|
||||
|
||||
// Handle stretch to tallest item in row height mode for current row
|
||||
|
||||
if itemModels[itemIndex].sizeMode.heightMode == .dynamicAndStretchToTallestItemInRow {
|
||||
stretchToTallestItemInRowItemIndices.insert(itemIndex)
|
||||
}
|
||||
|
||||
heightOfTallestItem = max(heightOfTallestItem, itemModels[itemIndex].size.height)
|
||||
}
|
||||
|
||||
for stretchToTallestItemInRowItemIndex in stretchToTallestItemInRowItemIndices{
|
||||
directlyMutableItemModels[stretchToTallestItemInRowItemIndex].size.height = heightOfTallestItem
|
||||
}
|
||||
|
||||
itemRowHeightsForRowIndices[rowIndex] = heightOfTallestItem
|
||||
return heightOfTallestItem
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private mutating func updateHeaderHeight(withMetricsFrom headerModel: HeaderModel) -> CGFloat {
|
||||
let height = headerModel.preferredHeight ?? headerModel.size.height
|
||||
self.headerModel?.size.height = height
|
||||
return height
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private mutating func updateFooterHeight(withMetricsFrom footerModel: FooterModel) -> CGFloat {
|
||||
let height = footerModel.preferredHeight ?? footerModel.size.height
|
||||
self.footerModel?.size.height = height
|
||||
return height
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Created by bryankeller on 2/25/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Represents a collection view update with more type-expressivity than
|
||||
/// `UICollectionViewUpdateItem`.
|
||||
enum CollectionViewUpdate<SectionModel, ItemModel> {
|
||||
|
||||
case sectionReload(sectionIndex: Int, newSection: SectionModel)
|
||||
case itemReload(itemIndexPath: IndexPath, newItem: ItemModel)
|
||||
|
||||
case sectionDelete(sectionIndex: Int)
|
||||
case itemDelete(itemIndexPath: IndexPath)
|
||||
|
||||
case sectionInsert(sectionIndex: Int, newSection: SectionModel)
|
||||
case itemInsert(itemIndexPath: IndexPath, newItem: ItemModel)
|
||||
|
||||
case sectionMove(initialSectionIndex: Int, finalSectionIndex: Int)
|
||||
case itemMove(initialItemIndexPath: IndexPath, finalItemIndexPath: IndexPath)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Created by bryankeller on 8/13/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents the location of an item in a section.
|
||||
///
|
||||
/// Initializing a `ElementLocation` is measurably faster than initializing an `IndexPath`.
|
||||
/// On an iPhone X, compiled with -Os optimizations, it's about 35x faster to initialize this struct
|
||||
/// compared to an `IndexPath`.
|
||||
struct ElementLocation: Hashable {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(elementIndex: Int, sectionIndex: Int) {
|
||||
self.elementIndex = elementIndex
|
||||
self.sectionIndex = sectionIndex
|
||||
}
|
||||
|
||||
init(indexPath: IndexPath) {
|
||||
elementIndex = indexPath.item
|
||||
sectionIndex = indexPath.section
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let elementIndex: Int
|
||||
let sectionIndex: Int
|
||||
|
||||
var indexPath: IndexPath {
|
||||
return IndexPath(item: elementIndex, section: sectionIndex)
|
||||
}
|
||||
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// Created by bryankeller on 8/17/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: - ElementLocationFramePairs
|
||||
|
||||
/// Stores pairs of `ElementLocationFramePair`s in an efficient way for appending to and
|
||||
/// iterating over.
|
||||
///
|
||||
/// The main reason this exists (and why its implementation uses a singly-linked-list) is so that,
|
||||
/// as we find the section and item index and frame for an element in a visible rect, we can append
|
||||
/// it to this data structure without copy-on-write performance issues, or array buffer resizing
|
||||
/// issues. (The cost of appending to an array is much more expensive due to copy-on-write and the
|
||||
/// backing buffer needing to be resized).
|
||||
struct ElementLocationFramePairs {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init() { }
|
||||
|
||||
init(elementLocationFramePair: ElementLocationFramePair) {
|
||||
append(elementLocationFramePair)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
mutating func append(_ elementLocationFramePair: ElementLocationFramePair) {
|
||||
if first == nil {
|
||||
first = elementLocationFramePair
|
||||
} else {
|
||||
last.next = elementLocationFramePair
|
||||
}
|
||||
|
||||
last = elementLocationFramePair
|
||||
}
|
||||
|
||||
// MARK: Fileprivate
|
||||
|
||||
fileprivate var first: ElementLocationFramePair?
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var last: ElementLocationFramePair!
|
||||
|
||||
}
|
||||
|
||||
// MARK: Sequence
|
||||
|
||||
extension ElementLocationFramePairs: Sequence {
|
||||
|
||||
func makeIterator() -> ElementLocationFramePairsIterator {
|
||||
return ElementLocationFramePairsIterator(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ElementLocationFramePairsIterator
|
||||
|
||||
/// Used for iterating through `ElementLocationFramePairs` instances
|
||||
struct ElementLocationFramePairsIterator: IteratorProtocol {
|
||||
|
||||
typealias Element = ElementLocationFramePair
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(_ elementLocationFramePairs: ElementLocationFramePairs) {
|
||||
self.elementLocationFramePairs = elementLocationFramePairs
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
mutating func next() -> ElementLocationFramePair? {
|
||||
if lastReturnedElement == nil {
|
||||
lastReturnedElement = elementLocationFramePairs.first
|
||||
} else {
|
||||
lastReturnedElement = lastReturnedElement?.next
|
||||
}
|
||||
|
||||
return lastReturnedElement
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let elementLocationFramePairs: ElementLocationFramePairs
|
||||
|
||||
private var lastReturnedElement: ElementLocationFramePair?
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ElementLocationFramePair
|
||||
|
||||
/// Encapsulates a `ElementLocation` and a `CGRect` frame for an element.
|
||||
final class ElementLocationFramePair {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(elementLocation: ElementLocation, frame: CGRect) {
|
||||
self.elementLocation = elementLocation
|
||||
self.frame = frame
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let elementLocation: ElementLocation
|
||||
let frame: CGRect
|
||||
|
||||
// MARK: Fileprivate
|
||||
|
||||
fileprivate var next: ElementLocationFramePair?
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension ElementLocationFramePair: Equatable {
|
||||
|
||||
static func == (lhs: ElementLocationFramePair, rhs: ElementLocationFramePair) -> Bool {
|
||||
return lhs.elementLocation == rhs.elementLocation && lhs.frame == rhs.frame
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+31
@@ -0,0 +1,31 @@
|
||||
// Created by bryankeller on 10/15/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
extension MagazineLayoutItemWidthMode {
|
||||
|
||||
/// Returns the divisor for a given item width mode.
|
||||
///
|
||||
/// When divided into the width of the collection view, the result equals the width of the item
|
||||
/// before taking into account horizontal insets.
|
||||
var widthDivisor: CGFloat {
|
||||
switch self {
|
||||
case .fullWidth: return 1
|
||||
case let .fractionalWidth(divisor): return CGFloat(divisor)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Created by bryankeller on 10/26/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Encapsulates all layout-affecting metrics relating to a section
|
||||
struct MagazineLayoutSectionMetrics: Equatable {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(
|
||||
forSectionAtIndex sectionIndex: Int,
|
||||
in collectionView: UICollectionView,
|
||||
layout: UICollectionViewLayout,
|
||||
delegate: UICollectionViewDelegateMagazineLayout)
|
||||
{
|
||||
collectionViewWidth = collectionView.bounds.width
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
collectionViewContentInset = collectionView.adjustedContentInset
|
||||
} else {
|
||||
collectionViewContentInset = collectionView.contentInset
|
||||
}
|
||||
|
||||
verticalSpacing = delegate.collectionView(
|
||||
collectionView,
|
||||
layout: layout,
|
||||
verticalSpacingForElementsInSectionAtIndex: sectionIndex)
|
||||
|
||||
horizontalSpacing = delegate.collectionView(
|
||||
collectionView,
|
||||
layout: layout,
|
||||
horizontalSpacingForItemsInSectionAtIndex: sectionIndex)
|
||||
|
||||
sectionInsets = delegate.collectionView(
|
||||
collectionView,
|
||||
layout: layout,
|
||||
insetsForSectionAtIndex: sectionIndex)
|
||||
|
||||
itemInsets = delegate.collectionView(
|
||||
collectionView,
|
||||
layout: layout,
|
||||
insetsForItemsInSectionAtIndex: sectionIndex)
|
||||
}
|
||||
|
||||
private init(
|
||||
collectionViewWidth: CGFloat,
|
||||
collectionViewContentInset: UIEdgeInsets,
|
||||
verticalSpacing: CGFloat,
|
||||
horizontalSpacing: CGFloat,
|
||||
sectionInsets: UIEdgeInsets,
|
||||
itemInsets: UIEdgeInsets)
|
||||
{
|
||||
self.collectionViewWidth = collectionViewWidth
|
||||
self.collectionViewContentInset = collectionViewContentInset
|
||||
self.verticalSpacing = verticalSpacing
|
||||
self.horizontalSpacing = horizontalSpacing
|
||||
self.sectionInsets = sectionInsets
|
||||
self.itemInsets = itemInsets
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
var width: CGFloat {
|
||||
return collectionViewWidth -
|
||||
collectionViewContentInset.left -
|
||||
collectionViewContentInset.right -
|
||||
sectionInsets.left -
|
||||
sectionInsets.right
|
||||
}
|
||||
|
||||
var verticalSpacing: CGFloat
|
||||
var horizontalSpacing: CGFloat
|
||||
var sectionInsets: UIEdgeInsets
|
||||
var itemInsets: UIEdgeInsets
|
||||
|
||||
static func defaultSectionMetrics(
|
||||
forCollectionViewWidth width: CGFloat)
|
||||
-> MagazineLayoutSectionMetrics
|
||||
{
|
||||
return MagazineLayoutSectionMetrics(
|
||||
collectionViewWidth: width,
|
||||
collectionViewContentInset: .zero,
|
||||
verticalSpacing: MagazineLayout.Default.VerticalSpacing,
|
||||
horizontalSpacing: MagazineLayout.Default.HorizontalSpacing,
|
||||
sectionInsets: MagazineLayout.Default.SectionInsets,
|
||||
itemInsets: MagazineLayout.Default.ItemInsets)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let collectionViewWidth: CGFloat
|
||||
private let collectionViewContentInset: UIEdgeInsets
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Created by bryankeller on 5/23/19.
|
||||
// Copyright © 2019 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
/// Tracks offsets for rows using a Segment Tree for O(log n) lookups and updates.
|
||||
struct RowOffsetTracker {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(numberOfRows: Int) {
|
||||
self.numberOfRows = numberOfRows
|
||||
|
||||
rowOffsets = Array(repeating: 0, count: 2 * numberOfRows)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
mutating func addOffset(_ offset: CGFloat, forRowsStartingAt rowIndex: Int) {
|
||||
var rowIndex = rowIndex + numberOfRows
|
||||
|
||||
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
|
||||
// and Swift retain / release calls.
|
||||
let rowOffsetsPointer = UnsafeMutableRawPointer(mutating: &rowOffsets)
|
||||
let directlyMutableRowOffsets = rowOffsetsPointer.assumingMemoryBound(to: CGFloat.self)
|
||||
directlyMutableRowOffsets[rowIndex] = rowOffsets[rowIndex] + offset
|
||||
|
||||
while rowIndex > 1 {
|
||||
rowIndex /= 2
|
||||
|
||||
let leftChild = rowOffsets[2 * rowIndex]
|
||||
let rightChild = rowOffsets[(2 * rowIndex) + 1]
|
||||
directlyMutableRowOffsets[rowIndex] = leftChild + rightChild
|
||||
}
|
||||
}
|
||||
|
||||
func offsetForRow(at rowIndex: Int) -> CGFloat {
|
||||
var lowerBound = numberOfRows
|
||||
var upperBound = rowIndex + numberOfRows + 1
|
||||
|
||||
var offset = CGFloat(0)
|
||||
|
||||
while lowerBound < upperBound {
|
||||
if lowerBound % 2 != 0 {
|
||||
offset += rowOffsets[lowerBound]
|
||||
lowerBound += 1
|
||||
}
|
||||
|
||||
if upperBound % 2 != 0 {
|
||||
upperBound -= 1
|
||||
offset += rowOffsets[upperBound]
|
||||
}
|
||||
|
||||
lowerBound /= 2
|
||||
upperBound /= 2
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let numberOfRows: Int
|
||||
|
||||
private var rowOffsets: [CGFloat]
|
||||
|
||||
}
|
||||
+1053
File diff suppressed because it is too large
Load Diff
Generated
+41
@@ -0,0 +1,41 @@
|
||||
// Created by bryankeller on 7/24/17.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Encapsulates the vertical fitting priority for an element laid out by `MagazineLayout`.
|
||||
///
|
||||
/// Used by `UICollectionViewCell` and `UICollectionReusableView` subclasses to determine which
|
||||
/// vertical fitting priority to pass into
|
||||
/// `systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority)` via
|
||||
/// `preferredLayoutAttributesFitting(_:)`.
|
||||
public final class MagazineLayoutCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
|
||||
|
||||
/// `MagazineLayout` supports self-sizing and static-sizing in the vertical direction. The value
|
||||
/// of this property will change the layout priority used for sizing.
|
||||
public var shouldVerticallySelfSize = true
|
||||
|
||||
override public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = super.copy(with: zone) as! MagazineLayoutCollectionViewLayoutAttributes
|
||||
copy.shouldVerticallySelfSize = shouldVerticallySelfSize
|
||||
return copy
|
||||
}
|
||||
|
||||
override public func isEqual(_ object: Any?) -> Bool {
|
||||
return super.isEqual(object) &&
|
||||
shouldVerticallySelfSize == (object as? MagazineLayoutCollectionViewLayoutAttributes)?.shouldVerticallySelfSize
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Created by bryankeller on 9/24/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// `MagazineLayout`'s invalidation context type.
|
||||
///
|
||||
/// Used to indicate that collection view properties and/or delegate layout metrics changed.
|
||||
public final class MagazineLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext {
|
||||
|
||||
/// Indicates whether to recompute the positions and sizes of elements based on the current
|
||||
/// collection view and delegate layout metrics.
|
||||
public var invalidateLayoutMetrics = true
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Created by bryankeller on 10/18/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension MagazineLayout {
|
||||
|
||||
/// Constants for layout sizing and spacing defaults.
|
||||
public enum Default {
|
||||
|
||||
public static let ItemSizeMode = MagazineLayoutItemSizeMode(
|
||||
widthMode: .fullWidth(respectsHorizontalInsets: true),
|
||||
heightMode: MagazineLayoutItemHeightMode.static(height: ItemHeight))
|
||||
public static let HeaderVisibilityMode = MagazineLayoutHeaderVisibilityMode.hidden
|
||||
public static let FooterVisibilityMode = MagazineLayoutFooterVisibilityMode.hidden
|
||||
public static let BackgroundVisibilityMode = MagazineLayoutBackgroundVisibilityMode.hidden
|
||||
|
||||
public static let ItemHeight: CGFloat = 150
|
||||
public static let HeaderHeight: CGFloat = 44
|
||||
public static let FooterHeight: CGFloat = 44
|
||||
public static let VerticalSpacing: CGFloat = 0
|
||||
public static let HorizontalSpacing: CGFloat = 0
|
||||
public static let SectionInsets: UIEdgeInsets = .zero
|
||||
public static let ItemInsets: UIEdgeInsets = .zero
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Generated
Executable
+27
@@ -0,0 +1,27 @@
|
||||
// Created by bryankeller on 10/18/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
extension MagazineLayout {
|
||||
|
||||
/// Constants for supported supplementary view element kinds.
|
||||
public enum SupplementaryViewKind {
|
||||
|
||||
public static let sectionHeader = "MagazineLayoutSupplementaryViewKindSectionHeader"
|
||||
public static let sectionFooter = "MagazineLayoutSupplementaryViewKindSectionFooter"
|
||||
public static let sectionBackground = "MagazineLayoutSupplementaryViewKindSectionBackground"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
// Created by bryankeller on 10/15/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Represents the visibility mode for a background.
|
||||
public enum MagazineLayoutBackgroundVisibilityMode {
|
||||
|
||||
/// This visiblity mode will cause the background to be displayed behind the items and headers in
|
||||
/// its respective section.
|
||||
case visible
|
||||
|
||||
/// This visibility mode will cause the background to not be visibile behind the items and headers
|
||||
/// in its respective section.
|
||||
case hidden
|
||||
|
||||
}
|
||||
Generated
Executable
+78
@@ -0,0 +1,78 @@
|
||||
// Created by Roman Laitarenko on 2/4/19.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: - MagazineLayoutFooterVisibilityMode
|
||||
|
||||
/// Represents the visibility mode for a footer.
|
||||
public enum MagazineLayoutFooterVisibilityMode {
|
||||
|
||||
/// This visibility mode will cause the footer to be displayed using the specified height mode in
|
||||
/// its respective section. If `pinToVisibleBounds` is true, the footer will pin to the visible
|
||||
/// bounds of the collection view while its containing section is visible.
|
||||
case visible(heightMode: MagazineLayoutFooterHeightMode, pinToVisibleBounds: Bool)
|
||||
|
||||
/// This visibility mode will cause the footer to not be visibile in its respective section.
|
||||
case hidden
|
||||
|
||||
/// This visibility mode will cause the footer to be displayed using the specified height mode in
|
||||
/// its respective section.
|
||||
public static func visible(
|
||||
heightMode: MagazineLayoutFooterHeightMode)
|
||||
-> MagazineLayoutFooterVisibilityMode
|
||||
{
|
||||
return .visible(heightMode: heightMode, pinToVisibleBounds: false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MagazineLayoutFooterHeightMode
|
||||
|
||||
/// Represents the vertical sizing mode for a footer.
|
||||
public enum MagazineLayoutFooterHeightMode {
|
||||
|
||||
/// This height mode will force the footer to be displayed with a height equal to `height`.
|
||||
///
|
||||
/// To properly support multiline labels, dynamic type, and other technologies that could affect
|
||||
/// the height of your footers dynamically, consider using the `dynamic` height mode.
|
||||
case `static`(height: CGFloat)
|
||||
|
||||
/// This height mode will cause the footer to self-size in the vertical direction.
|
||||
///
|
||||
/// In practice, self-sizing in the vertical direction means that the footer will get its height
|
||||
/// from the Auto Layout engine. Use this height mode for footers whose height is not known
|
||||
/// upfront. For example, if you support multiline labels or dynamic type, your height is likely
|
||||
/// not known until the Auto Layout engine resolves the layout at runtime.
|
||||
case dynamic
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension MagazineLayoutFooterHeightMode: Equatable {
|
||||
|
||||
public static func == (
|
||||
lhs: MagazineLayoutFooterHeightMode,
|
||||
rhs: MagazineLayoutFooterHeightMode)
|
||||
-> Bool
|
||||
{
|
||||
switch (lhs, rhs) {
|
||||
case (.static(let l), .static(let r)): return l == r
|
||||
case (.dynamic, .dynamic): return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Generated
Executable
+79
@@ -0,0 +1,79 @@
|
||||
// Created by bryankeller on 10/15/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: - MagazineLayoutHeaderVisibilityMode
|
||||
|
||||
/// Represents the visibility mode for a header.
|
||||
public enum MagazineLayoutHeaderVisibilityMode {
|
||||
|
||||
/// This visibility mode will cause the header to be displayed using the specified height mode in
|
||||
/// its respective section. If `pinToVisibleBounds` is true, the header will pin to the visible
|
||||
/// bounds of the collection view while its containing section is visible.
|
||||
case visible(heightMode: MagazineLayoutHeaderHeightMode, pinToVisibleBounds: Bool)
|
||||
|
||||
/// This visibility mode will cause the header to not be visibile in its respective section.
|
||||
case hidden
|
||||
|
||||
/// This visibility mode will cause the header to be displayed using the specified height mode in
|
||||
/// its respective section.
|
||||
public static func visible(
|
||||
heightMode: MagazineLayoutHeaderHeightMode)
|
||||
-> MagazineLayoutHeaderVisibilityMode
|
||||
{
|
||||
return .visible(heightMode: heightMode, pinToVisibleBounds: false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MagazineLayoutHeaderHeightMode
|
||||
|
||||
/// Represents the vertical sizing mode for a header.
|
||||
public enum MagazineLayoutHeaderHeightMode {
|
||||
|
||||
/// This height mode will force the header to be displayed with a height equal to `height`.
|
||||
///
|
||||
/// To properly support multiline labels, dynamic type, and other technologies that could affect
|
||||
/// the height of your headers dynamically, consider using the `dynamic` height mode.
|
||||
case `static`(height: CGFloat)
|
||||
|
||||
/// This height mode will cause the header to self-size in the vertical direction.
|
||||
///
|
||||
/// In practice, self-sizing in the vertical direction means that the header will get its height
|
||||
/// from the Auto Layout engine. Use this height mode for headers whose height is not known
|
||||
/// upfront. For example, if you support multiline labels or dynamic type, your height is likely
|
||||
/// not known until the Auto Layout engine resolves the layout at runtime.
|
||||
case dynamic
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension MagazineLayoutHeaderHeightMode: Equatable {
|
||||
|
||||
public static func == (
|
||||
lhs: MagazineLayoutHeaderHeightMode,
|
||||
rhs: MagazineLayoutHeaderHeightMode)
|
||||
-> Bool
|
||||
{
|
||||
switch (lhs, rhs) {
|
||||
case (.static(let l), .static(let r)): return l == r
|
||||
case (.dynamic, .dynamic): return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Created by bryankeller on 10/15/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: - MagazineLayoutItemSizeMode
|
||||
|
||||
/// Represents the horizontal and vertical sizing mode for an item.
|
||||
public struct MagazineLayoutItemSizeMode {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
public init(widthMode: MagazineLayoutItemWidthMode, heightMode: MagazineLayoutItemHeightMode) {
|
||||
self.widthMode = widthMode
|
||||
self.heightMode = heightMode
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
/// The width mode for the item.
|
||||
public let widthMode: MagazineLayoutItemWidthMode
|
||||
|
||||
/// The height mode for the item.
|
||||
public let heightMode: MagazineLayoutItemHeightMode
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MagazineLayoutItemWidthMode
|
||||
|
||||
/// Represents the horizontal sizing mode for an item.
|
||||
///
|
||||
/// Consecutive items with the same width mode will display on the same row until there is no more
|
||||
/// room.
|
||||
public enum MagazineLayoutItemWidthMode {
|
||||
|
||||
/// Full width items will fill the available width in a section.
|
||||
///
|
||||
/// Use this width mode to create lists of items.
|
||||
/// `respectsHorizontalInsets` specifies whether the item should be edge-to-edge in a section, or
|
||||
/// if it should be inset by the item insets specified for a section. `respectsHorizontalInsets`
|
||||
/// does not take into account section insets or the collection view's content inset.
|
||||
case fullWidth(respectsHorizontalInsets: Bool)
|
||||
|
||||
/// Fractional width items will take up `1/divisor` of the available width for a given row of
|
||||
/// items.
|
||||
///
|
||||
/// Use this width mode to create grids of items. Consider using `halfWidth`, `thirdWidth`,
|
||||
/// `fourthWidth`, or `fifthWidth`, which are equivalent to using `fractionalWidth` with a
|
||||
/// `divisor` of `2`, `3`, `4`, or `5`, respectively.
|
||||
///
|
||||
/// Fractional width items respect `contentInset.left` and `contentInset.right`, and are affected
|
||||
/// by the horizontal spacing specified for the section in which they're contained. On iOS 11 and
|
||||
/// higher, they will also take the safe area insets into account if the collection view's
|
||||
/// `contentInsetAdjustmentBehavior` property is set to a value that respects the safe area.
|
||||
///
|
||||
/// - Warning: `divisor` must be greater than `0`. Specifying `0` as the `divisor` is a programmer
|
||||
/// error and **will result in a runtime crash**.
|
||||
case fractionalWidth(divisor: UInt)
|
||||
|
||||
/// Half width items will take up `1/2` of the available width for a given row of items.
|
||||
public static var halfWidth: MagazineLayoutItemWidthMode {
|
||||
return .fractionalWidth(divisor: 2)
|
||||
}
|
||||
|
||||
/// Third width items will take up `1/3` of the available width for a given row of items.
|
||||
public static var thirdWidth: MagazineLayoutItemWidthMode {
|
||||
return .fractionalWidth(divisor: 3)
|
||||
}
|
||||
|
||||
/// Fourth width items will take up `1/4` of the available width for a given row of items.
|
||||
public static var fourthWidth: MagazineLayoutItemWidthMode {
|
||||
return .fractionalWidth(divisor: 4)
|
||||
}
|
||||
|
||||
/// Fifth width items will take up `1/5` of the available width for a given row of items.
|
||||
public static var fifthWidth: MagazineLayoutItemWidthMode {
|
||||
return .fractionalWidth(divisor: 5)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension MagazineLayoutItemWidthMode: Equatable {
|
||||
|
||||
public static func == (
|
||||
lhs: MagazineLayoutItemWidthMode,
|
||||
rhs: MagazineLayoutItemWidthMode)
|
||||
-> Bool
|
||||
{
|
||||
switch (lhs, rhs) {
|
||||
case (.fullWidth(let l), .fullWidth(let r)): return l == r
|
||||
case (.fractionalWidth(let l), .fractionalWidth(let r)): return l == r
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MagazineLayoutItemHeightMode
|
||||
|
||||
/// Represents the vertical sizing mode for an item.
|
||||
///
|
||||
/// `MagazineLayout` supports vertically self-sizing and statically sized items. Since height modes
|
||||
/// are specified for each item, you can mix vertically self-sizing and statically sized items in
|
||||
/// the same sections, and even in the same rows.
|
||||
public enum MagazineLayoutItemHeightMode {
|
||||
|
||||
/// This height mode mode will cause the item to be displayed with a height equal to `height`.
|
||||
///
|
||||
/// To properly support multiline labels, dynamic type, and other technologies that could affect
|
||||
/// the height of your items dynamically, consider using one of the dynamic height modes.
|
||||
case `static`(height: CGFloat)
|
||||
|
||||
/// This height mode will cause the item to self-size in the vertical direction.
|
||||
///
|
||||
/// In practice, self-sizing in the vertical direction means that the item will get its height
|
||||
/// from the Auto Layout engine. Use this height mode for items whose height is not known upfront.
|
||||
/// For example, if you support multiline labels or dynamic type, your height is likely not known
|
||||
/// until the Auto Layout engine resolves the layout at runtime.
|
||||
case dynamic
|
||||
|
||||
/// This height mode will cause the item to self-size in the vertical direction, then resize to
|
||||
/// match the height of the tallest item in the same row of items.
|
||||
///
|
||||
/// If the item _is_ the tallest item in the row (after being self-sized), then it will stay
|
||||
/// at its self-sized height until it's no longer the tallest item in the row.
|
||||
///
|
||||
/// Note that items with this height mode will resize to match the height of the tallest item in
|
||||
/// the same row of items, even if the tallest item has a `static` height mode.
|
||||
case dynamicAndStretchToTallestItemInRow
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension MagazineLayoutItemHeightMode: Equatable {
|
||||
|
||||
public static func == (
|
||||
lhs: MagazineLayoutItemHeightMode,
|
||||
rhs: MagazineLayoutItemHeightMode)
|
||||
-> Bool
|
||||
{
|
||||
switch (lhs, rhs) {
|
||||
case (.static(let l), .static(let r)): return l == r
|
||||
case (.dynamic, .dynamic): return true
|
||||
case (.dynamicAndStretchToTallestItemInRow, .dynamicAndStretchToTallestItemInRow): return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Generated
Executable
+138
@@ -0,0 +1,138 @@
|
||||
// Created by bryankeller on 7/17/17.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate {
|
||||
|
||||
/// Asks the delegate for the size mode of the specified item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - indexPath: The index path of the item.
|
||||
///
|
||||
/// - Returns: The size mode of the specified item.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
sizeModeForItemAt indexPath: IndexPath)
|
||||
-> MagazineLayoutItemSizeMode
|
||||
|
||||
/// Asks the delegate for the visibility mode of the header in the specified section.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section containing the header.
|
||||
///
|
||||
/// - Returns: The visibility mode of the header in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
visibilityModeForHeaderInSectionAtIndex index: Int)
|
||||
-> MagazineLayoutHeaderVisibilityMode
|
||||
|
||||
/// Asks the delegate for the visibility mode of the footer in the specified section.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section containing the footer.
|
||||
///
|
||||
/// - Returns: The visibility mode of the footer in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
visibilityModeForFooterInSectionAtIndex index: Int)
|
||||
-> MagazineLayoutFooterVisibilityMode
|
||||
|
||||
/// Asks the delegate for the visibility mode of the background in the specified section.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section containing the background.
|
||||
///
|
||||
/// - Returns: The visibility mode of the background in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
visibilityModeForBackgroundInSectionAtIndex index: Int)
|
||||
-> MagazineLayoutBackgroundVisibilityMode
|
||||
|
||||
/// Asks the delegate for the horizontal spacing for items in the specified section.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section whose horizontal item spacing is needed.
|
||||
///
|
||||
/// - Returns: The horizontal spacing for items in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
horizontalSpacingForItemsInSectionAtIndex index: Int)
|
||||
-> CGFloat
|
||||
|
||||
/// Asks the delegate for the vertical spacing for items in the specified section.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section whose vertical item spacing is needed.
|
||||
///
|
||||
/// - Returns: The vertical spacing for items in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
verticalSpacingForElementsInSectionAtIndex index: Int)
|
||||
-> CGFloat
|
||||
|
||||
/// Asks the delegate for the amount by which to inset elements in the specified section.
|
||||
///
|
||||
/// Section insets are relative to the content's bounds, which is impacted by the collection
|
||||
/// view's content inset.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section whose element insets are needed.
|
||||
///
|
||||
/// - Returns: The amount by which to inset elements in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
insetsForSectionAtIndex index: Int)
|
||||
-> UIEdgeInsets
|
||||
|
||||
/// Asks the delegate for the amount by which to inset items in the specified section.
|
||||
///
|
||||
/// Item insets are relative to the section's bounds, which is impacted by the section's insets
|
||||
/// and, transitively, the collection view's content inset.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - collectionView: The collection view using the layout.
|
||||
/// - collectionViewLayout: The layout requesting the information.
|
||||
/// - index: The index of the section whose item insets are needed.
|
||||
///
|
||||
/// - Returns: The amount by which to inset items in the specified section.
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
insetsForItemsInSectionAtIndex index: Int)
|
||||
-> UIEdgeInsets
|
||||
|
||||
}
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
// Created by bryankeller on 11/29/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A collection reusable view that coordinates with `MagazineLayoutCollectionViewLayoutAttributes`
|
||||
/// to determine how to size itself: with self-sizing, or without self-sizing. Use this class
|
||||
/// (or subclasses) for displaying header and background supplementary views with `MagazineLayout`.
|
||||
///
|
||||
/// Note that this class is very similar to `MagazineLayoutCollectionViewCell`.
|
||||
///
|
||||
/// `UIKit` invokes `preferredLayoutAttributesFitting(_:)` with an initial set of layout attributes,
|
||||
/// giving this reusable view subclass a chance to modify the `size` property of the attributes
|
||||
/// based on whether or not we want to self-size.
|
||||
///
|
||||
/// Subclassing and/or adding additional protocol conformances is encouraged, although modifying
|
||||
/// the behavior of `preferredLayoutAttributesFitting(_:)` is not recommended.
|
||||
///
|
||||
/// This class exists because `MagazineLayout` supports self-sizing supplementary views in just the
|
||||
/// vertical dimension - a use case that `UICollectionReusableView` does not support out-of-the-box.
|
||||
open class MagazineLayoutCollectionReusableView: UICollectionReusableView {
|
||||
|
||||
override open func preferredLayoutAttributesFitting(
|
||||
_ layoutAttributes: UICollectionViewLayoutAttributes)
|
||||
-> UICollectionViewLayoutAttributes
|
||||
{
|
||||
guard let attributes = layoutAttributes as? MagazineLayoutCollectionViewLayoutAttributes else {
|
||||
assertionFailure("`layoutAttributes` must be an instance of `MagazineLayoutCollectionViewLayoutAttributes`")
|
||||
return super.preferredLayoutAttributesFitting(layoutAttributes)
|
||||
}
|
||||
|
||||
let size: CGSize
|
||||
if attributes.shouldVerticallySelfSize {
|
||||
// Self-sizing is required in the vertical dimension.
|
||||
size = super.systemLayoutSizeFitting(
|
||||
layoutAttributes.size,
|
||||
withHorizontalFittingPriority: .required,
|
||||
verticalFittingPriority: .fittingSizeLevel)
|
||||
} else {
|
||||
// No self-sizing is required; respect whatever size the layout determined.
|
||||
size = layoutAttributes.size
|
||||
}
|
||||
|
||||
layoutAttributes.size = size
|
||||
|
||||
return layoutAttributes
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Created by bryankeller on 11/29/18.
|
||||
// Copyright © 2018 Airbnb, Inc.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A cell that coordinates with `MagazineLayoutCollectionViewLayoutAttributes` to determine how to
|
||||
/// size itself: with self-sizing, or without self-sizing. Use this class (or subclasses) for
|
||||
/// displaying cells with `MagazineLayout`.
|
||||
///
|
||||
/// Note that this class is very similar to `MagazineLayoutCollectionReusableView`.
|
||||
///
|
||||
/// `UIKit` invokes `preferredLayoutAttributesFitting(_:)` with an initial set of layout attributes,
|
||||
/// giving this cell subclass a chance to modify the `size` property of the attributes based on
|
||||
/// whether or not we want to self-size.
|
||||
///
|
||||
/// Subclassing and/or adding additional protocol conformances is encouraged, although modifying
|
||||
/// the behavior of `preferredLayoutAttributesFitting(_:)` is not recommended.
|
||||
///
|
||||
/// This class exists because `MagazineLayout` supports self-sizing in just the vertical dimension -
|
||||
/// a use case that `UICollectionViewCell` does not support out-of-the-box.
|
||||
///
|
||||
/// As of iOS 12, `UICollectionReusableView` is tightly coupled with `UICollectionViewFlowLayout`'s
|
||||
/// private `_estimatesSizes` property, which is used to determine how to size cells displayed in a
|
||||
/// `UICollectionViewFlowLayout`. If `_estimatesSizes` is `true`, then `UICollectionReusableView`
|
||||
/// will self-size in both the horizontal and vertical dimensions. If it is `false`, no self-sizing
|
||||
/// will occur. In short, `UICollectionReusableView` is only optimized to work correctly with
|
||||
/// Apple's own layout.
|
||||
open class MagazineLayoutCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
override open func preferredLayoutAttributesFitting(
|
||||
_ layoutAttributes: UICollectionViewLayoutAttributes)
|
||||
-> UICollectionViewLayoutAttributes
|
||||
{
|
||||
guard let attributes = layoutAttributes as? MagazineLayoutCollectionViewLayoutAttributes else {
|
||||
assertionFailure("`layoutAttributes` must be an instance of `MagazineLayoutCollectionViewLayoutAttributes`")
|
||||
return super.preferredLayoutAttributesFitting(layoutAttributes)
|
||||
}
|
||||
|
||||
// In some cases, `contentView`'s required width and height constraints
|
||||
// (created from its auto-resizing mask) will not have the correct constants before invoking
|
||||
// `systemLayoutSizeFitting(...)`, causing the cell to size incorrectly. This seems to be a
|
||||
// UIKit bug.
|
||||
// https://openradar.appspot.com/radar?id=5025850143539200
|
||||
// The issue seems most common when the collection view's bounds change (on rotation).
|
||||
// We correct for this by updating `contentView.bounds`, which updates the constants used by the
|
||||
// width and height constraints created by the `contentView`'s auto-resizing mask.
|
||||
|
||||
if contentView.bounds.width != layoutAttributes.size.width {
|
||||
contentView.bounds.size.width = layoutAttributes.size.width
|
||||
}
|
||||
|
||||
if
|
||||
!attributes.shouldVerticallySelfSize &&
|
||||
contentView.bounds.height != layoutAttributes.size.height
|
||||
{
|
||||
contentView.bounds.size.height = layoutAttributes.size.height
|
||||
}
|
||||
|
||||
let size: CGSize
|
||||
if attributes.shouldVerticallySelfSize {
|
||||
// Self-sizing is required in the vertical dimension.
|
||||
size = super.systemLayoutSizeFitting(
|
||||
layoutAttributes.size,
|
||||
withHorizontalFittingPriority: .required,
|
||||
verticalFittingPriority: .fittingSizeLevel)
|
||||
} else {
|
||||
// No self-sizing is required; respect whatever size the layout determined.
|
||||
size = layoutAttributes.size
|
||||
}
|
||||
|
||||
layoutAttributes.size = size
|
||||
|
||||
return layoutAttributes
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user