Categories
iOS Swift UIKit

Flow layout with self-sizing items and fixed spacing in UIKit

One of the really common layouts I have needed to implement with collection view is a simple flow layout but with fixed spacings. Apple provides us UICollectionViewFlowLayout, but the sad part is that it has dynamic spacing between items. Everything is there but not quite. Before UICollectionViewCompositionalLayout, one needs to create a subclass of the flow layout and then fixing spacings manually, which is pretty cumbersome to do. Therefore, let’s instead see what it takes to implement a simple self-sizing flow layout with fixed spacings when using UICollectionViewCompositionalLayout. The end goal is visible below, where we have a single section with 7 items.

Flow layout with fixed spacings.

UICollectionViewCompositionalLayout was created to be a flexible layout which allows building all sorts of layouts quickly. Data in that layout is divided into sections, where each section can have one or more groups of items. Grouping allows creating more complex layouts, where each group describes how items in the group are laid out in relation to each other. But in our case we have something really simple in mind, which is having self-sizing items which we can configure with NSCollectionLayoutSize and passing estimated dimensions. Then the next step is creating NSCollectionLayoutItem with that layout size and with some space around the item. The edge spacing with fixed edges gives us the wanted fixed spacing between items. After that, we’ll create NSCollectionLayoutGroup with horizontal layout direction and with a layout size which takes max width, but height is fitted based on item sizes. Creating layouts like this is so much better compared to subclassing UICollectionViewLayout and then calculating frames one by one. Down below is the configured layout object, which has fixed spacing and items are self-sizing.

extension UICollectionViewLayout {
static func fixedSpacedFlowLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .estimated(50),
heightDimension: .estimated(50)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(
leading: .fixed(8),
top: .fixed(4),
trailing: .fixed(8),
bottom: .fixed(4)
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(100)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: section)
}
}

Example project can be found here: GitHub

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 UIKit

Height fitting collection view

I have numerous times needed to show some sort of collection view which adjusts its height based on the content. Most of the time it has been a dynamic list within some more complex scrollable UI. Therefore, in this post, we’ll take a look at how to set up a collection view which has its height set to the content height. On the screenshot below, we have a collection view with light grey background and two sections.

Collection view with height fitting size.

The approach for making this working is pretty simple, which involves adding a height constraint with the constant value set to collection view content height. Content height can be retrieved from the layout object. The constraint’s constant value can be updated in viewWillLayoutSubviews.

private var collectionViewHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(TextCollectionViewCell.self, forCellWithReuseIdentifier: Self.reuseIdentifier)
collectionView.backgroundColor = UIColor(white: 0.9, alpha: 1)
collectionView.isScrollEnabled = false
collectionViewHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 50)
collectionViewHeightConstraint.isActive = true
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionViewHeightConstraint.constant = collectionViewLayout.collectionViewContentSize.height
}

The full example collection view implementation can be seen here: FittingHeightCollectionView.

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 SwiftUI

Exploring AttributedString and custom attributes

WWDC’21 brought new APIs for creating attributed strings. AttributedString is a new value type which provides a type-safe API for dealing with attributes, its also localizable, supports limited amount of Markdown syntax, and can be even archived and unarchived thanks to the Codable support. In this blog post, we’ll take a look at the new API and will compose some attributed strings.

In addition to AttributedString type, there are many supporting types. For example, AttributeContainer is a collection of attributes which can be applied to the attributed string with one go. AttributeScope protocol groups attributes and AttributeScopes type contains several groups of attributes like AttributeScopes.swiftUI for attributed strings rendered in SwiftUI views. There is also AttributeScopes.uiKit, AttributeScopes.appKit, and AttributeScopes.foundation. Let’s now see how to create attributed strings and render them in a SwiftUI view.

An AttributedString with attributes applied to multiple ranges.

The attributed string visible above contains multiple attributes starting with background color attribute and finishing with a link attribute. In the snippet below we can find different ways how to set attributes: searching for a range, manually creating a range, using AttributedContainer for setting multiple attributes at once, and also setting attributes to the whole string. As this string is displayed in a SwiftUI view then all the used attributes are part of the SwiftUI attribute scope.

var string = AttributedString(localized: "The quick brown fox jumps over the lazy dog")
// Add attribute to the whole string
string.font = Font.system(size: 14, weight: .heavy, design: .rounded)
// Add a single attribute to a range
if let range = string.range(of: "The") {
string[range].backgroundColor = Color.indigo
}
if let range = string.range(of: "lazy") {
string[range].strikethroughColor = .yellow
string[range].strikethroughStyle = .single
}
// Add a link
if let range = string.range(of: "dog") {
string[range].link = URL(string: "https://www.apple.com")!
}
// Create a range manually with indices
let start = string.characters.index(string.startIndex, offsetBy: 10)
let end = string.characters.index(start, offsetBy: 5)
string[start..<end].foregroundColor = .brown
// Add multiple attributes at once to a range
if let range = string.range(of: "jumps") {
var attributeContainer = AttributeContainer()
attributeContainer.baselineOffset = 4
attributeContainer.kern = 5
attributeContainer.underlineColor = .cyan
attributeContainer.underlineStyle = .patternDot
string[range].mergeAttributes(attributeContainer)
}

If we would like to see the ranges attributes were set to, we can use the runs API. A single run is a set of attributes shared by a single range. If we print all the runs, in this case 9, then it would look like this:

The {
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	SwiftUI.BackgroundColor = indigo
}
 quick  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
brown {
	NSPresentationIntent = [paragraph (id 1)]
	NSLanguage = en
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	SwiftUI.ForegroundColor = brown
}
 fox  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
jumps {
	SwiftUI.UnderlineColor = cyan
	NSLanguage = en
	SwiftUI.Kern = 5.0
	SwiftUI.BaselineOffset = 4.0
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSPresentationIntent = [paragraph (id 1)]
	NSUnderline = NSUnderlineStyle(rawValue: 256)
}
 over the  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
lazy {
	SwiftUI.StrikethroughColor = yellow
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
	NSStrikethrough = NSUnderlineStyle(rawValue: 1)
}
  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
dog {
	NSLink = https://www.apple.com
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}

The new AttributedString API also supports custom attributes. Custom attributes need to conform to a AttributeStringKey protocol in bare minimum. But when we would like to benefit from using the custom attribute in Markdown and also allowing to decode and encode it to data with Codable then we would need to conform to MarkdownDecodableAttributedStringKey and CodableAttributedStringKey respectively. In a simple example, let’s create a new attribute named MessageAttribute which can store a value of Message struct with id and value fields. The MessageAttribute needs to define the type it stores and a name used when encoding and decoding. In addition, we’ll need to add a new attribute scope which contains the new attribute. As we intend to use the new attribute in a SwiftUI app then we’ll add swiftUI attributes to the scope as well.

struct Message: Hashable, Codable {
let id: String
let value: String
}
struct MessageAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = Message
static var name: String = "secretMessage"
}
extension AttributeScopes {
struct MyAppAttributes: AttributeScope {
let message: MessageAttribute
let swiftUI: SwiftUIAttributes
}
var myApp: MyAppAttributes.Type { MyAppAttributes.self }
}

With this set we can create attributed strings with this attribute either using markdown syntax or adding the attribute manually. Markdown syntax for custom attributes uses caret followed with square brackets and with the content in brackets after that. We also need to make sure to pass custom attribute scope into the AttributedString initializer as well. One thing what I have not figured out is how to create a completely custom appearance for custom attributes in SwiftUI views, like we can do in UIKit views.

var string = AttributedString(localized: "This contains my message attribute", including: \.myApp)
let start = string.startIndex
let end = string.characters.index(start, offsetBy: 4)
string[start..<end].myApp.message = Message(id: "message_id", value: "secret")
// print(string.runs):
// This {
// NSPresentationIntent = [paragraph (id 1)]
// NSLanguage = en
// secretMessage = Message(id: "message_id", value: "secret")
// }
// contains my message attribute {
// NSPresentationIntent = [paragraph (id 1)]
// NSLanguage = en
// }
let string = AttributedString(localized: "This contains my ^[message](secretMessage: {id: 'message_id', value: 'Hello!'}) attribute in markdown", including: \.myApp)
// print(string.runs)
// This contains my {
// NSPresentationIntent = [paragraph (id 1)]
// NSLanguage = en
// }
// message {
// NSLanguage = en
// NSPresentationIntent = [paragraph (id 1)]
// secretMessage = Message(id: "message_id", value: "Hello!")
// }
// attribute in markdown {
// NSLanguage = en
// NSPresentationIntent = [paragraph (id 1)]
// }

Summary

We took a look on AttributedString, AttributeContainer, AttributeScope, and created attributed strings with the new API. With this knowledge, we got going with the new API and can continue exploring it further. The last thing to mention is that AttributedString can be converted to and from NSAttributedString with breeze.

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

Measuring collections with Apple’s Swift Collections Benchmark

Apple’s swift-collections package contains data structure implementations which have more specific use-cases and therefore not fitting into the Foundation. Along with collections Swift package Apple also released swift-collections-benchmark package which implements benchmarking. As like with any other Swift package we can use the benchmarking package for our own purposes as well. Therefore, in this blog post let’s take a look on how to use the benchmarking package.

Step one is to create a new Swift package with executable target which will be used to run benchmarks. In Xcode’s File menu we can select New > Swift Package. In the created package we will want to do some changes in the Package.swift file. We’ll need to add swift-collections-benchmark dependency and change the package type to executable instead of library.

// swift-tools-version:5.4
import PackageDescription
let package = Package(
name: "SwiftBenchmark",
products: [
.executable(
name: "SwiftBenchmark",
targets: ["SwiftBenchmark"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-collections-benchmark&quot;, from: "0.0.1"),
],
targets: [
.executableTarget(
name: "SwiftBenchmark",
dependencies: [.product(name: "CollectionsBenchmark", package: "swift-collections-benchmark")])
]
)
view raw Package.swift hosted with ❤ by GitHub
Package.swift file after changing the default library template to executable.

Secondly, we need to add main.swift file to the package or use the @main keyword. In this example I went for main.swift. When that is set as well, we can start using the benchmarking package and writing benchmarks for collections. For trying out the benchmarking I am going to use raywenderlicht/swift-algorithms-club’s queue implementations. It provides two examples of queues where one is a thin wrapper around the array type and the other one is an optimized version which looks into improving performance when dequeuing elements from the queue.

Setting up a benchmark requires defining a benchmark object and adding benchmark tasks to it and then calling main() on the benchmark object. Every added task will show up as a separate line on the chart when we render results. The package has some input generators defined and also allows adding more if we need to. Check the registerInputGenerator function on the Benchmark type for more information. But OK, back to benchmarks. The snippet below shows how to define a benchmark for dequeue operation over a variety of input sizes where the max input size is currently limited to 128000.

var queueBenchmark = Benchmark(title: "Queue")
queueBenchmark.add(title: "QueueSimple", input: [Int].self, maxSize: 128_000) { input in
return { timer in
var queue = QueueSimple<Int>()
input.forEach({ queue.enqueue($0) })
timer.measure {
for _ in 0..<input.count {
let value = queue.dequeue()
blackHole(value)
}
}
}
}
queueBenchmark.add(title: "QueueOptimized", input: [Int].self, maxSize: 128_000) { input in
return { timer in
var queue = QueueOptimized<Int>()
input.forEach({ queue.enqueue($0) })
timer.measure {
for _ in 0..<input.count {
let value = queue.dequeue()
blackHole(value)
}
}
}
}
queueBenchmark.main()
view raw main.swift hosted with ❤ by GitHub

When everything is set we can go ahead and run the benchmark. First navigate to the package’s folder and use the swift run command as shown below. When benchmarking finishes we can use the generated file, in this case “results”, to render a chart.

▶ swift run -c release SwiftBenchmark run results –cycles 3
[2/2] Linking SwiftBenchmark
* Build Completed!
Running 2 tasks on 76 sizes from 1 to 1M:
QueueSimple
QueueOptimized
Output file: /Users/toomas/Desktop/SwiftBenchmark/results
Appending to existing data (if any) for these tasks/sizes.
Collecting data:
1.2.4…8…16…32…64…128…256…512…1k…2k…4k…8k…16k…32k…64k…128k…256k…512k…1M — 15.1s
1.2.4…8…16…32…64…128…256…512…1k…2k…4k…8k…16k…32k…64k…128k…256k…512k…1M — 15s
1.2.4…8…16…32…64…128…256…512…1k…2k…4k…8k…16k…32k…64k…128k…256k…512k…1M — 15.1s
Finished in 45.2s
▶ swift run -c release SwiftBenchmark render results chart.png && open chart.png
view raw run.sh hosted with ❤ by GitHub

Like expected, the optimized queue performs much better in larger input sizes. Perfect!

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

Adding prefixMap for expensive operations in Swift

Swift Foundation contains a lot of useful functions on collection types. In the context of this blog post we are interested in map(_:) and prefix(while:). Map is used for transforming collection elements and prefix(while:) for getting a sequence containing initial elements until the predicate is true. In some cases the predicate used in the prefix(while:) can be expensive, or we just want to combine the information in prefix and map functions. One of such examples is when we use NSRegularExpression. More specifically, let’s take an example of processing a list of strings while the regular expression has matches and then extracting a range from the string. A concrete example could be parsing Fastlane’s Fastfile for visualization purposes.

Fastlane is used a lot in the iOS community for automating development related tasks. Lanes are added to a Fastfile where every individual lane has a name and optionally a description.

desc 'Build and upload app store build'
desc 'Captures screenshots, builds the app, uploads to app store, and posts a message to slack'
lane :appstore do
capture_screenshots
build_app
upload_to_app_store
slack
end
view raw Fastfile hosted with ❤ by GitHub

If we would like to extract the lane name and description from the Fastfile then we can use regular expressions. The flow could be something like this: firstly, we can read the Fastfile contents, divide the file into lines and match lines with regular expressions. Second step is finding lines which contain a lane keyword. Then we could loop over preceding lines and collect lines which contain description. All in all, the logic for getting description could look like this:

let descExpression = try! NSRegularExpression(pattern: "^\\s*desc [\"']{1}([^\"]*)[\"']{1}", options: [])
// …
let description = (0..<lineIndex)
.reversed()
.prefixMap({ index -> String? in
// Get a line
let line = lines[index]
// Use regular expression
guard let match = descExpression.firstMatch(in: line) else { return nil }
// Regular expression contains a capture group, therefore looking for 2 ranges
guard match.numberOfRanges >= 2 else { return nil }
// When there are matches then extract description
guard let range = Range(match.range(at: 1), in: line) else { return nil }
return String(line[range])
})
.reversed()
.joined(separator: "\n")

Now when we have seen a case where prefixMap can be useful, it is time to look into how it is implemented. The transform passed into the prefixMap function can return nil and the nil value means that the looping should be stopped and all the transformed elements should be returned. And yes, the implementation is pretty straight-forward.

// Swift Foundation
func prefix(while predicate: (Self.Element) throws -> Bool) rethrows -> Self.SubSequence
func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]
// prefixMap (new)
extension Collection {
@inlinable func prefixMap<T>(_ transform: (Self.Element) throws -> T?) rethrows -> [T] {
var result = [T]()
for element in self {
if let transformedElement = try transform(element) {
result.append(transformedElement)
}
else {
break
}
}
return result
}
}

Summary

Foundation types can be extended with new functions easily. Although we could use prefix(while:) first followed with a map(_:) but sometimes we’ll just need to combine functionalities into a single function.

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

PrefixMapPlayground (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
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
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 macOS Swift SwiftUI

@StateObject and MVVM in SwiftUI

A while ago I wrote about using MVVM in a SwiftUI project. During the WWDC’20 Apple announced @StateObject property wrapper which is a nice addition in the context of MVVM. @StateObject makes sure that only one instance is created per a view structure. This enables us to use @StateObject property wrappers for class type view models which eliminates the need of managing the view model’s lifecycle ourselves.

Comparing @StateObject and @ObservableObject for view model property

The most common flow in MVVM is creating and configuring a view model, which includes injecting dependencies, and then passing it into a view. This creates a question in SwiftUI: how to manage the lifecycle of the view model when view hierarchy renders and view structs are recreated. @StateObject property wrapper is going to solve this question in a nice and concise way. Let’s consider an example code.

import SwiftUI
struct ContentView: View {
@EnvironmentObject var dependencyContainer: DependencyContainer
@StateObject var viewModel = ContentViewModel()
var body: some View {
VStack(alignment: .center) {
VStack(spacing: 32) {
Text(viewModel.refreshTimestamp)
Button(action: viewModel.refresh, label: {
Text("Refresh")
})
}
Spacer()
BottomBarView(viewModel: BottomBarViewModel(entryStore: dependencyContainer.entryStore))
}
}
}
ContentView which has a subview.

ContentView is a simple view which has a view model managing the view state and DependencyContainer used for injecting a dependency to the BottomBarViewModel when it is created. As we can see, ContentView’s view model is managed by @StateObject property wrapper. This means that ContentViewModel is created once although ContentView can be recreated several times. BottomBarView has a little bit more complex setup where the view model requires external dependency managed by the DependencyContainer. Therefore, we’ll need to create the view model with a dependency and then initialize BottomBarView with it. BottomBarView’s view model property is also annotated with @StateObject property wrapper.

import Combine
import SwiftUI
struct BottomBarView: View {
@StateObject var viewModel: BottomBarViewModel
var body: some View {
Text(viewModel.text)
}
}
final class BottomBarViewModel: ObservableObject {
@Published var text: String = ""
private var cancellables = [AnyCancellable]()
private let entryStore: EntryStore
init(entryStore: EntryStore) {
self.entryStore = entryStore
print(self, #function)
cancellables.append(Timer.publish(every: 2, on: .main, in: .default).autoconnect().sink { [weak self] (_) in
self?.text = "Random number: \(Int.random(in: 0..<100))"
})
}
}
BottomBarView and its view model.

Magical aspect here is that when the ContentView’s body is accessed multiple times then BottomBarViewModel is not recreated when the BottomBarView struct is initialized. Exactly what we need – view will manage the lifecycle of its view model. This can be verified by adding a print to view model initializers and logging when ContentView’s body is accessed. Here is example log which compares BottomBarView’s view model property when it is annotated with @StateObject or @ObservableObject. Note how view model is not created multiple times when BottomBarView uses @StateObject.

BottomBarView uses @StateObject for its view model property
SwiftUIStateObject.ContentViewModel init()
1 ContentView.body accessed
SwiftUIStateObject.BottomBarViewModel init(entryStore:)
Triggering ContentView refresh
2 ContentView.body accessed
Triggering ContentView refresh
3 ContentView.body accessed

BottomBarView uses @ObservableObject for its view model property
SwiftUIStateObject.ContentViewModel init()
1 ContentView.body accessed
SwiftUIStateObject.BottomBarViewModel init(entryStore:)
Triggering ContentView refresh
2 ContentView.body accessed
SwiftUIStateObject.BottomBarViewModel init(entryStore:)
Triggering ContentView refresh
3 ContentView.body accessed
SwiftUIStateObject.BottomBarViewModel init(entryStore:)

Summary

WWDC’20 brought us @StateObject which simplifies handling view model’s lifecycle in apps using MVVM design pattern.

If you are looking more information about the MVVM design pattern then please check: MVVM in SwiftUI and MVVM and @dynamicMemberLookup in Swift.

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

SwiftUIStateObject (GitHub) Xcode 12.0 beta 3