はじめに
iOS Swiftでフォーム(TableViewのような画面)を簡易に作成できるOSS、Eurekaがあります。便利なので、設定画面などで利用されている例があるかと思います。
今回、あるプロジェクトをSwift 2.3→3.0にアップデートした関係で、Eureka 1.7.0→2.0.1というアップデートが必要になりました。カスタムで定義した部分が不整合を起こしました。その解決方法を、備忘のために残します。
今回説明する事例
今回不具合が出たクラスは2つありました。
説明の前半では、PushRow系の画像選択のためのローImagePushRowというカスタムクラスの直し方について触れます。
後半は、その内部で使うカスタムクラスImageSelectorViewControllerの直し方を示します。
ImagePushRowとは
あるとき、画像を選択肢にするためのRowが必要になりました。しかし、Eureka標準のPushRowでは選択肢に画像を表示できません。そこで、画像を選択肢にするためのカスタムクラスとして、ImagePushRowを作りました。
クラス宣言部
import Eureka
public class _FontPushRow<T: Equatable, Cell: CellType where Cell: BaseCell, Cell: TypedCellType, Cell.Value == T> : SelectorRow<T, Cell, FontSelectorViewController<T>> {
public required init(tag: String?) {
super.init(tag: tag)
presentationMode = .Show(controllerProvider: ControllerProvider.Callback { return FontSelectorViewController<T>(){ _ in } }, completionCallback: { vc in vc.navigationController?.popViewControllerAnimated(true) })
}
}
import Eureka
public class _ImagePushRow<Cell: CellType> : SelectorRow<Cell, ImageSelectorViewController<Cell.Value>> where Cell: BaseCell {
public required init(tag: String?) {
super.init(tag: tag)
presentationMode = .show(controllerProvider: ControllerProvider.callback { return ImageSelectorViewController<Cell.Value>(){ _ in } }, onDismiss: { vc in
let _ = vc.navigationController?.popViewController(animated: true) })
}
}
・OSSのEurekaは別モジュールになっているはずなので、その場合に必要となるimportをしています。
・ジェネリック引数の形が変わっているので合わせます。
・_PushRowを継承して作りたいところですが、ViewControllerをカスタマイズする必要がある関係で、ControllerProvider.callbackに入る型を合わせる必要があり、SelectorRow<>から継承させました。
・ImageSelectorViewControllerというのが後半で見るカスタムのViewControllerクラスのことです。
ImageSelectorViewControllerとは
上で述べたImagePushRowの内部で、標準のFormViewControllerに由来するImageSelectorViewControllerというカスタムクラスを使います。基本構造は、標準のSelectorViewControllerと同じです。
ベースとなるクラスの宣言部
public class _ImageSelectorViewController<T: Equatable, Row: SelectableRowType where Row: BaseRow, Row: TypedRowType, Row.Value == T, Row.Cell.Value == T>: FormViewController, TypedRowControllerType {
public class _FontSelectorViewController<Row: SelectableRowType>: _SelectorViewController<Row> where Row: BaseRow, Row: TypedRowType {
・Eureka側のジェネリック引数やプロトコルの宣言の形が変更されているので、対応させています。
・_SelectorViewControllerからの継承によって、変数の定義などのコード記述の手間を省きます。
ですので、以下にナンバリングされている項目にかかるコードは、実際は記述不要です。Eurekaの構造変化を押さえるために、念のため説明をメモしたいと思います。
1. 変数
増減・名称変更はこうなっています。
/// The row that pushed or presented this controller
public var row: RowOf<Row.Value>!
/// A closure to be called when the controller disappears.
public var completionCallback : ((UIViewController) -> ())?
public var selectableRowCellUpdate: ((cell: Row.Cell, row: Row) -> ())?
/// The row that pushed or presented this controller
public var row: RowOf<Row.Cell.Value>!
public var enableDeselection = true
public var dismissOnSelection = true
public var dismissOnChange = true
public var selectableRowCellSetup: ((_ cell: Row.Cell, _ row: Row) -> ())?
public var selectableRowCellUpdate: ((_ cell: Row.Cell,_ row: Row) -> ())?
/// A closure to be called when the controller disappears.
public var onDismissCallback : ((UIViewController) -> ())?
public var sectionKeyForValue: ((Row.Cell.Value) -> (String))?
public var sectionHeaderTitleForKey: ((String) -> String?)? = { $0 }
public var sectionFooterTitleForKey: ((String) -> String?)?
・変数が増えています。
・onDismissCallbackに名称変更されています。
2.イニシャライザー
override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
・Swift3なのでNSBundleのNSのプリフィックスがなくなります。
・requiredなinit?()を追記する必要があります。
3.viewDidLoad()
Eureka1.7.0では、viewDidLoad()の中でセクションやローの追加処理を直接に記述していました。
Eureka2.0.1では、次のような形で処理を小分けてして、見通しをよくします。
public override func viewDidLoad() {
super.viewDidLoad()
guard let options = row.dataProvider?.arrayData else { return }
//+++=
form +++ SelectableSection<Row, Row.Value>(row.title ?? "", selectionType: .SingleSelection(enableDeselection: true)) { [weak self] section in
if let sec = section as? SelectableSection<Row, Row.Value> {
sec.onSelectSelectableRow = { _, row in
self?.row.value = row.value
self?.completionCallback?(self!)
}
}
}
for option in options {
form.first! <<< Row.init(String(option)){ lrow in
if let fileName = row.displayValueFor?(option){
lrow.cell.imageView?.image = UIImage(named: fileName)
}
lrow.selectableValue = option
lrow.value = row.value == option ? option : nil
}.cellUpdate { [weak self] cell, row in
self?.selectableRowCellUpdate?(cell: cell, row: row)
}
}
}
open override func viewDidLoad() {
super.viewDidLoad()
setupForm()
}
open func setupForm() {
guard let options = row.dataProvider?.arrayData else { return }
if let optionsBySections = self.optionsBySections() {
for (sectionKey, options) in optionsBySections {
form +++ section(with: options, header: sectionHeaderTitleForKey?(sectionKey), footer: sectionFooterTitleForKey?(sectionKey))
}
} else {
form +++ section(with: options, header: row.title, footer: nil)
}
}
func optionsBySections() -> [(String, [Row.Cell.Value])]? {
guard let options = row.dataProvider?.arrayData, let sectionKeyForValue = sectionKeyForValue else { return nil }
let sections = options.reduce([:]) { (reduced, option) -> [String: [Row.Cell.Value]] in
var reduced = reduced
let key = sectionKeyForValue(option)
reduced[key] = (reduced[key] ?? []) + [option]
return reduced
}
return sections.sorted(by: { (lhs, rhs) in lhs.0 < rhs.0 })
}
func section(with options: [Row.Cell.Value], header: String?, footer: String?) -> SelectableSection<Row> {
let header = header ?? ""
let footer = footer ?? ""
let section = SelectableSection<Row>(header: header, footer: footer, selectionType: .singleSelection(enableDeselection: enableDeselection)) { [weak self] section in
if let sec = section as? SelectableSection<Row> {
sec.onSelectSelectableRow = { _, row in
let changed = self?.row.value != row.value
self?.row.value = row.value
if self?.dismissOnSelection == true || (changed && self?.dismissOnChange == true) {
self?.onDismissCallback?(self!)
}
}
}
}
for option in options {
section <<< Row.init(String(describing: option)){ lrow in
if let fileName = row.displayValueFor?(option){
lrow.cell.imageView?.image = UIImage(named: fileName)
}
lrow.selectableValue = option
lrow.value = self.row.value == option ? option : nil
}.cellSetup { [weak self] cell, row in
self?.selectableRowCellSetup?(cell, row)
}.cellUpdate { [weak self] cell, row in
self?.selectableRowCellUpdate?(cell, row)
}
}
return section
}
setupForm()以下
実装の話しに戻ります。
_ImageSelectorViewControllerは、_SelectorViewControllerを継承させたので、標準のsetupForm()以下の構造をそのまま利用できます。
3.viewDidLoad()で示した処理をするために、実際には、次のようなコードのみを書きました。
override public func setupForm() {
guard let options = row.dataProvider?.arrayData else { return }
if let optionsBySections = self.optionsBySections() {
for (sectionKey, options) in optionsBySections {
form +++ section(with: options, header: sectionHeaderTitleForKey?(sectionKey), footer: sectionFooterTitleForKey?(sectionKey))
}
} else {
form +++ section(with: options, header: row.title, footer: nil)
}
}
func optionsBySections() -> [(String, [Row.Cell.Value])]? {
guard let options = row.dataProvider?.arrayData, let sectionKeyForValue = sectionKeyForValue else { return nil }
let sections = options.reduce([:]) { (reduced, option) -> [String: [Row.Cell.Value]] in
var reduced = reduced
let key = sectionKeyForValue(option)
reduced[key] = (reduced[key] ?? []) + [option]
return reduced
}
return sections.sorted(by: { (lhs, rhs) in lhs.0 < rhs.0 })
}
func section(with options: [Row.Cell.Value], header: String?, footer: String?) -> SelectableSection<Row> {
let header = header ?? ""
let footer = footer ?? ""
let section = SelectableSection<Row>(header: header, footer: footer, selectionType: .singleSelection(enableDeselection: enableDeselection)) { [weak self] section in
if let sec = section as? SelectableSection<Row> {
sec.onSelectSelectableRow = { _, row in
let changed = self?.row.value != row.value
self?.row.value = row.value
if self?.dismissOnSelection == true || (changed && self?.dismissOnChange == true) {
self?.onDismissCallback?(self!)
}
}
}
}
for option in options {
section <<< Row.init(String(describing: option)){ lrow in
if let fileName = row.displayValueFor?(option){
lrow.cell.imageView?.image = UIImage(named: fileName)
}
lrow.selectableValue = option
lrow.value = self.row.value == option ? option : nil
}.cellSetup { [weak self] cell, row in
self?.selectableRowCellSetup?(cell, row)
}.cellUpdate { [weak self] cell, row in
self?.selectableRowCellUpdate?(cell, row)
}
}
return section
}
インターフェースとなるクラスの宣言部
public class ImageSelectorViewController<T:Equatable> : _ImageSelectorViewController<T, ListCheckRow<T>> {
override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
convenience public init(_ callback: (UIViewController) -> ()){
self.init(nibName: nil, bundle: nil)
completionCallback = callback
}
}
open class ImageSelectorViewController<T:Equatable> : _ImageSelectorViewController<ListCheckRow<T>> {
}
・ジェネリック引数の形を合わせます。
・これで書き換え完了です。
参考リンク
Eureka
TableViewのフォーム画面をEurekaで爆速開発しよう
EurekaのSwift 2.3対応の最終バージョンの指定の仕方←Swift 2.3をキープする方へ。