Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
mac-cain13 committed Dec 15, 2014
2 parents 48926cf + 664f45a commit a74514a
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 11 deletions.
58 changes: 57 additions & 1 deletion R.swift/func.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,46 @@ struct AssetFolder {
}
}

struct Storyboard {
let name: String
let segues: [String]
let usedImageIdentifiers: [String]

init(url: NSURL) {
name = url.filename!

let parserDelegate = StoryboardParserDelegate()
let parser = NSXMLParser(contentsOfURL: url)!
parser.delegate = parserDelegate
parser.parse()

segues = parserDelegate.segues
usedImageIdentifiers = parserDelegate.usedImageIdentifiers
}
}

class StoryboardParserDelegate: NSObject, NSXMLParserDelegate {
var segues: [String] = []
var usedImageIdentifiers: [String] = []

func parser(parser: NSXMLParser!, didStartElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!, attributes attributeDict: [NSObject : AnyObject]!) {
switch elementName {
case "segue":
if let segueIdentifier = attributeDict["identifier"] as? String {
segues.append(segueIdentifier)
}

case "image":
if let imageIdentifier = attributeDict["name"] as? String {
usedImageIdentifiers.append(imageIdentifier)
}

default:
break
}
}
}

// MARK: Functions

func inputDirectories(processInfo: NSProcessInfo) -> [NSURL] {
Expand Down Expand Up @@ -53,7 +93,23 @@ func filterDirectoryContentsRecursively(fileManager: NSFileManager, filter: (NSU
func swiftStructForAssetFolder(assetFolder: AssetFolder) -> String {
return distinct(assetFolder.imageAssets).reduce(" struct \(sanitizedSwiftName(assetFolder.name)) {\n") {
$0 + " static var \(sanitizedSwiftName($1)): UIImage? { return UIImage(named: \"\($1)\") }\n"
} + " } \n"
} + " }\n"
}

func swiftStructForStoryboard(storyboard: Storyboard) -> String {
let segueIdentifiers = distinct(storyboard.segues).reduce("") {
$0 + " static var \(sanitizedSwiftName($1)): String { return \"\($1)\" }\n"
}

let validateStoryboardImages = distinct(storyboard.usedImageIdentifiers).reduce(" static func validateStoryboardImages() {\n") {
$0 + " assert(UIImage(named: \"\($1)\") != nil, \"[R.swift] Image named '\($1)' is used in storyboard '\(storyboard.name)', but couldn't be loaded.\")\n"
} + " }\n"

return " struct \(sanitizedSwiftName(storyboard.name)) {\n" + segueIdentifiers + "\n" + validateStoryboardImages + " }\n"
}

func swiftCallStoryboardImageValidation(storyboard: Storyboard) -> String {
return " \(sanitizedSwiftName(storyboard.name)).validateStoryboardImages()\n"
}

func sanitizedSwiftName(name: String) -> String {
Expand Down
15 changes: 13 additions & 2 deletions R.swift/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,23 @@ import Foundation

let defaultFileManager = NSFileManager.defaultManager()
let findAllAssetsFolderURLsInDirectory = filterDirectoryContentsRecursively(defaultFileManager) { $0.isDirectory && $0.absoluteString!.pathExtension == "xcassets" }
let findAllStoryboardURLsInDirectory = filterDirectoryContentsRecursively(defaultFileManager) { !$0.isDirectory && $0.absoluteString!.pathExtension == "storyboard" }

inputDirectories(NSProcessInfo.processInfo()).each { directory in
let code = findAllAssetsFolderURLsInDirectory(url: directory)
// Storyboards
let storyboards = findAllStoryboardURLsInDirectory(url: directory)
.map { Storyboard(url: $0) }
let storyboardStructs = storyboards.map(swiftStructForStoryboard)
let validateAllStoryboardsFunction = storyboards.map(swiftCallStoryboardImageValidation)
.reduce(" static func validateStoryboardImages() {\n", +) + " }\n"

// Asset folders
let imageAssetStructs = findAllAssetsFolderURLsInDirectory(url: directory)
.map { AssetFolder(url: $0, fileManager: defaultFileManager) }
.map(swiftStructForAssetFolder)
.reduce("struct R {\n", +) + "}\n"

// Write out the code
let code = (storyboardStructs + imageAssetStructs + [validateAllStoryboardsFunction])
.reduce("struct R {\n") { $0 + "\n" + $1 } + "}\n"
writeResourceFile(code, toFolderURL: directory)
}
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
# R.swift
_Tool to get strong typed, autocompleted images in Swift_
_Tool to get strong typed, autocompleted images and segues in Swift_

## Why use this?

Normally you access your images from the `Images.xcassets` folder with `UIImage(names: "SomeIcon")` this sucks because the compiler won't warn you about unexisting images, which means you will get errors at runtime!
Normally you access your images, segues and Xibs based on strings. Like `UIImage(names: "settings-icon")` or `performSegueWithIdentifier("openSettingsSegue")` this is fragile since the compiler can't warn you about using the wrong identifier.

With R.swift we make sure you can use `R.images.someIcon` to get your image, the `R` struct will be automatically update on build. So it's never outdated and you will get compiler errors if you rename or delete an image.
With R.swift we make sure you can use strong typed identifiers like `R.images.someIcon` or `R.main.openSettingsSegue` to get your image or segue identifier, the `R` struct will be automatically update on build. So it's never outdated and you will get compiler errors if you rename or delete an image or segue.

## Usage

After installing R.swift into your project you can use `R.[xcassetsFolderName].[imageName]`. If the struct is outdated just build and R.swift will correct any missing/changed/added images.
After installing R.swift into your project you can use `R.[group].[identifier]`. If the struct is outdated just build and R.swift will correct any missing/changed/added images and segues. Below you find the different formats

Type | Format | Without R.swift | With R.swift
----------------------------------------------------------------------------------------------------------
Image | `R.[xcassetsFolderName].[imageName]` | `UIImage(named: "settings-icon")` | `R.images.settingsIcon`
Segue | `R.[storyboardName].[segueIdentifier]` | `"openSettingsSegue"` | `R.main.openSettingsSegue`

Validate usage of images in Storyboards with `R.validateStoryboardImages()` or to validate a specific storyboard use `R.[storyboardName].validateStoryboardImages()`. Please note that this will crash your app when an image used in a storyboard is not found, so it is advised to put this check into a `#ifdef DEBUG` block.

## Installation

1. [Download](https://github.com/mac-cain13/R.swift/releases) a R.swift release, unzip it and put it into your source root directory
2. In XCode: Click on your project in the file list, choose your target under `TARGETS`, click the `Build Phases` tab and add a `New Run Script Phase` by clicking the little plus icon in the top left
3. Drag the new `Run Script` phase *above* the `Compile Sources` phase, expand it and paste the following script: `"$SRCROOT/rswift" "$SRCROOT"`
4. Build your project, in Finder you will now see a `R.generated.swift` in the `$SRCROOT`-folder, drag the `R.generated.swift` files into your project and *uncheck* `Copy items if needed`
3. Drag the new `Run Script` phase **above** the `Compile Sources` phase, expand it and paste the following script: `"$SRCROOT/rswift" "$SRCROOT"`
4. Build your project, in Finder you will now see a `R.generated.swift` in the `$SRCROOT`-folder, drag the `R.generated.swift` files into your project and **uncheck** `Copy items if needed`

_Optional:_ Add the `*.generated.swift` pattern to your `.gitignore` file to prevent unnecessary conflicts.

## Tips and tricks

*R.swift also picks up asset files from submodules and CocoaPods, can I prevent this?*
*R.swift also picks up asset files/storyboards from submodules and CocoaPods, can I prevent this?*

You can by changing the second argument (`"$SRCROOT"` in the example) of the build phase script, this is the folder R.swift will scan through. If you make this your project folder by changing the script to `"$SRCROOT/rswift" "$SRCROOT/MyProject"` it will only scan that folder.

*Can I make R.swift scan multiple folder trees?*

You can by passing multiple folders to scan through. Change the build phase script to something like this: `"$SRCROOT/rswift" "$SRCROOT/MyProject" "$SRCROOT/SubmoduleA" "$SRCROOT/SubmoduleB"`
You can by passing multiple folders to scan through. Change the build phase script to something like this: `"$SRCROOT/rswift" "$SRCROOT/MyProject" "$SRCROOT/SubmoduleA" "$SRCROOT/SubmoduleB"`. Each folder will get it's own `R.generated.swift` file since R.swift assumes these folders will be different subprojects.

*When I launch `rswift` from Finder I get this "Unknown developer warning"?!*

For now I'm to lazy to sign my builds with a Developer ID and when running stuff from the commandline/XCode it's not a problem. It will just work, but maybe I'll fix this. Signed releases are nice, now I only need to find some time to fix this. :)

## Contribute

Expand Down

0 comments on commit a74514a

Please sign in to comment.