Categories
iOS macOS Swift

Building a memory cache for a file reader in Swift

In the previous blog post Reading data from a file with DispatchIO I built a small FileReader which enabled reading data from a file for random byte ranges. Signal Path uses a similar file reader but in addition it also caches read data in some interactions where there are a lot of read requests active, for example, while recording. Therefore, in this blog post we’ll build a small file data cache which stores data chunks for byte ranges. The cache needs to be performant, otherwise it would be faster to just read the data from the file if it is slow.

Defining an interface for the cache

The interface of the FileMemoryCache needs to provide a way for retrieving data, storing data, and limiting the overall memory usage.

final class FileMemoryCache {
init(byteCountLimit: Int)
/// Returns data for the byte range
func data(for byteRange: CountableRange<Int>) -> DispatchData?
/// Caches data for the byte range and evicts older data when exceeding byte count limit
func set(_ data: DispatchData, byteRange: CountableRange<Int>)
/// Removes everything from the cache
func removeAll()
}

Storing and retrieving cached data

The strategy of caching is to use a sorted array where the array is sorted by the first index of a byte range. Items in the array are structs containing a byte range and data. Having a sorted array makes it easier to merge data chunks when retrieving data for a longer byte range than the individual array items provide. As the aim is to keep it as fast as possible then data is merged only when cache is accessed which reduces the need to copy memory multiple times. The downside is that the cache can store overlapping chunks of data which reduces the efficiency. But on the other hand it is fast. Sorted array also enables to use binary search if linear search happens to be too slow with large amount of cached data chunks.

final class FileMemoryCache {
private var sortedItems = [StorageItem]()
struct StorageItem {
let byteRange: CountableRange<Int>
let data: DispatchData
}
}

Let’s start with the store method which takes a data and a byte range it corresponds to. For keeping the array sorted, we’ll use the first index of the byte range as the attribute for sorting. We’ll also make sure that the current item at the insertion index does not already contain the byte range we are trying to cache. If it does contain, we can safely ignore the current store request as the data is already cached. Additionally, we’ll check if the previous item in the array as it might also contain the byte range.

func set(_ data: DispatchData, byteRange: CountableRange<Int>) {
if let insertionIndex = sortedItems.firstIndex(where: { $0.byteRange.startIndex >= byteRange.startIndex }) {
let canDiscard: Bool = {
guard !sortedItems[insertionIndex].byteRange.contains(byteRange) else { return true }
guard insertionIndex > sortedItems.startIndex else { return false }
return sortedItems[insertionIndex.advanced(by: -1)].byteRange.contains(byteRange)
}()
if !canDiscard {
sortedItems.insert(StorageItem(byteRange: byteRange, data: data), at: insertionIndex)
}
}
else {
sortedItems.append(StorageItem(byteRange: byteRange, data: data))
}
}

When it comes to retrieving data then we’ll need to find intersecting cached data chunks and merge them if needed. Therefore, the first step is to collect cached data chunks which intersect with the requested range. For keeping it as simple as possible, will just loop over the array without using binary search and collect the items which are intersecting. The next step after that is to make sure the whole requested byte range is already present in the cache. This can be checked quite easily with IndexSet which contains indexes for every requested bytes. By removing byte indexes in intersecting ranges, the index set becomes empty. It means that every byte index is cached.

func data(for byteRange: CountableRange<Int>) -> DispatchData? {
// Find items which intersect with the search range
guard let firstIntersectingItemIndex = sortedItems.firstIndex(where: { byteRange.intersects($0.byteRange) }) else { return nil }
let intersectingItems = sortedItems[firstIntersectingItemIndex…].prefix(while: { byteRange.intersects($0.byteRange) })
// Check if the whole range is covered
var byteIndexesInCache = IndexSet(integersIn: byteRange)
intersectingItems.forEach({ byteIndexesInCache.remove(integersIn: $0.byteRange) })
guard byteIndexesInCache.isEmpty else { return nil }
// …
}

We’ll need to copy intersecting byte indexes into a final contiguous data. It could be that the whole cached data chunk can be copied over or just a part of it. After figuring out which part of the cached data chunk to copy, the next step is to calculate the byte index in the resulting memory region which starts with the zero byte index. The implementation can be seen below which includes byte range index conversions.

func data(for byteRange: CountableRange<Int>) -> DispatchData? {
// …
guard let byteRangePointer = malloc(byteRange.count) else { return nil }
for storageItem in intersectingItems {
let rangeToCopyInData: CountableRange<Int> = {
switch (byteRange.contains(storageItem.byteRange.startIndex), byteRange.contains(storageItem.byteRange.endIndex – 1)) {
case (true, true):
// Everything: |+++++++++++++|
return 0..<storageItem.byteRange.count
case (false, true):
// End: |—++++++++++|
return byteRange.startIndex – storageItem.byteRange.startIndex..<storageItem.byteRange.count
case (true, false):
// Start: |++++++++++—|
return 0..<byteRange.endIndex – storageItem.byteRange.startIndex
case (false, false):
// Middle: |—+++++++—|
return byteRange.startIndex – storageItem.byteRange.startIndex..<byteRange.endIndex – storageItem.byteRange.startIndex
}
}()
// Find the byte range in the allocated memory range where to copy the cached data section
let destinationByteIndex = storageItem.byteRange.startIndex – byteRange.startIndex + rangeToCopyInData.startIndex
let destinationPointer = byteRangePointer.advanced(by: destinationByteIndex)
let destinationBufferPointer = UnsafeMutableRawBufferPointer(start: destinationPointer, count: rangeToCopyInData.count)
storageItem.data.copyBytes(to: destinationBufferPointer, from: rangeToCopyInData)
// Ignore remaining intersections because everything is copied
if destinationByteIndex + rangeToCopyInData.count >= byteRange.endIndex {
break
}
}
let byteRangeBufferPointer = UnsafeRawBufferPointer(start: byteRangePointer, count: byteRange.count)
return DispatchData(bytesNoCopy: byteRangeBufferPointer, deallocator: .free)
}

Summary

We built a simple cache for storing data chunks for byte ranges. We used a sorted array for storing individual data chunks which made it easier to merge them into one when the cache is accessed. Make sure to check the full implementation on GitHub which also includes evicting cached data.

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

FileReaderPlayground (Xcode 12.4)

Categories
Foundation iOS macOS Swift

Measurement, Unit, Dimension, and MeasurementFormatter on iOS

I was looking at formatters provided by Foundation framework and this time I would like to put some attention on MeasurementFormatter. Like the name says, it is used for creating localized strings of some sort of measurements. Measurements are represented by a generic struct Measurement where the generic UnitType describes the unit represented by it. Apple provides an Unit subclass Dimension which in turn has a many subclasses on its own. At the time of writing there are 22 dimensional units available with each of those having multitude of related units. For example, UnitDuration provides units for seconds, minutes, and hours. The full list of Apple provided dimensions are available in a table here: Dimension.

Using MeasurementFormatter

MeasurementFormatter is simple to use. If we change the unitStyle property then we can configure how the unit is spelled in the localized string.

let formatter = MeasurementFormatter()
let unitStyles: [Formatter.UnitStyle] = [.short, .medium, .long]
for unitStyle in unitStyles {
formatter.unitStyle = unitStyle
let measurement = Measurement(value: 9.8, unit: UnitAcceleration.gravity)
print(formatter.string(from: measurement))
}
// Prints:
// 9.8Gs
// 9.8 G
// 9.8 g-force
Basic usage of MeasurementFormatter.

MeasurementFormatter also has an unitOptions property which controls the way how the final string is composed when taking account the current locale. For example, if locale is set to en_US then UnitTemperature measurement is formatted in Fahrenheits. If locale is set to en_GB then the measurement returns Celsius.

let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: Measurement(value: 293, unit: UnitTemperature.kelvin)))
// 67.73°F
formatter.locale = Locale(identifier: "en_GB")
print(formatter.string(from: Measurement(value: 293, unit: UnitTemperature.kelvin)))
// 19.85°C
MeasurementFormatter selecting output unit based on the locale.

In case we would like to make sure the same unit is used, then we can use the providedUnit option.

let formatter = MeasurementFormatter()
formatter.unitOptions = [.providedUnit]
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: Measurement(value: 293, unit: UnitTemperature.kelvin)))
// 293 K
Forcing the formatter to use the provided unit.

Another thing to note is that the Measurement type also supports comparing measurements and mathematical operations. For example, we can add two measurements.

let measurement1 = Measurement(value: 3.2, unit: UnitElectricCurrent.amperes)
let measurement2 = Measurement(value: 0.02, unit: UnitElectricCurrent.kiloamperes)
print(measurement1 + measurement2)
// 23.2 A
A sum of two measurements.

Creating additional units

MeasurementFormatter is built in a way that it can support custom units as well. We could create a pseudo unit for a children’s game named UnitBanana.

class UnitBanana: Dimension {
override static func baseUnit() -> Self {
return UnitBanana.banana as! Self
}
static let banana = UnitBanana(symbol: "bana", converter: UnitConverterLinear(coefficient: 1.0))
}
let formatter = MeasurementFormatter()
formatter.unitOptions = .providedUnit
print(formatter.string(from: Measurement(value: 2, unit: UnitBanana.banana)))
// 2 bana
Pseudo-unit called UnitBanana.

Summary

MeasurementFormatter and Measurement types create a powerful combination which can be used for creating localized strings of values with an unit. Next time when you need to present a value with an unit, then check out the MeasurementFormatter first.

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
CoreData iOS macOS Swift

Importing items from network to a Core Data store

Many apps use Core Data for persistence and also need to import data from a server. Imported items typically have a unique identifier which can be used for identifying them. The count of imported items can be high therefore it is preferred to batch insert the items instead of adding them one by one. Core Data framework has a specialized request for this called NSBatchInsertRequest which is available since iOS 13. If we combine batch insert with Core Data constraints then we can achieve a flow where new items are only created when the store does not have an item for the unique identifier. All the other items already available in the persistent store are updated (instead of deleting the old item and reinserting it). In this blog post let’s take a look on how it works with a sample app which displays a list of Product entities with a name and a unique serial code attributes.

Product entity with “name” and “serialCode” attributes.

Constraints on the entity can be set in the model editor. For making sure that only one Product with a serial code of X exists in the persistent store then we will need to add a constraint on the serialCode attribute. Core Data framework will then make sure that only one entity with unique serial code exists in the persistent store. Neat, no need to query the store first for existing products and manually checking for possible duplicates.

CoreData constraint set to Product entity.

With a constraint set up, let’s take a look on the batch insert. Apple added NSBatchInsertRequest to Core Data framework in iOS 13. As we added a constraint then we need to tell Core Data what to do if there is already an item for the unique serial code. If we set NSManagedObjectContext‘s merge policy to NSMergeByPropertyObjectTrumpMergePolicy before executing the batch insert then Core Data goes and updates existing items with incoming attribute values fetched from a server. If there is not an item in the store with serial code then a new item is inserted. In summary, we get a behaviour where existing items are updated and missing items are inserted when importing items from a server. The flow of fetching data from a server, running batch insert on a background context and then refreshing fetched results controller can be seen below.

final class ViewModel: ObservableObject {
func importProducts() {
ProductAPI.getAll { result in
switch result {
case .success(let products):
self.persistenceController.container.performBackgroundTask { context in
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
let batchInsert = NSBatchInsertRequest(entityName: "Product", objects: products)
do {
let result = try context.execute(batchInsert) as! NSBatchInsertResult
print(result)
}
catch {
let nsError = error as NSError
// TODO: handle errors
}
DispatchQueue.main.async {
objectWillChange.send()
// TODO: handle errors
try? resultsController.performFetch()
}
}
}
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub
Import function in a view model which fetches a list of products and inserts into a persistent store.

Summary

NSBatchInsertRequest is a welcoming change which makes it easy to insert and update existing items already in the persistent store. Setting up a constraint on a unique identifier and setting merge policy on a context enables us to handle SQL upserts without much code.

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

CoreDataBatchInsertConstraints (Xcode 12.3)

Categories
CoreData iOS macOS Swift SwiftUI UIKit

Most visited blog posts in 2020

I have been writing a new blog post at least every second week and therefore in 2020 I have published total of 27 blog posts. It is time to take a look back on the 2020 and see which blog posts were the most popular ones. I am just looking at the total count of views per blog post.

Most read posts in 2020

  1. Using CoreData with SwiftUI (January 19, 2020)
  2. Validating string in TextField with NumberFormatter in SwiftUI (April 26, 2020)
  3. Scanning text using SwiftUI and Vision on iOS (July 7, 2019)
  4. Adding custom attribute to NSAttributedString on iOS (November 10, 2019)
  5. Fetching and displaying data on Watch app in SwiftUI (February 16, 2020)
  6. Animating view transitions in SwiftUI (December 8, 2019)
  7. Text input in UITableView (November 4, 2018)
  8. NavigationLink and presentationMode environment value property for dismissing a view in SwiftUI (September 29, 2019)
  9. MVVM in SwiftUI (January 5, 2020)
  10. Dynamic user notification on Apple Watch with SwiftUI (February 2, 2020)

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
AppKit ImageIO iOS macOS Swift

CoreGraphics based image resizer for multiplatform apps in Swift

Just a couple of weeks ago I wrote about resizing images on iOS: Resizing UIImages with aspect fill on iOS. As I am currently building a macOS, iOS, watchOS app I realized that I need a multiplatform image resizer. As this app deals with full size photos then I need to resize those photos for avoiding excessive memory usage. Apple’s CoreGraphics framework provides APIs what are compatible with all the before mentioned platforms. Therefore, let’s revisit the image scaler created in Resizing UIImages with aspect fill on iOS and let’s refactor it to use purely multiplatform supported CGImages.

We’ll skip the part which discusses calculating a rectangle in the original image which is then resized for assuring the image is resized equally on both axes. It is important because the aspect ratio of the original image most of the time does not equal to the aspect ratio of the target size. Let’s now take a look at cropping and resizing the original image. Cropping is easy to do because CGImage already has a convenient cropping(to:) method. On the other hand, resizing requires setting up a CGContext with the same parameters as the cropped image but the pixel size must be set to the target size. When we now draw the cropped image to the context, then CoreGraphics takes care of resizing the image.


import CoreGraphics
struct ImageScaler {
static func scaleToFill(_ image: CGImage, from fromRect: CGRect = .zero, in targetSize: CGSize) -> CGImage? {
let imageSize = CGSize(width: image.width, height: image.height)
let rect = fromRect.isEmpty ? CGRect(origin: .zero, size: imageSize) : fromRect
let scaledRect = rect.scaled(toFillSize: targetSize)
return scale(image, fromRect: scaledRect, in: targetSize)
}
private static func scale(_ image: CGImage, fromRect: CGRect = .zero, in targetSize: CGSize) -> CGImage? {
let makeCroppedCGImage: (CGImage) -> CGImage? = { cgImage in
guard !fromRect.isEmpty else { return cgImage }
return cgImage.cropping(to: fromRect)
}
guard let croppedImage = makeCroppedCGImage(image) else { return nil }
let context = CGContext(data: nil,
width: Int(targetSize.width),
height: Int(targetSize.height),
bitsPerComponent: croppedImage.bitsPerComponent,
bytesPerRow: croppedImage.bytesPerRow,
space: croppedImage.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: croppedImage.bitmapInfo.rawValue)
context?.interpolationQuality = .high
context?.draw(croppedImage, in: CGRect(origin: .zero, size: targetSize))
return context?.makeImage()
}
}

I have created a multiplatform sample app which just has a single SwiftUI view shared with iOS and macOS (it is not a Mac Catalyst app). Because we use CGImages then all the code can be shared. The view just loads a full size photo, scales it, and displays it. In the example view we have resized the image by taking account the current displayScale. Depending on the displayScale value we need to make the pixel size of the CGImage larger by the factor of the displayScale. For example, if the point size is 200×200 points, displayScale is 3.0, then the pixel size of the CGImage needs to be 600×600. This will give us a nice and crisp end result when the image is rendered.


struct ContentView: View {
@Environment(\.displayScale) var displayScale: CGFloat
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
if viewModel.cgImage != nil {
Image(viewModel.cgImage!,
scale: displayScale,
orientation: .up,
label: Text("photo")
)
}
}
.padding()
.onAppear(perform: { viewModel.prepareImage(withScale: displayScale) })
}
}
extension ContentView {
final class ViewModel: ObservableObject {
static let queue = DispatchQueue(label: "com.augmentedcode.imageloader")
@Published var cgImage: CGImage?
func prepareImage(withScale displayScale: CGFloat) {
Self.queue.async {
guard let url = Bundle.main.url(forResource: "ExamplePhoto", withExtension: "jpeg") else { return }
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return }
guard let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else { return }
let targetSize = CGSize(width: CGFloat(200.0) * displayScale, height: CGFloat(200.0) * displayScale)
let scaledImage = ImageScaler.scaleToFill(image, in: targetSize)
DispatchQueue.main.async {
self.cgImage = scaledImage
}
}
}
}
}

Summary

CoreGraphics is available on multiple Apple platforms and it provides tools for cropping and resizing images. With just a little bit of refactoring we have turned UIImage based image scaler into CoreGraphics backed scaler which can be used on multiple platforms.

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

CoreGraphicsImageScaler (Xcode 12.2)

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 SwiftUI

Animating a custom wave shape in SwiftUI

Shape protocol in SwiftUI is used for defining views which render custom shapes. Shapes have one required method which takes in a rect and returns a Path. In addition to view protocol, shape conforms to Animatable protocol as well. Therefore we can quite easily make our custom shape to animate from one state to another. We’ll use two parameters for defining our Wave shape: amplitude and frequency. Amplitude dictates the height of the wave and frequency the distance between wave peaks.

Animating wave shape by changing amplitude and frequency
Animating wave shape.

SwiftUI view displaying an animatable wave shape

Let’s take a look on an example view which displays custom Wave shape. We’ll use @State property wrappers for storing amplitude and frequency because we want to change those values when running the app. Those properties are updated with random values when pressing a button. The wave has blue fill color, fixed height, and basic easeInOut animation. The animation is used when amplitude and/or frequency change.

struct ContentView: View {
@State private var amplitude = 10.0
@State private var frequency = 0.1
var body: some View {
ZStack {
Wave(amplitude: amplitude, frequency: frequency)
.fill(Color.blue)
.frame(height: 300.0)
.animation(.easeInOut(duration: 3))
Button(action: toggleAnimation, label: {
Text("Animate")
})
.padding(4)
.background(Color.red)
.cornerRadius(8)
.foregroundColor(.white)
}
}
func toggleAnimation() {
amplitude = amplitude <= 15.0 ? Double.random(in: 30.0…60.0) : Double.random(in: 5.0…15.0)
frequency = frequency <= 0.2 ? Double.random(in: 0.2…0.4) : Double.random(in: 0.05…0.2)
}
}
Content view rendering a wave shape with a button starting an animation.

Animatable wave shape

Like mentioned in the introduction, the Shape protocol defines a required method which has a rect argument and returns a Path. The path starts from the top left edge. Sine function is used for calculating y coordinates for every x coordinate with a 1 point step. Right, bottom and left edges are just straight lines.

Animatable protocol defines an animatableData property and because we have two parameters (amplitude and frequency) we’ll need to use AnimatablePair type. If there would be more parameters then AnimatablePair should contain one or more AnimatablePair types (and so on). Note that values in animatableData must conform to VectorArithmetic protocol which Double type already does.

When animation is running then SwiftUI calculates amplitude and frequency values based on the current animation frame and sets it to the animatableData property. Then new Path value is calculated and rendered.

struct Wave: Shape {
var amplitude: Double
var frequency: Double
func path(in rect: CGRect) -> Path {
let sinCenterY = amplitude
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: sinCenterY))
let width = Double(rect.width)
for x in stride(from: 0, through: width, by: 1) {
let y = sinCenterY + amplitude * sin(frequency * x)
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: Double(rect.minX), y: sinCenterY))
return Path(path)
}
var animatableData: AnimatablePair<Double, Double> {
get {
return AnimatablePair(amplitude, frequency)
}
set {
amplitude = newValue.first
frequency = newValue.second
}
}
}
view raw Wave.swift hosted with ❤ by GitHub
Animatable Wave shape.

Summary

We took a look at the Shape protocol and created a wave shape. In addition, we made the wave to animate from one amplitude and frequency state to a new state.

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
CoreData iOS macOS Swift WidgetKit

Sharing data from CoreData storage with a Widget on iOS

WWDC’20 introduced WidgetKit which is a new framework for building widgets on iOS, iPadOS, and macOS. Widgets provide a quick way for displaying content from your app either on the home screen on iOS or on the notification center on macOS. As I have an iOS app which stores data with CoreData then let’s see what it takes to share it with a widget. Note that we’ll only concentrate on sharing data between the app and the widget. For adding a widget to an existing project I would recommend taking a look at Apple’s excellent article: “Creating a Widget Extension”.

Configuring the project for sharing data with the widget

The project I have is an iOS app which keeps track of plants. Therefore, we’ll look into providing plants to a simple widget which just displays one of the plants which needs to be watered next. CoreData store contains all the plants with previous and next watering date. As widgets are meant to be lightweight extensions to your app we’ll aim at passing the minimum amount of data to the widget. WidgetKit does not provide a connectivity framework like WatchOS does because widgets are not running all the time. Therefore we will store data in a file and write the file to a shared container which the app and the widget can access. This can be done by adding app groups capability to both targets. The group name could be something like group.com.company.appname.widget. When this is set, then the url to a shared container can be created using FileManager like shown below.

extension WidgetPlantProvider {
static let sharedDataFileURL: URL = {
let appGroupIdentifier = "group.com.company.appname.widget"
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
return url.appendingPathComponent("Plants.plist")
}
else {
preconditionFailure("Expected a valid app group container")
}
}()
}
Creating an URL for shared file.

Updating the shared file

The iOS app has a class named WidgetPlantProvider which is responsible of updating the shared file and letting WidgetKit know when the data has changed. This class uses NSPersistentContainer for accessing CoreData storage and fetches dictionary representations of Plant entities. As those dictionaries contain NSDate objects then we’ll need to convert dates to double values which represent dates as seconds from the year of 1970. This enables us to archive the list of dictionaries to a data object with NSKeyedArchiver and writing the data object into the shared container. Last step is letting WidgetKit to know that timelines should be reloaded because data has changed. The implementation of the class is available below including observing managed object save notification.

final class WidgetPlantProvider {
private var cancellables = [AnyCancellable]()
private let plantContainer: PlantContainer // NSPersistentContainer subclass
init(plantContainer: PlantContainer, notificationCenter: NotificationCenter = .default) {
self.plantContainer = plantContainer
let notificationCancellable = notificationCenter.publisher(for: .NSManagedObjectContextDidSave, object: plantContainer.viewContext).sink { [weak self] _ in
self?.reloadData()
}
cancellables.append(notificationCancellable)
}
// Called when NSPersistentContainer is first loaded
func reloadData() {
plantContainer.performBackgroundTask { context in
let descriptors = [NSSortDescriptor(keyPath: \Plant.nextWateringDate, ascending: true)]
// fetchDictionaries is convenience method which creates and executes NSFetchRequest<NSDictionary> and sets resultType = .dictionaryResultType
let dictionaries = Plant.fetchDictionaries(context, sortDescriptors: descriptors, fetchLimit: 3) as! [[String: Any]]
// NSDate -> double conversion
let converted = dictionaries.map { (plantDictionary) -> [String: Any] in
return plantDictionary.mapValues { (value) -> Any in
guard let date = value as? Date else { return value }
return date.timeIntervalSince1970
}
}
do {
let needsFileReload: Bool = {
guard let storedData = try? Data(contentsOf: Self.sharedDataFileURL) else { return true }
guard let storedDictionaries = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(storedData) as? [NSDictionary] else { return true }
return storedDictionaries != converted as [NSDictionary]
}()
if !needsFileReload {
os_log(.debug, log: .widget, "Plants already up to date for widget")
return
}
let data = try NSKeyedArchiver.archivedData(withRootObject: converted, requiringSecureCoding: true)
try data.write(to: Self.sharedDataFileURL)
os_log(.debug, log: .widget, "Reloading widget because plants changed")
WidgetCenter.shared.reloadAllTimelines()
}
catch {
os_log(.debug, log: .widget, "Failed updating plants for widget with error %s", error.localizedDescription)
}
}
}
}
WidgetPlantProvider which stores Plant entities as dictionaries in the shared file.

Reading the shared file in the widget

Reading the file in the widget requires us to create an URL pointing at the shared container, reading the data, and converting the data to a list of plants. As the data contains a list of dictionary objects then we can take advantage of JSONDecoder and convert dictionaries to PlantRepresentation value type. PlantRepresentation struct conforms to Codable protocol which enables converting dictionary object to a JSON data representation and then decoding the JSON data to a value type. Date properties are represented as seconds from the year of 1970, then JSONDecoder’s dateDecodingStrategy must be set to DateDecodingStrategy.secondsSince1970. This approach of converting dictionary to a value type is discussed in detail in “Storing struct in UserDefaults”. An example provider type with described approach is available below.

struct Provider: TimelineProvider {
// …
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let plants = loadPlants()
let entry = PlantEntry(date: Date(), plants: plants)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
private func loadPlants() -> [PlantRepresentation] {
do {
let data = try Data(contentsOf: Self.sharedDataFileURL)
let plantDictionaries = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [[String: Any]]
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
return plantDictionaries?.compactMap({ PlantRepresentation(dictionary: $0, decoder: decoder) }) ?? []
}
catch {
print(error.localizedDescription)
return []
}
}
private static var sharedDataFileURL: URL {
let identifier = "group.com.company.appname.widget"
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) {
return url.appendingPathComponent("Plants.plist")
}
else {
preconditionFailure("Expected a valid app group container")
}
}
}
// DictionaryDecodable: https://augmentedcode.io/2019/05/12/storing-struct-in-userdefaults/
struct PlantRepresentation: Identifiable, Decodable, DictionaryDecodable {
let id: String
let name: String
let lastWateringDate: Date
let nextWateringDate: Date
}
struct PlantEntry: TimelineEntry {
let date: Date
let plants: [PlantRepresentation]
}
view raw Provider.swift hosted with ❤ by GitHub
Timeline provider for a Widget which reads the data from the shared file.

Summary

We went through the steps of setting up app groups and sharing data in CoreData store with a widget. Next steps would be to use the timeline and polishing the Widget’s appearance.

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.