Categories
iOS Swift

Swift 6 suitable notification observers in iOS

I have a couple of side projects going on, although it is always a challenge to find time of them. One of them, SignalPath, is what I created back in 2015. Currently, I have been spending some time to bump the Swift version to 6 which brought a quite a list of errors. In many places I had code what dealt with observing multiple notifications, but of course Swift 6 was not happy about it.

let handler: (Notification) -> Void = { [weak self] notification in
self?.keyboardInfo = Info(notification: notification)
}
let names: [Notification.Name] = [
UIResponder.keyboardWillShowNotification,
UIResponder.keyboardWillHideNotification,
UIResponder.keyboardWillChangeFrameNotification
]
observers = names.map({ name -> NSObjectProtocol in
return NotificationCenter.default.addObserver(forName: name,
object: nil,
queue: .main,
using: handler)
// Converting non-sendable function value to '@Sendable (Notification) -> Void' may introduce data races
})
view raw Observer.swift hosted with ❤ by GitHub

After moving all of the notification observing to publishers instead, I can ignore the whole sendable closure problem all together.

Publishers.Merge3(
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
)
.map(Info.init)
.assignWeakly(to: \.keyboardInfo, on: self)
.store(in: &notificationCancellables)
view raw Observer.swift hosted with ❤ by GitHub

Great, compiler is happy again although this code could cause trouble if any of the notifications are posted from a background thread. But since this is not a case here, I went for skipping .receive(on: DispatchQueue.main). Assign weakly is a custom operator and the implementation looks like this:

public extension Publisher where Self.Failure == Never {
func assignWeakly<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable where Root: AnyObject {
return sink { [weak object] value in
object?[keyPath: keyPath] = value
}
}
}

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

AnyClass protocol and Objective-C methods

AnyClass is a protocol all classes conform to and it comes with a feature I was not aware of. But first, how to I ended up with using AnyClass. While working on code using CoreData, I needed a way to enumerate all the CoreData entities and call a static function on them. If that function is defined, it runs an entity specific update. Let’s call the function static func resetState().

It is easy to get the list of entity names of the model and then turn them into AnyClass instances using the NSClassFromString() function.

let entityClasses = managedObjectModel.entities
.compactMap(\.name)
.compactMap { NSClassFromString($0) }
view raw AnyClass.swift hosted with ❤ by GitHub

At this point I had an array of AnyClass instances where some of them implemented the resetState function, some didn’t. While browsing the AnyClass documentation, I saw this:

You can use the AnyClass protocol as the concrete type for an instance of any class. When you do, all known @objcclass methods and properties are available as implicitly unwrapped optional methods and properties, respectively.

Never heard about it, probably because I have never really needed to interact with AnyClass in such way. Therefore, If I create an @objc static function then I can call it by unwrapping it with ?. Without unwrapping it safely, it would crash because Department type does not implement the function.

class Department: NSManagedObject {
}
class Employee: NSManagedObject {
@objc static func resetState() {
print("Resetting Employee")
}
}
// This triggers Employee.resetState and prints the message to the console
for entityClass in entityClasses {
entityClass.resetState?()
}
view raw AnyClass.swift hosted with ❤ by GitHub

It has been awhile since I wrote any Objective-C code, but its features leaking into Swift helped me out here. Reminds me of days filled with respondsToSelector and performSelector.

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 Xcode

AnyView is everywhere in Xcode 16

Loved to see this entry in Xcode 16’s release notes:

Xcode 16 brings a new execution engine for Previews that supports a larger range of projects and configurations. Now with shared build products between Build and Run and Previews, switching between the two is instant. Performance between edits in the source code is also improved for many projects, with increases up to 30%.

It has been difficult at times to use SwiftUI previews when they sometimes just stop working with error messages leaving scratch head. Turns out, it comes with a hidden cost of Xcode 16 wrapping views with AnyView in debug builds which takes away performance. If you don’t know it only affects debug builds, one could end up on journey of trying to improve the performance for debug builds and making things worse for release builds. Not sure if this was ever mentioned in any of the WWDC videos, but feels like this kind of change should have been highlighted.

As of Xcode 16, every SwiftUI view is wrapped in an AnyView _in debug builds only_. This speeds switching between previews, simulator, and device, but subverts some List optimizations.

Add this custom build setting to the project to override the new behavior:

`SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO`

Wrapping in Equatable is likely to make performance worse as it introduces an extra view in the hierarchy for every row.

Curt Clifton on Mastodon

Fortunately, one can turn off this if this becomes an issue in debug builds.

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 Swift

Sorting arrays in Swift: multi-criteria

Swift’s foundation library provides a sorted(by:) function for sorting arrays. The areInIncreasingOrder closure needs to return true if the closure’s arguments are increasing, false otherwise. How to use the closure for sorting by multiple criteria? Let’s take a look at an example of sorting an array of Player structs.

  1. Sort by score in descending order
  2. Sort by name in ascending order
  3. Sort by id in ascending order
struct Player {
let id: Int
let name: String
let score: Int
}
extension Player: CustomDebugStringConvertible {
var debugDescription: String {
"id=\(id) name=\(name) score=\(score)"
}
}
let players: [Player] = [
Player(id: 0, name: "April", score: 7),
Player(id: 1, name: "Nora", score: 8),
Player(id: 2, name: "Joe", score: 5),
Player(id: 3, name: "Lisa", score: 4),
Player(id: 4, name: "Michelle", score: 6),
Player(id: 5, name: "Joe", score: 5),
Player(id: 6, name: "John", score: 7)
]
view raw Sort.swift hosted with ❤ by GitHub

As said before, the closure should return true if the left element should be ordered before the right element. If they happen to be equal, we should use the next sorting criteria. For comparing strings, we’ll go for case-insensitive sorting using Foundation’s built-in localizedCaseInsensitiveCompare.

let sorted = players.sorted { lhs, rhs in
if lhs.score == rhs.score {
let nameOrdering = lhs.name.localizedCaseInsensitiveCompare(rhs.name)
if nameOrdering == .orderedSame {
return lhs.id < rhs.id
} else {
return nameOrdering == .orderedAscending
}
} else {
return lhs.score > rhs.score
}
}
print(sorted.map(\.debugDescription).joined(separator: "\n"))
// id=1 name=Nora score=8
// id=0 name=April score=7
// id=6 name=John score=7
// id=4 name=Michelle score=6
// id=2 name=Joe score=5
// id=5 name=Joe score=5
// id=3 name=Lisa score=4
view raw Sort.swift hosted with ❤ by 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
Foundation iOS Swift

How to keep Date’s microseconds precision in Swift

DateFormatter is used for converting string representation of date and time to a Date type and visa-versa. Something to be aware of is that the conversion loses microseconds precision. This is extremely important if we use these Date values for sorting and therefore ending up with incorrect order. Let’s consider an iOS app which uses API for fetching a list of items and each of the item contains a timestamp used for sorting the list. Often, these timestamps have the ISO8601 format like 2024-09-21T10:32:32.113123Z. Foundation framework has a dedicated formatter for parsing these strings: ISO8601DateFormatter. It is simple to use:

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let date = formatter.date(from: "2024-09-21T10:32:32.113123Z")
print(date?.timeIntervalSince1970) // 1726914752.113
view raw ISO8601.swift hosted with ❤ by GitHub

Great, but there is on caveat, it ignores microseconds. Fortunately this can be fixed by manually parsing microseconds and adding the missing precision to the converted Date value. Here is an example, how to do this using an extension.

extension ISO8601DateFormatter {
func microsecondsDate(from dateString: String) -> Date? {
guard let millisecondsDate = date(from: dateString) else { return nil }
guard let fractionIndex = dateString.lastIndex(of: ".") else { return millisecondsDate }
guard let tzIndex = dateString.lastIndex(of: "Z") else { return millisecondsDate }
guard let startIndex = dateString.index(fractionIndex, offsetBy: 4, limitedBy: tzIndex) else { return millisecondsDate }
// Pad the missing zeros at the end and cut off nanoseconds
let microsecondsString = dateString[startIndex..<tzIndex].padding(toLength: 3, withPad: "0", startingAt: 0)
guard let microseconds = TimeInterval(microsecondsString) else { return millisecondsDate }
return Date(timeIntervalSince1970: millisecondsDate.timeIntervalSince1970 + microseconds / 1_000_000.0)
}
}
view raw ISO8601.swift hosted with ❤ by GitHub

That this code does is first converting the string using the original date(from:) method, followed by manually extracting digits for microseconds by handling cases where there are less than 3 digits or event there are nanoseconds present. Lastly a new Date value is created with the microseconds precision. Here are examples of the output (note that float’s precision comes into play).

let dateStrings = [
"2024-09-21T10:32:32.113Z",
"2024-09-21T10:32:32.1131Z",
"2024-09-21T10:32:32.11312Z",
"2024-09-21T10:32:32.113123Z",
"2024-09-21T10:32:32.1131234Z",
"2024-09-21T10:32:32.11312345Z",
"2024-09-21T10:32:32.113123456Z"
]
let dates = dateStrings.compactMap(formatter.microsecondsDate(from:))
for (string, date) in zip(dateStrings, dates) {
print(string, "->", date.timeIntervalSince1970)
}
/*
2024-09-21T10:32:32.113Z -> 1726914752.113
2024-09-21T10:32:32.1131Z -> 1726914752.1130998
2024-09-21T10:32:32.11312Z -> 1726914752.1131198
2024-09-21T10:32:32.113123Z -> 1726914752.113123
2024-09-21T10:32:32.1131234Z -> 1726914752.113123
2024-09-21T10:32:32.11312345Z -> 1726914752.113123
2024-09-21T10:32:32.113123456Z -> 1726914752.113123
*/
view raw ISO8601.swift hosted with ❤ by 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
Swift

Wrapping async-await with a completion handler in Swift

It is not often when we need to wrap an async function with a completion handler. Typically, the reverse is what happens. This need can happen in codebases where the public interface can’t change just right now, but internally it is moving towards async-await functions. Let’s jump in and see how to wrap an async function, an async throwing function and an async throwing function what returns a value.

To illustrate how to use it, we’ll see an example of how a PhotoEffectApplier type has a public interface consisting of completion handler based functions and how it internally uses PhotoProcessor type what only has async functions. The end result looks like this:

struct PhotoProcessor {
func process(_ photo: Photo) async throws -> Photo {
// …
return Photo(name: UUID().uuidString)
}
func setConfiguration(_ configuration: Configuration) async throws {
// …
}
func cancel() async {
// …
}
}
public final class PhotoEffectApplier {
private let processor = PhotoProcessor()
public func apply(effect: PhotoEffect, to photo: Photo, completion: @escaping (Result<Photo, Error>) -> Void) {
Task(operation: { try await self.processor.process(photo) }, completion: completion)
}
public func setConfiguration(_ configuration: Configuration, completion: @escaping (Error?) -> Void) {
Task(operation: { try await self.processor.setConfiguration(configuration) }, completion: completion)
}
public func cancel(completion: @escaping (Error?) -> Void) {
Task(operation: { await self.processor.cancel() }, completion: completion)
}
}

In this example, we have all the interested function types covered: async, async throwing and async throwing with a return type. Great, but let’s have a look at these Task initializers what make this happen. The core idea is to create a Task, run an operation, and then make a completion handler callback. Since most of the time we need to run the completion on the main thread, then we have a queue argument with the default queue set to the main thread.

extension Task {
@discardableResult
init<T>(
priority: TaskPriority? = nil,
operation: @escaping () async throws -> T,
queue: DispatchQueue = .main,
completion: @escaping (Result<T, Failure>) -> Void
) where Success == Void, Failure == any Error {
self.init(priority: priority) {
do {
let value = try await operation()
queue.async {
completion(.success(value))
}
} catch {
queue.async {
completion(.failure(error))
}
}
}
}
}
extension Task {
@discardableResult
init(
priority: TaskPriority? = nil,
operation: @escaping () async throws -> Void,
queue: DispatchQueue = .main,
completion: @escaping (Error?) -> Void
) where Success == Void, Failure == any Error {
self.init(priority: priority) {
do {
try await operation()
queue.async {
completion(nil)
}
} catch {
queue.async {
completion(error)
}
}
}
}
}
extension Task {
@discardableResult
init(
priority: TaskPriority? = nil,
operation: @escaping () async -> Void,
queue: DispatchQueue = .main,
completion: @escaping () -> Void
) where Success == Void, Failure == Never {
self.init(priority: priority) {
await operation()
queue.async {
completion()
}
}
}
}
view raw Async.swift hosted with ❤ by 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 SwiftUI

Cancellable withObservationTracking in Swift

Observation framework came out along with iOS 17 in 2023. Using this framework, we can make objects observable very easily. Please refer to @Observable macro in SwiftUI for quick recap if needed. It also has a function withObservationTracking(_:onChange:) what can be used for cases where we would want to manually get a callback when a tracked property is about to change. This function works as a one shot function and the onChange closure is called only once. Note that it is called before the value has actually changed. If we want to get the changed value, we would need to read the value on the next run loop cycle. It would be much more useful if we could use this function in a way where we could have an observation token and as long as it is set, the observation is active. Here is the function with cancellation support.

func withObservationTracking(
_ apply: @escaping () -> Void,
token: @escaping () -> String?,
willChange: (@Sendable () -> Void)? = nil,
didChange: @escaping @Sendable () -> Void
) {
withObservationTracking(apply) {
guard token() != nil else { return }
willChange?()
RunLoop.current.perform {
didChange()
withObservationTracking(
apply,
token: token,
willChange: willChange,
didChange: didChange
)
}
}
}

The apply closure drives which values are being tracked, and this is passed into the existing withObservationTracking(_:onChange:) function. The token closure controls if the change should be handled and if we need to continue tracking. Will and did change are closures called before and after the value has changed.

Here is a simple example where we have a view which controls if the observation should be active or not. Changing the value in the view model only triggers the print lines when observation token is set.

struct ContentView: View {
@State private var viewModel = ViewModel()
@State private var observationToken: String?
var body: some View {
VStack {
Text(viewModel.title)
Button("Add") {
viewModel.add()
}
Button("Start Observing") {
guard observationToken == nil else { return }
observationToken = UUID().uuidString
observeAndPrint()
}
Button("Stop Observing") {
observationToken = nil
}
}
.padding()
}
func observeAndPrint() {
withObservationTracking({
_ = viewModel.title
}, token: {
observationToken
}, willChange: { [weak viewModel] in
guard let viewModel else { return }
print("will change \(viewModel.title)")
}, didChange: { [weak viewModel] in
guard let viewModel else { return }
print("did change \(viewModel.title)")
})
}
}
@Observable final class ViewModel {
var counter = 0
func add() {
counter += 1
}
var title: String {
"Number of items: \(counter)"
}
}

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 Swift

Referencing itself in a struct in Swift

It took a long time, I mean years, but it finally happened. I stumbled on a struct which had a property of the same type.

struct Message {
let id: Int
// This is OK:
let replies: [Message]
// This is not OK
// Value type 'Message' cannot have a stored property that recursively contains it
let parent: Message?
}
view raw Struct.swift hosted with ❤ by GitHub

At first, it is kind of interesting that the replies property compiles fine, although it is a collection of the same type. I guess it is so because array’s storage type is a reference type.

The simplest workaround is to use a closure for capturing the actual value.

struct Message {
let id: Int
let replies: [Message]
private let parentClosure: () -> Message?
var parent: Message? { parentClosure() }
}
view raw Struct2.swift hosted with ❤ by GitHub

Or we could go for using a boxed wrapper type.

struct Message {
let id: Int
let replies: [Message]
private let parentBoxed: Boxed<Message>?
var parent: Message? { parentBoxed?.value}
}
class Boxed<T> {
let value: T
init(value: T) {
self.value = value
}
}
view raw Struct3.swift hosted with ❤ by GitHub

Or if we prefer property wrappers, using that instead.

struct Message {
let id: Int
let replies: [Message]
@Boxed var parent: Message?
}
@propertyWrapper
class Boxed<Value> {
var value: Value
init(wrappedValue: Value) {
value = wrappedValue
}
var wrappedValue: Value {
get { value }
set { value = newValue }
}
}
view raw Struct4.swift hosted with ❤ by GitHub

Then there are also options like changing the struct into class instead, but that is something to consider. Or finally, creating a

All in all, it is fascinating how something simple like this actually has a pretty complex background.

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 SwiftUI

ScrollView phase changes on iOS 18

In addition to scroll related view modifiers covered in the previous blog post, there is another one for detecting scroll view phases aka the state of the scrolling. The new view modifier is called onScrollPhaseChange(_:) and has three arguments in the change closure: old phase, new phase and a context.

ScrollPhase is an enum with the following values:

  • animating – animating the content offset
  • decelerating – user interaction stopped and scroll velocity is decelerating
  • idle – no scrolling
  • interacting – user is interacting
  • tracking – potential user initiated scroll event is going to happen

The enum has a convenience property of isScrolling which is true when the phase is not idle.

ScrollPhaseChangeContext captures additional information about the scroll state, and it is the third argument of the closure. The type gives access to the current ScrollGeometry and the velocity of the scroll view.

Here is an example of a scroll view which has the new view modifier attached.

struct ContentView: View {
@State private var scrollState: (
phase: ScrollPhase,
context: ScrollPhaseChangeContext
)?
let data = (0..<100).map({ "Item \($0)" })
var body: some View {
NavigationStack {
ScrollView {
ForEach(data, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity)
.padding()
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.cyan)
}
.padding(.horizontal, 8)
}
}
.onScrollPhaseChange { oldPhase, newPhase, context in
scrollState = (newPhase, context)
}
Divider()
VStack {
Text(scrollStateDescription)
}
.font(.footnote.monospaced())
.padding()
}
}
private var scrollStateDescription: String {
guard let scrollState else { return "" }
let velocity: String = {
guard let velocity = scrollState.context.velocity else { return "none" }
return "\(velocity)"
}()
let geometry = scrollState.context.geometry
return """
State at the scroll phase change
Scrolling=\(scrollState.phase.isScrolling)
Phase=\(scrollState.phase)
Velocity
\(velocity)
Content offset
\(geometry.contentOffset)
Visible rect
\(geometry.visibleRect.integral)
"""
}
}

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 SwiftUI

Scroll geometry and position view modifiers in SwiftUI on iOS 18

WWDC’24 brought some updates to scrolling. One of which is onScrollGeometryChange(for:of:action:) what we can use for reacting to scroll geometry changes. The view modifier has two closures, where the first one is transforming the scroll geometry into an arbitrary equatable type of our liking. If that value changes, the action closure is called. It is a convenient way for triggering view updates or updating other states.

The new ScrollGeometry type provides the current scroll state:

  • bounds
  • containerSize
  • contentInsets
  • contentOffset
  • contentSize
  • visibleRect
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
.onScrollGeometryChange(
for: CGRect.self,
of: { scrollGeometry in
scrollGeometry.visibleRect
},
action: { oldValue, newValue in
print("visibleRect =", newValue)
}
)
}
view raw Scroll.swift hosted with ❤ by GitHub

Here is another example where we can use the new modifier for showing a scroll to top button in combination with the new scrollPosition(_:anchor:) view modifier.

struct ContentView: View {
let items: [String] = (0..<100).map({ "Item \($0)" })
@State private var canScrollToTop = false
@State private var scrollPosition = ScrollPosition(idType: String.self)
var body: some View {
ScrollView {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity)
.padding()
.onScrollVisibilityChange() { visible in
print(item, visible)
}
}
}
.scrollPosition($scrollPosition)
.overlay(alignment: .top) {
if canScrollToTop {
Button("Top") {
withAnimation {
scrollPosition.scrollTo(edge: .top)
}
}
}
}
.onScrollGeometryChange(
for: Bool.self,
of: { scrollGeometry in
scrollGeometry.contentOffset.y > 50
},
action: { oldValue, newValue in
canScrollToTop = newValue
}
)
}
}

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.