Last active
April 17, 2020 08:03
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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