Skip to content

Meeting minutes and learnings from the physical space meeting.

Notifications You must be signed in to change notification settings

aflockofswifts/meetings

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A Flock of Swifts

Flock 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.
RSVP: https://www.meetup.com/A-Flock-of-Swifts/

Archives


Notes

2024.06.15

A review of WWDC 2024. We talked about our favorite features announced at the event. It is going to be an exciting year as they roll out many of the features later this summer. Many of the announced features are not yet included in the first beta.

Lots of great Swift content this year:

VisionPro

The Talk Show was apparently shot for an immersive experience on VisionPro.

https://apps.apple.com/app/theater-the-future-of-cinema/id6502666560

Noncopyable

Also performance of Swift by John McCall

Swift Testing

To name just a few.

AI Stuff

Some things discussed:

Greater standardization?

The platforms state of the union referred to Swift as a successor to C++. Are we moving toward greater standardization outside of Apple.

Other topics included embedded Swift and Swift being used in other environments.


2024.06.08

Discussion

WWDC

Events happening next week:

Lots of rumors about AI. What will they license from OpenAI?

Interesting side note from carlyn:

Outline Group

Swift Concurrency

Steadily making headway on reducing the false positive rate:

Inserting images into test

https://www.hackingwithswift.com/example-code/system/how-to-insert-images-into-an-attributed-string-with-nstextattachment

Presentation

We talked about algorithms and the Swift standard library. In particular, Josh led us through how the sort() algorithm has evolved and become a stable sort. Much of the discussion was based on the information in this blog post:

https://ohmyswift.com/blog/2019/09/29/swift-5-replaces-introsort-with-timsort-in-the-sort-method/


2024.06.01

Making a quartiles generator:

image

import SwiftUI

@Observable
@MainActor
final class WordViewModel: ObservableObject {
    var searchTerm: String = "" {
        didSet { update() }
    }
    private(set) var words: [Word] = []
    private(set) var selectedWords: [Word] = []
    private(set) var solutions: [Word] = []
    private var allWords: [Word] = []
    private var fourSyllableWords: [Word] = []
    func load() async {
        async let makeWords = Word.makeFromFewestSyllables()
        allWords = await makeWords
        async let makeFourSyllableWords = {
            await allWords.filter { $0.syllables.count == 4 }
        }()
        fourSyllableWords = await makeFourSyllableWords
        await updateWords()
    }
    func select(_ word: Word) {
        selectedWords.append(word)
        update()
    }
    func delete(at offsets: IndexSet) {
        selectedWords.remove(atOffsets: offsets)
        update()
    }
    private func update() {
        Task { await updateWords() }
    }
    private func updateWords() async {
        let term = searchTerm
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil)
        let usedSyllables = selectedWords.reduce(into: Set<String>()) { accumulated, word in
            word.syllables.forEach { accumulated.insert($0) }
        }
        async let availableWords = {
            let fourSyllableWordsWithUnusedSyllables = await self.fourSyllableWords.filter {
                $0.syllables.intersection(usedSyllables).isEmpty
            }
            return fourSyllableWordsWithUnusedSyllables
        }()
        let fourSyllableWordsWithUnusedSyllables = await availableWords
        if term.isEmpty {
            words = fourSyllableWordsWithUnusedSyllables
        } else {
            async let filteredWords = {
                fourSyllableWordsWithUnusedSyllables.filter { $0.description.hasPrefix(term) }
            }()
            words = await filteredWords
        }
        async let makeSolutions = {
            let allWords = await self.allWords
            return allWords.filter { $0.syllables.isSubset(of: usedSyllables) && $0.syllables.count > 1}
        }()
        solutions = await makeSolutions
    }
}

@MainActor
struct ContentView: View {
    var viewModel = WordViewModel()
    var body: some View {
        NavigationSplitView(columnVisibility: .constant(.all)) {
            NavigationStack {
                @Bindable var vm = viewModel
                List(viewModel.words) { word in
                    HStack {
                        Text(word.description)
                        Text(word.syllableDescription).foregroundStyle(.secondary)
                    }
                    .contentShape(Rectangle())
                    .onTapGesture { viewModel.select(word) }
                }
                .navigationTitle("Available Words")
                .searchable(text: $vm.searchTerm)
            }
        } content: {
            List {
                ForEach(viewModel.selectedWords) { word in
                    HStack {
                        Text(word.description)
                        Text(word.syllableDescription).foregroundStyle(.secondary)
                    }
                }
                .onDelete(perform: viewModel.delete(at:))
            }
            .navigationTitle("Selected Words")
        } detail: {
            List(viewModel.solutions) { word in
                HStack {
                    Text(word.description)
                    Text(word.syllableDescription).foregroundStyle(.secondary)
                }
            }
            .navigationTitle("Solutions")
        }
        .task { await viewModel.load() }
    }
}

import OSLog

let signposter = OSSignposter(subsystem: "com.josh.quartilesolver", category: "signposts")

struct Word: CustomStringConvertible, Identifiable, Hashable, Sendable {
    var id: String { description }
    var description: String
    var syllableDescription: String
    var syllables: Set<String>
}

extension Word {
    static func makeFromFewestSyllables() -> [Word] {
        func stringsFromFile(name: String) -> [String] {
            Bundle.main.url(forResource: name, withExtension: "json")
                .flatMap { try? Data(contentsOf: $0) }
                .flatMap { try? JSONDecoder().decode([String].self, from: $0) } ?? []
        }
        let words = stringsFromFile(name: "words")
        let syllables = Set(stringsFromFile(name: "syllables"))
        let longest = syllables.lazy.map(\.count).max()
        let start = Date()
        let wordSignpostID = signposter.makeSignpostID()
        let signpostWordState = signposter.beginInterval("Words", id: wordSignpostID)
        let validWords = words.reduce(into: [Word]()) { validWords, candidate in
            var suffix = candidate
            var foundSyllables: [String] = []
            while !suffix.isEmpty {
                let found = stride(from: min(5, candidate.count), to: 0, by: -1)
                    .lazy
                    .compactMap { window in
                        let prefixCandidate = String(suffix.prefix(window))
                        return syllables.contains(prefixCandidate) ? prefixCandidate : nil
                    }
                    .first
                guard let found else { return }
                foundSyllables.append(found)
                suffix = String(suffix.dropFirst(found.count))
                let foundSyllablesSet = Set(foundSyllables)
                if suffix.isEmpty, foundSyllablesSet.count == foundSyllables.count {
                    validWords.append(.init(description: candidate, syllableDescription: foundSyllables.joined(separator: ""), syllables: foundSyllablesSet))
                }
            }
        }
        signposter.endInterval("Words", signpostWordState)
        return validWords
    }
}

2024.05.25

Dates and Times

Modernizing code

We took a look at modernizing this project: https://github.com/joshuajhomann/AsciiFilter image

2024.05.18

Accessibility

https://developer.apple.com/documentation/accessibility/performing-accessibility-audits-for-your-app
https://nfb.org/programs-services/center-excellence-nonvisual-access/blind-users-innovating-and-leading-design
https://a11y-guidelines.orange.com/en/mobile/ios/wwdc/nota11y/2023/2310035/

Swift replica of the Manim transform matching parts animation.

Core Text Programming Guide
iOS Text

Flock

import SwiftUI
import simd

@Observable
final class AnagramViewModel {
    let sourcePathsAndRects: [(CGPath, CGRect)]
    let destinationPathAndRects: [(CGPath, CGRect)]
    var sourceWidth: CGFloat {
        sourcePathsAndRects.last?.1.maxX ?? .zero
    }
    let source = "I am Lord Voldemort"
    let destination = "Tom Marvolo Riddle"
    let sourceIndexToDestinationIndex: [Int : Int]
    let lineHeight: Double
    init() {
        let normalizedSource = source.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil).replacingOccurrences(of: " ", with: "")
        let normalizedDestination = destination
            .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil).replacingOccurrences(of: " ", with: "")
        var destinationIndices = normalizedDestination.enumerated().reversed().reduce(into: [Character: [Int]]()) { accumulated, next in
            accumulated[next.element, default: []].append(next.offset)
        }
        sourceIndexToDestinationIndex = normalizedSource.enumerated().reduce(into: [Int: Int]()) { accumulated, next in
            let target = destinationIndices[next.element]?.popLast()
            accumulated[next.offset] = target
        }
        let font = UIFont.preferredFont(forTextStyle: .largeTitle)
        lineHeight = font.lineHeight
        sourcePathsAndRects = source.glyphs(applying: font)
        destinationPathAndRects = destination.glyphs(applying: font)
    }
}

struct ContentView: View {
    @State private var t = 0.0
    @State var viewModel = AnagramViewModel()
    var body: some View {
        Slider(value: $t)
        TimelineView(.animation) { context in
            let curve = UnitCurve.easeInOut
            let t = curve.value(at: abs(fmod(context.date.timeIntervalSince1970, 4) / 2 - 1))
            Canvas {
                context,
                size in
                context.transform = .init(scaleX: 1, y: -1)
                    .translatedBy(x: size.width/2, y: -size.height/2)
                let xOffset = viewModel.sourceWidth / 2
                for (index, (path, rect)) in viewModel.sourcePathsAndRects.enumerated() {
                    let (destinationPath, destinationRect) = viewModel.sourceIndexToDestinationIndex[index].map { index in
                        viewModel.destinationPathAndRects[index]
                    } ?? (nil, nil)
                    var point: SIMD2<Double>
                    if let destinationRect {
                        let height = (rect.minX - destinationRect.minX) / 2.5
                        let curve = CubicBezierCurve(
                            start: .init(rect.minX, rect.minY),
                            control1: .init(rect.minX, rect.minY + height),
                            control2: .init(destinationRect.minX, destinationRect.minY  + height),
                            end: .init(destinationRect.minX, destinationRect.minY)
                        )
                        point = curve(t)
                    } else {
                        point = .init(rect.minX, rect.minY)
                    }
                    point.x -= xOffset
                    var pathContext = context
                    pathContext.transform = pathContext.transform
                        .translatedBy(x: point.x, y: point.y)
                    pathContext.stroke(Path(path), with: .color(Color.red.opacity(1-t)))
                    guard let destinationPath else { continue }
                    var destinationPathContext = context
                    destinationPathContext.transform = destinationPathContext.transform
                        .translatedBy(x: point.x, y: point.y)
                    destinationPathContext.fill(Path(destinationPath), with: .color(Color.black.opacity(t)))
                }
            }
            Canvas { context, size in
                context.transform = .init(scaleX: 1, y: -1)
                    .translatedBy(x: size.width/2, y: -size.height/2)
                let xOffset = viewModel.sourceWidth / 2
                for (index, (path, rect)) in viewModel.sourcePathsAndRects.enumerated() {
                    let (destinationPath, destinationRect) = viewModel.sourceIndexToDestinationIndex[index].map { index in
                        viewModel.destinationPathAndRects[index]
                    } ?? (nil, nil)
                    let rectMin = (destinationRect.map { destinationRect in
                        simd_mix(rect.minX, destinationRect.minX, t)
                    } ?? rect.minX) - xOffset

                    var pathContext = context
                    pathContext.transform = pathContext.transform
                        .translatedBy(x: rectMin, y: 0)
                    pathContext.fill(Path(path), with: .color(Color.black.opacity(1 - t)))
                    guard let destinationPath else { return }
                    var desintationPathContext = context
                    desintationPathContext.transform = desintationPathContext.transform
                        .translatedBy(x: rectMin, y: 0)
                    desintationPathContext.fill(Path(destinationPath), with: .color(Color.black.opacity(t)))
                }
            }
        }
        Canvas { context, size in
            context.transform = .init(scaleX: 1, y: -1)
                .translatedBy(x: size.width/2, y: -size.height/2)
            let xOffset = viewModel.sourceWidth / 2
            for (path, rect) in viewModel.sourcePathsAndRects {
                var pathContext = context
                pathContext.transform = pathContext.transform
                    .translatedBy(x: rect.minX - xOffset, y: 0)
                pathContext.fill(Path(path), with: .color(Color.black))
                var squareContext = context
                squareContext.transform = squareContext.transform
                    .translatedBy(x: -xOffset, y: 0)
                squareContext.stroke(Path(roundedRect: rect, cornerSize: .zero), with: .color(Color.red))
            }
        }
    }
}

extension String {
    func glyphs(applying font: UIFont) -> [(CGPath, CGRect)] {
        let attributedString = NSAttributedString(string: self, attributes: [.font: font])
        let line = CTLineCreateWithAttributedString(attributedString)
        return (CTLineGetGlyphRuns(line) as? [CTRun]).map { runs in
            runs.flatMap { run in
                let count = CTRunGetGlyphCount(run)
                var advances = [CGSize](repeating: .zero, count: count)
                CTRunGetAdvances(run, CFRangeMake(0, count), &advances)
                var transform = CGAffineTransform.identity
                let paths = [CGGlyph](unsafeUninitializedCapacity: count) { buffer, allocatedCount in
                    CTRunGetGlyphs(run, CFRange(), buffer.baseAddress!)
                    allocatedCount = count
                }.lazy.map { glyph in
                    CTFontCreatePathForGlyph(font as CTFont, glyph, &transform)
                }
                return zip(
                    paths,
                    advances.map(\.width).scanMap(initial: 0.0) { x, width in
                        defer { x += width }
                        return CGRect(x: x, y:font.descender + font.leading, width: width, height: font.lineHeight)
                    }
                )
                .compactMap { paths, rect in paths.map { ($0, rect) } }
            }
        } ?? []
    }
}

extension Sequence {
    func scanMap<State, Transformed>(
        initial: consuming State,
        transform: @escaping (inout State, Element) -> Transformed
    ) -> some Sequence<Transformed> {
        sequence(state: (accumulated: initial, iterator: self.makeIterator())) { state in
            state.iterator.next().map { element in
                transform(&state.accumulated, element)
            }
        }
    }
}

struct QuadraticBezierCurve {
    typealias Point = SIMD2<Double>
    typealias Vector = SIMD3<Double>
    typealias Matrix = matrix_double3x3
    private let x: Vector
    private let y: Vector
    static let matrix = Matrix([
        .init(1, -2,  1),
        .init(0,  2, -2),
        .init(0,  0,  1)
    ])
    init(start: Point, control: Point, end: Point) {
        x = Vector(start.x, control.x, end.x)
        y = Vector(start.y, control.y, end.y)
    }
    init(start: CGPoint, control: CGPoint, end: CGPoint) {
        x = Vector(start.x, control.x, end.x)
        y = Vector(start.y, control.y, end.y)
    }
    func callAsFunction(_ t: Double) -> Point {
        let powerSeries = Vector(1, t, t*t)
        let scaleVector = powerSeries * Self.matrix
        let xProduct = scaleVector * x
        let yProduct = scaleVector * y
        return Point(xProduct.sum(), yProduct.sum())
    }
    func cgPoint(at t: Double) -> CGPoint {
        let point = self(t)
        return .init(x: point.x, y: point.y)
    }
}

struct CubicBezierCurve {
    typealias Point = SIMD2<Double>
    typealias Vector = SIMD4<Double>
    typealias Matrix = matrix_double4x4
    private let x: Vector
    private let y: Vector
    static let matrix = Matrix([
        .init(1, -3,  3, -1),
        .init(0,  3, -6,  3),
        .init(0,  0,  3, -3),
        .init(0,  0,  0,  1)
    ])
    init(start: Point, control1: Point, control2: Point, end: Point) {
        x = Vector(start.x, control1.x, control2.x, end.x)
        y = Vector(start.y, control1.y, control2.y, end.y)
    }
    init(start: CGPoint, control1: CGPoint, control2: CGPoint, end: CGPoint) {
        x = Vector(start.x, control1.x, control2.x, end.x)
        y = Vector(start.y, control1.y, control2.y, end.y)
    }
    func callAsFunction(_ t: Double) -> Point {
        let powerSeries = Vector(1, t, t*t, t*t*t)
        let scaleVector = powerSeries * Self.matrix
        let xProduct = scaleVector * x
        let yProduct = scaleVector * y
        return Point(xProduct.sum(), yProduct.sum())
    }
    func cgPoint(at t: Double) -> CGPoint {
        let point = self(t)
        return .init(x: point.x, y: point.y)
    }
}

2024.05.11

We looked at the transform matching parts animation in Manim
We started recreating this by making a Bezier Curve. First a quadratic:

struct QuadraticBezierCurve {
    typealias Point = SIMD2<Double>
    typealias Vector = SIMD3<Double>
    typealias Matrix = matrix_double3x3
    private let x: Vector
    private let y: Vector
    static let matrix = Matrix(rows: [
        .init(1, -2,  1),
        .init(0,  2, -2),
        .init(0,  0,  1)
    ])
    init(start: Point, control: Point, end: Point) {
        x = Vector(start.x, control.x, end.x)
        y = Vector(start.y, control.y, end.y)
    }
    init(start: CGPoint, control: CGPoint, end: CGPoint) {
        x = Vector(start.x, control.x, end.x)
        y = Vector(start.y, control.y, end.y)
    }
    func callAsFunction(_ t: Double) -> Point {
        let powerSeries = Vector(1, t, t*t)
        let scaleVector = Self.matrix * powerSeries
        let xProduct = scaleVector * x
        let yProduct = scaleVector * y
        return Point(xProduct.sum(), yProduct.sum())
    }
    func cgPoint(at t: Double) -> CGPoint {
        let point = self(t)
        return .init(x: point.x, y: point.y)
    }
}

and then a Cubic:

struct CubicBezierCurve {
    typealias Point = SIMD2<Double>
    typealias Vector = SIMD4<Double>
    typealias Matrix = matrix_double4x4
    private let x: Vector
    private let y: Vector
    static let matrix = Matrix([
        .init(1, -3,  3, -1),
        .init(0,  3, -6,  3),
        .init(0,  0,  3, -3),
        .init(0,  0,  0,  1)
    ])
    init(start: Point, control1: Point, control2: Point, end: Point) {
        x = Vector(start.x, control1.x, control2.x, end.x)
        y = Vector(start.y, control1.y, control2.y, end.y)
    }
    init(start: CGPoint, control1: CGPoint, control2: CGPoint, end: CGPoint) {
        x = Vector(start.x, control1.x, control2.x, end.x)
        y = Vector(start.y, control1.y, control2.y, end.y)
    }
    func callAsFunction(_ t: Double) -> Point {
        let powerSeries = Vector(1, t, t*t, t*t*t)
        let scaleVector =  powerSeries * Self.matrix
        let xProduct = scaleVector * x
        let yProduct = scaleVector * y
        return Point(xProduct.sum(), yProduct.sum())
    }
    func cgPoint(at t: Double) -> CGPoint {
        let point = self(t)
        return .init(x: point.x, y: point.y)
    }
}

then demonstrating that we can trace the same path as SwiftUI and use that path to drive an animation position:

struct ContentView: View {
    var body: some View {
        GeometryReader { proxy in
            let size = proxy.size
            let points: [CGPoint] = [
                .init(x: 0, y: size.height),
                .init(x: 0, y: size.height * 0.33),
                .init(x: size.width, y: size.height * 0.33),
                .init(x: size.width, y: size.height),
            ]
            let cubic = CubicBezierCurve(start: points[0], control1: points[1], control2: points[2], end: points[3])
            Canvas { context, size in
                let path = Path { path in
                    path.move(to: points[0])
                    path.addCurve(to: points[3], control1: points[1], control2: points[2])
                }
                context.stroke(path, with: .foreground, style: .init(lineWidth: 2))
                stride(from: 0.0, through: 1.0, by: 0.1).forEach { t in
                    let center = cubic.cgPoint(at: t)
                    context.fill(Path { path in
                        path.addArc(center: center, radius: 10, startAngle: .zero, endAngle: .radians(.pi * 2), clockwise: true)
                    }, with: .color(.red))
                }
            }
            TimelineView(.animation) { timeline in
                Canvas { context, size in
                    let t = fmod(timeline.date.timeIntervalSince(.distantPast), 5) / 5
                    let center = cubic.cgPoint(at: t)
                    context.fill(Path { path in
                        path.addArc(center: center, radius: 10, startAngle: .zero, endAngle: .radians(.pi * 2), clockwise: true)
                    }, with: .color(.green))
                }
            }
        }
    }
}

Flock

Matrix form of a quadratic bezier
Matrix form of a cubic bezier

2024.05.04

Topics Discussed

  • Dependency injection how and why.
    Monty asked about making his dates and times testable:
import Foundation

protocol SystemServiceProtocol {
    var now: Date { get }
}

final class SystemService: SystemServiceProtocol {
    var now: Date {
        .now
    }
}

final class MockSystemService: SystemServiceProtocol {
    var now: Date = .distantPast
}

We also discussed remote configuration and making a debug menu and debugService

@dynamicMemberLookup
struct Flagged<Wrapped> {
    var isLocked: Bool
    var wrapped: Wrapped
    subscript<Value>(dynamicMember dynamicMember: WritableKeyPath<Self, Value>) -> Value {
        self[dynamicMember: dynamicMember]
    }
}
func two(sum target: Int, from values: [Int]) -> (Int, Int)? {
    let lookup = values.enumerated().reduce(into: [Int: [Int]]()) { accumulated, next in
        accumulated[next.element, default: []].append(next.element)
    }
    return values.print().enumerated().lazy.compactMap { (index, value) -> (Int, Int)? in
        guard let other = lookup[target - value], let otherIndex = other.last, otherIndex != index else { return nil }
        return (index, otherIndex)
    }
    .first
}

Peter asked about a print for Sequence mirroring Combine's print operator. We looked at the general form:

extension Sequence {
    func sideEffect(_ effect: (Element) -> Void) -> some Sequence<Element> {
        map { value in
            effect(value)
            return value
        }
    }
}

and the specific solution:

extension Sequence where Element: CustomStringConvertible {
    func print() -> some Sequence<Element> {
        sideEffect { Swift.print(String(describing: $0)) }
    }
}

Carlyn shared two free computer science courses:


2024.04.27

Questions and Discussion

LLDB Debugging

You can use the LLDB prompt in Xcode that comes up when you hit a breakpoint in the console window.

As an example:

memory read -s1 -fu -c10000 0xb0987654 --force

LLDB Basics in 11 Minutes

Computers

Some shows about how computers are awesome

Swift 6

In this SPI podcast, Holly Borla notes that the transition to Swift 6 shouldn't be as bad as many people are fearing.

Glow Shine Effect

AI

A ton of research materials from Georgi with regard many advanced attribution topics (including Revtrival Augmented Generation [RAG]):

Ed noting that Apple released some on-device models.


2024.04.20

Presentation: Swift Package Manager

Carlyn showed us a new command line tool she is working on for managing her many Swift Packages.

  - https://github.com/carlynorama/TemplatePackageToolLibrary

Presentation: Maps and SwiftUI

Frank showed us how to implement a map today in SwiftUI. His implementation let us put down points of interest with a long tap in pure SwiftUI.

import SwiftUI
import MapKit
  
struct POI: Identifiable {
  let id = UUID().uuidString
  var location: CLLocationCoordinate2D
      
  init(coordinate: CLLocationCoordinate2D) {
    location = coordinate
  }
      
  init(latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
    location = .init(latitude: latitude, longitude: longitude)
  }
}

struct ContentView: View {
  @State private var points: [POI] = [
          .init(latitude: 48.85, longitude: 2.33),
          .init(latitude: 48.87, longitude: 2.38)
      ]
      
  var body: some View {
      MapReader { mapProxy in
          Map {
            ForEach(points) { point in
                Marker(coordinate: point.location) {
                    Image(systemName: "globe")
                }
            }
          }.onLongPressGestureWithLocation { point in
            if let coordinate = mapProxy.convert(point, from: .local) {
                  points.append(.init(coordinate: coordinate))
            }
        }
    }
  }
}

struct LongPressGestureWithLocation: ViewModifier {
  private var perform: (CGPoint) -> Void
  @State private var location: CGPoint?
      
  init(perform: @escaping (CGPoint) -> Void) {
    self.perform = perform
  }
      
  @ViewBuilder
  func body(content: Content) -> some View {
    content
      .gesture(DragGesture(minimumDistance: 0)
      .onChanged { value in
          location = value.location
      }
      .simultaneously(with: LongPressGesture()
      .onEnded { done in
          if done, let location {
            perform(location)
          }
        })
      )
    }
  }
  
extension View {
  func onLongPressGestureWithLocation(perform: @escaping (CGPoint) -> Void) -> some View {
    modifier(LongPressGestureWithLocation(perform: perform))
  }
}

Related resources:

We came to the consensus that you need UIKit to implement full gesture capabilities:

Presentation: SwiftUI Layout

Josh continued to review the details of SwiftUI layout. Most of the code was presented in the previous week with special attention this week put on fixedSize.

Related resource:

Discussion and Questions

Swift for C++ Practitioners


2024.04.13

Presentation: SwiftUI Layout

Josh showed the details of how SwiftUI layout works by creating a custom layout that inspects the calls of the layout system.

Also see:

Fixing up Geometry Reader

As a bonus side-topic, Josh showed how to "fix" how layout of a geometry reader works.

var body some View {
  let _ = Self._printChanges()
  GeometryReader { geometry in
    VStack {
      Image (systemName: "globe")
        .imageScale(.large)
      Text ("Hello, world!")
        .padding()
    }
    .frame(
        width: geometry.size.width, 
        height: geometry.size.height, 
        alignment: .center)
    }
}

Here is the code for the inspector:

struct InspectorLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let size = subviews.first?.sizeThatFits(proposal) ?? .zero
        print(
        """
        \(subviews.first?[LayoutNameKey.self] ?? ""):
        Received proposal: width \(proposal.width.proposalDescription) height: \(proposal.height.proposalDescription)
        Returning \(size)
        """
        )
        return size
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        subviews.forEach {
            print("placing \(subviews.first?[LayoutNameKey.self] ?? ""): width: \(proposal.width.proposalDescription) height: \(proposal.height.proposalDescription)")
            $0.place(at: .init(x: bounds.midX, y: bounds.midY), anchor: .center, proposal: proposal)
        }
    }
}


private struct LayoutNameKey: LayoutValueKey {
    static let defaultValue = "[unnamed view]"
}

extension View {
    func inspectLayout(name: some CustomStringConvertible) -> some View {
        InspectorLayout() {
            layoutValue(key: LayoutNameKey.self, value: String(describing: name))
        }
    }
}

private extension Optional where Wrapped == CGFloat {
    var proposalDescription: String {
        map {
            switch $0 {
            case .infinity: "max"
            case .zero: "min"
            default: "custom \(self!)"
            }
        } ?? "ideal"
    }
}

Format

Bonus topic about how to format numbers.

https://goshdarnformatstyle.com

Swift Talk Reimplements SwiftUI

Questions and Discussion

One More Thing

A new AltWWDC/Alt-Conf/Layers Inspired Conference happening WWDC week.

Scheduling Background Work

Mark asking about how to schedule background work.

Assembly Language

Carlyn posting three links on assembly:

Also,

Async From a Low Level

2024.04.06

Presentation: Ordering Async Work

Josh showed us an outline for a solution to order async work. It uses an actor to organize (and protect) a list of prioritized sendable closures that it can execute using a discardable task group. It uses the Heap structure from Swift Collection to establish the work order and an async stream to feed in and process work.

import Foundation
import HeapModule

actor AsyncPriorityWorkQueue<Priority: Comparable & Sendable, Output> {
    private let maxConcurrentElements: Int
    private let postWorkNotification: AsyncStream<Void>.Continuation
    private var concurrentItems = 0
    private var subscriptions = Subscriptions()
    private var priorityQueue = Heap<Work>()
    init(of output: Output.Type, priorityOfType: Priority.Type = Int.self, maxConcurrentElements: Int) {
        self.maxConcurrentElements = maxConcurrentElements
        let (workNotifications, postWorkNotification) = AsyncStream.makeStream(of: Void.self, bufferingPolicy: .unbounded)
        self.postWorkNotification = postWorkNotification
        subscriptions += Task {
            await withDiscardingTaskGroup { [weak self] group in
                for await _ in workNotifications {
                    while let work = await self?.dequeueWork() {
                        group.addTask {
                            await work.operation()
                            postWorkNotification.yield()
                        }
                    }
                }
            }
        }
    }
    private func dequeueWork() -> Work? {
        concurrentItems < maxConcurrentElements
            ? priorityQueue.popMax()
            : nil
    }
    private func enqueue(priority: Priority, operation: @escaping @Sendable () async -> Void) {
        priorityQueue.insert(.init(priority: priority, operation: operation))
        postWorkNotification.yield()
    }
    func perform(priority: Priority, operation: @escaping @Sendable () async throws -> Output) async throws -> Output {