I want an array of ALL the Objects in a Window

Discussion in 'Mac Programming' started by BadWolf13, Dec 4, 2010.

  1. macrumors 6502

    Joined:
    Dec 17, 2009
    #1
    I've got an app, where I'm trying to get an array of all the objects within a window. I tried subViews, because technically, all objects in a window are views, but unfortunately, that only returns the immediate objects, and some of my buttons and fields are within boxes and other subviews.

    Anyone got any ideas on this?
     
  2. Moderator emeritus

    robbieduncan

    Joined:
    Jul 24, 2002
    Location:
    London
    #2
    There is no one-line answer. You need to get all the subviews, then all the subviews of them and so on until there are no more subviews.
     
  3. macrumors 68030

    jared_kipe

    Joined:
    Dec 8, 2003
    Location:
    Seattle
    #3
    Code:
    - (void) allSubviews: (NSMutableArray *) storage {
         NSArray *subviews = [self subviews];
         [storage addObjectsFromArray: subviews];
    
         for (NSView *view in subviews) {
              [view allSubviews: storage];
         }
    }
    
    Make this a category method on NSView, then make a new empty mutable array and pass it into this method.
     
  4. thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #4
    Thanks Jared, but I already got it.

    I am curious though, can you actually do recursive methods? Seems like the computer would get stuck in an infinite loop if it had to process that code.
     
  5. macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #5
    You need one or more base cases as well as one or more recursive cases. When subviews returns nil you get an implicit base case in this example. Also, this example does not return anything, so you don't see a return nil, etc. for the base case like you might with other recursive functions.

    You can definitely write recursive functions that work.

    -Lee
     
  6. thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #6
    Sorry Lee, but you kinda went right over my head with that one. What is a base case, and would this code work, or not?
     
  7. macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #7
    The base case of a recursive function is the case in which the function does not call itself and instead returns to its caller immediately. Generally this happens when you're "done" for whatever value of done is for your recursion. In this specific example the base case is when allSubviews is passed a view with no subviews. The for...in will run 0 times, and allSubviews will return when control reaches the }. This will happen each time the bottom of a view hierarchy is reached and there are no more subviews to explore for a given view. On return the calling instance will call itself again for the next subview in its list or it will also return if it has explored all of its subviews.

    I have not run the given code but it's a short function that's logically sound, so as long as there are no cycles (a view being a subview of itself directly or indirectly) it should work correctly.

    -Lee
     
  8. BadWolf13, Dec 5, 2010
    Last edited: Dec 5, 2010

    thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #8
    Ok, I see what you mean. I used a do...while loop to achieve the effect of stopping at the bottom of the hierarchy. I also prefer returning a value as opposed to modifying the argument.
     
  9. lee1210, Dec 5, 2010
    Last edited: Dec 5, 2010

    macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #9
    The code you posted is not good. There are memory leaks, and I am not sure that it will ever finish. The solution jared_kipe posted is much easier to follow, will terminate excepting the case of a cycle which may be impossible, and doesn't leak.

    -Lee

    Edit: it looks like it might terminate, but it might not pull in the whole hierarchy, and some views will be duplicated. I like jared_kipe's solution much more, doing this without recursion would require a lot of logic to track what you've already looked at, etc. I would not want to write a non-recursive solution, especially when the recursive solution is a good fit (and when someone already wrote it for me =] ).
     
  10. macrumors 603

    whooleytoo

    Joined:
    Aug 2, 2002
    Location:
    Cork, Ireland.
    #10
    If you're worried about recursion, you could simply add a test to ensure the view you're adding isn't already in the array - if it is, ignore it and move on.

    That said, I don't see how the subview hierarchy could be recursive, but people are always doing weird things, so it's good to programme defensively. :)
     
  11. macrumors G5

    gnasher729

    Joined:
    Nov 25, 2005
    #11
    If you had loops in your view hierarchy, lots of things would crash immediately, like the first responder chain, the whole view drawing code would crash, nothing would work at all. Anything going up the chain of superviews would crash. And a view can only have one superview, so it cannot be subview of two different views either.
     
  12. macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #12
    Good to know. I wasn't 100% sure this was the case, so I said "excepting the case of a cycle which may be impossible". I was pretty confident that jared_kipe's code was fine, but a cycle would lead to infinite recursion, so I thought i'd mention it to be thorough.

    -Lee
     
  13. macrumors 603

    whooleytoo

    Joined:
    Aug 2, 2002
    Location:
    Cork, Ireland.
    #13
    That sounds perfectly logical. :)

    I'd still add checks to recursive code, even if it's only out of uber-cautiousness. I'm always wary when doing any iterative/recursive code working through 'external' data over which you have little or no control ("how many subviews in a view", "how many files on a drive", "how long is a piece of string.." ;) ) .
     
  14. thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #14
    The code actually worked well, and terminated after only 3 loops, but when I looked at it more closely, I realized that it only worked because of the order of the object in my test window, and could fail to capture all the objects in certain situations. Jared's recursive idea may work fine, but I still prefer something that returns a value, as opposed to modifying an argument. No offense, Jared.
     
  15. macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #15
    Something leaking memory and failing to capture the values desired while duplicating others is not "working well" in my book =).

    Anyhow, Jared_kipe's solution could be modified in-place to return an NS(Mutable)Array or a very small wrapper could be written that allocates an NSMutableArray, pass it in, then autoreleases if the name dictates it should, and returns the array.

    -Lee
     
  16. thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #16
    I'm curious how it was leaking memory. I admittedly don't know much about memory and how to spot memory leakage.

    By saying "working well," it did capture all the objects in the window, without duplicating any. The reason I changed it, as stated above, is that I realized it was only working due to the specific setup of the test window. In case you're curious, here's the working version I ended up with. If you can spot any memory leaks, please point them out, as I'm still unclear with a lot of this memory stuff.

    Code:
    -(NSArray *)allSubviews{
    	// This method returns an array that contains all of the subviews in the window
    	NSMutableArray *fields = [NSMutableArray arrayWithArray:[[[self window] contentView] subviews]];
    	NSMutableArray *views = [NSMutableArray arrayWithArray:[[[self window] contentView] subviews]];
    	NSMutableArray *newViews = [[NSMutableArray alloc] init];
    	
    	do {
    		[newViews removeAllObjects];	// clears the array to start the new cycle
    		for (NSView *view in views){
    			NSLog(@"Object:%@", [view description]);
    			[newViews addObjectsFromArray:[view subviews]];
    		}	
    		[fields addObjectsFromArray:newViews];
    		[views removeAllObjects];		// clears the views before replacing them with the new views
    		[views addObjectsFromArray:newViews];
    	} while ([newViews count] > 0);
    	
    	return fields;
    }
     
  17. macrumors 68030

    jared_kipe

    Joined:
    Dec 8, 2003
    Location:
    Seattle
    #17
    Code:
    - (NSArray *) allSubviews{
         static NSMutableArray *storage = nil;
         BOOL isFirstCall = NO;
    
         if (!storage) {
              isFirstCall = YES;
              storage = [[NSMutableArray alloc] init];
         }
    
         NSArray *subviews = [self subviews];
         [storage addObjectsFromArray: subviews];
    
         for (NSView *view in subviews) {
              [view allSubviews];
         }
    
         if (!isFirstCall) {
              return nil;
         }
         
         NSMutableArray *arrayToReturn = storage;
         storage = nil;
         return [arrayToReturn autorelease];
    }
    
    Sheesh people can be picky. I checked this thread a couple times with no response then there are all kinds of debate here.

    As with the first one, this should be a category on NSView. I didn't type this in a compiler, just in the little MR reply box, so YMMV but I was pretty careful. You MIGHT want to tear down storage and make a strict NSArray instead of returning the NSMutableArray if you think stylistically it matters.
     
  18. thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #18
    It's not a matter of being picky, it's a matter of being stubborn. I can't stand someone else doing the work for me, so even after you posted your solution, I still had to come up with my own.
     
  19. macrumors 68030

    jared_kipe

    Joined:
    Dec 8, 2003
    Location:
    Seattle
    #19
    Well then I hope you LOVED the new one ;) ;)
     
  20. macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #20
    This does look a lot better than the last one. There were no duplicates and all of the items were captured with the simple layout I looked at that didn't work with your original code. There is still a memory leak, but it's worlds better than the first go round. You alloc/init and assign to newViews, so you own it. You never release it, so there's a leak. Otherwise a lot of the shenanigans with assigning over pointers to things you owned, copy, etc. are cleaned up.

    Now that you've come up with a more involved solution that bit you on the first try, it might behoove you to learn more about recursion. Then the next time a problem comes along that is very well suited to it you can write your own recursive solution with confidence that it will work and that the computer won't get caught in an infinite loop.

    -Lee
     
  21. thread starter macrumors 6502

    Joined:
    Dec 17, 2009
    #21
    Thanks. As I understand, I don't need to bother with releasing because the garbage collector does that for me. Also, when I declare and initialize within a method, isnt't the scope limited to within that method?
     
  22. macrumors 603

    whooleytoo

    Joined:
    Aug 2, 2002
    Location:
    Cork, Ireland.
    #22
    You're right, you don't need to release if you have garbage collection on. There are times when that's not an option (iOS development, anywhere you're allocating memory in a tight loop and you need more 'manual' memory management), so it's useful to understand Cocoa memory management, but you're right.

    As for the scope - that applies to basic types (Boolean, int, char, struct) which are allocated on the stack. These are freed when the method returns; no need to worry about them.

    If you're dealing with pointers which you alloc (or malloc) memory for, they are allocated on the heap, and are not freed when the method returns. If garbage collection is off and you don't free them, they leak.
     
  23. macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #23
    This is only my opinion, but GC seems like a dangerous trap, especially when Apple put together an elegant memory management mechanism with retain/release/autorelease. The concept of ownership takes a pretty small amount of time to figure out, and once you get it it's pretty easy to apply. With GC you give up a lot of control, and "stop caring" about memory management. The GC is likely pretty good in Objective-C 2.0, but with the compatibility limitations (i guess 10.4 support is going to be dwindling soon, anyway, but iOS is a huge deal) it seems best to just "do it right" without GC so you can easily move your code to iOS.

    Even with GC having your app bloat up unnecessarily just to have the GC clean up after you seems undesirable to me.

    In any event, with GC you don't have to retain/release/autorelease so that code should work. Just a word of warning, people will point out what they perceive to be leaks in your code because they won't know if you're using GC and once you are familiar with memory management when you see it being done "wrong" it sticks out like a sore thumb.

    Since you don't have to worry about memory management right now because of GC that means you've got the extra time to learn recursion now =).

    -Lee
     
  24. macrumors 603

    Joined:
    Aug 9, 2009
    #24
    Solution: if you're using GC, always say so in the first post.
     
  25. macrumors 68030

    jared_kipe

    Joined:
    Dec 8, 2003
    Location:
    Seattle
    #25
    Obviously you are correct but I want to address this a little more because I believe the question is mostly related to my new method of doing this.

    First thing to remember, all objects in Obj-C are allocated on the heap meaning if you make it and its retain count is greater than zero it will still be in the same place with the same data inside it for the life of your application.

    Second thing to know is that the keyword static makes a variable into what is basically an instance variable for that single method*. Every time the method is called it has access to those static declared pieces of memory.

    So the way the new method works is EXTREMELY similar to the way the old method worked. The only major change is that the very first call to the method CREATES the storage the method will use to store all the subviews.

    It creates it and stores the pointer to that storage inside its own little static space.

    Then essentially all that needs to happen is that all subsequent calls store all their subviews int that storage as before, and then they all return, the very first call returns the storage.

    The key important ideas to keep in mind is how does each subsequent call to allSubviews know not to make the storage again, and not to return anything.**

    1: Checking if storage is nil, makes it possible to make the storage if it isn't there, and use the storage if it is.

    2: Each function call will get its own BOOL isFirstCall with the initial value set to NO. IF storage is nil, then it gets toggled to YES, but ONLY for the funcion call where storage is nil. For all the others it stays NO.

    3: After adding all the subviews and recursively calling allSubviews on those subviews, the method checks that isFirstCall and simply returns nil right then if it is NO. This stops that particular function from continuing at that moment.

    3: After doing the exact same thing, and all of them returned, the initially called allSubviews basically wants to return the storage and then set storage to nil to prime the next call of the allSubviews method. This is not possible in this order so it makes a stack copy of that pointer, sets the static pointer storage to nil, and returns the stack copy instead.

    *works for C functions too
    ** they could return something as long as they don't set the storage pointer back to nil
     

Share This Page