It appears you got most your questions answered in the comments but I thought I would write up an answer to to fill in any missing gaps.
NSArray takes in an id and you can pass in any NSObject subclass you want and the complier will allow it. This is NOT RECOMMENDED but can be done.
NSArray* animals = [NSArray arrayWithObjects:
[[Dog alloc] init],
[[Cat alloc] init],
@"This is a string",
@(42),
nil];
Even cooler is that when you pull the items out of an array they come out as an id. This gets casted to whatever variables you are using.
Dog *dog = animals[0];
Cat *cat = animals[1];
The compiler will not freak out if you do this.
NSString *aString = animals[0];
NSLog(@"%@", aString);
NSLog(@"%@", [aString lowercaseString]);
This is what you get in the log...
2015-06-07 08:55:53.715 DogsAndCats2[5717:4029814] <Dog: 0x7fe790c71b80>
2015-06-07 08:55:53.715 DogsAndCats2[5717:4029814] -[Dog lowercaseString]: unrecognized selector sent to instance 0x7fe790c71b80
You are casting an id to a NSString which is allowed however, you are trying to call a method that does not exist on a Dog and will crash on the second NSLog. Note the first log says it is still a Dog even though it is casted as a NSString.
Now if we add a property to your Dog.h we can see some neat stuff in the for loop.
Dog.h
#import <UIKit/UIKit.h>
@interface Dog : NSObject
@property (nonatomic, strong)NSString *dogName;
-(void)bark;
@end
Now when we loop consider the following.
for (id thing in animals)
{
if ([thing respondsToSelector:@selector(bark)])
{
//You are allowed to send messages to id
[thing bark];
[thing setDogName:@"Lassie"];
//compiler error because it doesn't know it is a Dog class
//thing.dogName = @"Lassie";
}
if ([thing isKindOfClass:[Dog class]])
{
[thing bark];
//this is safe because we checked the class first and now we can cast it as so
Dog *dog = (Dog *)thing;
dog.dogName = @"Lassie";
}
}
Hopefully that answers more of your questions and isn't too confusing.