Skip to content

Instantly share code, notes, and snippets.

@ts95
Last active April 17, 2020 08:03
Show Gist options
  • Save ts95/fd73beaedfb2a93171164423ab89e96a to your computer and use it in GitHub Desktop.
Save ts95/fd73beaedfb2a93171164423ab89e96a to your computer and use it in GitHub Desktop.
Calculates the size of a configured view given its constraints and intrinsic size.
import UIKit
/// Implemented by views that can be configured with a view model
///
/// The view model must conform to Hashable.
public protocol ConfigurableView: UIView {
associatedtype ViewModel: Hashable
func configure(with viewModel: ViewModel)
}
extension ConfigurableView {
func configure(with viewModel: ViewModel) {}
}
import UIKit
/// Implemented by view classes with a content view
public protocol ContentViewContainer {
var contentView: UIView { get }
}
extension UICollectionViewCell: ContentViewContainer {}
extension UITableViewCell: ContentViewContainer {}
extension UITableViewHeaderFooterView: ContentViewContainer {}
extension UIVisualEffectView: ContentViewContainer {}
import Foundation
public protocol NibLoadableView: class {
static var nibName: String { get }
static func fromNib() -> Self
}
extension NibLoadableView where Self: UIView {
public static var nibName: String {
return String(describing: self)
}
public static func fromNib() -> Self {
guard let nib = Bundle(for: self).loadNibNamed(nibName, owner: nil, options: nil) else {
fatalError("Failed loading the nib named \(nibName) for 'NibLoadableView' view of type '\(self)'.")
}
guard let view = (nib.first { $0 is Self }) as? Self else {
fatalError("Did not find 'NibLoadableView' view of type '\(self)' inside '\(nibName).xib'.")
}
return view
}
}
import UIKit
protocol ReusableView: class {
static var defaultReuseIdentifier: String { get }
}
extension ReusableView where Self: UIView {
static var defaultReuseIdentifier: String {
return String(describing: self)
}
}
extension UIView: ReusableView {}
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) {
register(T.self, forCellReuseIdentifier: T.defaultReuseIdentifier)
}
func register<T: UITableViewCell>(_: T.Type) where T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forCellReuseIdentifier: T.defaultReuseIdentifier)
}
func registerHeaderFooterView<T: UITableViewHeaderFooterView>(_: T.Type) {
register(T.self, forHeaderFooterViewReuseIdentifier: T.defaultReuseIdentifier)
}
func registerHeaderFooterView<T: UITableViewHeaderFooterView>(_: T.Type) where T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forHeaderFooterViewReuseIdentifier: T.defaultReuseIdentifier)
}
func dequeueReusableCell<T: UITableViewCell>(forIndexPath indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
}
return cell
}
func dequeueReusableCell<T: UITableViewCell>(_ type: T.Type, indexPath: IndexPath) -> T {
return dequeueReusableCell(forIndexPath: indexPath)
}
func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(_: T.Type) -> T {
guard let view = dequeueReusableHeaderFooterView(withIdentifier: T.defaultReuseIdentifier) as? T else {
fatalError("Could not dequeue header / footer view with identifier: \(T.defaultReuseIdentifier)")
}
return view
}
}
extension UICollectionView {
func register<T: UICollectionViewCell>(_: T.Type) {
register(T.self, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
}
func register<T: UICollectionViewCell>(_: T.Type) where T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
}
func register<T: UICollectionReusableView>(_: T.Type, forSupplementaryViewOfKind kind: String) where T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.defaultReuseIdentifier)
}
func dequeueReusableCell<T: UICollectionViewCell>(_ type: T.Type, indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withReuseIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
}
return cell
}
func dequeueReusableSupplementaryView<T: UICollectionReusableView>(ofKind kind: String, for indexPath: IndexPath) -> T {
guard let cell = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.defaultReuseIdentifier,
for: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
}
return cell
}
}
import UIKit
/// Struct containing the arguments for the UIView.systemLayoutSizeFitting() method
public struct LayoutSizeFit {
let targetSize: CGSize
let horizontalFittingPriority: UILayoutPriority
let verticalFittingPriority: UILayoutPriority
public static let `default` = LayoutSizeFit(
targetSize: UIView.layoutFittingCompressedSize,
horizontalFittingPriority: .fittingSizeLevel,
verticalFittingPriority: .fittingSizeLevel
)
public static func row(width: CGFloat) -> LayoutSizeFit {
LayoutSizeFit(
targetSize: CGSize(width: width, height: 0),
horizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
}
}
/// Calculates the size of a view given its constraints and intrinsic size
open class ViewSizeCalculator {
private var viewCache: [ReuseIdentifier : UIView]
private var sizeCache: [SizeCacheKey : CGSize]
typealias ReuseIdentifier = String
public init() {
self.viewCache = [:]
self.sizeCache = [:]
}
public func height<View: ConfigurableView & NibLoadableView, ViewModel>(
of viewType: View.Type,
viewModel: ViewModel,
width: CGFloat
) -> CGFloat
where View.ViewModel == ViewModel
{
return size(
of: viewType,
viewModel: viewModel,
layoutSizeFit: .row(width: width)
).height
}
public func height<View: ConfigurableView, ViewModel>(
of viewType: View.Type,
viewModel: ViewModel,
width: CGFloat
) -> CGFloat
where View.ViewModel == ViewModel
{
return size(
of: viewType,
viewModel: viewModel,
layoutSizeFit: .row(width: width)
).height
}
public func size<View: ConfigurableView & NibLoadableView, ViewModel>(
of viewType: View.Type,
viewModel: ViewModel,
layoutSizeFit: LayoutSizeFit = .default
) -> CGSize
where View.ViewModel == ViewModel
{
size(
initializer: { View.fromNib() },
viewModel: viewModel,
configure: { $0.configure(with: $1) },
layoutSizeFit: layoutSizeFit
)
}
public func size<View: ConfigurableView, ViewModel>(
of viewType: View.Type,
viewModel: ViewModel,
layoutSizeFit: LayoutSizeFit = .default
) -> CGSize
where View.ViewModel == ViewModel
{
size(
initializer: { View() },
viewModel: viewModel,
configure: { $0.configure(with: $1) },
layoutSizeFit: layoutSizeFit
)
}
public func size<View: UIView, ViewModel: Hashable>(
initializer: () -> View,
viewModel: ViewModel,
configure: (View, ViewModel) -> Void,
layoutSizeFit: LayoutSizeFit = .default
) -> CGSize
{
let view: View
if let cachedView = viewCache[View.defaultReuseIdentifier] as? View {
view = cachedView
} else {
view = initializer()
viewCache[View.defaultReuseIdentifier] = view
}
configure(view, viewModel)
let key = SizeCacheKey(
reuseIdentifier: View.defaultReuseIdentifier,
viewModel: viewModel
)
return size(for: view, withKey: key, layoutSizeFit: layoutSizeFit)
}
private func size(
for view: UIView,
withKey key: SizeCacheKey,
layoutSizeFit: LayoutSizeFit
) -> CGSize {
if let size = sizeCache[key] {
return size
}
let size: CGSize
switch view {
case let container as ContentViewContainer:
size = container.contentView.size(given: layoutSizeFit)
default:
size = view.size(given: layoutSizeFit)
}
sizeCache[key] = size
return size
}
private struct SizeCacheKey: Hashable {
let reuseIdentifier: ReuseIdentifier
let viewModel: AnyHashable
}
}
private extension UIView {
func size(given layoutSizeFit: LayoutSizeFit) -> CGSize {
systemLayoutSizeFitting(
layoutSizeFit.targetSize,
withHorizontalFittingPriority: layoutSizeFit.horizontalFittingPriority,
verticalFittingPriority: layoutSizeFit.verticalFittingPriority
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment