Skip to content

Instantly share code, notes, and snippets.

@sahara-ooga
Forked from kazuhiro4949/MyViewController.swift
Created January 6, 2019 05:23
Show Gist options
  • Save sahara-ooga/0c10d20a1c52b038c9aa3f021a0c1f15 to your computer and use it in GitHub Desktop.
Save sahara-ooga/0c10d20a1c52b038c9aa3f021a0c1f15 to your computer and use it in GitHub Desktop.
UICollectionViewController including type erased delegate
import UIKit
extension CGSize {
var inversed: CGSize {
return CGSize(width: self.height, height: self.width)
}
}
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
}
}
}
extension Device {
public enum Orientation {
case horizontal
case portrait
}
}
/*
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
}
}
}
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
}
}
}
}
//: 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