So how do you implement a NSTextStorage subclass in Swift?


(Michel Fortin) #1

The `string` property of `NSTextStorage` is of type `String`, but the contract it must implement is that it should return the backing store of the attributed string (the underlying `NSMutableString` used as the backing store). It seems to me that this makes it impossible to implement correctly a subclass of `NSTextStorage` in Swift, because Swift automatically wraps the `NSString` into a `String` and the underlying mutable storage is not passed around.

Here's the documentation for that method:
https://developer.apple.com/reference/foundation/nsattributedstring/1412616-string

In case the contract isn't clear from the documentation (it wasn't for me), I got a confirmation as a response in radar 30314719:

It’s returning a copy of the backing store, but the contract is actually returning the backing store string. The copy storage declaration in the property spec is only used for setter implementation.

So looks like this is impossible to model correctly in Swift due to the automatic bridging to `String`. Some APIs in AppKit expect the `NSString` they receive to mutate when mutating the text storage (touch bar suggestions in `NSTextView`) and will do bad things this isn't the case.

Obviously, I can work around this by writing some Objective-C code, but it'd be better if I could avoid splitting the class implementation between two languages. There's another way using swizzling to map the Objective-C method to a Swift implementation of the same method that has the correct signature, and that's probably what I'll end up doing unless a better solution can be pointed out to me.

···

--
Michel Fortin
https://michelf.ca


(Michel Fortin) #2

In case this is useful to someone, this is the workaround I'll be using:

class CustomTextStorage: NSTextStorage {

	private let backingStore: NSMutableAttributedString

	// This method should never get called from Objective-C as it doesn't respect 
	// the API contract because of the wrapping in `String`.
	//
	// This could get called directly from Swift code when dispatching without 
	// passing by the Objective-C runtime. So it must still produce the right 
	// thing for Swift.
	override var string: String {
		return backingStore.string
	}

	// Objective-C method implementation for `string` is remapped to this method to 
	// avoid wrapping the result in `String`. With the correct method signature 
	// we can return the backing store string object.
	var backingNSString: NSString {
		return backingStore.mutableString
	}

	// call once at program initialization:
	static func fixupStringMethod() {
		let theClass = CustomTextStorage.self
		let badStringMeth = class_getInstanceMethod(theClass, #selector(getter: string))
		let goodStringMeth = class_getInstanceMethod(theClass, #selector(getter: backingNSString))
		let goodImp = method_getImplementation(goodStringMeth)
		method_setImplementation(badStringMeth, goodImp)
	}

	// ... rest of the class goes here ...
}
···

Le 9 févr. 2017 à 18:12, Michel Fortin via swift-users <swift-users@swift.org> a écrit :

The `string` property of `NSTextStorage` is of type `String`, but the contract it must implement is that it should return the backing store of the attributed string (the underlying `NSMutableString` used as the backing store). It seems to me that this makes it impossible to implement correctly a subclass of `NSTextStorage` in Swift, because Swift automatically wraps the `NSString` into a `String` and the underlying mutable storage is not passed around.

Here's the documentation for that method:
https://developer.apple.com/reference/foundation/nsattributedstring/1412616-string

In case the contract isn't clear from the documentation (it wasn't for me), I got a confirmation as a response in radar 30314719:

It’s returning a copy of the backing store, but the contract is actually returning the backing store string. The copy storage declaration in the property spec is only used for setter implementation.

So looks like this is impossible to model correctly in Swift due to the automatic bridging to `String`. Some APIs in AppKit expect the `NSString` they receive to mutate when mutating the text storage (touch bar suggestions in `NSTextView`) and will do bad things this isn't the case.

Obviously, I can work around this by writing some Objective-C code, but it'd be better if I could avoid splitting the class implementation between two languages. There's another way using swizzling to map the Objective-C method to a Swift implementation of the same method that has the correct signature, and that's probably what I'll end up doing unless a better solution can be pointed out to me.

--
Michel Fortin
https://michelf.ca


(Jacob Bandes-Storch) #3

This seems like a bug (missing feature?) in how the API is imported for
Swift. You might consider filing a Radar.

···

On Thu, Feb 9, 2017 at 3:12 PM Michel Fortin via swift-users < swift-users@swift.org> wrote:

The `string` property of `NSTextStorage` is of type `String`, but the
contract it must implement is that it should return the backing store of
the attributed string (the underlying `NSMutableString` used as the backing
store). It seems to me that this makes it impossible to implement correctly
a subclass of `NSTextStorage` in Swift, because Swift automatically wraps
the `NSString` into a `String` and the underlying mutable storage is not
passed around.

Here's the documentation for that method:

https://developer.apple.com/reference/foundation/nsattributedstring/1412616-string

In case the contract isn't clear from the documentation (it wasn't for
me), I got a confirmation as a response in radar 30314719:
> It’s returning a copy of the backing store, but the contract is actually
returning the backing store string. The copy storage declaration in the
property spec is only used for setter implementation.

So looks like this is impossible to model correctly in Swift due to the
automatic bridging to `String`. Some APIs in AppKit expect the `NSString`
they receive to mutate when mutating the text storage (touch bar
suggestions in `NSTextView`) and will do bad things this isn't the case.

Obviously, I can work around this by writing some Objective-C code, but
it'd be better if I could avoid splitting the class implementation between
two languages. There's another way using swizzling to map the Objective-C
method to a Swift implementation of the same method that has the correct
signature, and that's probably what I'll end up doing unless a better
solution can be pointed out to me.

--
Michel Fortin
https://michelf.ca

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Jacob Bandes-Storch) #4

Got some clarity on this from Apple folks on Twitter:
https://twitter.com/jtbandes/status/830159670559993856

···

On Fri, Feb 10, 2017 at 12:54 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:

I did file one (30314719). I might not have explained the problem clearly
enough, I suppose, because at the time I was misinterpreting the API
contract thinking it was the new AppKit Touch Bar stuff that was violating
it instead. That bug now sits closed and I hesitate opening a new bug for
the same problem just to ask it to be fixed in another way. Meanwhile I
found an acceptable workaround that I attached to the existing bug report
in addition and I posted all this to the list. Hopefully someone at the
right place will notice.

But yeah... maybe I should file another bug, against Foundation's Swift
interface this time, since NSTextStorage's string property comes from
NSAttributedString. I'll think about it.

Le 10 févr. 2017 à 11:36, Jacob Bandes-Storch <jtbandes@gmail.com> a
écrit :

This seems like a bug (missing feature?) in how the API is imported for
Swift. You might consider filing a Radar.

On Thu, Feb 9, 2017 at 3:12 PM Michel Fortin via swift-users < > swift-users@swift.org> wrote:

The `string` property of `NSTextStorage` is of type `String`, but the
contract it must implement is that it should return the backing store of
the attributed string (the underlying `NSMutableString` used as the backing
store). It seems to me that this makes it impossible to implement correctly
a subclass of `NSTextStorage` in Swift, because Swift automatically wraps
the `NSString` into a `String` and the underlying mutable storage is not
passed around.

Here's the documentation for that method:
https://developer.apple.com/reference/foundation/
nsattributedstring/1412616-string

In case the contract isn't clear from the documentation (it wasn't for
me), I got a confirmation as a response in radar 30314719:
> It’s returning a copy of the backing store, but the contract is
actually returning the backing store string. The copy storage declaration
in the property spec is only used for setter implementation.

So looks like this is impossible to model correctly in Swift due to the
automatic bridging to `String`. Some APIs in AppKit expect the `NSString`
they receive to mutate when mutating the text storage (touch bar
suggestions in `NSTextView`) and will do bad things this isn't the case.

Obviously, I can work around this by writing some Objective-C code, but
it'd be better if I could avoid splitting the class implementation between
two languages. There's another way using swizzling to map the Objective-C
method to a Swift implementation of the same method that has the correct
signature, and that's probably what I'll end up doing unless a better
solution can be pointed out to me.

--
Michel Fortin
https://michelf.ca

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users

--
Michel Fortin
https://michelf.ca


(Karl) #5

I've seen this before; I considered it something to be resolved in AppKit. Strings in Swift are values, and the framework expects it to be a reference. Since NSTextStorage is itself a reference-type and informs its delegate of changes, those frameworks should probably retain the NSTS itself and pull Strings out as-needed.
  
Once the new String model is done (long time yet), I would love it if Apple's CoreText team created a truly Swift (i.e. Protocol-based) NSTextStorage API. You wouldn't necessarily have to provide a stdlib String; any UTF16-encoded Unicode/StringProtocol instance could possibly suffice, allowing you to optimise storage through buffer-gaps and whatnot (doable today with NSMutableString subclass, but you can't subclass a struct).
  
- Karl

···

  
On Feb 10, 2017 at 10:51 pm, <Jacob Bandes-Storch via swift-users (mailto:swift-users@swift.org)> wrote:
  
Got some clarity on this from Apple folks on Twitter: https://twitter.com/jtbandes/status/830159670559993856

On Fri, Feb 10, 2017 at 12:54 PM, Michel Fortin <michel.fortin@michelf.ca (mailto:michel.fortin@michelf.ca)> wrote:
  
>
> I did file one (30314719). I might not have explained the problem clearly enough, I suppose, because at the time I was misinterpreting the API contract thinking it was the new AppKit Touch Bar stuff that was violating it instead. That bug now sits closed and I hesitate opening a new bug for the same problem just to ask it to be fixed in another way. Meanwhile I found an acceptable workaround that I attached to the existing bug report in addition and I posted all this to the list. Hopefully someone at the right place will notice.
>
>
> But yeah... maybe I should file another bug, against Foundation's Swift interface this time, since NSTextStorage's string property comes from NSAttributedString. I'll think about it.
>
>
>
>
>
>
> >
> > Le 10 févr. 2017 à 11:36, Jacob Bandes-Storch <jtbandes@gmail.com (mailto:jtbandes@gmail.com)> a écrit :
> >
> >
> >
> > This seems like a bug (missing feature?) in how the API is imported for Swift. You might consider filing a Radar.
> >
> >
> >
> >
> > On Thu, Feb 9, 2017 at 3:12 PM Michel Fortin via swift-users <swift-users@swift.org (mailto:swift-users@swift.org)> wrote:
> >
> > > The `string` property of `NSTextStorage` is of type `String`, but the contract it must implement is that it should return the backing store of the attributed string (the underlying `NSMutableString` used as the backing store). It seems to me that this makes it impossible to implement correctly a subclass of `NSTextStorage` in Swift, because Swift automatically wraps the `NSString` into a `String` and the underlying mutable storage is not passed around.
> > >
> > > Here's the documentation for that method:
> > > https://developer.apple.com/reference/foundation/nsattributedstring/1412616-string
> > >
> > > In case the contract isn't clear from the documentation (it wasn't for me), I got a confirmation as a response in radar 30314719:
> > > > It’s returning a copy of the backing store, but the contract is actually returning the backing store string. The copy storage declaration in the property spec is only used for setter implementation.
> > >
> > > So looks like this is impossible to model correctly in Swift due to the automatic bridging to `String`. Some APIs in AppKit expect the `NSString` they receive to mutate when mutating the text storage (touch bar suggestions in `NSTextView`) and will do bad things this isn't the case.
> > >
> > > Obviously, I can work around this by writing some Objective-C code, but it'd be better if I could avoid splitting the class implementation between two languages. There's another way using swizzling to map the Objective-C method to a Swift implementation of the same method that has the correct signature, and that's probably what I'll end up doing unless a better solution can be pointed out to me.
> > >
> > >
> > > --
> > > Michel Fortin
> > > https://michelf.ca (https://michelf.ca/)
> > >
> > > _______________________________________________
> > > swift-users mailing list
> > > swift-users@swift.org (mailto:swift-users@swift.org)
> > > https://lists.swift.org/mailman/listinfo/swift-users
> > >
>
>
>
>
>
>
>
>
> --
> Michel Fortin
> https://michelf.ca
>
>
>
>
>
>
>
>
>
   _______________________________________________ swift-users mailing


(Michel Fortin) #6

Well, this is apparently not an AppKit problem because the `string` method is first defined in `NSAttributedString` in Foundation. `NSTextStorage` only inherits that method.

The cleanest fix I can see at the framework level is expose it as two properties in `NSAttributedString`:

  // swift-only getter for the string, can't override this one
  @nonobjc
  final public var string: String {
    get { return backingString as String }
  }

  // this is the one mapped to `string` in objc, you can override this one
  @objc(string)
  public var backingString: NSString { get }

This is somewhat analogous to what I'm doing with `method_setImplementation` in my solution to remap the method to a Swift method that has the right signature.

That would be source-breaking, but only for those who override the property. Pretty much all of these overrides are going to be violating the API contract anyway. They probably deserve the error so they can be fixed.

At this point though, I think the topic belong to somewhere else than swift-users. I'm just not sure where. I should probably file a radar against Foundation at the very least.

···

Le 12 févr. 2017 à 18:41, Karl Wagner <razielim@gmail.com> a écrit :

I've seen this before; I considered it something to be resolved in AppKit. Strings in Swift are values, and the framework expects it to be a reference. Since NSTextStorage is itself a reference-type and informs its delegate of changes, those frameworks should probably retain the NSTS itself and pull Strings out as-needed.

--
Michel Fortin
https://michelf.ca