stronglyboards 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 74195599601bd31170e951aaf41ecbf811a32694
4
+ data.tar.gz: 495710653267bd90a9d856e72ce367281d538cea
5
+ SHA512:
6
+ metadata.gz: 287f024d27278f24ec0263189ef00e62fbb76eb6b4a29dc649ed9cae589c61cd7af71b4ad87667e2f902eb8304bedd3d2197aa92732017cf7ade87abcaa2019e
7
+ data.tar.gz: 1a39d4402e55a4915b83b92fed4ceaf6f8724dfba923a36abda590caed519a405f99ab26afbdee6b6542ec7e8858c30e89a091a5b578eb907d6977bd0995c4e2
@@ -0,0 +1,83 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalisation:
25
+ /.bundle/
26
+ /vendor/bundle
27
+ /lib/bundler/man/
28
+
29
+ # for a library or gem, you might want to ignore these files since the code is
30
+ # intended to run in multiple environments; otherwise, check them in:
31
+ # Gemfile.lock
32
+ # .ruby-version
33
+ # .ruby-gemset
34
+
35
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36
+ .rvmrc
37
+
38
+ # Below covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
39
+
40
+ *.iml
41
+
42
+ ## Directory-based project format:
43
+ .idea/
44
+ # if you remove the above rule, at least ignore the following:
45
+
46
+ # User-specific stuff:
47
+ # .idea/workspace.xml
48
+ # .idea/tasks.xml
49
+ # .idea/dictionaries
50
+
51
+ # Sensitive or high-churn files:
52
+ # .idea/dataSources.ids
53
+ # .idea/dataSources.xml
54
+ # .idea/sqlDataSources.xml
55
+ # .idea/dynamic.xml
56
+ # .idea/uiDesigner.xml
57
+
58
+ # Gradle:
59
+ # .idea/gradle.xml
60
+ # .idea/libraries
61
+
62
+ # Mongo Explorer plugin:
63
+ # .idea/mongoSettings.xml
64
+
65
+ ## File-based project format:
66
+ *.ipr
67
+ *.iws
68
+
69
+ ## Plugin-specific files:
70
+
71
+ # IntelliJ
72
+ /out/
73
+
74
+ # mpeltonen/sbt-idea plugin
75
+ .idea_modules/
76
+
77
+ # JIRA plugin
78
+ atlassian-ide-plugin.xml
79
+
80
+ # Crashlytics plugin (for Android Studio and IntelliJ)
81
+ com_crashlytics_export_strings.xml
82
+ crashlytics.properties
83
+ crashlytics-build.properties
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Steve Wilford
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,107 @@
1
+ # Overview #
2
+
3
+ Stronglyboards generates code that can be used to provide a strongly-typed interface to [iOS Storyboards](https://developer.apple.com/library/ios/recipes/xcode_help-IB_storyboard/chapters/AboutStoryboards.html).
4
+
5
+ Reduce mistakes introduced when using string based storyboard and view controller identifiers.
6
+
7
+ It is inspired by [Natalie](https://github.com/krzyzanowskim/Natalie) but is a [Ruby gem](https://rubygems.org) and generates a slightly different API,
8
+ and I believe [competition is a good thing](https://vimeo.com/124317403).
9
+
10
+ # Features
11
+
12
+ - Safe instantiation of:-
13
+ - storyboards,
14
+ - the storyboard's initial view controller,
15
+ - view controllers with storyboard identifiers
16
+ - Outputs **Objective-C** or **Swift** depending on your preference.
17
+ - Integrates seamlessly into your project.
18
+ - Creates a build phase to automatically keep the generated code up-to-date with storyboard changes.
19
+ - Supports **localized** and non-localized Storyboards.
20
+
21
+ # Todo #
22
+
23
+ - Segues
24
+ - Table and Collection View cells
25
+ - More...
26
+
27
+ # Installation & Basic Usage #
28
+
29
+ Use the `gem` command to install Stronglyboards:
30
+
31
+ ```gem install stronglyboards```
32
+
33
+ Run stronglyboards on your Xcode project file:
34
+
35
+ ```stronglyboards install MyProject.xcodeproj```
36
+
37
+ By default it will generate Objective-C files in the current directory.
38
+
39
+ Stronglyboards will automatically add the generated files into your project
40
+ and setup a new "Run Script" build phase to keep up-to-date with any
41
+ storyboard changes you might make.
42
+
43
+ # Usage #
44
+
45
+ `install <PROJECT>`
46
+
47
+ This will install Stronglyboards into the specified Xcode project. Replace `<PROJECT>` with your `.xcodeproj` file.
48
+
49
+ `update <PROJECT>`
50
+
51
+ This will attempt to update Stronglyboards in a project where it is already installed. Replace `<PROJECT>` with your `.xcodeproj` file.
52
+ Note that things will likely go wrong if you have manually renamed any of the generated files.
53
+
54
+ `uninstall <PROJECT>`
55
+
56
+ This will attempt to remove Stronglyboards from a project where it has been previously installed. Replace `<PROJECT>` with your `.xcodeproj` file.
57
+ Note that things will likely go wrong if you have manually renamed any of the generated files.
58
+
59
+ # Installation Options #
60
+
61
+ `--output` specifies the name of the output file(s).
62
+ You can use this parameter to output to a different directory.
63
+ e.g. `Classes/GeneratedStoryboardAPI`.
64
+ You should **not** provide a file extension as part of this file name.
65
+ This is an optional parameter, default is `Stronglyboards`.
66
+ Note that this path is essentially a template, the actual generated filenames
67
+ will be different.
68
+
69
+ `--language` specifies the output language as either `objc` or `swift`.
70
+ This is an optional parameter, default is `objc`.
71
+
72
+ `--prefix` specifies a string to be used as the prefix for all generated classes.
73
+ This is an optional parameter, default is no prefix.
74
+ Note that the prefix does not affect the output file name.
75
+
76
+ # Contributing #
77
+
78
+ Submit an issue, or ideally a pull request.
79
+
80
+ # Authors & Contributors #
81
+
82
+ - [@nxsteveo](http://twitter.com/nxsteveo)
83
+ - \<Your name here>
84
+
85
+ # License #
86
+
87
+ The MIT License (MIT)
88
+
89
+ Copyright (c) 2015 Steve Wilford
90
+
91
+ Permission is hereby granted, free of charge, to any person obtaining a copy
92
+ of this software and associated documentation files (the "Software"), to deal
93
+ in the Software without restriction, including without limitation the rights
94
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
95
+ copies of the Software, and to permit persons to whom the Software is
96
+ furnished to do so, subject to the following conditions:
97
+
98
+ The above copyright notice and this permission notice shall be included in all
99
+ copies or substantial portions of the Software.
100
+
101
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
102
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
103
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
104
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
105
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
106
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
107
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'stronglyboards'
@@ -0,0 +1,253 @@
1
+ require 'xcodeproj'
2
+ require 'optparse'
3
+ require 'thor'
4
+ require 'yaml'
5
+
6
+ require_relative 'stronglyboards/version'
7
+ require_relative 'stronglyboards/lock_file'
8
+ require_relative 'stronglyboards/storyboard'
9
+ require_relative 'stronglyboards/source_generator_objc'
10
+ require_relative 'stronglyboards/source_generator_swift'
11
+
12
+ module Stronglyboards
13
+
14
+ class Stronglyboards < Thor
15
+
16
+ BUILD_SCRIPT_NAME = 'Update Stronglyboards'
17
+
18
+ # ---- Begin external interface ----
19
+
20
+ desc 'install PROJECT', 'Installs Stronglyboards into your .xcodeproj file'
21
+ option :output, :desc => 'Path to the output file'
22
+ option :language, :default => 'objc', :desc => 'Output language (objc [default], swift)'
23
+ option :prefix, :default => '', :desc => 'Class and category method prefix'
24
+ def install(project_file)
25
+ lock_file = LockFile.new(project_file)
26
+ if lock_file.exists?
27
+ puts 'It appears that Stronglyboards has already been installed on this project.'
28
+ exit
29
+ end
30
+
31
+ puts "Installing Stronglyboards into #{project_file}"
32
+
33
+ # Open the existing Xcode project
34
+ project = Xcodeproj::Project.open(project_file)
35
+
36
+ # Do main processing
37
+ process(project, options)
38
+
39
+ # Finalise installation
40
+ lock_file.update(options)
41
+ project.save
42
+ end
43
+
44
+ desc 'update', 'Updates the generated source code for the project'
45
+ def update(project_file)
46
+ configuration = require_lock_file(project_file).contents
47
+
48
+ puts "Updating Stronglyboards in #{project_file}"
49
+
50
+ # Open the Xcode project
51
+ project = Xcodeproj::Project.open(project_file)
52
+
53
+ process(project, configuration)
54
+ end
55
+
56
+ desc 'uninstall PROJECT', 'Uninstalls Stronglyboards from the specified .xcodeproj file.'
57
+ def uninstall(project_file)
58
+ lock_file = require_lock_file(project_file)
59
+ configuration = lock_file.contents
60
+
61
+ base_output_file = configuration[:output]
62
+ language = configuration[:language]
63
+ prefix = configuration[:prefix]
64
+
65
+ puts "Uninstalling Stronglyboards from #{project_file}"
66
+
67
+ files_to_delete = Array.new
68
+
69
+ # Open the Xcode project
70
+ project = Xcodeproj::Project.open(project_file)
71
+ project_root = project.path.dirname
72
+
73
+ # Gather the targets that we're interested in
74
+ targets = interesting_targets(project)
75
+
76
+ targets.each do |target|
77
+ # Find and delete the build script phase for this target
78
+ target.build_phases.select { |phase|
79
+ phase.is_a? Xcodeproj::Project::PBXShellScriptBuildPhase and phase.name == BUILD_SCRIPT_NAME
80
+ }.each { |phase|
81
+ target.build_phases.delete(phase)
82
+ }
83
+
84
+ # Get a source generator for this target
85
+ output_file = base_output_file_for_target(base_output_file, target, prefix)
86
+ source_generator = source_generator(language, prefix, output_file)
87
+
88
+ # Gather the files that would have been generated for this target.
89
+ # Bare in mind that this target may have been created since the
90
+ # last time the install or update command ran, so there may be
91
+ # files in this list that don't actually exist.
92
+ files_to_delete << source_generator.output_files
93
+ end
94
+
95
+ # Expand the paths of the files to be deleted to be absolute
96
+ files_to_delete.flatten!.uniq!
97
+ paths_to_delete = files_to_delete.collect do |file|
98
+ project_root + file.file.path
99
+ end
100
+
101
+ # Look through each target to see if this file is a member,
102
+ # removing it from the source build phase if necessary.
103
+ # TODO: There's got to be a better way, question asked.
104
+ # http://stackoverflow.com/questions/32908231/how-to-get-the-targets-for-a-pbxfilereference-in-xcodeproj
105
+ targets.each do |target|
106
+ target.source_build_phase.files.each do |build_file|
107
+ full_path = build_file.file_ref.real_path
108
+ if paths_to_delete.include?(full_path)
109
+
110
+ # Need to get a reference to the underlying file reference
111
+ # as it will be removed when the build file is removed
112
+ # from the target's build phase.
113
+ file = build_file.file_ref
114
+
115
+ # Remove the file from the build phase, project, and file system
116
+ target.source_build_phase.remove_build_file(build_file)
117
+ file.remove_from_project
118
+ File.delete(full_path)
119
+ end
120
+ end
121
+ end
122
+
123
+ # Iterate through the files to delete to get rid of any non-source files
124
+ files_to_delete.each do |file|
125
+ unless file.is_source
126
+ full_path = File.realpath(file.file)
127
+ file_ref = project.reference_for_path(full_path)
128
+ file_ref.remove_from_project
129
+ File.delete(full_path)
130
+ end
131
+ end
132
+
133
+ project.save
134
+
135
+ # Finally delete the lock file
136
+ lock_file.delete
137
+ end
138
+
139
+ # ---- End external interface ----
140
+
141
+ private
142
+ def process(project, options)
143
+ base_output_file = options[:output]
144
+ language = options[:language]
145
+ prefix = options[:prefix]
146
+
147
+ interesting_targets(project).each do |target|
148
+
149
+ # Provide a default output filename
150
+ output_file = base_output_file_for_target(base_output_file, target, prefix)
151
+
152
+ # Instantiate a source generator appropriate for the selected language
153
+ source_generator = source_generator(language, prefix, output_file)
154
+
155
+ # Iterate the target's resource files looking for storyboards
156
+ target.resources_build_phase.files.each do |build_file|
157
+ next unless build_file.display_name.end_with? Storyboard::EXTENSION
158
+
159
+ file_or_group = build_file.file_ref
160
+
161
+ if file_or_group.is_a? Xcodeproj::Project::Object::PBXFileReference
162
+ # Getting the real path is sufficient for non-localized storyboards
163
+ # as it will return the absolute path to the .storyboard
164
+ path = file_or_group.real_path
165
+ elsif file_or_group.is_a? Xcodeproj::Project::Object::PBXVariantGroup
166
+ # Localized storyboards will be a group and will
167
+ # need the path constructing from the Base storyboard.
168
+ base_storyboard_file = file_or_group.children.find { |f| f.name == 'Base' }
169
+ if base_storyboard_file == nil
170
+ puts "No Base storyboard found for #{file_or_group}!!!"
171
+ next
172
+ end
173
+ path = base_storyboard_file.real_path
174
+ end
175
+
176
+ storyboard = Storyboard.new(path)
177
+
178
+ source_generator.add_storyboard(storyboard)
179
+ end # end project file iterator
180
+
181
+ output_files = source_generator.parse_storyboards
182
+
183
+ # Add the output files to the target
184
+ add_files_to_target(project, target, output_files)
185
+ add_build_script(project, target)
186
+ end
187
+ end
188
+
189
+ private
190
+ def interesting_targets(project)
191
+ project.native_targets.select { |target| target.product_type == 'com.apple.product-type.application' }
192
+ end
193
+
194
+ private
195
+ def base_output_file_for_target(base_file, target, prefix)
196
+ if base_file == nil
197
+ base_file = prefix + 'Stronglyboards'
198
+ end
199
+ base_file + "_#{target.name}"
200
+ end
201
+
202
+ private
203
+ def add_files_to_target(project, target, output_files)
204
+ puts "Adding files to target \"#{target}\""
205
+
206
+ output_files.each do |output_file|
207
+ # Insert the file into the root group in the project
208
+ file_reference = project.new_file(output_file.file)
209
+
210
+ # Add the file to the target to ensure it is compiled
211
+ target.source_build_phase.add_file_reference(file_reference) if output_file.is_source
212
+ end
213
+
214
+ end
215
+
216
+ private
217
+ def add_build_script(project, target)
218
+ puts 'Adding build script'
219
+
220
+ phase = project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
221
+ phase.name = BUILD_SCRIPT_NAME
222
+ phase.shell_script = 'stronglyboards update ${PROJECT_NAME}'
223
+ target.build_phases.insert(0, phase)
224
+ end
225
+
226
+ private
227
+ def source_generator(language, prefix, output_file)
228
+ case language
229
+ when 'objc'
230
+ SourceGeneratorObjC.new(prefix, output_file)
231
+ when 'swift'
232
+ SourceGeneratorSwift.new(prefix, output_file)
233
+ else
234
+ puts 'Language must be objc or swift.'
235
+ exit
236
+ end
237
+ end
238
+
239
+ private
240
+ def require_lock_file(project_file)
241
+ lock_file = LockFile.new(project_file)
242
+ unless lock_file.exists?
243
+ puts 'Stronglyboards must first be installed using the install command.'
244
+ exit
245
+ end
246
+ lock_file
247
+ end
248
+
249
+ end
250
+
251
+ Stronglyboards.start(ARGV)
252
+
253
+ end
@@ -0,0 +1,34 @@
1
+ module Stronglyboards
2
+ class LockFile
3
+
4
+ def initialize(project_file)
5
+ @path = File.dirname(project_file) + '/' + LOCK_FILE_NAME
6
+ end
7
+
8
+ def contents
9
+ # Load the lock file containing configuration
10
+ file = File.open(@path, 'r')
11
+ YAML::load(file)
12
+ end
13
+
14
+ def update(options)
15
+ puts "Writing lock file at #{@path}"
16
+ File.open(@path, 'w+') do |file|
17
+ file.write(YAML::dump(options))
18
+ end
19
+ end
20
+
21
+ def delete
22
+ File.delete(@path)
23
+ end
24
+
25
+ def exists?
26
+ File.exists?(@path)
27
+ end
28
+
29
+ private
30
+
31
+ LOCK_FILE_NAME = 'Stronglyboards.lock'
32
+
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Stronglyboards
2
+
3
+ class OutputFile < Struct.new(:file, :is_source)
4
+ end
5
+
6
+ class AbstractSourceGenerator
7
+
8
+ protected
9
+ attr_accessor :prefix
10
+
11
+ public
12
+ def initialize(prefix, output_file_name)
13
+ @prefix = prefix
14
+ @storyboards = Array.new
15
+
16
+ @implementation_file = File.open(output_file_name, 'w+')
17
+ end
18
+
19
+ # Gathers a set of view controller classes from all storyboards
20
+ protected
21
+ def view_controller_classes
22
+ @storyboards.collect { |storyboard|
23
+ storyboard.view_controllers.collect { |vc| vc.class_name }
24
+ }.flatten.uniq
25
+ end
26
+
27
+ public
28
+ def add_storyboard(storyboard)
29
+ @storyboards.push(storyboard)
30
+ end
31
+
32
+ public
33
+ def parse_storyboards
34
+ raise 'This method should be overridden.'
35
+ end
36
+
37
+ public
38
+ def output_files
39
+ [OutputFile.new(@implementation_file, true)]
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,175 @@
1
+ require_relative 'source_generator'
2
+
3
+ module Stronglyboards
4
+ class SourceGeneratorObjC < AbstractSourceGenerator
5
+
6
+ def initialize(prefix, output_file_name)
7
+ @implementation_file_path = output_file_name + '.m'
8
+
9
+ super(prefix, @implementation_file_path)
10
+
11
+ @header_file_path = output_file_name + '.h'
12
+ @header_file = File.open(@header_file_path, 'w+')
13
+ end
14
+
15
+ # Parses the storyboards
16
+ public
17
+ def parse_storyboards
18
+
19
+ puts "Header: #{@header_file_path}"
20
+ puts "Implementation: #{@implementation_file_path}"
21
+
22
+ # Generate framework and header imports
23
+ @header_file.write("@import UIKit;\n\n")
24
+ @implementation_file.write("#import \"#{File.basename(@header_file_path)}\"\n\n")
25
+
26
+ @header_file.write("NS_ASSUME_NONNULL_BEGIN\n\n")
27
+
28
+ # Generate the base storyboard class
29
+ base_class_name = create_base_storyboard_class
30
+
31
+ # Generate forward declaration of view controller classes
32
+ view_controller_classes.each do |class_name|
33
+ @header_file.write("@class #{class_name};\n")
34
+ end
35
+ @header_file.write("\n")
36
+
37
+ # Generate classes for each storyboard
38
+ @storyboards.each { |s| create_storyboard_class(s, base_class_name) }
39
+
40
+ # Generate the storyboard category
41
+ create_storyboard_category
42
+
43
+ @header_file.write("\n\nNS_ASSUME_NONNULL_END")
44
+
45
+ output_files
46
+ end
47
+
48
+ private
49
+ def create_base_storyboard_class
50
+ class_name = "#{@prefix}Stronglyboard"
51
+
52
+ # Create the public interface
53
+ interface = Array.new
54
+ interface.push("@interface #{class_name} : NSObject")
55
+ interface.push('@property (nonatomic, strong, readonly) UIStoryboard *storyboard;')
56
+ interface.push('@end')
57
+
58
+ # Create the private interface to expose the storyboard as a read-write property
59
+ implementation = Array.new
60
+ implementation.push("@interface #{class_name} ()")
61
+ implementation.push('@property (nonatomic, strong) UIStoryboard *storyboard;')
62
+ implementation.push('@end')
63
+
64
+ # Create the implementation of the base storyboard class
65
+ implementation.push("@implementation #{class_name}")
66
+ implementation.push('- (instancetype)initWithName:(NSString *)name bundle:(NSBundle *)bundleOrNil {')
67
+ implementation.push("\tself = [super init];")
68
+ implementation.push("\tif (self) {")
69
+ implementation.push("\t\t_storyboard = [UIStoryboard storyboardWithName:name bundle:bundleOrNil];")
70
+ implementation.push("\t}")
71
+ implementation.push("\treturn self;")
72
+ implementation.push('}')
73
+ implementation.push('@end')
74
+
75
+ # Convert to string
76
+ interface = interface.join("\n")
77
+ implementation = implementation.join("\n")
78
+
79
+ @header_file.write(interface + "\n\n")
80
+ @implementation_file.write(implementation + "\n\n")
81
+
82
+ class_name
83
+ end
84
+
85
+ # Generate the class for the provided storyboard
86
+ private
87
+ def create_storyboard_class(storyboard, base_class_name)
88
+ class_name = storyboard.class_name(@prefix)
89
+ puts "Processing storyboard class #{class_name}."
90
+
91
+ interface = Array.new(1, "@interface #{class_name} : #{base_class_name}")
92
+ implementation = Array.new(1, "@implementation #{class_name}")
93
+
94
+ storyboard.view_controllers.each do |vc|
95
+ if vc.initial_view_controller?
96
+ method_signature = "- (#{vc.class_name} *)instantiateInitialViewController;"
97
+ method_body = create_initial_view_controller_instantiation(vc)
98
+ else
99
+ method_signature = "- (#{vc.class_name} *)instantiate#{vc.storyboard_identifier}ViewController;"
100
+ method_body = create_view_controller_instantiation(vc)
101
+ end
102
+
103
+ interface.push(method_signature)
104
+ implementation.push(method_signature + ' {')
105
+ implementation.push("\t" + method_body)
106
+ implementation.push('}')
107
+ end # view controller iterator
108
+
109
+ interface.push('@end')
110
+ implementation.push('@end')
111
+
112
+ # Convert to string
113
+ interface = interface.join("\n")
114
+ implementation = implementation.join("\n")
115
+
116
+ # Output to files
117
+ @header_file.write(interface)
118
+ @header_file.write("\n\n")
119
+ @implementation_file.write(implementation)
120
+ @implementation_file.write("\n\n")
121
+ end
122
+
123
+ # Generate the category for UIStoryboard with methods
124
+ # for each storyboard that has been provided.
125
+ private
126
+ def create_storyboard_category
127
+ interface = Array.new(1, '@interface UIStoryboard (Stronglyboards)')
128
+ implementation = Array.new(1, '@implementation UIStoryboard (Stronglyboards)')
129
+
130
+ @storyboards.each do |storyboard|
131
+ method_signature = "+(#{storyboard.class_name(@prefix)} *)#{storyboard.lowercase_name(@prefix)}Storyboard;"
132
+ interface.push(method_signature)
133
+ implementation.push(method_signature + ' {')
134
+ implementation.push("\t" + create_storyboard_instantiation(storyboard))
135
+ implementation.push('}')
136
+ end
137
+ interface.push('@end')
138
+ implementation.push('@end')
139
+
140
+ # Convert to a string
141
+ interface = interface.join("\n")
142
+ implementation = implementation.join("\n")
143
+
144
+ # Output to file
145
+ puts 'Writing UIStoryboard category.'
146
+ @header_file.write(interface)
147
+ @implementation_file.write(implementation)
148
+
149
+ end
150
+
151
+ # ---- Helpers ----
152
+
153
+ public
154
+ def output_files
155
+ super.push OutputFile.new(@header_file, false)
156
+ end
157
+
158
+ private
159
+ def create_storyboard_instantiation(storyboard)
160
+ class_name = storyboard.class_name(@prefix)
161
+ "return [[#{class_name} alloc] initWithName:@\"#{storyboard.name}\" bundle:nil];"
162
+ end
163
+
164
+ private
165
+ def create_initial_view_controller_instantiation(view_controller)
166
+ "return (#{view_controller.class_name} *)[self.storyboard instantiateInitialViewController];"
167
+ end
168
+
169
+ private
170
+ def create_view_controller_instantiation(view_controller)
171
+ "return (#{view_controller.class_name} *)[self.storyboard instantiateViewControllerWithIdentifier:@\"#{view_controller.storyboard_identifier}\"];"
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,107 @@
1
+ require_relative 'source_generator'
2
+ require_relative 'view_controller'
3
+
4
+ module Stronglyboards
5
+ class SourceGeneratorSwift < AbstractSourceGenerator
6
+
7
+ public
8
+ def initialize(prefix, output_file_name)
9
+ @implementation_file_path = output_file_name + '.swift'
10
+
11
+ super(prefix, @implementation_file_path)
12
+ end
13
+
14
+ def parse_storyboards
15
+
16
+ puts "Source file: #{@implementation_file_path}"
17
+
18
+ # Generate framework imports
19
+ @implementation_file.write("import UIKit\n\n")
20
+
21
+ # Generate the base storyboard class
22
+ base_class_name = create_base_storyboard_class
23
+
24
+ # Generate classes for each storyboard
25
+ @storyboards.each { |s| create_storyboard_class(s, base_class_name) }
26
+
27
+ # Generate the storyboard category
28
+ create_storyboard_category
29
+
30
+ output_files
31
+ end
32
+
33
+ private
34
+ def create_base_storyboard_class
35
+ class_name = "#{@prefix}Stronglyboard"
36
+ output = Array.new(1, "class #{class_name} {")
37
+ output.push "\tlet storyboard: UIStoryboard"
38
+ output.push "\tinit(name: String, bundle: NSBundle?) {"
39
+ output.push "\t\tstoryboard = UIStoryboard(name: name, bundle: bundle)"
40
+ output.push "\t}"
41
+ output.push '}'
42
+
43
+ # Convert to string and write to file
44
+ output = output.join("\n")
45
+ @implementation_file.write(output + "\n\n")
46
+
47
+ class_name
48
+ end
49
+
50
+ # Generate the class for the provided storyboard
51
+ private
52
+ def create_storyboard_class(storyboard, base_class_name)
53
+ class_name = storyboard.class_name(@prefix)
54
+ puts "Processing storyboard class #{class_name}."
55
+
56
+ output = Array.new(1, "class #{class_name} : #{base_class_name} {")
57
+
58
+ storyboard.view_controllers.each do |vc|
59
+ cast = " as! #{vc.class_name}" unless vc.class_name == ViewController::UIVIEWCONTROLLER
60
+ if vc.initial_view_controller?
61
+ cast = '!' if vc.class_name == ViewController::UIVIEWCONTROLLER
62
+ output.push "\tfunc instantiateInitialViewController() -> #{vc.class_name} {"
63
+ output.push "\t\treturn self.storyboard.instantiateInitialViewController()#{cast}"
64
+ else
65
+ output.push "\tfunc instantiate#{vc.storyboard_identifier}ViewController() -> #{vc.class_name} {"
66
+ output.push "\t\treturn self.storyboard.instantiateViewControllerWithIdentifier(\"#{vc.storyboard_identifier}\") #{cast}"
67
+ end
68
+ output.push "\t}"
69
+ end # view controller iterator
70
+
71
+ # End the storyboard subclass
72
+ output.push '}'
73
+
74
+ # Convert to string
75
+ output = output.join("\n")
76
+
77
+ # Output to files
78
+ @implementation_file.write(output)
79
+ @implementation_file.write("\n\n")
80
+ end
81
+
82
+ # Generate the category for UIStoryboard with methods
83
+ # for each storyboard that has been provided
84
+ private
85
+ def create_storyboard_category
86
+ output = Array.new(1, 'extension UIStoryboard {')
87
+
88
+ @storyboards.each do |storyboard|
89
+ class_name = storyboard.class_name(@prefix)
90
+ func_name = "#{storyboard.lowercase_name(@prefix)}Storyboard"
91
+ output.push "\tclass func #{func_name}() -> #{class_name} {"
92
+ output.push "\t\treturn #{class_name}(name: \"#{storyboard.name}\", bundle: nil)"
93
+ output.push "\t}"
94
+ end
95
+
96
+ output.push '}'
97
+
98
+ # Convert to a string
99
+ output = output.join("\n")
100
+
101
+ # Output to file
102
+ puts 'Writing UIStoryboard category.'
103
+ @implementation_file.write(output)
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,69 @@
1
+ require 'nokogiri'
2
+ require_relative 'view_controller'
3
+
4
+ module Stronglyboards
5
+ class Storyboard
6
+
7
+ EXTENSION = '.storyboard'
8
+
9
+ attr_reader :name
10
+ attr_reader :view_controllers
11
+
12
+ def initialize(full_path)
13
+ @name = File.basename(full_path, EXTENSION)
14
+
15
+ file = File.open(full_path)
16
+ @xml = Nokogiri::XML(file)
17
+ file.close
18
+
19
+ @view_controllers = Array.new
20
+
21
+ # Find the initial view controller
22
+ initial_view_controller = find_initial_view_controller
23
+ @view_controllers.push(initial_view_controller) if initial_view_controller
24
+
25
+ # Find other view controllers
26
+ @view_controllers += find_view_controllers_with_storyboard_identifiers
27
+ end
28
+
29
+ # Searches for the initial view controller
30
+ private
31
+ def find_initial_view_controller
32
+ initial_vc_identifier = @xml.at_xpath('document').attr('initialViewController')
33
+ view_controller_xml = object_with_identifier(initial_vc_identifier) if initial_vc_identifier
34
+ if view_controller_xml
35
+ ViewController.new(view_controller_xml, true)
36
+ end
37
+ end
38
+
39
+ # Searches for view controllers
40
+ private
41
+ def find_view_controllers_with_storyboard_identifiers
42
+ view_controllers = @xml.xpath('//scene/objects/*[@storyboardIdentifier]')
43
+ view_controllers.collect { |xml| ViewController.new(xml) } if view_controllers
44
+ end
45
+
46
+ # --------- Helpers ---------
47
+
48
+ private
49
+ def object_with_identifier(identifier)
50
+ @xml.at_xpath("//scene/objects/*[@id='#{identifier}']")
51
+ end
52
+
53
+ public
54
+ def class_name(prefix = nil)
55
+ prefix + @name + 'Storyboard'
56
+ end
57
+
58
+ def lowercase_name(prefix = nil)
59
+ lower = @name.dup
60
+ lower[0] = lower[0].downcase
61
+ if prefix == nil || prefix.length == 0
62
+ lower
63
+ else
64
+ prefix.downcase + '_' + lower
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module Stronglyboards
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,47 @@
1
+ module Stronglyboards
2
+ class ViewController
3
+
4
+ attr_reader :class_name
5
+ attr_reader :storyboard_identifier
6
+
7
+ UIVIEWCONTROLLER = 'UIViewController'
8
+ UITABLEVIEWCONTROLLER = 'UITableViewController'
9
+ UINAVIGATIONCONTROLLER = 'UINavigationController'
10
+ UITABBARCONTROLLER = 'UITabBarController'
11
+ UICOLLECTIONVIEWCONTROLLER = 'UICollectionViewController'
12
+ UISPLITVIEWCONTROLLER = 'UISplitViewController'
13
+ UIPAGEVIEWCONTROLLER = 'UIPageViewController'
14
+
15
+ def initialize(xml, is_initial_view_controller = false)
16
+ @class_name = xml.attr('customClass') || class_name_from_type(xml)
17
+ @storyboard_identifier = xml.attr('storyboardIdentifier')
18
+ @is_initial_view_controller = is_initial_view_controller
19
+ end
20
+
21
+ def initial_view_controller?
22
+ @is_initial_view_controller
23
+ end
24
+
25
+ # Determines the name of the class from this view controller's type
26
+ private
27
+ def class_name_from_type(xml)
28
+ case xml.name
29
+ when 'viewController'
30
+ UIVIEWCONTROLLER
31
+ when 'tableViewController'
32
+ UITABLEVIEWCONTROLLER
33
+ when 'navigationController'
34
+ UINAVIGATIONCONTROLLER
35
+ when 'tabBarController'
36
+ UITABBARCONTROLLER
37
+ when 'collectionViewController'
38
+ UICOLLECTIONVIEWCONTROLLER
39
+ when 'splitViewController'
40
+ UISPLITVIEWCONTROLLER
41
+ when 'pageViewController'
42
+ UIPAGEVIEWCONTROLLER
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'stronglyboards/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'stronglyboards'
8
+ spec.version = Stronglyboards::VERSION
9
+ spec.date = '2015-10-16'
10
+
11
+ spec.summary = 'A strongly-typed interface for your storyboards, view controllers, and segues.'
12
+ spec.description = 'Generates a strongly-typed interface for your storyboards, view controllers, and segues.'
13
+ spec.license = 'MIT'
14
+
15
+ spec.authors = ['Steve Wilford']
16
+ spec.email = 'steve@offtopic.io'
17
+ spec.homepage = 'http://stevewilford.co.uk/stronglyboards'
18
+
19
+ spec.files = `git ls-files`.split($/)
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.required_ruby_version = '>= 2.0.0'
25
+
26
+ spec.add_runtime_dependency 'xcodeproj', '>= 0.28.2'
27
+ spec.add_runtime_dependency 'nokogiri', '>= 1.6.6.2'
28
+ spec.add_runtime_dependency 'thor', '>= 0.19.1'
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stronglyboards
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Steve Wilford
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: xcodeproj
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.28.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.28.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.6.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.6.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.19.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.19.1
55
+ description: Generates a strongly-typed interface for your storyboards, view controllers,
56
+ and segues.
57
+ email: steve@offtopic.io
58
+ executables:
59
+ - stronglyboards
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .gitignore
64
+ - LICENSE.md
65
+ - README.md
66
+ - bin/stronglyboards
67
+ - lib/stronglyboards.rb
68
+ - lib/stronglyboards/lock_file.rb
69
+ - lib/stronglyboards/source_generator.rb
70
+ - lib/stronglyboards/source_generator_objc.rb
71
+ - lib/stronglyboards/source_generator_swift.rb
72
+ - lib/stronglyboards/storyboard.rb
73
+ - lib/stronglyboards/version.rb
74
+ - lib/stronglyboards/view_controller.rb
75
+ - stronglyboards.gemspec
76
+ homepage: http://stevewilford.co.uk/stronglyboards
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: 2.0.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.0.14
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: A strongly-typed interface for your storyboards, view controllers, and segues.
100
+ test_files: []