stronglyboards 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []