-
-
Save sahara-ooga/0c10d20a1c52b038c9aa3f021a0c1f15 to your computer and use it in GitHub Desktop.
UICollectionViewController including type erased delegate
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 | |
extension CGSize { | |
var inversed: CGSize { | |
return CGSize(width: self.height, height: self.width) | |
} | |
} |
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
extension Device { | |
public enum Kind { | |
public enum IPhone { | |
case four, fourS, iPodTouch4thGen | |
case five, fiveS, fiveC, SE, iPodTouch5thGen, iPodTouch6thGen | |
case six, sixS, seven, eight | |
case sixPlus, sixSPlus, sevenPlus, eightPlus | |
case x, xs | |
case xr | |
case xsmax | |
} | |
public enum IPad { | |
case mini | |
case iPad | |
case proTenInch | |
case proTwelveInch | |
} | |
} | |
} |
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
extension Device { | |
public enum Orientation { | |
case horizontal | |
case portrait | |
} | |
} |
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
/* | |
https://github.com/Ekhoo/Device/blob/master/Source/Size.swift | |
*/ | |
extension Device { | |
public enum ScreenSizeKind: Int, Comparable { | |
case unknownSize = 0 | |
#if os(iOS) | |
/// iPhone 4, 4s, iPod Touch 4th gen. | |
case screen3_5Inch | |
/// iPhone 5, 5s, 5c, SE, iPod Touch 5-6th gen. | |
case screen4Inch | |
/// iPhone 6, 6s, 7, 8 | |
case screen4_7Inch | |
/// iPhone 6+, 6s+, 7+, 8+ | |
case screen5_5Inch | |
/// iPhone X, Xs | |
case screen5_8Inch | |
/// iPhone Xr | |
case screen6_1Inch | |
/// iPhone Xs Max | |
case screen6_5Inch | |
/// iPad Mini | |
case screen7_9Inch | |
/// iPad | |
case screen9_7Inch | |
/// iPad Pro (10.5-inch) | |
case screen10_5Inch | |
/// iPad Pro (12.9-inch) | |
case screen12_9Inch | |
#elseif os(OSX) | |
//FIXME: Retina | |
case screen11Inch | |
case screen12Inch | |
case screen13Inch | |
case screen15Inch | |
case screen17Inch | |
case screen20Inch | |
case screen21_5Inch | |
case screen24Inch | |
case screen27Inch | |
#endif | |
} | |
} | |
public func <(lhs: Device.ScreenSizeKind, rhs: Device.ScreenSizeKind) -> Bool { | |
return lhs.rawValue < rhs.rawValue | |
} | |
public func ==(lhs: Device.ScreenSizeKind, rhs: Device.ScreenSizeKind) -> Bool { | |
return lhs.rawValue == rhs.rawValue | |
} | |
import UIKit | |
extension Device.ScreenSizeKind { | |
var screenSizePointInPortrait: CGSize { | |
switch self { | |
case .unknownSize: | |
return CGSize(width: 0, height: 0) | |
#if os(iOS) | |
/// iPhone 4, 4s, iPod Touch 4th gen. | |
case .screen3_5Inch: | |
return CGSize(width: 320, height: 480) | |
/// iPhone 5, 5s, 5c, SE, iPod Touch 5-6th gen. | |
case .screen4Inch://320x568 | |
return CGSize(width: 320, height: 568) | |
/// iPhone 6, 6s, 7, 8 | |
case .screen4_7Inch://375x667 | |
return CGSize(width: 375, height: 667) | |
/// iPhone 6+, 6s+, 7+, 8+ | |
case .screen5_5Inch://414x736 | |
return CGSize(width: 414, height: 736) | |
/// iPhone X, Xs | |
case .screen5_8Inch://375x812 | |
return CGSize(width: 375, height: 812) | |
/// iPhone Xr | |
case .screen6_1Inch://414x896 | |
return CGSize(width: 414, height: 896) | |
/// iPhone Xs Max | |
case .screen6_5Inch://414x896 | |
return CGSize(width: 414, height: 896) | |
/// iPad Mini | |
case .screen7_9Inch://768x1024 | |
return CGSize(width: 768, height: 1024) | |
/// iPad | |
case .screen9_7Inch://768x1024 | |
return CGSize(width: 768, height: 1024) | |
/// iPad Pro (10.5-inch) | |
case .screen10_5Inch://834x1112 | |
return CGSize(width: 834, height: 1112) | |
/// iPad Pro (12.9-inch) | |
case .screen12_9Inch://1024x1366 | |
return CGSize(width: 1024, height: 1366) | |
#elseif os(OSX) | |
// FIXME: Size, Retina | |
case .screen11Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen12Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen13Inch://e.g. MBA13inch | |
return CGSize(width: 1440, height: 900) | |
case .screen15Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen17Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen20Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen21_5Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen24Inch: | |
return CGSize(width: 0, height: 0) | |
case .screen27Inch: | |
return CGSize(width: 0, height: 0) | |
#endif | |
} | |
} | |
} |
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 | |
public enum Device {} | |
extension Device { | |
//機種・端末の向きを指定するとCGSizeが取得できる | |
public static func size(device: Kind.IPhone, orientation: Orientation = .portrait) -> CGSize { | |
switch orientation { | |
case .horizontal: | |
return size(device: device).inversed | |
case .portrait: | |
switch device { | |
case .four, .fourS, .iPodTouch4thGen: | |
return Device.ScreenSizeKind.screen3_5Inch.screenSizePointInPortrait | |
case .five, .fiveS, .fiveC, .SE, .iPodTouch5thGen, .iPodTouch6thGen: | |
return Device.ScreenSizeKind.screen4Inch.screenSizePointInPortrait | |
case .six, .sixS, .seven, .eight: | |
return Device.ScreenSizeKind.screen4_7Inch.screenSizePointInPortrait | |
case .sixPlus, .sixSPlus, .sevenPlus, .eightPlus: | |
return Device.ScreenSizeKind.screen5_5Inch.screenSizePointInPortrait | |
case .x, .xs: | |
return Device.ScreenSizeKind.screen5_8Inch.screenSizePointInPortrait | |
case .xr: | |
return Device.ScreenSizeKind.screen6_1Inch.screenSizePointInPortrait | |
case .xsmax: | |
return Device.ScreenSizeKind.screen6_5Inch.screenSizePointInPortrait | |
} | |
} | |
} | |
public static func size(device: Kind.IPad, orientation: Orientation = .portrait) -> CGSize { | |
switch orientation { | |
case .horizontal: | |
return size(device: device).inversed | |
case .portrait: | |
switch device { | |
case .mini: | |
return Device.ScreenSizeKind.screen7_9Inch.screenSizePointInPortrait | |
case .iPad: | |
return Device.ScreenSizeKind.screen9_7Inch.screenSizePointInPortrait | |
case .proTenInch: | |
return Device.ScreenSizeKind.screen10_5Inch.screenSizePointInPortrait | |
case .proTwelveInch: | |
return Device.ScreenSizeKind.screen12_9Inch.screenSizePointInPortrait | |
} | |
} | |
} | |
} |
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
//: cf: https://gist.github.com/kotowo/a3d16b1033eb0dd1af6e | |
// https://qiita.com/kazuhiro4949/items/c52fa47d350bec375fae | |
import UIKit | |
import PlaygroundSupport | |
// MARK: MyViewController | |
class MyViewController<Delegate: MyViewControllerDelegate>: UICollectionViewController, UICollectionViewDelegateFlowLayout { | |
// デリゲート先のオブジェクトはAnyMyViewControllerDelegateが持っていて、 | |
// そこでweakになっているのでここをweakにする必要はない | |
var delegate: AnyMyViewControllerDelegate<Delegate>? | |
var items = [Array<Delegate.Item>]() { | |
didSet { | |
collectionView?.reloadData() | |
} | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// delegateが実装されていなかった時用にとりあえず登録しておく | |
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "identifier") | |
} | |
/// セルの登録は初期化の時にcollectionViewを経由せずに行います。 | |
/// collectionViewに対してクラスを登録するクロージャを使います。初期化の時に型消去クラスも渡してしまうようにします。 | |
init(_ delegate: AnyMyViewControllerDelegate<Delegate>, | |
configure: (_ register: (_ cellClass: AnyClass, _ identifier: String) -> Void) -> Void = { _ in }) { | |
super.init(collectionViewLayout: UICollectionViewFlowLayout()) | |
// CollectionViewに対してセルを登録するクロージャ | |
let register = { [weak self] (cellClass: AnyClass, identifier: String) -> Void in | |
self?.collectionView?.register(cellClass, forCellWithReuseIdentifier: identifier) | |
} | |
self.delegate = delegate | |
configure(register) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
// MARK: UICollectionViewController | |
//sectionとrowの数はitemsを元に取る | |
override func collectionView( | |
_ collectionView: UICollectionView, | |
numberOfItemsInSection section: Int) -> Int { | |
return items[section].count | |
} | |
override func numberOfSections(in collectionView: UICollectionView) -> Int { | |
return items.count | |
} | |
// indexPathに対応するデータだけを予め取ってきて、デリゲート先に渡す。 | |
override func collectionView( | |
_ collectionView: UICollectionView, | |
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | |
//カリー化 | |
func dequeue(from indexPath: IndexPath) | |
-> ((_ identifier: String) -> UICollectionViewCell?) { | |
return { (identifier: String) -> UICollectionViewCell? in | |
return self.collectionView.dequeueReusableCell( | |
withReuseIdentifier: identifier, for: indexPath | |
) | |
} | |
} | |
let dequeueHandler = dequeue(from: indexPath) | |
let item = items[indexPath.section][indexPath.row] | |
let cell = delegate? | |
.myViewController( | |
vc: self, | |
dequeue: dequeueHandler, | |
cellFor: item | |
) | |
return cell ?? collectionView | |
.dequeueReusableCell( | |
withReuseIdentifier: "identifier", for: indexPath | |
) | |
} | |
override func collectionView( | |
_ collectionView: UICollectionView, | |
didSelectItemAt indexPath: IndexPath) { | |
delegate?.myViewController( | |
vc: self, didSelect: items[indexPath.section][indexPath.row] | |
) | |
} | |
// Layout | |
func collectionView( | |
_ collectionView: UICollectionView, | |
layout collectionViewLayout: UICollectionViewLayout, | |
sizeForItemAt indexPath: IndexPath) -> CGSize { | |
let contentSize = delegate? | |
.myViewController( | |
vc: self, | |
layout: collectionViewLayout, | |
sizeFor: items[indexPath.section][indexPath.row] | |
) | |
return contentSize ?? collectionViewLayout.collectionViewContentSize | |
} | |
//セル間のスペースを指定 | |
func collectionView( | |
_ collectionView: UICollectionView, | |
layout collectionViewLayout: UICollectionViewLayout, | |
minimumLineSpacingForSectionAt section: Int) -> CGFloat { | |
return 1 | |
} | |
func collectionView( | |
_ collectionView: UICollectionView, | |
layout collectionViewLayout: UICollectionViewLayout, | |
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { | |
return 0 | |
} | |
} | |
// MARK: Type Erasure | |
// MyViewControllerDelegateの型消去クラス。 | |
// Associated TypeをクラスのGenericsへ置き換える。 | |
class AnyMyViewControllerDelegate<Delegate: MyViewControllerDelegate> { | |
private weak var delegate: Delegate? | |
init(delegate: Delegate) { | |
self.delegate = delegate | |
} | |
// MyViewControllerDelegateのメソッドに1対1で対応するメソッドを用意していく | |
func myViewController( | |
vc: MyViewController<Delegate>, | |
dequeue: (_ identifier: String) -> UICollectionViewCell?, | |
cellFor item: Delegate.Item) -> UICollectionViewCell? { | |
return delegate?.myViewController(vc: vc, dequeue: dequeue, cellFor: item) | |
} | |
func myViewController( | |
vc: MyViewController<Delegate>, | |
layout collectionViewLayout: UICollectionViewLayout, | |
sizeFor item: Delegate.Item) -> CGSize? { | |
return delegate?.myViewController(vc: vc, layout: collectionViewLayout, sizeFor: item) | |
} | |
func myViewController( | |
vc: MyViewController<Delegate>, | |
didSelect item: Delegate.Item) { | |
delegate?.myViewController(vc: vc, didSelect: item) | |
} | |
} | |
// MARK: MyViewControllerDelegate | |
protocol MyViewControllerDelegate: class { | |
associatedtype Item //セルに渡すデータの型 | |
func myViewController( | |
vc: MyViewController<Self>, | |
dequeue: ((_ identifier: String) -> UICollectionViewCell?), | |
cellFor item: Item) -> UICollectionViewCell? | |
func myViewController( | |
vc: MyViewController<Self>, | |
layout collectionViewLayout: UICollectionViewLayout, | |
sizeFor item: Item) -> CGSize? | |
func myViewController( | |
vc: MyViewController<Self>, | |
didSelect item: Item) | |
} | |
extension MyViewControllerDelegate { | |
func myViewController(vc: MyViewController<Self>, layout collectionViewLayout: UICollectionViewLayout, sizeFor item: Item) -> CGSize? { return nil } | |
func myViewController(vc: MyViewController<Self>, didSelect item: Item) {} | |
} | |
// ################################################# | |
// Create sample ViewController the above Framework | |
// It has types of cell in CollectionView. | |
// ################################################# | |
enum Element { | |
case banner(name: String) | |
case content(name: String) | |
case ad(name: String) // 広告コンテンツを追加 | |
} | |
/// MyViewControllerを子クラスとして持ち、MyViewControllerのDelegate先として使われる | |
final class ViewController: UIViewController { | |
static let cellIdentifier = "cell" | |
typealias Item = Element | |
lazy var childVc: MyViewController<ViewController> = { | |
return MyViewController( | |
AnyMyViewControllerDelegate(delegate: self)) { (register) in | |
register(UICollectionViewCell.self, ViewController.cellIdentifier) | |
} | |
}() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
title = "Title" | |
addChild(childVc) | |
childVc.view.frame = view.bounds | |
childVc.view.autoresizingMask = [.flexibleHeight, .flexibleWidth] | |
view.addSubview(childVc.view) | |
childVc.didMove(toParent: self) | |
childVc.items = [[ | |
.banner(name: "バナー"), | |
.content(name: "コンテンツ"), .content(name: "コンテンツ"), | |
.ad(name: "広告"), | |
.content(name: "コンテンツ"), .content(name: "コンテンツ"), | |
.ad(name: "広告"), | |
.content(name: "コンテンツ"), .content(name: "コンテンツ"), | |
.ad(name: "広告"), | |
.content(name: "コンテンツ"), .content(name: "コンテンツ"), | |
.ad(name: "広告") | |
]] | |
} | |
override func didReceiveMemoryWarning() { | |
super.didReceiveMemoryWarning() | |
} | |
} | |
// MARK:- MyViewControllerDelegate | |
extension ViewController: MyViewControllerDelegate { | |
static let bannerHeight: CGFloat = 60 | |
func myViewController( | |
vc: MyViewController<ViewController>, | |
dequeue: ((_ identifier: String) -> UICollectionViewCell?), | |
cellFor item: Item) -> UICollectionViewCell? { | |
let cell = dequeue(ViewController.cellIdentifier) | |
switch item { | |
case .banner(_): | |
cell?.backgroundColor = .red | |
case .content(_): | |
cell?.backgroundColor = .white | |
case .ad(_): | |
cell?.backgroundColor = .blue | |
} | |
return cell | |
} | |
func myViewController( | |
vc: MyViewController<ViewController>, | |
layout collectionViewLayout: UICollectionViewLayout, | |
sizeFor item: Item) -> CGSize? { | |
let contentLength = Int(vc.view.frame.width) / 2 | |
switch item { | |
case .banner(_): | |
return CGSize( | |
width: vc.view.frame.width, | |
height: ViewController.bannerHeight | |
) | |
case .content(_), .ad(_): | |
return CGSize( | |
width: contentLength, | |
height: contentLength | |
) | |
} | |
} | |
} | |
// MARK: Configure Playground Page | |
PlaygroundPage.current.needsIndefiniteExecution = true | |
// MARK: 直接 liveViewの画面サイズを指定 | |
let navVC = UINavigationController(rootViewController: ViewController()) | |
navVC.view.frame = CGRect(x: 0, y: 0, width: 375, height: 812)//iPhone X | |
PlaygroundPage.current.liveView = navVC.view | |
// MARK: LiveViewの画面サイズを端末種別で指定するオプション | |
/// 指定された端末の画面サイズでLiveviewを設定する | |
/// | |
/// - Parameter device: iPhoneの端末種別 | |
//func configurePlaygroundPageLiveView(for device: Device.Kind.IPhone = .x) { | |
// let nc = UINavigationController(rootViewController: ViewController()) | |
// let deviceSize = Device.size(device: device) | |
// nc.view.frame = CGRect(x: 0, y: 0, width: deviceSize.width, height: deviceSize.height) | |
// PlaygroundPage.current.liveView = nc.view | |
//} | |
//configurePlaygroundPageLiveView() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment