1import fs from 'fs'; 2import path from 'path'; 3 4import { getAppDelegate, getSourceRoot } from './Paths'; 5import { withBuildSourceFile } from './XcodeProjectFile'; 6import { addResourceFileToGroup, getProjectName } from './utils/Xcodeproj'; 7import { ConfigPlugin, XcodeProject } from '../Plugin.types'; 8import { withXcodeProject } from '../plugins/ios-plugins'; 9 10const templateBridgingHeader = `// 11// Use this file to import your target's public headers that you would like to expose to Swift. 12// 13`; 14 15/** 16 * Ensure a Swift bridging header is created for the project. 17 * This helps fix problems related to using modules that are written in Swift (lottie, FBSDK). 18 * 19 * 1. Ensures the file exists given the project path. 20 * 2. Writes the file and links to Xcode as a resource file. 21 * 3. Sets the build configuration `SWIFT_OBJC_BRIDGING_HEADER = [PROJECT_NAME]-Bridging-Header.h` 22 */ 23export const withSwiftBridgingHeader: ConfigPlugin = (config) => { 24 return withXcodeProject(config, (config) => { 25 config.modResults = ensureSwiftBridgingHeaderSetup({ 26 project: config.modResults, 27 projectRoot: config.modRequest.projectRoot, 28 }); 29 return config; 30 }); 31}; 32 33export function ensureSwiftBridgingHeaderSetup({ 34 projectRoot, 35 project, 36}: { 37 projectRoot: string; 38 project: XcodeProject; 39}) { 40 // Only create a bridging header if using objective-c 41 if (shouldCreateSwiftBridgingHeader({ projectRoot, project })) { 42 const projectName = getProjectName(projectRoot); 43 const bridgingHeader = createBridgingHeaderFileName(projectName); 44 // Ensure a bridging header is created in the Xcode project. 45 project = createBridgingHeaderFile({ 46 project, 47 projectName, 48 projectRoot, 49 bridgingHeader, 50 }); 51 // Designate the newly created file as the Swift bridging header in the Xcode project. 52 project = linkBridgingHeaderFile({ 53 project, 54 bridgingHeader: path.join(projectName, bridgingHeader), 55 }); 56 } 57 return project; 58} 59 60function shouldCreateSwiftBridgingHeader({ 61 projectRoot, 62 project, 63}: { 64 projectRoot: string; 65 project: XcodeProject; 66}): boolean { 67 // Only create a bridging header if the project is using in Objective C (AppDelegate is written in Objc). 68 const isObjc = getAppDelegate(projectRoot).language !== 'swift'; 69 return isObjc && !getDesignatedSwiftBridgingHeaderFileReference({ project }); 70} 71 72/** 73 * @returns String matching the default name used when Xcode automatically creates a bridging header file. 74 */ 75function createBridgingHeaderFileName(projectName: string): string { 76 return `${projectName}-Bridging-Header.h`; 77} 78 79export function getDesignatedSwiftBridgingHeaderFileReference({ 80 project, 81}: { 82 project: XcodeProject; 83}): string | null { 84 const configurations = project.pbxXCBuildConfigurationSection(); 85 // @ts-ignore 86 for (const { buildSettings } of Object.values(configurations || {})) { 87 // Guessing that this is the best way to emulate Xcode. 88 // Using `project.addToBuildSettings` modifies too many targets. 89 if (typeof buildSettings?.PRODUCT_NAME !== 'undefined') { 90 if ( 91 typeof buildSettings.SWIFT_OBJC_BRIDGING_HEADER === 'string' && 92 buildSettings.SWIFT_OBJC_BRIDGING_HEADER 93 ) { 94 return buildSettings.SWIFT_OBJC_BRIDGING_HEADER; 95 } 96 } 97 } 98 return null; 99} 100 101/** 102 * 103 * @param bridgingHeader The bridging header filename ex: `ExpoAPIs-Bridging-Header.h` 104 * @returns 105 */ 106export function linkBridgingHeaderFile({ 107 project, 108 bridgingHeader, 109}: { 110 project: XcodeProject; 111 bridgingHeader: string; 112}): XcodeProject { 113 const configurations = project.pbxXCBuildConfigurationSection(); 114 // @ts-ignore 115 for (const { buildSettings } of Object.values(configurations || {})) { 116 // Guessing that this is the best way to emulate Xcode. 117 // Using `project.addToBuildSettings` modifies too many targets. 118 if (typeof buildSettings?.PRODUCT_NAME !== 'undefined') { 119 buildSettings.SWIFT_OBJC_BRIDGING_HEADER = bridgingHeader; 120 } 121 } 122 123 return project; 124} 125 126export function createBridgingHeaderFile({ 127 projectRoot, 128 projectName, 129 project, 130 bridgingHeader, 131}: { 132 project: XcodeProject; 133 projectName: string; 134 projectRoot: string; 135 bridgingHeader: string; 136}): XcodeProject { 137 const bridgingHeaderProjectPath = path.join(getSourceRoot(projectRoot), bridgingHeader); 138 if (!fs.existsSync(bridgingHeaderProjectPath)) { 139 // Create the file 140 fs.writeFileSync(bridgingHeaderProjectPath, templateBridgingHeader, 'utf8'); 141 } 142 143 // This is non-standard, Xcode generates the bridging header in `/ios` which is kinda annoying. 144 // Instead, this'll generate the default header in the application code folder `/ios/myproject/`. 145 const filePath = `${projectName}/${bridgingHeader}`; 146 // Ensure the file is linked with Xcode resource files 147 if (!project.hasFile(filePath)) { 148 project = addResourceFileToGroup({ 149 filepath: filePath, 150 groupName: projectName, 151 project, 152 // Not sure why, but this is how Xcode generates it. 153 isBuildFile: false, 154 verbose: false, 155 }); 156 } 157 return project; 158} 159 160export const withNoopSwiftFile: ConfigPlugin = (config) => { 161 return withBuildSourceFile(config, { 162 filePath: 'noop-file.swift', 163 contents: [ 164 '//', 165 '// @generated', 166 '// A blank Swift file must be created for native modules with Swift files to work correctly.', 167 '//', 168 '', 169 ].join('\n'), 170 }); 171}; 172