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.07.13

Presentation: Noncopyable Types (Ray)

We looked at some of the motivations of non-copyable types. The key benefit is precise lifetime control which also means that the compiler doesn't have to put things on the heap (which is intrinsically more expressive and less predictable).

Here are some resources:

A WWDC talk "Consume noncopyable types in Swift":

Evolution Proposals

Documentation

We used a playground to mess around with:

struct Tracker: ~Copyable {
  var count: Int128 = 0
  
  init() {
    // write a file
  }
  
  consuming func cancel() {
    // delete the file
    discard self
  }
  
  deinit {
    // delete the file
  }
}

func test() {
  let t = Optional(Tracker())
}

func compute0<T>(_ value: borrowing T) {
  
}

func compute<T: ~Copyable>(_ value: borrowing T) {
  
}
compute("blob")
compute(Tracker())



struct UDT {
  var value = ""
  
  borrowing func method1(param: borrowing String)  {
    print(copy param)
  }
  consuming func method2(param: consuming String)  {
    param += "1"
    print(param)
  }
  mutating func method3(param: inout String) {
    
  }
}
var hello = "hello"
UDT().method2(param: hello)
print(hello)

Question: Proper Navigation

It is not a good idea to put views into your model objects. Keep the models pure and build the models around them. An example from Josh last year:

https://github.com/joshuajhomann/PokemonNavigation

Question: Using Apple Maps instead of Google Maps

Question: Crash in Swift Data?

In the log, the crash is coming from:

ModelContext._processRecentChanges(validate:)

Very difficult to debug. Might consider trying to get a syslog if QA can reproduce it.


2024.07.06

Presentation: Maps in SwiftUI (Frank)

Previously, Frank showed us how to use MapKit from SwiftUI. The only way to get a long press gesture and be able to pinch and zoom a map was to drop down to UIViewRepresentable. This year, with iOS 18, you can now use UIKit gestures in your SwiftUI so it makes implementing long press features a lot easier.

Starting with an abstraction for a point-of-interest:

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)
        }
    }

You can use this SwiftUI view:

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")
                        }
                    }
                }.onLongPress { point in
                    if let coordinate = mapProxy.convert(point, from: .global) {
                        points.append(.init(coordinate: coordinate))
                    }
                } cancel: {
                    _ = points.popLast()
                }
            }
        }
    }

To implement the modifier onLongPress do this:

struct LongPressGesture: UIGestureRecognizerRepresentable {
        let perform: (CGPoint) -> Void
        let cancel: () -> Void
    
        func makeUIGestureRecognizer(context: Context) -> some UIGestureRecognizer {
            UILongPressGestureRecognizer()
        }
        
        func handleUIGestureRecognizerAction(_ recognizer: UIGestureRecognizerType, context: Context) {
            switch recognizer.state {
            case .began:
                perform(recognizer.location(in: nil))
            case .cancelled:
                cancel()
            default:
                break
            }
        }
    }

With a modifier:

extension View {
        func onLongPress(perform: @escaping (CGPoint) -> Void, cancel: @escaping () -> Void) -> some View {
            gesture(LongPressGesture(perform: perform, cancel: cancel))
        }
    }  

Presentation: Mesh Gradient Animation (Josh)

Josh continued his epic presentation of mesh gradients by creating a full on lava lamp style animation.

Source code: TBD.

Discussion: What's new in Swift 6?

What is the best way to find out what all the changes are?

How do you type ∆ on a mac?

  • Option J on a US keyboard (Option d is ∂)

Frank notes that on a French keyboard Option d is also ∂, but option shift d is ∆. Makes more sense, indeed!


2024.06.29

Presentation: Mesh Gradient (Josh)

Josh continued his discussion of the new MeshGradient. He pointed out some of the sharp edges in this new API. Thinking about these issues deeply--how to make an API easy to understand and hard to misuse is a key to good software engineering. Thinking about issues like this separates coding (just getting the job done) from engineering where next level concerns are top of mind.

He was able to create the basics of a mesh gradient editor where you can manipulate the bezier control points in real time. Future discussion may include animation of these control points.

Code:

import SwiftUI
import CxxStdlib


@Observable
final class ViewModel: ObservableObject {
    var color: Color = .white
    private(set) var mesh: MeshGradient.Model
    private(set) var points: [MeshPoint]
    var width: Int
    var height: Int
    init() {
        let width = 4
        let height = 4
        let points = ViewModel.makePoints(width: width, height: height)
        mesh = .init(width: width, height: height, points: points)
        self.width = width
        self.height = height
        self.points = points

    }
    private static func makePoints(width: Int, height: Int) -> [MeshPoint] {
        (0..<width).flatMap { x in
            (0..<height).map { y in
                MeshPoint(
                    id: .init(x: x, y: y),
                    location: .init(
                        x: Float(x) / Float(width - 1),
                        y: Float(y) / Float(height - 1)
                    ),
                    color: .random
                )
            }
        }
    }
    func set(location: SIMD2<Float>, for id: MeshPoint.ID) {
        guard let index = points.firstIndex(where: { $0.id == id}) else { return }
        points[index].location = location
        mesh = .init(width: width, height: height, points: points)
    }
}

extension Color {
    static var random: Self {
        .init(
            red: Double.random(in: 0..<1),
            green: Double.random(in: 0..<1),
            blue: Double.random(in: 0..<1)
        )
    }
}

struct ContentView: View {
    @State private var showInspector = true
    @State private var showPoints = true
    @StateObject var viewModel = ViewModel()
    @State private var selectedPointID: MeshPoint.ID?
    enum Space: Hashable {
        case gradient
    }
    var body: some View {
        GeometryReader { proxy in
            Rectangle()
                .foregroundStyle(MeshGradient(viewModel.mesh))
                .coordinateSpace(name: Space.gradient)
                .overlay(alignment: .topLeading) {
                    if showPoints {
                        ForEach(viewModel.points) { point in
                            Circle()
                                .background(Circle().foregroundStyle(point.id == selectedPointID ? Color.accentColor : Color.white).padding(-2))
                                .foregroundStyle(point.color)
                                .frame(width: 20, height: 20)
                                .offset(
                                    x: CGFloat(point.location.x) * proxy.size.width - 10,
                                    y: CGFloat(point.location.y) * proxy.size.height - 10
                                )
                                .onTapGesture {
                                    selectedPointID = point.id
                                }
                                .gesture(DragGesture(minimumDistance: 1, coordinateSpace: .named(Space.gradient))
                                    .onChanged { gesture in
                                        guard let selectedPointID else { return }
                                        let unitPoint = SIMD2<Float>(
                                            x: Float(gesture.location.x / proxy.size.width),
                                            y: Float(gesture.location.y / proxy.size.height)
                                        )
                                        viewModel.set(location: unitPoint, for: selectedPointID)
                                    }
                                )
                        }
                    }
                }
        }
        .onTapGesture {
            selectedPointID = nil
        }