A case for exact versioning and ignoring Package.resolved file

While SPM offers numerous benefits, it also poses challenges, particularly for larger projects rather than smaller ones. Frustration arises when the Package.resolved file frequently changes, particularly with numerous in-house packages, especially if they are pinned to a Git branch instead of a specific version or commit. If you get yourself in such situation, changing git branches and opening Xcode often causes SPM to resolve packages again and again. Further more, this all introduces annoyance that expresses itself in form of not being able to open Xcode project while offline.

How SPM uses Package.resolved file?

During the initial fetching of dependencies, SPM utilizes either the Package.swift manifest or the .pbxproj file to establish their version constraints. SPM then resolves these dependencies, taking into account the version constraints specified in the manifest. After resolving the dependencies, SPM creates or updates Package.resolved file. This file contains a record of the exact versions of each dependency that SPM selected during the resolution process. The Package.resolved file essentially locks down the versions of dependencies to ensure a consistent build environment. It prevents unintended updates to dependencies that could introduce breaking changes.

Well, that last sentence is not always true.

What’s the problem then?

As I said earlier, on the huge projects you often have a lot of internal dependencies that are distributed via central git repository. To ease the development and to not always have to worry about correct version, you may want to set your version strategy such that it always points at the develop branch (what ever branch suits your needs). Yes, this definitely makes development easier but it also introduces all the problems outlined above.

A case for exact version constraints

Having explicit version constraints solves all of these problems. Instead of declaring dependency version range, you always opt into setting exact version. That way there is no need for Package.resolved file (.lock file in other technologies). If you pin your local dependencies to a branch that undergoes frequent changes, you can either ensure to update packages manually when needed or pin them to a specific commit.

But what about the release?

If you have chosen to go with git branch route as your versioning strategy, it could allow for untested code to slip in to the release. To remedy that sort of issue, pinnig dependencies to specific version (git tag in this case) or commit is the way to go. You can always write a script which sets up your dependency constraints differently for the release phase such that you have easy time in development phase with your dependencies while production code is still locked to specific revision.

Ignoring Package.resolved file completely

After pinning all dependencies to the exact version, you can safely delete the Package.resolved file and add it to the list of ignored files in your .gitignore. Now Xcode will not resolve package graph whenever you change your branch or open Xcode which should save your sanity.

Final thoughts

While this approach has worked out for me so far, it is important to note that it might not be for everyone. For starters, transitive dependencies are still not in your control, pinning package against branches instead of specific commits can be dangerous if not attended, Package.resolved file exists for a reason and who knows what else I might have forgotten. As always, never do something just because random engineer wrote on the internet. Carefuly consider multiple approaches and use the requirements of your project to make informed decision that will best serve your team and project.

Integrating Conan with Xcode to manage C/C++ libraries

In my last post I went over how to manually link C++ libraries to Xcode project. While that is useful to know, it gets tedious to maintain once you have multiple C++ dependencies. In addition, if the library you want to link does not come with built binary, you are responsible for that too which in some cases may not be fun at all.

Enter Conan

Conan is a package manager for C/C++ that in addition to getting libraries, it allows for easy building of libraries for various CPU architectures which I personally find incredibly useful.

Why?

Past few weeks I spent some time building cross-platform library using C++ for iOS and Android which ended up depending on Crypto++. This meant that besides building the Crypto++ from source for iOS and iOS simulator, now I needed to build it for four more architectures (armV7, armV8), x86 and x86_64) that Android runs on.

Integrating Conan with Xcode

First things first, you need to make sure that you have Conan installed. The easiest way is with Homebrew, simply open terminal and run brew install conan. Once that’s sorted out, change directory to where your Xcode project is and create new “conanfile.txt” file. Make sure that it contains the following:

[requires]
cryptopp/8.8.0
[generators]
XcodeDeps
XcodeToolchain
[layout]
cmake_layout

this sets up Conan to look for 8.8.0 version of Crypto++. Then in the “generators” section of the file it tells Conan to generate xcconfig files that will ultimately help us link the library.

Next, it is required to create a Conan profile that describes how to build the library. It contains information like which CPU architecture to build for, whether to build in debug or release mode etc. So still in directory where your Xcode project is, go ahead and create empty file and give it “simulator-profile” name. You can pick whatever name you like, this is just my preference. After that, it should contain the following:

[settings]
arch=armv8
build_type=Debug
compiler=apple-clang
compiler.cppstd=gnu17
compiler.libcxx=libc++
compiler.version=15
os=iOS
os.version=17.0
os.sdk=iphonesimulator

this is pretty self-explanatory. It tells Conan to build the library for armV8 architecture using Clang version 15 and it tells what is the minimum iOs deployment target in addition to which SDK to build for.

Building and linking

After installing Conan, setting up “conanfile.txt” and “simulator-profile” it is time to build. Make sure your working directory in the terminal is the one that contains “conanfile.txt” and run

conan install . --build=missing --profile=simulator-profile --output-folder=conan-generated

Here is the breakdown of the entire command:

  • conan install . runs “install” command from conan. The “.” is used to look for “conanfile.txt” in the current working directory.
  • --build=missing explicitly tells Conan that the build for the library is missing which makes it build the library from source, hence the word “missing”.
  • --profile=simulator-profile this is passing profile file that I created earlier.
  • --output-folder=conan-generated this is the directory where Conan will generate files using generators I specified in “conanfile.txt”. I named it “conan-generated” but you can name that whatever you like, popular one is “build”.

So after command ran, you should see “conan-generated” directory next to your other project files. I recommend adding “conan-generated/” to .gitignore. All that’s left is to open your Xcode project and add “conan_config.xcconfig” file that is in “conan-generated” directory. I won’t go into specifics of using config files in Xcode, there is plenty of articles about that, like the one from NSHipster. It’s important to note that XcodeDeps aggregates all files and settings. Therefore, running the command above multiple times is not only acceptable but also necessary if you want it to generate configurations for all cases. For example if you want to have library linked both for simulator and device in debug and release configurations, you should run the command with different parameters multiple times.

Closing words

Even though there is a bit of learning curve and setup when it comes to Conan, it makes our lives much easier. Once you grasp the concepts you realize that integration between Conan and Xcode is fundamentally very simple. To deepen your understanding of Conan and its generators, it is best to consult official Conan documentation page. And to explore the vast universe of C/C++ libraries available for use with Conan, Conan center is the best place for that.

Linking C++ static library in iOS project

Linking against a static C++ library in Xcode tends to get complicated. Even though the idea is simple, there are few traps that you can run into.

The idea

  1. Write C++ code.
  2. Write interface which will be usable from Objective-C and Swift.
  3. Package it as a static “.a” library.
  4. Link it to iOS app project.

Let’s start with simple C++ code

We will create a class named ExampleCode which will expose single function helloWorld() that returns static string.

    #ifndef ExampleCode_hpp
    #define ExampleCode_hpp

    #include <stdio.h>

    using namespace std;

    class ExampleCode {
    public:
        const char* helloWorld();
    };

    #endif /* ExampleCode_hpp */

And here is the implementation:

    #include "ExampleCode.hpp"

    const char* ExampleCode::helloWorld() {
        char const *str = "This is my library";
        return str;
    }

Creating interface for Objective-C and Swift

Because Swift still doesn’t have interoperability with C++, we need to utilize Objective-C++ to achieve our goal of using the library from Swift. In the same project for our dummy library, create a new Objective-C file with header:

#import <Foundation/Foundation.h>

@interface NewLibrary : NSObject
- (NSString*)hello;
@end

Now for the implementation file it is important to change its .m extension to .mm because that is what makes it tap into C++ (aka makes it Objective-C++).

#import "NewLibrary.h"
#import "ExampleCode.hpp"

@implementation NewLibrary
- (NSString *)hello {
    ExampleCode* example = new ExampleCode();
    NSString* str = [NSString stringWithUTF8String:example->helloWorld()];
    return str;
}
@end

Also remember ExampleCode.hpp is the C++ header file that I created above, so that is why I import it here.

Packaging this code as static C++ .a library

This is fairly simple stuff, but here are the steps:

  1. Set your scheme to Release configuration.
  2. Build both for “Any iOS Simulator Device (arm64, x86_64)” and for “Any iOS Device (arm64)”. No, you can’t do it both at once.
  3. Find your build products in Xcode’s derived data folder. You should see two folders “Release-iphonesimulator” and “Release-iphoneos”. In there there is your “.a” library file and “include” folder containing “.h” header Objective-C file that we created earlier.

Now, most of the old advice goes to say that you should use lipo command to create universal (fat) binary. However this will only get you in trouble. Firstly, if you try creating universal binary from simulator and iOS device “.a” library files you will get an error telling you that both files are built for arm64 architecture. That is because they actually are since Apple Silicon was introduced. Secondly, don’t try to remove arm64 architecture from simulator “.a” library using lipo -remove arm64 path-to-simulator-lib.a -output library.a, not because it won’t work but because it will create troubles when debugging on simulator later on. Actually, you don’t need to do anything with those files at this point.

Linking against your library in separate iOS project

So in a new iOS project in Xcode it is required to perform a couple of steps to link your newly created static C++ library:

  1. Before you add your “.a” files into iOs project, it would be helpful to rename them such that you can differentiate simulator from real device “.a” libraries because they are different. You can name them something like NewLibrary-sim.a and NewLibrary-device.a.
  2. Add your “.a” files to Xcode project. Make sure that you check the box “Copy files if needed” when dropping “.a” files. Also make sure that you don’t add them to your target because we will link them conditionally in later step.
  3. Add “.h” header file. Just because you have two “.a” files, you don’t need two header files. Also make sure that you check “Copy files if needed” when dropping it into your project.
  4. In project build settings look for “other linker flags” and next to Debug and Release configurations click + icon and add two new entries, one for the simulator SDK and the other for the iOS SDK. In the entry for simulator SDK add path to your simulator .a file, you can write it like this $(SRCROOT)/Libraries/NewLibrary-sim.a. $(SRCROOT) gives you the path to the root of your project. And repeat the same for iOS SDK $(SRCROOT)/Libraries/NewLibrary-device.a.
  5. Now for the library to work, you also need to link against C++ standard library. Fortunately it is pretty straight forward. Go to build phases for your target and add “libc++.tbd” under “Link Binary With Libraries”. This is very important step and one that I see so many other articles fail to mention.
  6. Finally, because interface for our library is written in Objective-C, wee need to create bridging header. You can do that manually or you can add empty Objective-C file to your project and Xcode will offer to create bridging header for you. What ever you choose, just make sure to import your library header in bridging header to make Swift recognize public interface for your library.
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "NewLibrary.h"

At this point you should be able to build the app either for simulator or device without any issues.

Things to keep in mind

  • Swift will get support for direct interoperability with C++ very soon. It actually already supports it but the current stable Xcode version still does not ship Swift 5.9. This means that Objective-C won’t be necessary anymore.
  • Don’t fall into traps with removing arm64 from simulator version of your library. Also don’t go into build settings and add arm64 to “Excluded architectures”. If you do so, simulator will utilize Rosetta to run your app and debugging experience gets a lot slower and simulator starts to freeze.

Conclusion

Utilizing language like C++ can be very beneficial as it allows for code sharing between different platforms. However it can get challenging if you are doing it for the first time or you fail to perform one of the steps outlined in this article.

Introducing the existentialannotator: A Swift Command Line Tool that automatically marks all existential types with

I am pleased to present my command line tool that I hacked together on a Saturday morning, the “existentialannotator,” which can be found on github. As the name suggests, this tool performs a specific function: scanning your Swift files, identifying all declared protocols, and annotating all existential types with any. This feature will prove invaluable with the upcoming release of Swift 6. To get started, you have two options: installing it via Homebrew or obtaining the source code directly from GitHub and building it yourself. Once installed, simply navigate to your working directory and execute the command existentialannotator . to let the tool do its job.

Background

Concept of existential types in Swift has not been heavily discussed topic until recently when it was introduced as part of the Swift Evolution process on GitHub. Essentially, an existential type represents the existence of any specific type without specifying that particular type explicitly. This means that certain code, like the example below, will not compile in Swift 6:

protocol Vehicle {
  var batteryPercentage: Float { get }
}

struct HumanDriver {
  let vehicle: Vehicle

  func drive() {
    // Drive the vehicle
  }
}

The compilation fails because let vehicle: Vehicle utilizes an existential type without being explicitly annotated with the new keyword any.

Understanding Existential Types

In Swift, an existential type provides a way to handle values of different types in a unified manner, abstracting their specific type information. In the example above, we used the protocol Vehicle instead of a concrete type, demonstrating the essence of an existential type.

What’s New in Swift 6?

In Swift 6, every existential type must be marked with any. Failure to do so will result in a compilation error. Consequently, the code above let vehicle: Vehicle would now require the notation let vehicle: any Vehicle. This is where my tool, the Existential Annotator, comes in handy, particularly when dealing with large codebases.

Final Remarks

Although I put this tool together on Saturday morning, it is not flawless. There are numerous potential performance improvements that could be implemented. Nonetheless, I consider this tool complete, given its limited lifespan. As we move past the initial release of Swift 6, this tool will likely lose its relevance. Nevertheless, if you believe it could benefit from enhancements, please feel free to submit a pull request.

Using Swift withCheckedThrowingContinuation in methods without return value

When refactoring old closure-based code to new Swift concurrency it is inevitable that you come accross scenario where you need to call withCheckedThrowingContinuation where enclosing method has no return value. In that case, you should get error in Xcode: Generic parameter ’T’ could not be inferred

Let’s consider the following block of code

func fetchData() async throws {
    try await withCheckedThrowingContinuation({ continuation in
        URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { data, response, error in
            if let error = error {
                continuation.resume(throwing: error)
                return
            }
            continuation.resume()
        }.resume()
    })
}

This code produces the error above because withCheckedThrowingContinuation method has generic parameter which compiler usuallly infers from return value of enclosing method. However, our enclosing method fetchData has no return value thus compiler raises the error.

Fortunately the fix is incredibly simple, just cast return type to Void

func fetchData() async throws {
    try await withCheckedThrowingContinuation({ continuation in
        URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { data, response, error in
            if let error = error {
                continuation.resume(throwing: error)
                return
            }
            continuation.resume()
        }.resume()
    }) as Void
}

It is important to note that this approach works for all methods not just for withCheckedThrowingContinuation or other methods specific to Swift concurrency.

How to check if Xcode is building for previews

Lately, I find myself often dealing with a lot of Xcode build phases. One common problem that I encounter is that SwiftUI previews won’t work if some build phase runs a script which messes with Xcode project file or with individual files. For example, script which sorts files alphabetically may modify Xcode project file which will prevent previews to work. To work around this problem, you can check if Xcode is building for previews and then decide whether to run the script or not.

if [ ${ENABLE_PREVIEWS} == NO ];
then
echo "Running sorting script"
fi

I have spent some time playing with Apple’s Multi Peer connectivity framework on iOS. It is incredible what kind of apps it enables. Here is my unfinished sample app that allows for voice calls in cases where there is no internet access or even infrastructure Wi-Fi. I have to say that most difficult part was configuring AVAudioEngine it is extremely easy to mess things up. Audio is hard.