I'm bored at work (but don't tell my boss), so I'll take a stab at an explanation.
There are rhetorical questions littered through the explanation. Try to stop and answer the question before reading on.
As you know retain increments the retain count of an object so that it isn't deallocated while you still hold a pointer to it.
Copy, naturally, copies an object. What's the goals of copying an object instead of just retaining it? The goal is to have an independent copy from the original so that if the original changes, the copy you have doesn't, or vice-versa.
Now think about an immutable class like NSString. If I was implementing the copy method of NSString, I could actually copy the NSString object so that the object exists twice in memory. But what's the point? A NSString object can never change. I can give you a pointer to the same object and you'd be just as happy as with a complete copy. Why? Because the object you have will not change on you, because it can't change. Similarly you can't change the object on someone else, because it can't change.
The only thing I'd need to do is make sure I retain the object before I return it back to you when you call my NSString copy. Why? Because of the Cocoa memory rules. If you call copy, you own the returned object and so are responsible for calling release. I need to ensure your call to release doesn't deallocate the object if there are other co-owners. So I match the expected future release with a retain.
That is why retain and copy do the same thing for NSString. It's an optimization.
Now think about creating your own class that has an NSString* ivar or property. You can assume an NSString is immutable and it won't change, right, because NSString is immutable? So you could do something like cache the length of the string into another ivar, right?
Well, not always. When is an NSString* not immutable? When the pointer is actually to an NSMutableString object.
If I used your class described above and set the property to a NSMutableString it would work because NSMutableString is subclass of NSString. By way of the substitution principle of OOP, I can assign a pointer to a subclass object to a pointer to a superclass type. Your class would go ahead and cache the length of the string I gave you. But I would break your class if I changed that NSMutableString; perhaps innocently, perhaps deliberately.
How can you guard against this? How can you be sure you actually have an NSString and not an NSMutableString? Make your sure you use a copy property or you call copy when you set the ivar. Don't use retain. By way of convention (not enforcement), when there's an immutable version of a class and a mutable subclass, copy will return an immutable copy.
This is way Steinberg is saying you should use copy and not retain. It's not right that you can't use retain. It's just that you better off in terms of robustness to use copy.