type safe assets handling in swift
TRANSCRIPT
Type Safe Resource Handling in Swift
2015/10/13potatotips #22
About Me
@tasanobu
Kazunobu Tasaka
本日の内容
• リソースを静的型付けし、Type Safeに扱う方法をお話しします。
• リソースとはAsset Catalog UIStoryboard Localizable.strings
• リソースのインスタンス生成時String型の値で指定
Objective-C style
// Assets Catalog let img = UIImage(named: "Sample") // Storyboard let sb = UIStoryboard(name: "Sample", bundle: nil) .instantiateViewControllerWithIdentifier("Sample") as? SampleViewController // Localizable.strings let str = NSLocalizedString("Sample", comment: “")
問題点• コンパイラによるチェックが不可
→ 指定方法がStringのため💦
• 実行時にリソースの存在チェック → ランタイムエラーが発生するリスクあり
• 戻り値の型 : Optional<T> → 使用時にunwrapする必要がある
// 存在しない場合 let sb = UIStoryboard(name: UIApplicationDidBecomeActiveNotification, bundle: nil) // Terminating app due to uncaught exception 'NSInvalidArgumentException', // reason: 'Could not find a storyboard named 'Sample' in bundle NSBundle
let img: UIImage? = UIImage(named: NSURLErrorDomain)
let vc: SampleViewController? = sb.instantiateViewControllerWithIdentifier("Sample") as? SampleViewController
改善ポイント
Swift は Type Safe な言語!
• リソースのStringに型情報を持たせる
App Specific Enum
extension UIImage { enum Asset: String { case Camera = "Camera" case Home = "Home" case Mail = "Mail" }
convenience init!(asset: Asset) { self.init(named: asset.rawValue) } }
Asset ID / Enumの対応付け
• Initializer: Enumでリソースを指定• Non Optional な戻り値 (リソースの存在が担保されている)
Usage
let camera = UIImage(asset: .Camera)
let home = UIImage(asset: .Home)
let mail = UIImage(asset: .Mail)
Pros / Cons• Pros
引数を型付けできる
→ コンパイル時チェックが可能
→ Non-optionalな型が戻り値となる
• Cons
リソースを更新する度にEnum値を更新する必要がある😓
• CLツール : リソース用Swiftコードを自動生成
Asset Catalog (UIImage) UIStoryboard
Localizable.strings UIColor
• Homebrewに対応
https://github.com/AliSoftware/SwiftGen
SwiftGen
$ brew install swiftgen
Usage: UIStoryboard
• コマンドラインを実行
swiftgen-‐storyboard /dir/to/storyboards > Storyboards.swift
// Storyboards.swift
protocol StoryboardScene : RawRepresentable { static var storyboardName : String { get } static func storyboard() -> UIStoryboard static func initialViewController() -> UIViewController func viewController() -> UIViewController static func viewController(identifier: Self) -> UIViewController }
extension StoryboardScene where Self.RawValue == String { static func storyboard() -> UIStoryboard { return UIStoryboard(name: self.storyboardName, bundle: nil) } static func initialViewController() -> UIViewController { return storyboard().instantiateInitialViewController()! } func viewController() -> UIViewController { return Self.storyboard().instantiateViewControllerWithIdentifier(self.rawValue) } static func viewController(identifier: Self) -> UIViewController { return identifier.viewController() } }
via https://github.com/AliSoftware/SwiftGen
// Storyboards.swift
extension UIStoryboard { struct Scene { enum Wizard : String, StoryboardScene { static let storyboardName = "Wizard" case CreateAccount = "CreateAccount" static func createAccountViewController() -> CreateAccViewController { return Wizard.CreateAccount.viewController() as! CreateAccViewController } case ValidatePassword = "Validate_Password" static func validatePasswordViewController() -> UIViewController { return Wizard.ValidatePassword.viewController() } } enum Message : String, StoryboardScene { static let storyboardName = "Message" case Composer = "Composer" static func composerViewController() -> UIViewController { return Message.Composer.viewController() } case URLChooser = "URLChooser" static func urlChooserViewController() -> XXPickerViewController { return Message.URLChooser.viewController() as! XXPickerViewController } } } struct Segue { enum Message : String { case Custom = "Custom" case Back = "Back" case NonCustom = "NonCustom" } } }
Storyboard IDに対応Storyboardファイルに対応
via https://github.com/AliSoftware/SwiftGen
Usage// Initial VC let initialVC = UIStoryboard.Scene.Wizard.initialViewController()
// Generic ViewController constructor, returns a UIViewController instance let validateVC = UIStoryboard.Scene.Wizard.ValidatePassword.viewController()
// Dedicated type var that returns the right type of VC (CreateAccViewController here) let createVC = UIStoryboard.Scene.Wizard.createAccountViewController()
override func prepareForSegue(_ segue: UIStoryboardSegue, sender sender: AnyObject?) { switch UIStoryboard.Segue.Message(rawValue: segue.identifier)! { case .Custom: // Prepare for your custom segue transition case .Back: // Prepare for your custom segue transition case .NonCustom: // Prepare for your custom segue transition } }
via https://github.com/AliSoftware/SwiftGen
まとめ• App Specific Enum を利用すると静的型付けしてリソースを扱える😄
• SwiftGen: リソース用コードを自動生成可能😄
References
• SwiftGenhttps://github.com/AliSoftware/SwiftGen
• WWDC’15: Swift in Practice https://developer.apple.com/videos/wwdc/2015/?id=411
ご静聴ありがとうございました
🙇