pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.22k stars 1.42k forks source link

observe in AppKit is crashing #3297

Closed zachwaugh closed 2 weeks ago

zachwaugh commented 3 weeks ago

Description

After the update to 1.13.1, observe is crashing in my macOS app. The actual error is:

Could not cast value of type 'SwiftNavigation.ObserveToken' (0x103ba9f88) to 'NSObject' (0x2010c88d0).
Screenshot 2024-08-22 at 3 43 28 PM

I'm assuming because ObserveToken in SwiftNavigation does not conform to NSObject - https://github.com/pointfreeco/swift-navigation/blob/main/Sources/SwiftNavigation/Observe.swift#L162. Where as the previous implementation of observe in TCA did use a class derived from NSObject https://github.com/pointfreeco/swift-composable-architecture/blob/1.12.1/Sources/ComposableArchitecture/UIKit/NSObject%2BObservation.swift#L200

Checklist

Expected behavior

No response

Actual behavior

No response

Steps to reproduce

No response

The Composable Architecture version information

1.13.1

Destination operating system

macOS 14.5

Xcode version information

Xcode 15.4

Swift Compiler version information

No response

mbrandonw commented 3 weeks ago

Hi @zachwaugh, I'm not able to reproduce this with the following test

import AppKit
import XCTest

class MacTests: XCTestCase {
  @MainActor
  func testTokenStorage() {
    class Controller: NSViewController {
      override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        observe {}
      }

      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    }

    let controller = Controller(nibName: nil, bundle: nil)
    _ = controller
  }
}

Can you provide a repro for us to look into?

zachwaugh commented 3 weeks ago

Interesting! I can't reproduce with that either, but noticed the one difference in my app is multiple observe calls and that seems to be the thing. This will reproduce it:

import AppKit
import XCTest

class MacTests: XCTestCase {
  @MainActor
  func testTokenStorage() {
    class Controller: NSViewController {
      override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        observe {}
        // 🚨 Crashes on second observe
        observe {}
      }

      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    }

    let controller = Controller(nibName: nil, bundle: nil)
    _ = controller
  }
}
mbrandonw commented 3 weeks ago

Ah, interesting! That does reproduce for me. Thanks for that, will look into it more.

mbrandonw commented 3 weeks ago

This will be fixed once we merge and release https://github.com/pointfreeco/swift-navigation/pull/212/. If you want an immediate fix you could copy-paste the observe helper into your code and change the Set<ObserveToken> to an [Any], then it will work.

stephencelis commented 2 weeks ago

This is fixed in swift-navigation@2.1.0.