Search code examples
rubyxcodeproj

Xcodeproj add custom property to object


I want to add onlyGenerateCoverageForSpecifiedTargets property to TestAction object programmatically. According to the documentation this property is not yet supported. So I need to add a custom property to an object. Also I need to add CodeCoverageTargets group. Here is my code:

scheme = Xcodeproj::XCScheme.new
scheme.add_build_target(app_target)
scheme.set_launch_target(app_target)
scheme.add_test_target(target)

test_action = scheme.test_action
test_action.code_coverage_enabled = true

# add onlyGenerateCoverageForSpecifiedTargets = true

scheme.test_action = test_action
scheme.save_as(xcode_proj_dir, name)

Here is xml structure when I add property from Xcode GUI.

   <TestAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      shouldUseLaunchSchemeArgsEnv = "YES"
      codeCoverageEnabled = "YES"
      onlyGenerateCoverageForSpecifiedTargets = "YES">
      <MacroExpansion>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "D7CE66BC1C7DE6F700FC64CC"
            BuildableName = "AppName.app"
            BlueprintName = "AppName"
            ReferencedContainer = "container:buddyui.xcodeproj">
         </BuildableReference>
      </MacroExpansion>
      <CodeCoverageTargets>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "D7CE66BC1C7DE6F700FC64CC"
            BuildableName = "AppName.app"
            BlueprintName = "AppName"
            ReferencedContainer = "container:buddyui.xcodeproj">
         </BuildableReference>
      </CodeCoverageTargets>

Solution

  • I'll say it first: I know nothing about the Xcodeproj Gem nor the logic behind Xcode metadata. Take my code as a starter for further improvements.

    You have a few ways of achieving what you asked:

    1. MonkeyPatch Xcodeproj. That is what I did, sorry for that :-P

    2. Extend Xcodeproj classes. That would be the recommended solution.

    3. Manipulate the XML file or the XCScheme object directly, with REXML.

    Here comes my proposal. I added a few methods to TestAction (based on the code of similar existing methods) and created the additional class CodeCoverageTargets (based on the class MacroExpansion). As I don't know how Xcode works, I chose to create the method add_code_coverage_targets in XCScheme instead of overwriting set_launch_target (where MacroExpansion is instantiated).

    require 'xcodeproj'
    
    class Xcodeproj::XCScheme
    
      def add_code_coverage_targets(build_target)
        code_cov_targets = CodeCoverageTargets.new(build_target)
        test_action.add_code_coverage_targets(code_cov_targets)
      end
    
      class CodeCoverageTargets < XMLElementWrapper
        def initialize(target_or_node = nil)
          create_xml_element_with_fallback(target_or_node, 'CodeCoverageTargets') do
            self.buildable_reference = BuildableReference.new(target_or_node) if target_or_node
          end
        end
        def buildable_reference
          @buildable_reference ||= BuildableReference.new @xml_element.elements['BuildableReference']
        end
        def buildable_reference=(ref)
          @xml_element.delete_element('BuildableReference')
          @xml_element.add_element(ref.xml_element)
          @buildable_reference = ref
        end
      end
    
      class TestAction
        def only_generate_coverage_for_specified_targets?
          string_to_bool(@xml_element.attributes['onlyGenerateCoverageForSpecifiedTargets'])
        end
        def only_generate_coverage_for_specified_targets=(flag)
          @xml_element.attributes['onlyGenerateCoverageForSpecifiedTargets'] = bool_to_string(flag)
        end
        def code_coverage_targets
          @xml_element.get_elements('CodeCoverageTargets').map do |node|
            CodeCoverageTargets.new(node)
          end
        end
        def add_code_coverage_targets(code_coverage_targets)
          @xml_element.add_element(code_coverage_targets.xml_element)
        end
    
      end
    end
    

    You can use it like this:

    xcode_proj_dir = 'Desktop/SO/66719313/DummyApp.xcodeproj'
    xcode_proj = Xcodeproj::Project.open(xcode_proj_dir)
    app_target = xcode_proj.targets.first
    
    scheme = Xcodeproj::XCScheme.new
    scheme.add_build_target(app_target)
    scheme.set_launch_target(app_target)
    #scheme.add_test_target(app_target)
    scheme.add_code_coverage_targets(app_target) # new method
    
    test_action = scheme.test_action
    test_action.code_coverage_enabled = true
    test_action.only_generate_coverage_for_specified_targets = true # new method
    puts test_action