A Flock of Swifts
We are a group of people excited by the Swift language. We meet each Saturday morning to share and discuss Swift-related topics.
All people and all skill levels are welcome to join.
Archives
2023.02.11
2023.02.04
Parsing RSS Feeds
RSS is simple XML, so the the old XML Parser API should work.
Frank did some work on this previously. This project might help.
Josh implemented a partial SVG reader where he didn't implement all of the callbacks but got the information that he was interested in. See: https://github.com/joshuajhomann/SVGPasteBoard/blob/master/SVGPasteBoard/ContentView.swift
Using Custom Fonts
Ed wants his custom fonts to play nicely with the OS (and respect dynamic type).
Franklin mentioned:
@ScaledMetric(relativeTo: .largeTitle) var dynamicHeader1Size: CGFloat = 24
iOS Podcast GPT
Writing Apps with ChatGPT
Emil reports that ChatGPT is helping him to get going with SwiftUI. He has reproduced a major portion of his previous app in SwiftUI in a few days where the original took him months (as he was learning).
Noting that some of the code is old (because ChatGPT was trained before 2021).
The licensing of the code is ambiguous.
Space, the final Frontier
Carlyn mentioned that many of her space friends are excited about this new release:
https://www.penguinrandomhouse.com/books/651844/critical-mass-by-daniel-suarez/
The Nature of Code
Carlyn has been working through these (knows the author) in the last couple of months:
Wordle Clone!
Josh implemented all of the game logic for his wordle clone. He used a reducer that took inputs through a tap gesture recognizer and reduced the state, modifying two properties that drive the UI updates.
Code TBD
2023.01.28
AsyncImage Fix
Jake had an update to how he was able to fix the animation problem with AsyncImage
. Here is the code:
AsyncImage(url: user.profileURL,
transaction: Transaction(animation: .default)) { phase in
switch phase {
case .success(let image):
image.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
default:
ZStack {
Circle().stroke(.black)
.frame(width: 90, height: 90)
Image(systemName: "person")
.font(.title.bold())
.scaledToFill()
}
}
}.frame(width: 90, height: 90)
Non-uniform shuffling
Carlyn asked about the most Swifty way to enable non-uniform probability picking.
Some suggestions from the group:
- Look around in GameKit? (Perhaps look at this: https://www.youtube.com/watch?v=gXnuMk7AVwc)
- Create an array with duplicates of the number of elements in the probablilty you want.
- Create a special collection that vends the duplicates without actually hosting them in memory.
A related topic: https://en.wikipedia.org/wiki/Rejection_sampling
Speeding up Conformance Checking
We reviewed this blog post about how you can re-order conformance records to get a 20% performance boost.
Wurdle (Wordle Clone)
Josh continued his epic presentation on a Wordle clone. Attempted to make it work pretty. Namely, GeometryReader
is not greedy and anchors things to the upper-left. You can work around it by adding a ZStack that contains a greedy view like Color.clear or Rectangle().hidden() to the ZStack.
We used the Layout
protocol which is a type of view that can explicitly control the layout of a view and its subviews. Josh created a AnchorInParentLayout that lets you align an arbitrary position (UnitPoint
) with an arbitrary position of the subview(s). A view modifier makes it easy to use.
2023.01.21
Layout with AsyncImage
Jake was seeing a problem with AsyncImage
where the transition animation would be cancelled when the async image loaded. The image would appear at the destination without loading. Josh theorized that the problem was happening because its identity was changing. However, we could not seem to fix the problem by explicitly setting id
on the views.
One recommendation is to use a much more capable third party library like Nuke.
Dates
Using Core Data to sort by date. Ed says, "CalendarComponents
is your friend." Trevor recommended this resource: https://nsdateformatter.com/
Inspiration for UI
- https://dribbble.com
- https://twitter.com/_kavsoft?lang=en
- Edward Tufte
Jacey noted that SnapKit is also good (easy) way to layout views.
Sendable
Sendable conformance will be one of the important areas to be aware of as come into Swift 6. You can enable strict concurrency warnings in your build settings. The default is minimal but you can use "targetted" to check your own code.
When you enabled this checking, you will see warnings where an instance is passed across a concurrency domain and is not Sendable
. For example:
func findInBackground(quadTree: QuadTree,
region: CGRect) async -> Task<[CGPoint], Never> {
Task.detached {
quadTree.find(in: region) // WARNING: QuadTree is not Sendable
}
}
You can mark QuadTree
as Sendable
to fix this warning. This will, in turn, lead to a warning that Node
, the reference type, is not sendable. If you mark Node
sendable you get more warnings. This is because the class contains multiple immutable stored properties that could get modified from another concurrency domain. In this case you can mark Node
with @unchecked Sendable
since you know that all mutation is protected by isKnownUniquelyReferenced
and makes a deep copy if it is not unique. (Aka COW.)
Benchmarks
The collection-benchmark project allows you to create benchmarks where the time might be dependent on the size of the input. We created a command-line target and included the benchmark package.
We wrote the following benchmarks:
import CollectionsBenchmark
import CoreGraphics.CGBase
struct TestPoints {
let region: CGRect
let points: [CGPoint]
init(size: Int) {
region = CGRect(origin: .zero, size: CGSize(width: size, height: size))
points = zip((0..<size).shuffled(), (0..<size).shuffled())
.map { CGPoint(x: $0.0, y: $0.1) }
}
}
var benchmark = Benchmark(title: "QuadTree Benchmarks")
benchmark.registerInputGenerator(for: TestPoints.self) { size in
TestPoints(size: size)
}
benchmark.add(title: "QuadTree find",
input: TestPoints.self) { testPoints in
let tree = QuadTree(region: testPoints.region, points: testPoints.points)
return { timer in
testPoints.points.forEach { point in
let searchRegion = CGRect(origin: point, size: .zero).insetBy(dx: -1, dy: -1)
blackHole(tree.find(in: searchRegion))
}
}
}
benchmark.addSimple(title: "Array<CGPoint> filter",
input: TestPoints.self) { testPoints in
testPoints.points.forEach { point in
let searchRegion = CGRect(origin: point, size: .zero).insetBy(dx: -1, dy: -1)
blackHole(testPoints.points.filter { candidate in
searchRegion.contains(candidate)
})
}
}
benchmark.main()
Then we ran the following commands arguments:
run QuadFindResult.json --cycles 1
render QuadFindResults.json QuadFindResults.png
This produced the following results:
This is a log-log chart and you can see that the growth of the array implementation is linear.
You can see the jump at 4 items which is where the QuadTree logic is kicking in. I found that on my machine, I can boost this constant to 512 to make it always perform better than array.
You can also use a special group file to automatically produce sets of benchmarks and multiple graphs.
Wurdle
Josh continued working on the Wordle game example getting through a lot of the layout issues of the words and keyboard (adding return and backspace).
import SwiftUI
extension Color {
static let darkGray = Color(#colorLiteral(red: 0.4784313725, green: 0.4823529412, blue: 0.4980392157, alpha: 1))
static let lightGray = Color(#colorLiteral(red: 0.8274509804, green: 0.8431372549, blue: 0.8549019608, alpha: 1))
static let darkGreen = Color(#colorLiteral(red: 0.4117647059, green: 0.6705882353, blue: 0.3803921569, alpha: 1))
static let darkYellow = Color(#colorLiteral(red: 0.7960784314, green: 0.7098039216, blue: 0.3137254902, alpha: 1))
}
struct Row: Hashable, Identifiable {
var id: Int
var letters: [Letter]
}
struct Letter: Hashable, Identifiable {
var id: Int
var character: Character
var status: Status
enum Status: Int, Comparable {
static func < (lhs: Letter.Status, rhs: Letter.Status) -> Bool { lhs.rawValue < rhs.rawValue }
case unguessed, wrong, wrongPosition, correct
}
}
@MainActor
final class GameViewModel: ObservableObject {
@Published private(set) var words: [Row] = []
@Published private(set) var keys: [Row] = []
init() {
words = ["SWIFT", "CODER", "PLAYA", " ", " ", " "]
.enumerated()
.map { word in
Row(
id: word.offset,
letters: word.element.enumerated().map { character in
Letter(
id: word.offset * 10 + character.offset,
character: character.element,
status: {
switch character.element {
case " ": return Letter.Status.unguessed
case "O", "A": return .correct
case "L": return .wrongPosition
default: return .wrong
}
}()
)
}
)
}
$words
.map { rows in
let keys = rows
.lazy
.flatMap(\.letters)
.reduce(into: [Character: Letter.Status]()) { accumulated, next in
accumulated[next.character] = accumulated[next.character].map { max($0, next.status) } ?? next.status
}
return ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
.enumerated()
.map { string in
Row(
id: string.offset,
letters: string.element.enumerated().map { character in
Letter(
id: string.offset * 100 + character.offset,
character: character.element,
status: keys[character.element] ?? .unguessed
)
}
)
}
}
.assign(to: &$keys)
}
}
struct ContentView: View {
@StateObject var viewModel = GameViewModel()
var body: some View {
GeometryReader { reader in
VStack(spacing: 36) {
Grid(alignment: .topLeading, horizontalSpacing: 12, verticalSpacing: 12) {
ForEach(viewModel.words) { row in
GridRow {
ForEach(row.letters) { letter in
LetterView(style: .word(letter))
}
}
}
}
.frame(maxWidth: 600)
VStack (spacing: 12) {
ForEach(viewModel.keys) { row in
HStack(spacing: 8) {
if viewModel.keys.last == row {
LetterView(style: .keyImage("return"))
keys(for: row)
LetterView(style: .keyImage("delete.backward"))
} else {
keys(for: row)
}
}
}
}
}
.padding()
}
}
private func keys(for row: Row) -> some View {
ForEach(row.letters) { letter in
LetterView(style: .key(letter))
}
}
}
struct LetterView: View {
var style: Style
enum Style {
case key(Letter), word(Letter), keyImage(String)
}
private let fontSize: CGFloat
private let aspectRatio: CGFloat
private let textColor: Color
private let backgroundColor: Color
private let outlineColor: Color
init(style: Style) {
self.style = style
switch style {
case .keyImage:
aspectRatio = 0.66 * 1.5
fontSize = 48
textColor = .black
backgroundColor = .lightGray
outlineColor = .clear
case let .key(letter):
aspectRatio = 0.66
fontSize = 48
(textColor, backgroundColor, outlineColor) = colors(for: letter, isKey: true)
case let .word(letter):
aspectRatio = 1
fontSize = 100
(textColor, backgroundColor, outlineColor) = colors(for: letter, isKey: false)
}
func colors(for letter: Letter, isKey: Bool) -> (Color, Color, Color) {
switch (letter.status, isKey) {
case (.unguessed, true): return (.black, .lightGray, .clear)
case (.unguessed, false): return (.black, .white, .black)
case (.correct, _): return (.white, .darkGreen, .clear)
case (.wrongPosition, _): return (.white, .darkYellow, .clear)
case (.wrong, _): return (.white, .darkGray, .clear)
}
}
}
var body: some View {
GeometryReader { proxy in
RoundedRectangle(cornerRadius: 8)
.strokeBorder(outlineColor, lineWidth: 4)
.background(RoundedRectangle(cornerRadius: 8).fill(backgroundColor))
.overlay {
switch style {
case let .keyImage(name):
Image(systemName: name)
case let .key(letter), let .word(letter):
Text(String(describing: letter.character))
}
}
.foregroundColor(textColor)
.font(.system(size: fontSize, weight: .bold))
}
.aspectRatio(aspectRatio, contentMode: .fit)
}
}
2023.01.14
PointFree Dependency Injection
A new library for dependency injection was announced this week by the folks at pointfree.co. Josh gave us a quick tour of the library and an additions library:
https://github.com/pointfreeco/swift-dependencies
Peter posted this example of using the additions library:
https://twitter.com/tgrapperon/status/1612698675356250114
Ed Launches Testflight
Ed launched a private testfligt build for his new app. During the coarse of the meeting was able to find and fix an out of bounds crasher when there is no data. The power of testing in action.
Learning Swift
Some of the tried and true:
Async Result
You can create an async init for result types to get clean monadic chaining instead of nested do {} catch {}
blocks. Daniel and Josh showed us how!
CoreData
Core Data is a deep subject. A good place to start:
Noise Generation
GameKit although old and written in ObjectiveC, has API for creating "natural" noise often used for procedural terrain generation in games.
Wurdle
As part of another epic, multipart demo, Josh is implementing a version of the popular game in SwiftUI. Today he created the basic model for Rows and Letters and Keys as well as the Status for each.
2023.01.07
Using contraMap Example
You can write a contramap function on CurrentValueSubject<Int>
to make functions
that can send other types into the current value.
import Foundation
import Combine
extension CurrentValueSubject {
func contraMap<Value>(transform: @escaping (Value) -> Output) -> (Value) -> Void {
{ [weak self] input in
self?.send(transform(input))
}
}
}
let subject = CurrentValueSubject<Int, Never>(0)
let sendBool = subject.contraMap { (bool: Bool) in bool ? 1 : 0 }
sendBool(true)
print(subject.value)
Learning SwiftUI
Core Data Debugging
Suggestions for Dan's app that has a crashing problem:
- Log non-fatal errors
- Think about all of the validations done by the Core Data model
- It reliably crashes on start
- Take note of the OS / devices it is happening on in the crash logs
- Debugging concurrency issues with -com.apple.CoreData.ConcurrencyDebug 1
Returning Swift Conferences
- https://deepdishswift.com
- https://www.swiftconf.to
- https://tryswift.jp (meetup style on 1/21)
Swift Charts Performance
Ed wrote his own charts with GeometryReader
instead of SwiftUI Charts because of
performance problems on rotation and scrolling.
Photo Picker iOS16
- Jake reports it is awesome.
- Requires user iteraction so no permission is required.
- https://developer.apple.com/documentation/photokit/photospicker
New Swift Proposals
https://www.swift.org/swift-evolution/
- SE-0383 Deprecate @UIApplicationMain and @NSApplicationMain
- SE-0384 Importing Forward Declared Objective-C Interfaces and Protocols
- SE-0382 Expression Macros
Compression
The new multicore APFS aware Apple Archive framework: https://developer.apple.com/documentation/applearchive
Swift Collections
Josh took us on a guided tour of the Swift Collections package.
Exploration of CHAMP:
Aside: A Benchmarking Tool
JSON with decode indirect enum
You can represent JSON with swift with this:
enum JSON {
indirect case array([JSON])
indirect case dictionary([String: JSON])
case boolean(Bool)
case number(Double)
case string(String)
case null
}
You can implement Decodable
using a single value container. Naively it is a bunch of nested do {} catch {}
blocks but it can be done quite succinctly by using a Result
type and flatMapError
to implement successive retries.
enum JSON {
indirect case array([JSON])
indirect case dictionary([String: JSON])
case boolean(Bool)
case number(Double)
case string(String)
case null
}
extension JSON: Decodable {
init(from decoder: Decoder) throws {
self = try Result { try decoder.singleValueContainer() }
.flatMap { container in
container.decodeNil()
? .success(JSON.null)
: Result { JSON.boolean(try container.decode(Bool.self)) }
.flatMapError { _ in Result { JSON.number(try container.decode(Double.self)) } }
.flatMapError { _ in Result { JSON.string(try container.decode(String.self)) } }
.flatMapError { _ in Result { JSON.array(try container.decode([JSON].self)) } }
.flatMapError { _ in Result { JSON.dictionary(try container.decode([String: JSON].self)) } }
}.get()
}
}