I am migrating an existing project in Xcode to using Tuist. The application has a main app, Watch App, and a notification extension. There is loads of shared code between each project.
How can I use Tuist to share a framework between the iOS and watchOS code?
I think I have a solution for sharing code between watchOS and iOS. The first step is that a framework needs to be generated for each platform. To make the frameworks integrate into the larger application better I did the follow:
Set PRODUCT_MODULE_NAME and PRODUCT_NAME to the target name of the iOS app. This will allow you to import your platform-specific framework using the same name.
Included optional platform-specific source folders.
Example:
/* File structure
Project.swift
Tuist
ProjectDescriptionHelpers
Project+Templates.swift
Projects
MyFramework
Sources
Sources-watchOS
MyLogging
Sources
Sources-iOS
Sources-watchOS
*/
//Project.swift
var targets: [Target] = []
targets += Target.makeFrameworkTargets(name: "MyLogging", platforms: [.iOS, .watchOS])
targets += Target.makeFrameworkTargets(name: "MyFramework", platforms: [.iOS, .watchOS], dependencies:["MyLogging"])
// Tuist/ProjectDescriptionHelpers/Project+Templates.swift
extension Target {
// Project Root. Not sure the best way to do this in Tuist
static let rootDir: URL = URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
public static func makeFrameworkTargets(name: String,
platforms: [Platform],
dependencies: [String] = []) -> [Target] {
var projectTargets: [Target] = []
for platform in platforms {
let targetDependencies: [TargetDependency] = dependencies.map {
let title = platform == .iOS ? $0 : "\($0)-\(platform)"
return .target(name: title)
}
let title = platform == .iOS ? name : "\(name)-\(platform)"
var settingsDict: SettingsDictionary = [:]
settingsDict["PRODUCT_MODULE_NAME"] = .string(name)
settingsDict["PRODUCT_NAME"] = .string(name)
var sources = ["Projects/\(name)/Sources/**/*.swift"]
let platformSource = rootDir.appendingPathComponent("/Projects/\(name)/Sources-\(platform)")
if FileManager.default.fileExists(atPath: platformSource.path) {
sources.append("Projects/\(name)/Sources-\(platform)/**/*.swift")
}
let settings = Settings.settings(base: settingsDict, configurations: [])
projectTargets.append(Target(name: title,
platform: platform,
product: .framework,
bundleId: "io.tuist.\(title)",
infoPlist: .default,
sources: SourceFilesList(globs: sources),
dependencies: targetDependencies,
settings: settings))
}
return projectTargets
}
}
Related
We are using the Facebook Unity SDK 15.1.0 in Unity 2019.4.40f1. This SDK has some serious bugs. One of them is that it won't add the required frameworks to the Unity-iPhone target. The project will build, but immediately crash on startup.
The frameworks are there, but in a Pod:
I can add them manually in the General => Frameworks, Libraries, and Embedded Content section of the target:
Everything works fine then.
However, doing this after every build is quite tedious, so I would like to automate this task via a post build script. I am scratching my head about this, since I cannot find good samples online that actually work.
So my question is: How do you add a .xcframework buried in Facebook SDK's pods so it correctly shows up in the target?
Here is how i would do it. This may not be 100% correct but good enough to modify it to make it work for you. Basically you need to use PBXProject.AddFrameworkToProject to add frameworks.
#if UNITY_IOS
[PostProcessBuild(1)]
public static void ChangeXcodePlist(BuildTarget buildTarget, string pathToBuiltProject) {
if (buildTarget == BuildTarget.iOS) {
// get pbx project path
var projPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
if (File.Exists(projPath))
{
var proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
string mainTargetGuid = null, testTargetGuid = null, frameworkTargetGuid = null;
#if UNITY_2019_4_OR_NEWER // APIs are different for getting main unity targets changes based on versions
mainTargetGuid = proj.GetUnityMainTargetGuid();
frameworkTargetGuid = proj.GetUnityFrameworkTargetGuid();
#else
mainTargetGuid =
proj.TargetGuidByName(PBXProject.GetUnityTargetName());
testTargetGuid =
proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());
frameworkTargetGuid = proj.TargetGuidByName("UnityFramework");
#endif
// add your frameworks here
if (!String.IsNullOrEmpty(mainTargetGuid))
{
Debug.Log("Adding targets to mainTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
// add to test target aswell if exists
if (!String.IsNullOrEmpty(testTargetGuid))
{
Debug.Log("Adding targets to testTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
if (!String.IsNullOrEmpty(frameworkTargetGuid))
{
Debug.Log("Adding targets to frameworkTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
proj.WriteToFile(projPath);
}
}
}
#endif
You can also use PBXProject.ContainsFramework before you include the frameworks.
The code in the previous answer is not working. Freimworks are not added like that.
Correct code for adding facebook's frameworks (working):
[PostProcessBuild(1000)]
public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject) {
if (buildTarget != BuildTarget.iOS) return;
string projectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
PBXProject project = new PBXProject();
project.ReadFromFile(projectPath);
string mainTargetGuid = project.GetUnityMainTargetGuid();
List<string> frameworks = new List<string> {
"FBSDKCoreKit",
"FBSDKGamingServicesKit"
};
foreach (string framework in frameworks) {
string frameworkName = framework + ".xcframework";
var src = Path.Combine("Pods", framework, "XCFrameworks", frameworkName);
var frameworkPath = project.AddFile(src, src);
project.AddFileToBuild(mainTargetGuid, frameworkPath);
project.AddFileToEmbedFrameworks(mainTargetGuid, frameworkPath);
}
// Write.
project.WriteToFile(projectPath);
}
I am new to Android/KotlinMultiplatform , I am trying to create a library for iOS/Android using Kotlin Multiplatform.
When I run the command on terminal
./gradlew :shared:packForXcode
It succeeds but could not find a /build/xcode-frameworks folder inside the root folder.
Could anyone help me to find where it is going wrong...?
IntelliJ CE Version : 2020.2.3
My Gradle file Content:
plugins {
id("org.jetbrains.kotlin.multiplatform") version "1.4.10"
id("com.android.library")
id("kotlin-android-extensions")
"maven-publish"
}
repositories {
mavenCentral()
}
group "me.myname"
version "0.0.1"
kotlin {
targets {
android()
ios {
binaries {
framework {
baseName = "MyLib"
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
}
}
val androidMain by getting {
dependencies { }
}
val iosMain by getting {
dependencies { }
}
}
}
android {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(24)
targetSdkVersion(29)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
}
val packForXcode by tasks.creating(Sync::class) {
val targetDir = File(buildDir, "xcode-frameworks")
/// selecting the right configuration for the iOS
/// framework depending on the environment
/// variables set by Xcode build
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
val framework = kotlin.targets
.getByName<KotlinNativeTarget>(
if(isiOSDevice) {
"iosArm64"
} else {
"iosX64"
}
)
.binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
from({ framework.outputDirectory })
into(targetDir)
println("Build Folder => $targetDir")
/// generate a helpful ./gradlew wrapper with embedded Java path
doLast {
val gradlew = File(targetDir, "gradlew")
gradlew.writeText("#!/bin/bash\n"
+ "export 'JAVA_HOME=${System.getProperty("java.home")}'\n"
+ "cd '${rootProject.rootDir}'\n"
+ "./gradlew \$#\n")
gradlew.setExecutable(true)
}
}
tasks.build.dependsOn("packForXCode")
UPDATE
Project Created using IntelliJ IDEA, as below screenshot:
My project structure looks like below:
I've only been able to see the template of your screenshot by using
IntelliJ 2020.2.3 Ultimate
This template doesn't have the packForXcode task set by default, so you would have put it by hands I suppose.
Anyway, with a cleaned project, if you run it, you could have the debug framework in the build folder where you want to have it.
You should have, of course, at least one source (Greeting.kt) file like the one I've shown you in my pic.
I suggest you to look deep at the documentation starting from here and here.
If I remember correctly, this task is not designed to be executed manually. It should be triggered as a part of the Xcode project build, see in the documentation. Please try to follow the steps from the documentation, and see if the framework connects and works fine from Xcode.
I've heard with the Xcode 12 (now in Beta 6), Swift package manager is now able to include resources. But I am not able to open a reality (.rcproject) file.
Here is what I have tried; (& you can reproduce)
I created a new Augmented Reality App project. (RealityKit + SwiftUI + Swift)
Now if you try to run the project, everything works, you see a default metallic box.
Now I created a new SPM (Swift package manager)
Now I dragged locally created SPM to the project and added it to frameworks in General > Targets tab. (To inform the project about locally added spm)
I dragged Experience.rcproject & ContentView (also copied the autogenerated Experience enum, you can reach it via Cmd+Click) to SPM
Fixed some access initializer issue for ContentView & added platform support platforms: [.iOS(.v13)], in the SPM
Added resources in the SPM for the path Experience.rcproject exist
After those steps finished I'd except to have an AR included swift package manager.
But auto generated Experience enum throws .fileNotFound("Experience.reality") error.
Seems still not able to find reality file in Bundle?
Have you tried something similar. Waiting any helps. Thanks..
Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ARSPM",
platforms: [.iOS(.v13)],
products: [
.library(
name: "ARSPM",
targets: ["ARSPM"]),
],
dependencies: [],
targets: [
.target(
name: "ARSPM",
dependencies: [], resources: [
.copy("Resources")
]),
.testTarget(
name: "ARSPMTests",
dependencies: ["ARSPM"]),
]
)
ARView.swift
import SwiftUI
import RealityKit
public struct EKARView : View {
public init() { }
public var body: some View {
return ARViewContainer().edgesIgnoringSafeArea(.all)
}
}
public struct ARViewContainer: UIViewRepresentable {
public func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
// Load the "Box" scene from the "Experience" Reality File
let boxAnchor = try! Experience.loadBox()
// Add the box anchor to the scene
arView.scene.anchors.append(boxAnchor)
return arView
}
public func updateUIView(_ uiView: ARView, context: Context) {}
}
GeneratedExperienceFile.swift
//
// Experience.swift
// GENERATED CONTENT. DO NOT EDIT.
//
import Foundation
import RealityKit
import simd
import Combine
internal enum Experience {
public enum LoadRealityFileError: Error {
case fileNotFound(String)
}
private static var streams = [Combine.AnyCancellable]()
public static func loadBox() throws -> Experience.Box {
guard let realityFileURL =
// Also tried >> Foundation.Bundle.module
Foundation.Bundle(for: Experience.Box.self)
.url(forResource: "Experience", withExtension: "reality") else {
throw Experience.LoadRealityFileError.fileNotFound("Experience.reality")
}
let realityFileSceneURL = realityFileURL.appendingPathComponent("Box", isDirectory: false)
let anchorEntity = try Experience.Box.loadAnchor(contentsOf: realityFileSceneURL)
return createBox(from: anchorEntity)
}
public static func loadBoxAsync(completion: #escaping (Swift.Result<Experience.Box, Swift.Error>) -> Void) {
guard let realityFileURL = Foundation.Bundle(for: Experience.Box.self).url(forResource: "Experience", withExtension: "reality") else {
completion(.failure(Experience.LoadRealityFileError.fileNotFound("Experience.reality")))
return
}
var cancellable: Combine.AnyCancellable?
let realityFileSceneURL = realityFileURL.appendingPathComponent("Box", isDirectory: false)
let loadRequest = Experience.Box.loadAnchorAsync(contentsOf: realityFileSceneURL)
cancellable = loadRequest.sink(receiveCompletion: { loadCompletion in
if case let .failure(error) = loadCompletion {
completion(.failure(error))
}
streams.removeAll { $0 === cancellable }
}, receiveValue: { entity in
completion(.success(Experience.createBox(from: entity)))
})
cancellable?.store(in: &streams)
}
private static func createBox(from anchorEntity: RealityKit.AnchorEntity) -> Experience.Box {
let box = Experience.Box()
box.anchoring = anchorEntity.anchoring
box.addChild(anchorEntity)
return box
}
public class Box: RealityKit.Entity, RealityKit.HasAnchoring {
public var steelBox: RealityKit.Entity? {
return self.findEntity(named: "Steel Box")
}
}
}
And in ContentView file, I simple show EKARView.
Xcode knows how to process a .rcproject file into the .reality file it needs when building an application. Unfortunately this processing isn't done when accessing the project file using a Swift Package.
The steps you've outlined will almost work. What it comes down to is using the already compiled .reality file in place of the .rcproject file. In order to transform the default .rcproject file, you'll need to use Apple's Reality Composer application.
steps outlined using Xcode 12...
With the Experience.rcproject file selected, click the 'Open in Reality Composer' button.
Once open, export the project through the 'File' menu.
Choose 'Project' and click export. This produces a Experience.reality file.
Place that file in your swift package resources.
Make sure replace the Bundle references in your Experience.swift file with Bundle.module, as the existing reference will target your application bundle.
I'm trying to use a resource file in unit tests and access it with Bundle.path, but it returns nil.
This call in MyProjectTests.swift returns nil:
Bundle(for: type(of: self)).path(forResource: "TestAudio", ofType: "m4a")
Here is my project hierarchy. I also tried moving TestAudio.m4a to a Resources folder:
├── Package.swift
├── Sources
│ └── MyProject
│ ├── ...
└── Tests
└── MyProjectTests
├── MyProjectTests.swift
└── TestAudio.m4a
Here is my package description:
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "MyProject",
products: [
.library(
name: "MyProject",
targets: ["MyProject"])
],
targets: [
.target(
name: "MyProject",
dependencies: []
),
.testTarget(
name: "MyProjectTests",
dependencies: ["MyProject"]
),
]
)
I am using Swift 4 and the Swift Package Manager Description API version 4.
Swift 5.3
See Apple Documentation: "Bundling Resources with a Swift Package"
Swift 5.3 includes Package Manager Resources SE-0271 evolution proposal with "Status: Implemented (Swift 5.3)".
Resources aren't always intended for use by clients of the package; one use of resources might include test fixtures that are only needed by unit tests. Such resources would not be incorporated into clients of the package along with the library code, but would only be used while running the package's tests.
Add a new resources parameter in target and testTarget APIs to allow declaring resource files explicitly.
SwiftPM uses file system conventions for determining the set of source files that belongs to each target in a package: specifically, a target's source files are those that are located underneath the designated "target directory" for the target. By default this is a directory that has the same name as the target and is located in "Sources" (for a regular target) or "Tests" (for a test target), but this location can be customized in the package manifest.
// Get path to DefaultSettings.plist file.
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")
// Load an image that can be in an asset archive in a bundle.
let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))
// Find a vertex function in a compiled Metal shader library.
let shader = try mtlDevice.makeDefaultLibrary(bundle: Bundle.module).makeFunction(name: "vertexShader")
// Load a texture.
let texture = MTKTextureLoader(device: mtlDevice).newTexture(name: "Grass", scaleFactor: 1.0, bundle: Bundle.module, options: options)
Example
// swift-tools-version:5.3
import PackageDescription
targets: [
.target(
name: "Example",
dependencies: [],
resources: [
// Apply platform-specific rules.
// For example, images might be optimized per specific platform rule.
// If path is a directory, the rule is applied recursively.
// By default, a file will be copied if no rule applies.
// Process file in Sources/Example/Resources/*
.process("Resources"),
]),
.testTarget(
name: "ExampleTests",
dependencies: [Example],
resources: [
// Copy Tests/ExampleTests/Resources directories as-is.
// Use to retain directory structure.
// Will be at top level in bundle.
.copy("Resources"),
]),
Reported Issues & Possible Workarounds
Swift 5.3 SPM Resources in tests uses wrong bundle path?
Swift Package Manager - Resources in test targets
Xcode
Bundle.module is generated by SwiftPM (see Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) and thus not present in Foundation.Bundle when built by Xcode.
A comparable approach in Xcode would be to:
manually add a Resources reference folder to the Xcode project,
add an Xcode build phase copy to put the Resource into some *.bundle directory,
add a some custom #ifdef XCODE_BUILD compiler directive for the Xcode build to work with the resources.
#if XCODE_BUILD
extension Foundation.Bundle {
/// Returns resource bundle as a `Bundle`.
/// Requires Xcode copy phase to locate files into `ExecutableName.bundle`;
/// or `ExecutableNameTests.bundle` for test resources
static var module: Bundle = {
var thisModuleName = "CLIQuickstartLib"
var url = Bundle.main.bundleURL
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
url = bundle.bundleURL.deletingLastPathComponent()
thisModuleName = thisModuleName.appending("Tests")
}
url = url.appendingPathComponent("\(thisModuleName).bundle")
guard let bundle = Bundle(url: url) else {
fatalError("Foundation.Bundle.module could not load resource bundle: \(url.path)")
}
return bundle
}()
/// Directory containing resource bundle
static var moduleDir: URL = {
var url = Bundle.main.bundleURL
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
// remove 'ExecutableNameTests.xctest' path component
url = bundle.bundleURL.deletingLastPathComponent()
}
return url
}()
}
#endif
SwiftPM (5.1) does not support resources natively yet, however...
When unit tests are running, the repository can be expected to be available, so simply load the resource with something derived from #file. This works with all extant versions of SwiftPM.
let thisSourceFile = URL(fileURLWithPath: #file)
let thisDirectory = thisSourceFile.deletingLastPathComponent()
let resourceURL = thisDirectory.appendingPathComponent("TestAudio.m4a")
In cases other than tests, where the repository will not be around at runtime, resources can still be included, albeit at the expense of the binary size. Any arbitrary file can be embedded into Swift source by expressing it as base 64 data in a string literal. Workspace is an open‐source tool that can automate that process: $ workspace refresh resources. (Disclaimer: I am its author.)
Bundle.module started to work for me after right file structure and dependencies setup.
File structure for test target:
Dependencies setup in Package.swift:
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Parser",
dependencies: []),
.testTarget(
name: "ParserTests",
dependencies: ["Parser"],
resources: [
.copy("Resources/test.txt")
]
),
]
Usage in the project:
private var testData: Data {
let url = Bundle.module.url(forResource: "test", withExtension: "txt")!
let data = try! Data(contentsOf: url)
return data
}
A Swift script approach for Swift 5.2 and earlier...
Swift Package Manager (SwiftPM)
It is possible to use resources in unit tests with SwiftPM for both macOS and Linux with some additional setup and custom scripts. Here is a description of one possible approach:
The SwiftPM does not yet provide a mechanism for handling resources. The following is a workable approach for using test resources TestResources/ within a package; and, also provides for a consistent TestScratch/ directory for creating test files if needed.
Setup:
Add test resources directory TestResources/ in the PackageName/ directory.
For Xcode use, add test resources to project "Build Phases" for the test bundle target.
Project Editor > TARGETS > CxSQLiteFrameworkTests > Build Phases > Copy Files: Destination Resources, + add files
For command line use, set up Bash aliases which include swift-copy-testresources.swift
Place an executable version of swift-copy-testresources.swift on an appropriate path which is included $PATH.
Ubuntu: nano ~/bin/ swift-copy-testresources.swift
Bash Aliases
macOS: nano .bash_profile
alias swiftbuild='swift-copy-testresources.swift $PWD; swift build -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.13";'
alias swifttest='swift-copy-testresources.swift $PWD; swift test -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.13";'
alias swiftxcode='swift package generate-xcodeproj --xcconfig-overrides Package.xcconfig; echo "REMINDER: set Xcode build system."'
Ubuntu: nano ~/.profile. Apppend to end. Change /opt/swift/current to where Swift is installed for a given system.
#############
### SWIFT ###
#############
if [ -d "/opt/swift/current/usr/bin" ] ; then
PATH="/opt/swift/current/usr/bin:$PATH"
fi
alias swiftbuild='swift-copy-testresources.swift $PWD; swift build;'
alias swifttest='swift-copy-testresources.swift $PWD; swift test;'
Script: swift-copy-testresources.sh chmod +x
#!/usr/bin/swift
// FILE: swift-copy-testresources.sh
// verify swift path with "which -a swift"
// macOS: /usr/bin/swift
// Ubuntu: /opt/swift/current/usr/bin/swift
import Foundation
func copyTestResources() {
let argv = ProcessInfo.processInfo.arguments
// for i in 0..<argv.count {
// print("argv[\(i)] = \(argv[i])")
// }
let pwd = argv[argv.count-1]
print("Executing swift-copy-testresources")
print(" PWD=\(pwd)")
let fm = FileManager.default
let pwdUrl = URL(fileURLWithPath: pwd, isDirectory: true)
let srcUrl = pwdUrl
.appendingPathComponent("TestResources", isDirectory: true)
let buildUrl = pwdUrl
.appendingPathComponent(".build", isDirectory: true)
let dstUrl = buildUrl
.appendingPathComponent("Contents", isDirectory: true)
.appendingPathComponent("Resources", isDirectory: true)
do {
let contents = try fm.contentsOfDirectory(at: srcUrl, includingPropertiesForKeys: [])
do { try fm.removeItem(at: dstUrl) } catch { }
try fm.createDirectory(at: dstUrl, withIntermediateDirectories: true)
for fromUrl in contents {
try fm.copyItem(
at: fromUrl,
to: dstUrl.appendingPathComponent(fromUrl.lastPathComponent)
)
}
} catch {
print(" SKIP TestResources not copied. ")
return
}
print(" SUCCESS TestResources copy completed.\n FROM \(srcUrl)\n TO \(dstUrl)")
}
copyTestResources()
Test Utility Code
////////////////
// MARK: - Linux
////////////////
#if os(Linux)
// /PATH_TO_PACKAGE/PackageName/.build/TestResources
func getTestResourcesUrl() -> URL? {
guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
else { return nil }
let packageUrl = URL(fileURLWithPath: packagePath)
let testResourcesUrl = packageUrl
.appendingPathComponent(".build", isDirectory: true)
.appendingPathComponent("TestResources", isDirectory: true)
return testResourcesUrl
}
// /PATH_TO_PACKAGE/PackageName/.build/TestScratch
func getTestScratchUrl() -> URL? {
guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
else { return nil }
let packageUrl = URL(fileURLWithPath: packagePath)
let testScratchUrl = packageUrl
.appendingPathComponent(".build")
.appendingPathComponent("TestScratch")
return testScratchUrl
}
// /PATH_TO_PACKAGE/PackageName/.build/TestScratch
func resetTestScratch() throws {
if let testScratchUrl = getTestScratchUrl() {
let fm = FileManager.default
do {_ = try fm.removeItem(at: testScratchUrl)} catch {}
_ = try fm.createDirectory(at: testScratchUrl, withIntermediateDirectories: true)
}
}
///////////////////
// MARK: - macOS
///////////////////
#elseif os(macOS)
func isXcodeTestEnvironment() -> Bool {
let arg0 = ProcessInfo.processInfo.arguments[0]
// Use arg0.hasSuffix("/usr/bin/xctest") for command line environment
return arg0.hasSuffix("/Xcode/Agents/xctest")
}
// /PATH_TO/PackageName/TestResources
func getTestResourcesUrl() -> URL? {
let testBundle = Bundle(for: CxSQLiteFrameworkTests.self)
let testBundleUrl = testBundle.bundleURL
if isXcodeTestEnvironment() { // test via Xcode
let testResourcesUrl = testBundleUrl
.appendingPathComponent("Contents", isDirectory: true)
.appendingPathComponent("Resources", isDirectory: true)
return testResourcesUrl
}
else { // test via command line
guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
else { return nil }
let packageUrl = URL(fileURLWithPath: packagePath)
let testResourcesUrl = packageUrl
.appendingPathComponent(".build", isDirectory: true)
.appendingPathComponent("TestResources", isDirectory: true)
return testResourcesUrl
}
}
func getTestScratchUrl() -> URL? {
let testBundle = Bundle(for: CxSQLiteFrameworkTests.self)
let testBundleUrl = testBundle.bundleURL
if isXcodeTestEnvironment() {
return testBundleUrl
.deletingLastPathComponent()
.appendingPathComponent("TestScratch")
}
else {
return testBundleUrl
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("TestScratch")
}
}
func resetTestScratch() throws {
if let testScratchUrl = getTestScratchUrl() {
let fm = FileManager.default
do {_ = try fm.removeItem(at: testScratchUrl)} catch {}
_ = try fm.createDirectory(at: testScratchUrl, withIntermediateDirectories: true)
}
}
#endif
File Locations:
Linux
During the swift build and swift test the process environment variable PWD provides a path the package root …/PackageName. The PackageName/TestResources/ files are copied to $PWD/.buid/TestResources. The TestScratch/ directory, if used during test runtime, is created in $PWD/.buid/TestScratch.
.build/
├── debug -> x86_64-unknown-linux/debug
...
├── TestResources
│ └── SomeTestResource.sql <-- (copied from TestResources/)
├── TestScratch
│ └── SomeTestProduct.sqlitedb <-- (created by running tests)
└── x86_64-unknown-linux
└── debug
├── PackageName.build/
│ └── ...
├── PackageNamePackageTests.build
│ └── ...
├── PackageNamePackageTests.swiftdoc
├── PackageNamePackageTests.swiftmodule
├── PackageNamePackageTests.xctest <-- executable, not Bundle
├── PackageName.swiftdoc
├── PackageName.swiftmodule
├── PackageNameTests.build
│ └── ...
├── PackageNameTests.swiftdoc
├── PackageNameTests.swiftmodule
└── ModuleCache ...
macOS CLI
.build/
|-- TestResources/
| `-- SomeTestResource.sql <-- (copied from TestResources/)
|-- TestScratch/
| `-- SomeTestProduct.sqlitedb <-- (created by running tests)
...
|-- debug -> x86_64-apple-macosx10.10/debug
`-- x86_64-apple-macosx10.10
`-- debug
|-- PackageName.build/
|-- PackageName.swiftdoc
|-- PackageName.swiftmodule
|-- PackageNamePackageTests.xctest
| `-- Contents
| `-- MacOS
| |-- PackageNamePackageTests
| `-- PackageNamePackageTests.dSYM
...
`-- libPackageName.a
macOS Xcode
PackageName/TestResources/ files are copied into the test bundle Contents/Resources folder as part of the Build Phases. If used during tests, TestScratch/ is placed alongside the *xctest bundle.
Build/Products/Debug/
|-- PackageNameTests.xctest/
| `-- Contents/
| |-- Frameworks/
| | |-- ...
| | `-- libswift*.dylib
| |-- Info.plist
| |-- MacOS/
| | `-- PackageNameTests
| `-- Resources/ <-- (aka TestResources/)
| |-- SomeTestResource.sql <-- (copied from TestResources/)
| `-- libswiftRemoteMirror.dylib
`-- TestScratch/
`-- SomeTestProduct.sqlitedb <-- (created by running tests)
I also posted a GitHubGist of this same approach at 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref
I found another solution looking at this file.
It's possible to create a bundle with a path, for example:
let currentBundle = Bundle.allBundles.filter() { $0.bundlePath.hasSuffix(".xctest") }.first!
let realBundle = Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/MyProjectTests/Resources")
It's a bit ugly, but if you want to avoid a Makefile, it works.
starting on Swift 5.3, thanks to SE-0271, you can add bundle resources on swift package manager by adding resources on your .target declaration.
example:
.target(
name: "HelloWorldProgram",
dependencies: [],
resources: [.process(Images), .process("README.md")]
)
if you want to learn more, I have written an article on medium, discussing this topic. I don't specifically discuss .testTarget, but looking on the swift proposal, it looks alike.
I'm using:
extension Bundle {
func locateFirst(forResource: String, withExtension: String) -> URL? {
for b in Bundle.allBundles {
if let u = b.url(forResource: forResource, withExtension: withExtension) {
return u
}
}
return nil
}
}
'''
And then just call locateFirst, which gives the first item.
like:
'''
let p12 = Bundle().locateFirst(forResource: "Certificates", withExtension: "p12")!
'''
A made a simple solution that works for legacy swift and future swift:
Add your assets in the root of your project
In your swift code: ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path
Works in Xcode and swift build in terminal or github actions 🎉
https://eon.codes/blog/2020/01/04/How-to-include-assets-with-swift-package-manager/ and https://github.com/eonist/ResourceHelper/
Is it possible to detect at runtime that an application has been installed through TestFlight Beta (submitted through iTunes Connect) vs the App Store? You can submit a single app bundle and have it available through both. Is there an API that can detect which way it was installed? Or does the receipt contain information that allows this to be determined?
For an application installed through TestFlight Beta the receipt file is named StoreKit/sandboxReceipt vs the usual StoreKit/receipt. Using [NSBundle appStoreReceiptURL] you can look for sandboxReceipt at the end of the URL.
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSString *receiptURLString = [receiptURL path];
BOOL isRunningTestFlightBeta = ([receiptURLString rangeOfString:#"sandboxReceipt"].location != NSNotFound);
Note that sandboxReceipt is also the name of the receipt file when running builds locally and for builds run in the simulator.
Swift Version:
let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
Based on combinatorial's answer I created the following SWIFT helper class. With this class you can determine if it's a debug, testflight or appstore build.
enum AppConfiguration {
case Debug
case TestFlight
case AppStore
}
struct Config {
// This is private because the use of 'appConfiguration' is preferred.
private static let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
// This can be used to add debug statements.
static var isDebug: Bool {
#if DEBUG
return true
#else
return false
#endif
}
static var appConfiguration: AppConfiguration {
if isDebug {
return .Debug
} else if isTestFlight {
return .TestFlight
} else {
return .AppStore
}
}
}
We use these methods in our project to supply different tracking id's or connection string per environment:
func getURL(path: String) -> String {
switch (Config.appConfiguration) {
case .Debug:
return host + "://" + debugBaseUrl + path
default:
return host + "://" + baseUrl + path
}
}
OR:
static var trackingKey: String {
switch (Config.appConfiguration) {
case .Debug:
return debugKey
case .TestFlight:
return testflightKey
default:
return appstoreKey
}
}
UPDATE 05-02-2016:
A prerequisite to use a preprocessor macro like #if DEBUG is to set some Swift Compiler Custom Flags. More information in this answer: https://stackoverflow.com/a/24112024/639227
Modern Swift version, which accounts for Simulators (based on accepted answer):
private func isSimulatorOrTestFlight() -> Bool {
guard let path = Bundle.main.appStoreReceiptURL?.path else {
return false
}
return path.contains("CoreSimulator") || path.contains("sandboxReceipt")
}
I use extension Bundle+isProduction on Swift 5.2:
import Foundation
extension Bundle {
var isProduction: Bool {
#if DEBUG
return false
#else
guard let path = self.appStoreReceiptURL?.path else {
return true
}
return !path.contains("sandboxReceipt")
#endif
}
}
Then:
if Bundle.main.isProduction {
// do something
}
There is one way that I use it for my projects. Here are the steps.
In Xcode, go to the the project settings (project, not target) and add "beta" configuration to the list:
Then you need to create new scheme that will run project in "beta" configuration. To create scheme go here:
Name this scheme whatever you want. The you should edit settings for this scheme. To do this, tap here:
Select Archive tab where you can select Build configuration
Then you need to add a key Config with value $(CONFIGURATION) the projects info property list like this:
Then its just the matter what you need in code to do something specific to beta build:
let config = Bundle.main.object(forInfoDictionaryKey: "Config") as! String
if config == "Debug" {
// app running in debug configuration
}
else if config == "Release" {
// app running in release configuration
}
else if config == "Beta" {
// app running in beta configuration
}