Categories
Xcode

Looking into Xcode 14 beta 1

It is a WWDC week and the first day is over. I am eager to see what Xcode 14 is about to bring and therefore let’s dive into Xcode’s release notes where I pick out some of the changes which caught my interest.

Bitcode is deprecated

Turns out that bitcode is deprecated. In the release notes we can read that bitcode for iOS, tvOS, and watchOS is turned off by default. In addition, bitcode uploads are going to be rejected when using Xcode 14. That is the one I did not expect to change, but it did.

Single 1024*1024 app icon

Whoever has dealt with app icons are going to be happy to hear that in Xcode 14 we can just use a single image for the app icon. Of course, the option is going to be there for using separate images for each of the sizes who need it.

Better parallelism in build system

Release notes mention that Xcode 14 can build targets in parallel with their Swift target dependencies. Sounds like we can hope for faster builds.

Recommended deployment targets

There are new build settings like RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET etc. Not sure yet what are the default values, since I did not find them in Xcode beta 1. But still, interesting addition.

Shell script sandboxing

There is a new build setting ENABLE_USER_SCRIPT_SANDBOXING for turning on sandboxing in shell script build phases.

LLDB swift-healthcheck

It is a frustrating when using a debugger and then any expression is resolving. Seems like there is a way to investigate these issues and possibly solve them with the new swift-healthcheck command. I am gonna definitely check it out.

Interface builder is not forgotten

The platforms State of the Union mentioned Swift and SwiftUI being the future of building apps. But on the other hand, there are multiple changes happening in Interface builder as well. More options and more supported views.

Swift packages and localization

At my work, we have struggled with Swift packages which contain localization. The only way for importing and exporting localizations was having Xcode project only for that. Happy to see that xcodebuild -importLocalizations and -exportLocalizations work with Swift packages.

Better reloading for SwiftUI previews

Many items about improving SwiftUI previews and reloading them when making edits. Sounds like there is going to be less need to trigger render preview manually. That is really nice.

Xcode Server is deprecated

As Xcode Cloud is not any more in beta, Apple is removing Xcode Server from Xcode 14.

That is a quick overview of things coming with Xcode 14. There is so much more. Feel free to dig into Xcode 14 release notes yourself as well.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift SwiftUI Xcode

LibraryContentProvider in Xcode

LibraryContentProvider protocol in Xcode is a way for adding custom views and view modifiers to the Xcode library, which enables drag and dropping them to the SwiftUI preview. Xcode automatically looks for types implementing the protocol and then adds items to the library. It takes so little code that it makes sense to do it if you are using the library for building views.

As an example, we’ll add a custom SwiftUI view SubtitledButton to the library.

struct SubtitledButton: View {
let title: LocalizedStringKey
let subtitle: LocalizedStringKey
let action: () -> Void
var body: some View {
Button(action: action, label: {
VStack(spacing: 4) {
Text(title)
Text(subtitle)
.font(.footnote)
}
})
}
}

The only thing we need to do is creating a new type which conforms to LibraryContentProvider and then returning the button as a LibraryItem.

struct LibraryContent: LibraryContentProvider {
var views: [LibraryItem] {
return [
LibraryItem(SubtitledButton(title: "Title",
subtitle: "Subtitle",
action: {}),
title: "SubtitledButton",
category: .control)
]
}
}

After adding the new type, Xcode updates the library automatically.

Xcode library view with SubtitledButton showing up.
Xcode library with SubtitledButton.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift Swift Package Xcode

Linking a Swift package only in debug builds

One of the cases where we would like to build debug and release configurations differently is when we want to enable some extra features. A concrete example could be a debug view which has a list of commands or displays additional information. In this blog post, we are going to take a look at a Xcode project which only links a local Swift package in debug builds.

The project setup

In the example project, we have an iOS app and a local Swift package “DebugFeatures”. The Swift package description also contains swiftSettings which defines a DEBUG compilation condition when the package is built with debug configuration. This is just an extra measure to make sure we do not compile any of the code in this package accidentally in release configuration.

.target(
name: "DebugFeatures",
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
]
),
view raw Package.swift hosted with ❤ by GitHub

Since it is a compilation condition, then we should also wrap our code in the package with if/endif DEFINE.

#if DEBUG
import SwiftUI
public struct DebugView: View {
public init() {}
public var body: some View {
Text("Debug View")
}
}
#endif
view raw DebugView.swift hosted with ❤ by GitHub

Linking the package only in debug builds

The next step is to change app project settings and make sure we do not link the package in release builds. The simplest way for this is to first add the package as the app dependency in “Frameworks, Libraries, and Embedded Content”.

App target's libraries containing DebugFeatures package.
App target’s libraries.

Then we’ll open build settings and look for “Excluded Source File Names” and configure release builds to ignore “DebugFeatures*”.

Build settings configured to ignore the package in release builds.
Build settings configured to ignore the package in release builds.

To verify this change, we can make a release build with shift+command+i (Product -> Build For -> Profiling which builds release configuration). If we check the latest build log with command+9 and clicking on the top most build item, scrolling to app target’s linker step, we can see that Xcode did not link “DebugFeatures”. Exactly what we wanted to achieve.

Output of the linker step of the app target showing that DebugeFeatures package was not linked.
Output of the linker step of the app target.

Summary

This is one way how to link some package only for debug builds. Although it sounds a bit unexpected that “Excluded Source File Names” also removes the package from the linking phase, but I am happy it does since it means only changing one build setting to get it working like this.

Example project

DebugOnlySwiftPackage (Xcode 13.3.1)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Tab view in SwiftUI

TabView is a container view which enables navigating between multiple flows by selecting one of the items on the tab bar. Tapping on a tab item replaces the visible view with a view associated with the item. Tab view is set up by creating views which have tab items attached. Tab items are created with the tabItem() view modifier, which supports setting a text and an image. In addition, there is a badge() view modifier if we would like to show a badge on top of the item.

Tab view also supports selection handling. Selection handling is needed when we need to programmatically control which tab is selected. For that, we’ll need to choose a type which represents the selection. The only requirement is that the type is Hashable. Therefore, we can use an enum with raw values and have a clear and readable representation of tabs. Next, all the views managed the tab view need to have a tag set with one of the enum cases. Then we can create a binding with the selection type and pass it into TabView and SwiftUI will select the tab view item which has a tag equal to the selection. Just to reiterate that we can use any other type for representing the selection, as long as it conforms to Hashable. Could be just integers as well.

struct ContentView: View {
@State private var selection: Tab = .schedule
enum Tab: Int {
case conversations, schedule, settings
}
var body: some View {
TabView(selection: $selection) {
ConversationListView()
.badge(1)
.tabItem {
Image(systemName: "bubble.left.and.bubble.right.fill")
Text("Messages")
}
.tag(Tab.conversations)
ScheduleView()
.tabItem {
Image(systemName: "clock.fill")
Text("Schedule")
}
.tag(Tab.schedule)
SettingsView()
.tabItem {
Image(systemName: "gearshape.fill")
Text("Settings")
}
.tag(Tab.settings)
}
}
}
view raw TabView.swift hosted with ❤ by GitHub
iOSsimulator with an app which shows tab view with 3 items: messages with a badge showing 1, schedule, and settings.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS macOS Swift

Publishing API documentation in GitHub with Jazzy

Jazzy is an excellent tool for generating API documentation for Swift and Objective-C projects. Let’s take a look how to generate and publish API documentation in GitHub with Jazzy.

Installing Jazzy

Installation: sudo gem install jazzy

Jazzy config file for a Swift package

Configuration options can be passed into Jazzy directly with the command or adding them to the a file, by default it is .jazzy.yaml. We’ll use the default path as then we can run Jazzy without any command line arguments: the configuration is read from the configuration file. For seeing all the available configuration options run jazzy config -h. Just note that the configuration file expects snakecase (build-tool-arguments becomes build_tool_arguments). Let’s take a look on Swift package IndexedDataStore which can be built both for macOS and iOS. It has some additional functions for iOS and therefore it is preferred to build iOS target when running Jazzy. Otherwise API documentation would not contain those public functions meant for iOS. Typically Swift package is built using swift build command. The current state is that there is no easy way for just specifying the target OS to the swift build command. What we can do instead is using xcodebuild command which knows how to build Swift packages as well. We’ll just need to specify the scheme, sdk, and destination arguments. If we now run jazzy command without any arguments, it will read the configuration file, and generate API documentation which includes functions which require UIKit.

author: Toomas Vahter
author_url: https://www.augmentedcode.io
github_url: https://github.com/laevandus/IndexedDataStore
output: Docs
swift_build_tool: xcodebuild
build_tool_arguments:
-scheme
IndexedDataStore
-sdk
iphoneos
-destination,platform=iOS Simulator,OS=latest,name=iPhone 11 Pro
view raw .jazzy.yaml hosted with ❤ by GitHub
Configuration file for Jazzy which builds documentation for iOS.

GitHub action for publish API documentation

Thankfully there is a GitHub action available for publishing API documentation with Jazzy. We can set up a GitHub action with a name PublishDocumentation and store it in the repository’s .github/workflows folder.

name: PublishDocumentation
on:
workflow_dispatch:
release:
types: [ published ]
jobs:
publish:
runs-on: macos-latest
steps:
– uses: actions/checkout@v2
– name: Publish Jazzy Docs
uses: steven0351/publish-jazzy-docs@v1
with:
personal_access_token: ${{ secrets.ACCESS_TOKEN }}
config: .jazzy.yaml
GitHub action which publishes API documentation to GitHub pages.

The GitHub action is triggered either manually or when publishing a release. Manual trigger is called workflow_dispatch and when it is set, GitHub webpage will display a “Run workflow” button. This is very handy when testing GitHub actions. Another thing to note is that publish-jazzy-docs requires repository access because it needs to write the documentation files to the gh-pages branch. For giving repository access we’ll need to set up personal access token with repo scope. Secondly, we’ll need to paste the created token to the repository’s secrets. In this example, we have added a secret named ACCESS_TOKEN and the value is the personal access token. Now, if we have committed and pushed the GitHub action then we can open the repository on GitHub.com, navigate to actions, selecting PublishDocumentation, and using Run workspace button for triggering the wokrflow. If everything goes well, then the workspace creates a gh-pages branch which in turn creates a new GitHub page. In this case the URL to the new GitHub page looks like: https://laevandus.github.io/IndexedDataStore/ (link). This is what we wanted to achieve, API documentation publiched on GitHub.

Summary

We set up Jazzy for a Swift package and used it to generate API documentation. Generated API documentation was published to a GitHub page.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS macOS Swift Xcode

Adding SwiftLint to a project in Xcode

SwiftLint is a tool for validating the style and conventions in Swift files. It is easy to integrate to a project. Minimum set up consists of installing SwiftLint and adding a build phase to your app target in Xcode. SwiftLint comes with a default list of rules, but if you would like to change the rules then it is required to create a configuration file in yaml. Let’s take a look on how to install, add build step, and use a separate configuration file with a custom set of rules.

SwiftLint is available in homebrew so the easies way is to install it with brew install swiftlint command. Next step is to add the build phase to a target. I am gonna use one of my example app which is available on GitHub: SwiftPackageAppWorkspace. This project uses a workspace and includes an app project and Swift package.

git clone https://github.com/laevandus/SwiftPackageAppWorkspace.git
open ButtonGallery.xcworkspace
view raw homebrew.sh hosted with ❤ by GitHub
Cloning example project and opening it in Xcode.

Click on the ButtonGallery project in the file navigator, then on the iOS target and build phases. Will use the + button for adding a new run script phase. Note that we already use the config argument for letting SwiftLint know where the config file exists (by default SwiftLint looks for .swiftlint.yml file in the same folder the project file is).

if which swiftlint >/dev/null; then
# Relative path from the .xcodeproj which contains this script
swiftlint lint –config ../swiftlint.yml
else
echo "warning: SwiftLint not installed"
fi
view raw LintSources.sh hosted with ❤ by GitHub
Build phase calling swiftlint with custom configuration file in one folder up from the .xcodeproj.
Build phase which triggers swiftlint with custom configuration.

Last step is to add a custom configuration file to the repository checkout. We’ll add it to the checkout’s root folder which is the parent folder of the ButtonGallery.xcodeproj. I have went through the full list of rules available for SwiftLint and picked the ones which match with my coding style. SwiftLint has a list of default rules. The list of evaluated rules can be expanded with opt_in_rules and rules from default list can be disabled with disabled_rules list. Also I prefer to have else on a newline so I added statement_position configuration with statement_mode: uncuddled_else. Included defines a list of folder paths relative to the .xcodeproj calling swiftlint.

included:
– ../ButtonKit
– ../ButtonGallery
disabled_rules:
– compiler_protocol_init
– cyclomatic_complexity
– file_length
– force_cast
– function_body_length
– function_parameter_count
– identifier_name
– multiple_closures_with_trailing_closure
– notification_center_detachment
– line_length
– trailing_whitespace
– type_body_length
– type_name
– todo
opt_in_rules:
– anyobject_protocol
– array_init
– closure_end_indentation
– closure_spacing
– collection_alignment
– contains_over_filter_count
– contains_over_filter_is_empty
– contains_over_first_not_nil
– contains_over_range_nil_comparison
– convenience_type
– discouraged_object_literal
– discouraged_optional_boolean
– empty_collection_literal
– empty_count
– empty_string
– empty_xctest_method
– enum_case_associated_values_count
– expiring_todo
– explicit_init
– fallthrough
– fatal_error_message
– file_name_no_space
– first_where
– flatmap_over_map_reduce
– identical_operands
– joined_default_parameter
– last_where
– legacy_multiple
– legacy_random
– literal_expression_end_indentation
– lower_acl_than_parent
# useful for frameworks with public interface – missing_docs
– multiline_function_chains
– multiline_parameters
– multiline_parameters_brackets
– nslocalizedstring_key
– operator_usage_whitespace
– optional_enum_case_matching
– overridden_super_call
– override_in_extension
– pattern_matching_keywords
– prefer_self_type_over_type_of_self
– prefer_zero_over_explicit_init
– private_outlet
– prohibited_super_call
– reduce_into
– redundant_nil_coalescing
– redundant_type_annotation
– single_test_class
– sorted_first_last
– sorted_imports
– static_operator
– toggle_bool
– unneeded_parentheses_in_closure_argument
– unused_declaration
– unused_import
– vertical_parameter_alignment_on_call
– vertical_whitespace_closing_braces
# if {
# }
# else {
# }
statement_position:
statement_mode: uncuddled_else
view raw swiftlint.yml hosted with ❤ by GitHub
Custom configuration file for SwiftLint.

The next time the target is built it will run SwiftLint with custom configuration and show warnings and/or errors in Xcode.

Warnings generated by SwiftLint in the example project.

Summary

SwiftLint is easy to set up and helps to keep the code style consistent in projects. In additional to style SwiftLint is capable of suggesting different coding conventions like use enum instead of struct with only static functions. For making it easy to add custom configuration to new project I have set up a command alias in ~/.zshrc which looks like this: alias xcode_lint_add='cp ~/Dev/swiftlint.yml swiftlint.yml && mate swiftlint.yml' Run xcode_lint_add in the root of the cloned project.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Foundation iOS macOS Swift Xcode

Creating a pre-push git hook in Swift script

Git hooks are scripts written in any scripting language and are triggered when important actions occur. Hooks are stored in the repository’s .git/hooks folder. The script needs to have an appropriate filename without a path extension and also have executable permissions. Push event is an excellent time for triggering unit-tests and making sure local changes have not broken any. Therefore we’ll look into how to create a pre-push script for an iOS project in Swift.

Quick introduction to scripts written in Swift

Setting up a simple script in Swift follows steps familiar from other languages. The script file needs to start with #!/usr/bin/swift followed by the actual script. A simple example script can look like this.

#!/usr/bin/swift
print("Example")
view raw Script.swift hosted with ❤ by GitHub
Example script written in Swift.

The command for running the script is swift Script.swift (if the filename is Script.swift). Another way is making the script executable by adding executable permissions to the file by running chmod +x Script.swift command. Then the script can be run with ./Script.swift (makes sense to drop the file extension).

Building a Xcode project for testing

The pre-push script contains of 3 steps: building the project for testing, running tests, and finally printing out the code coverage results. The first step catches build errors, the second step test failures, and the third step prints out code coverage results. Code coverage can be enabled in the scheme’s test action or adding -enableCodeCoverage YES to the xcodebuild command. Before we jump into creating a xcodebuild command with correct arguments then we’ll need to tackle the problem of calling the xcodebuild command line application from the Swift script. Command line applications can be invoked with the Foundation’s Process class. We’ll add an extension which deals with launching a specified command with zsh and printing out the standard output and error.

extension Process {
@discardableResult
static func runZshCommand(_ command: String) -> Int32 {
let process = Process()
process.launchPath = "/bin/zsh"
process.arguments = ["-c", command]
process.standardOutput = {
let pipe = Pipe()
pipe.fileHandleForReading.readabilityHandler = { handler in
guard let string = String(data: handler.availableData, encoding: .utf8), !string.isEmpty else { return }
print(string)
}
return pipe
}()
process.standardError = {
let pipe = Pipe()
pipe.fileHandleForReading.readabilityHandler = { handler in
guard let string = String(data: handler.availableData, encoding: .utf8), !string.isEmpty else { return }
print(string)
}
return pipe
}()
process.launch()
process.waitUntilExit()
(process.standardOutput as! Pipe).fileHandleForReading.readabilityHandler = nil
(process.standardError as! Pipe).fileHandleForReading.readabilityHandler = nil
return process.terminationStatus
}
}
view raw Process.swift hosted with ❤ by GitHub
Launching command line application with Process.

The next step in the script is to define the project related configuration and create the xcodebuild command. All the user defined arguments are wrapped in quotes for avoiding any issues with whitespaces. The command is pretty straight-forward. If there are any build errors then the result code is not equal to 0. Then we can use the same error code for exiting the Swift script with exit() function.

#!/usr/bin/swift
import Foundation
let projectType = "-workspace"
let projectPath = "SignalPath.xcworkspace"
let scheme = "SignalPathiOS"
let destinationDevice = "platform=iOS Simulator,name=iPhone 11 Pro Max"
let resultBundlePath = "PrePush.xcresult"
removeResultBundle(at: resultBundlePath)
print("Building for testing…")
let buildCommand = [
"xcodebuild",
"build-for-testing",
"-quiet",
projectType, projectPath.wrappedInQuotes,
"-scheme", scheme.wrappedInQuotes,
"-destination", destinationDevice.wrappedInQuotes
].joined(separator: " ")
let buildStatus = Process.runZshCommand(buildCommand)
if buildStatus != 0 {
exit(buildStatus)
}
extension String {
var wrappedInQuotes: String {
return "\"\(self)\""
}
}
Building a Xcode project for testing.

Running unit-tests

The command used for running unit-tests is fairly similar. Instead of build-without-testing we are using test-without-building argument and additionally provide a path where the result bundle is written to. This bundle contains information about the test run. Note that this path must not exist, otherwise xcodebuild stops with an error. Therefore we delete the existing file before running the command. Moreover, when there is a failure, we’ll clean up the path as well – pre-push script should not leave any temporary files.

removeResultBundle(at: resultBundlePath)
print("Running tests…")
let testCommand = [
"xcodebuild",
"test-without-building",
"-quiet",
projectType, projectPath.wrappedInQuotes,
"-scheme", scheme.wrappedInQuotes,
"-destination", destinationDevice.wrappedInQuotes,
"-resultBundlePath", resultBundlePath.wrappedInQuotes
].joined(separator: " ")
let testStatus = Process.runZshCommand(testCommand)
if testStatus != 0 {
removeResultBundle(at: resultBundlePath)
exit(testStatus)
}
func removeResultBundle(at path: String) {
guard FileManager.default.fileExists(atPath: path) else { return }
try? FileManager.default.removeItem(atPath: path)
}
Running unit-tests in a pre-built project.

Printing out code coverage

Last step is optional but it is nice to see code coverage information when pushing changes to a server. Xcode provides a command line application for viewing coverage data in human readable form. One of the options is printing out code coverage per target which gives a nice and concise overview.

let coverageCommand = [
"xcrun",
"xccov",
"view",
"–only-targets",
"–report", resultBundlePath.wrappedInQuotes
].joined(separator: " ")
Process.runZshCommand(coverageCommand)
removeResultBundle(at: resultBundlePath)
print("Success")
exit(0)
Printing out code coverage per target.

Summary

We looked into how to create a pre-push script in Swift. It called other command line applications for building the project, running the tests, and printing out code coverage information. The full script is available below, feel free to copy-paste it to your projects. The one last thing to consider is adding an alias in Terminal for easy installation: alias xcode_pre_push_add='cp ~/Dev/pre-push .git/hooks/pre-push && mate .git/hooks/pre-push' This just copies it from predefined location to the repository checkout and opens it in an editor for setting project related settings (replace mate with any editor).

#!/usr/bin/swift
import Foundation
let projectType = "-workspace"
let projectPath = "SignalPath.xcworkspace"
let scheme = "SignalPathiOS"
let destinationDevice = "platform=iOS Simulator,name=iPhone 11 Pro Max"
let resultBundlePath = "PrePush.xcresult"
print("Building for testing…")
let buildCommand = [
"xcodebuild",
"build-for-testing",
"-quiet",
projectType, projectPath.wrappedInQuotes,
"-scheme", scheme.wrappedInQuotes,
"-destination", destinationDevice.wrappedInQuotes
].joined(separator: " ")
let buildStatus = Process.runZshCommand(buildCommand)
if buildStatus != 0 {
exit(buildStatus)
}
removeResultBundle(at: resultBundlePath)
print("Running tests…")
let testCommand = [
"xcodebuild",
"test-without-building",
"-quiet",
projectType, projectPath.wrappedInQuotes,
"-scheme", scheme.wrappedInQuotes,
"-destination", destinationDevice.wrappedInQuotes,
"-resultBundlePath", resultBundlePath.wrappedInQuotes
].joined(separator: " ")
let testStatus = Process.runZshCommand(testCommand)
if testStatus != 0 {
removeResultBundle(at: resultBundlePath)
exit(testStatus)
}
let coverageCommand = [
"xcrun",
"xccov",
"view",
"–only-targets",
"–report", resultBundlePath.wrappedInQuotes
].joined(separator: " ")
Process.runZshCommand(coverageCommand)
removeResultBundle(at: resultBundlePath)
print("Success")
exit(0)
// MARK: –
extension String {
var wrappedInQuotes: String {
return "\"\(self)\""
}
}
extension Process {
@discardableResult
static func runZshCommand(_ command: String) -> Int32 {
let process = Process()
process.launchPath = "/bin/zsh"
process.arguments = ["-c", command]
process.standardOutput = {
let pipe = Pipe()
pipe.fileHandleForReading.readabilityHandler = { handler in
guard let string = String(data: handler.availableData, encoding: .utf8), !string.isEmpty else { return }
print(string)
}
return pipe
}()
process.standardError = {
let pipe = Pipe()
pipe.fileHandleForReading.readabilityHandler = { handler in
guard let string = String(data: handler.availableData, encoding: .utf8), !string.isEmpty else { return }
print(string)
}
return pipe
}()
process.launch()
process.waitUntilExit()
(process.standardOutput as! Pipe).fileHandleForReading.readabilityHandler = nil
(process.standardError as! Pipe).fileHandleForReading.readabilityHandler = nil
return process.terminationStatus
}
}
func removeResultBundle(at path: String) {
guard FileManager.default.fileExists(atPath: path) else { return }
try? FileManager.default.removeItem(atPath: path)
}
view raw pre-push.swift hosted with ❤ by GitHub
Full pre-push script for building, running, and printing code coverage.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift Swift Package Xcode

Creating and publishing a Swift package

In the previous post we looked into how to separate code with local Swift packages within a project. This time let’s create a Swift package, publish it on GitHub, and add it to a separate project. We’ll create a package which extends UIImage and enables calculating color contrast ratios. Color contrast is important factor for keeping text readable in apps.

Creating a Swift package

Open Xcode and select “Swift Package” item from the File > New menu. We’ll set the name of the package to “ColorContrastRatio”.

Selecting a new Swift Package.
Saving a new Swift package.

Xcode’s template of new Swift packages is configured to have a basic hello world example with an unit-test. Before we change the package’s implementation, we’ll add minimum platform versions to the Package.swift file, in other words, minimum deployment target.

// 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: "ColorContrastRatio",
platforms: [
.iOS(.v13), .macOS(.v10_15)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "ColorContrastRatio",
targets: ["ColorContrastRatio"]),
],
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: "ColorContrastRatio",
dependencies: []),
.testTarget(
name: "ColorContrastRatioTests",
dependencies: ["ColorContrastRatio"]),
]
)
view raw Package.swift hosted with ❤ by GitHub

Next step is to implement functionality of the package. We’ll keep the package simple and make it UIKit only which can be built for iOS and macCatalyst apps. Package is adding an UIColor extension and providing functionality for calculating relative luminance and contrast ratio. Functionality is set, next step is to publish it.

import UIKit
/*
Contrast ratio is calculated using the proceedure here:
https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-procedure
*/
public extension UIColor {
/// Relative luminance of the color.
var relativeLuminance: CGFloat {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
guard getRed(&red, green: &green, blue: &blue, alpha: nil) else { return 1.0 }
let convert: (CGFloat) -> CGFloat = { component in
guard component > 0.03928 else { return component / 12.92 }
return pow(((component + 0.055) / 1.055), 2.4)
}
return 0.2126 * convert(red) + 0.7152 * convert(green) + 0.0722 * convert(blue)
}
/// Returns contrast ratio with other color.
/// – Parameter otherColor: UIColor in RGB color space.
/// – Returns: Contrast ratio of two colors.
func contrastRatio(_ otherColor: UIColor) -> CGFloat {
let luminance1 = relativeLuminance
let luminance2 = otherColor.relativeLuminance
return (min(luminance1, luminance2) + 0.05) / (max(luminance1, luminance2) + 0.05)
}
}

Publishing a Swift package

First step is to go to GitHub and adding a new repository. I chose to include an automatically created license file, cloned the repository on my mac and then moved the code we added in the previous step to that checkout, then committed and pushed. Alternative is to add a remote to your local git repository. Either way is fine as long as our project ends up on GitHub. In addition, will add a tag which will mark the first release 0.1.0. Tags can be added by running those commands in Terminal:

git tag -a 0.1.0 -m “0.1 release of the package”

git push origin 0.1.0

Another option is creating a tag in the GitHub’s web interface: releasing projects on GitHub.

Adding the published Swift package to another project

We published our package on GitHub and it requires only a few steps for adding it to an existing project. In an existing project, open the target settings and click on the plus button in the “Frameworks, Libraries, and Embedded Content”. In the opened view, click on the “Add Other” and select “Add Package Dependency”. Then we can paste the package’s GitHub url (https://github.com/laevandus/ColorContrastRatio) to the search field and complete the flow. From then on, it is just a matter of importing the new package and using it in the main project. Described steps are shown below:

Selecting the target where to the package is added.
Selecting “Add Package Dependency” which allows adding Swift package.
Searching for the Swift package by url.
Setting the package update rule.
Choosing products from the package.
Finished example application using the Swift package’s color contrast ratio function.
import ColorContrastRatio
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Luminance for red color").padding()
Rectangle()
.frame(width: 50, height: 50)
.fixedSize()
.foregroundColor(.red)
Text("\(UIColor.red.relativeLuminance)")
}
}
}

Summary

We created a Swift package and a repository on GitHub. Then we proceed with making the first release by adding a git tag. After that, we went ahead and added the package to another example project.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Resources

ColorContrastRatio (GitHub, Swift Package)

Swift Packages (Apple)

Categories
iOS macOS Swift Swift Package SwiftUI Xcode

Separating code with Swift packages in Xcode

Xcode 12 comes with Swift toolchain 5.3 which brings resource and localisation support to Swift packages. Nice thing is that Swift package support only depends on the toolchain’s version and does not have additional OS requirements. At the same time, let’s keep in mind that OS requirements come from the code we actually add to the package. This means that it is a good time to start using Swift packages for separating code into separate libraries and stop using separate framework projects in a workspace. Additional benefit is that, if needed, it is pretty easy to move the package out of the workspace and creating a sharable package what can be publish and reused in other projects. But for now, let’s take a look on how to set up a new workspace with an app project and a Swift package which represents a design library with custom button style.

File structure of the workspace

The app project will have a name “ButtonGallery” and the Swift package will have a name “ButtonKit”. But first, let’s create a folder named “SwiftPackageAppWorkspace” which is the root folder of the project. The app project and the Swift package will go to that folder in separate folders.

Adding the workspace, the app project, and the Swift package

New workspace can be created by selecting the Workspace menu item in File>New menu in the Xcode. Save the workspace in the “SwiftPackageAppWorkspace” folder what we created just before. Xcode opens the created workspace after clicking on save and then the next step is to add a new Swift package. Easiest is to use the plus button at the bottom of the left corner, selecting “New Swift Package”, and saving the package in the “SwiftPackageAppWorkspace” folder. Uncheck the option on the save panel for creating a git repository because the git repository should be added in the “SwiftPackageAppWorkspace” instead (we skipped this step). Third step is to add the app project by using File>New menu. Xcode also offers an option to add the new project to the workspace. Therefore make sure “Add to” and “Group” have the workspace selected on the save panel. Described steps are shown below.

Selecting a new workspace in the main menu.
Using the plus button in the workspace for creating a new package.
Saving Swift package in the root folder.
Adding a new Xcode project.
Selecting template for the project.
Adding a name to the app project.
Saving a new app project and adding to an existing workspace.
Workspace with a Swift package and an app project.

Linking the Swift package in the app project

Swift package needs to be added to the app target: select “ButtonGallery” in the project navigator, click on the iOS target, General, and then on the plus button in the “Frameworks, Libraries, and Embedded Content” section, select the “ButtonKit” library.

Navigating to iOS target’s general settings.
Linking with the ButtonKit.

Now the workspace is configured but there is not any useful code in the “ButtonKit”. Let’s fix that next and add a FunkyButtonStyle.swift to the package and set minimum platforms in the package manifest because we’ll use SwiftUI in the implementation and it has minimum platform requirements. Because FunkyButtonStyle is in a separate module and by default access control is set to internal, then we’ll need to make it public before it can be imported to the app target.

// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "ButtonKit",
platforms: [
.iOS(.v14), .macOS(.v10_15)
],
products: [
.library(
name: "ButtonKit",
targets: ["ButtonKit"]),
],
targets: [
.target(
name: "ButtonKit", dependencies: []),
.testTarget(name: "ButtonKitTests", dependencies: ["ButtonKit"]),
]
)
view raw Package.swift hosted with ❤ by GitHub
import SwiftUI
public struct FunkyButtonStyle: ButtonStyle {
public init() {}
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label.padding()
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
}
}
import ButtonKit
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Title", action: tap).buttonStyle(FunkyButtonStyle())
}
func tap() {
print("Tapped")
}
}
ContentView in the app target which imports ButtonKit and uses its FunkyButtonStyle.

Summary

We created a new workspace what contains a Swift package and an app project. We looked into how to provide functionality in the package and making it available for the main app target.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Project

SwiftPackageAppWorkspace (Xcode 12b1)

Categories
iOS Swift Xcode

Getting started with device framed screenshots for iOS App Store using fastlane

Creating screenshots manually for App Store is time consuming because of the number of devices and localisations. Moreover, as soon as UI changes we would need to update screenshots again. Therefore, it is best to automate the process. One way is to use fastlane and creating nice device framed screenshots with additional localised text. This time we’ll see how to add fastlane to existing project, setting up UI-testing target, scheme for App Store screenshot tests, and framing screenshots with additional text.

End result of a framed screenshot with additional text.

Installing required tools

First step is installing fastlane and as frameit command requires imagemagick, let’s install it now as well. When using homebrew installation goes as: brew install fastlane imagemagick.

Adding UI-testing target

Fastlane uses UI-tests for displaying views and then capturing screenshots. If project does not have UI-testing target set up, then this is the first step. In Xcode, open add target view (File > New > Target), choose UI-Testing Bundle and add it to the project. In our case the testing target’s name is FastlaneAppStoreScreenshotsUITests.

Support files for capturing screenshots

First thing we’ll do with fastlane is running the fastlane snapshot init command in the project folder. It will generate 2 files: Snapfile and SnapshotHelper.swift. First one contains configuration for snapshot command: devices, locales etc. Helper file we’ll move to UI-testing target: first drag it to testing target folder (FastlaneAppStoreScreenshotsUITests) and then to Xcode’s file navigator. Make sure to add it to the UI-testing target. Create subfolder named fastlane and move Snapfile to there. We’ll modify it a little bit later.

Project structure after adding UI-testing target and supporting SnapshotHelper.swift

Adding UI-test case for screenshots

We are going to define tests generating App Store screenshots in a separate XCTestCase. In Xcode, add a new UI-test case class (File > New File > UI-Test Case Class) and name it to AppStoreScreenshotUITests.

Best is when it is possible to record a UI-test which does not need any special configuration: launch the app, tap here and there, take a screeshot. In more complicated apps, we need to use launch arguments and configuring the app when it launches based on the launch arguments. In document based apps it might be needed to bundle example files in the app and then using launch arguments for choosing the file to open when running the test.

Firstly, let’s add 3 UI-tests which will cover 3 different views and calling the function snapshot(“file_name”) which is part of the SnapshotHelper.swift and handles creating screenshots and storing them in the folder specified by Snapfile.

final class AppStoreScreenshotUITests: XCTestCase {
    private var app: XCUIApplication!
    
    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        // Example of passing in launch arguments which can be read in the app
        // by calling CommandLine.arguments.contains("--uitesting")
        // app.launchArguments.append("--uitesting")
        setupSnapshot(app)
        app.launch()
    }

    override func tearDownWithError() throws {
        app = nil
    }

    func testScreenshot1() throws {
        snapshot("1-mainview")
    }
    
    func testScreenshot2() throws {
        XCTAssertTrue(app.buttons["Button1"].waitForExistence(timeout: 10))
        app.buttons["Button1"].tap()
        snapshot("2-sheet1")
    }
    
    func testScreenshot3() throws {
        XCTAssertTrue(app.buttons["Button2"].waitForExistence(timeout: 10))
        app.buttons["Button2"].tap()
        snapshot("3-sheet2")
    }
}

Separate scheme for App Store screenshots

UI-tests are set up for App Store screenshots, next is to add a separate scheme and adding those tests to the Test action. This scheme is the one fastlane is going to use for running tests.

For adding a new scheme, open manage schemes view (Product > Scheme > Manage Schemes). Click on the + button, select the app target and name it to AppStoreScreenshots.

AppStoreScreenshots scheme in Manage Scheme view.

After that, select the scheme and click on the Edit, open Test action and select only App Store tests. Also make sure the Shared checkbox is selected for allowing fastlane to run it.

Selecting tests in Test action for AppStoreScreenshots schemes.

Configuring Snapfile

Snapfile contains configuration for the snapshot command. There we define all the devices, locales, scheme name, location where to store generated screenshots etc. For getting started, we can start with one locale and two devices. We also need to select the scheme we added before.

devices([
   "iPhone X",
   "iPhone 11 Pro Max"
])
languages([
  "en-US",
])
scheme("AppStoreScreenshots")
output_directory("./AppStoreScreenshots")
clear_previous_screenshots(true)

Configuring Fastfile

App Store screen generation contains of two main steps: capturing screenshots and framing those. Fastfile allows us to create custom commands, lanes, and it is useful of adding one for app store screenshots. Create a file named Fastfile and add it to previously created Fastlane folder.

default_platform(:ios)

platform :ios do
  desc 'Generate App Store screenshots'
  lane :app_store_screenshots do
    capture_screenshots(
      clean: true
    )
    frame_screenshots
  end
end

Running fastlane app_store_screenshots will then generate screenshots and create framed versions as well. If you run it now, you’ll get screenshots with and without device frames.

Adding titles and keyword to framed screenshots

Now we have nice framed screenshots, only what is missing is adding some text to all the screenshots. If we ran fastlane app_store_screenshots in the previous step we can see that AppStoreScreenshots folder has appeared and taken screenshots are in the en-US folder. Adding text and background to each of the screenshot requires a couple of steps.

First we’ll create Framefile.json file in the AppStoreScreenshots folder which contains of configuration for frameit.

{
	"device_frame_version": "latest",
	"default": {
		"keyword": {
			"font": "./Fonts/SF-Pro-Display-Medium.otf",
			"color": "#FFFFFF"
		},
		"title": {
			"font": "./Fonts/SF-Pro-Display-Medium.otf",
			"color": "#FFFFFF"
		},
		"padding": 50,
		"show_complete_frame": false,
		"stack_title": true,
		"use_platform": "IOS"
	},
	"data": [ 
		{
			"filter": "mainview",
			"background": "./AppStoreBackgroundPortrait1.jpg"
		},
		{
			"filter": "sheet1",
			"background": "./AppStoreBackgroundPortrait2.jpg"
		},
		{
			"filter": "sheet2",
			"background": "./AppStoreBackgroundPortrait3.jpg"
		}
	]
}

Default dictionary contains shared settings for every frame operation. We can customise by adding data array which contains screenshot specific configuration (more). Filter in data dictionaries are used for matching the configuration to screenshot files (filenames used in UI-tests). In this example we have different backgrounds for every screenshot (background files are in the same AppStoreScreenshots folder). Fonts are configured by adding a path to Framefile.json and storing fonts in a subfolder.

Localised titles and keywords are handled with strings files. We’ll need to add title.strings and keyword.strings to the locale folder, in our case, en-US. Keys in strings files work the same way as filters in Framefile.json, therefore partial match with filename works. Example strings file for title.strings is shown here:

"mainview" = "Title1";
"sheet1" = "Title2";
"sheet2" = "Title3";

I suggest creating strings files with Xcode making sure the file encoding is correct. If we run the fastlane app_store_screenshots command again, we’ll get nice framed screenshots with additional text.

Summary

Automating App Store screenshots with fastlane is not difficult to set up. The most difficult part in this process is writing UI-tests and providing proper data in the app which is highly app specific.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

FastlaneAppStoreScreenshots (Xcode 11.4)