Using a UICollectionView over an SKScene - objective-c

I have an app with several SKScenes. To keep it snappy, I have a single UIViewController, that handles 1 SKView.
I'm trying to add a UICollectionView to one of the SKScenes. However the problem comes when I try to set the delegate for the collection view to the SKScene initialising it.
I initialise it here:
- (id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size]) {
//Initialise collectionView
UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height) collectionViewLayout:layout];
_collectionView.delegate = self;
_collectionView.dataSource = self;
[_collectionView setBackgroundColor:[UIColor redColor]];
}
return self;
}
After initialising, I add the collection view as a subview of the UIViewController's view by calling:
- (void)didMoveToView:(SKView *)view
{
[self.view addSubview:_collectionView];
}
But the delegate or datasource methods aren't called. I've set up the header:
#interface BrowseScene : SKScene <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
I'm guessing that despite setting self as the delegate, once the collection view is added, it is added as a subview of the UIViewController's SKView, and therefore has no reference to the SKScene that initialised it. I tried calling:
_collectionView.delegate = self.view.scene;
But I get a compiler error saying SKScene is an incompatible type for UICollectionViewDelegate.
So my question is, how best to approach this?

It turns out, the problem lay in how I was initialising the UICollectionView, not where. And it's perfectly ok to set an SKScene as the delegate class.
The main reason the delegates weren't being called was because I'd initialised the collectionView with an empty layout object (as UICollectionViewLayout is just an abstract class). I initialised with a standard UICollectionViewFlowLayout instead, and the methods were then called when the class initialised.
I was also missing a call to
[_collectionView registerClass:[collectionViewCell class] forCellWithReuseIdentifier:#"collectionViewCell"];
(In the complete code, I had calls to reloadData, so that wasn't the problem here).

Related

UIView Subclass backed by xib with Size Classes wrong frame

I am working on a project for iOS 7.0+ with a storyboard, using Size Classes with AutoLayout and I'm using a UIView subclass backed by a xib file of the same name.
What I'am trying to do is I'am instantiating a UIView from xib programmatically and adding it to a ViewController from a Storyboard. This ViewController has AutoLayout up and running but the UIView I am adding doesn't respect the frame of the ViewController.
I'm instantiating my UIView subclass like this:
tabBarView = [[SHDTabBarView alloc] initWithFrame:CGRectMake(0, self.view.height-50, self.view.width, 50)];
[self.view addSubview:tabBarView];
And inside the subclass I'm using a set up of creating a UIView IBOutlet called container to instantiate it form code like this:
-(id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self == nil) return nil;
[self initalizeSubviews];
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self == nil) return nil;
[self initalizeSubviews];
return self;
}
-(void)initalizeSubviews{
NSString *nibName = NSStringFromClass([self class]);
UINib *nib = [UINib nibWithNibName:nibName bundle:nil];
[nib instantiateWithOwner:self options:nil];
//Add the view loaded from the nib into self.
[self addSubview:self.container];
}
This is how my xib looks in the Interface Builder (notice the width of the canvas is 320 px):
And that's how it looks on the iPhone 6 (notice how it's getting cut off from the right side):
I've tried to use a multitude of solutions, including doing it all in code with an open-source solution PureLayout, using a manual constraint set up, etc.
None of my findings seem to work right. Ideally, I want to set up everything in Interface Builder, then just add the view to the superview of the ViewController with according frame and let AutoLayout do its magic.
How should I approach this task? Any advices are more than welcome.
Try to set the frame of your subview in the viewDidLayoutSubviews(). Looks like you init your subview before view fully layouted

UIRefreshControl without UITableViewController

Just curious, as it doesn't immediately seem possible, but is there a sneaky way to leverage the new iOS 6 UIRefreshControl class without using a UITableViewController subclass?
I often use a UIViewController with a UITableView subview and conform to UITableViewDataSource and UITableViewDelegate rather than using a UITableViewController outright.
On a hunch, and based on DrummerB's inspiration, I tried simply adding a UIRefreshControl instance as a subview to my UITableView. And it magically just works!
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(handleRefresh:) forControlEvents:UIControlEventValueChanged];
[self.myTableView addSubview:refreshControl];
This adds a UIRefreshControl above your table view and works as expected without having to use a UITableViewController :)
EDIT: This above still works but as a few have pointed out, there is a slight "stutter" when adding the UIRefreshControl in this manner. A solution to that is to instantiate a UITableViewController, and then setting your UIRefreshControl and UITableView to that, i.e.:
UITableViewController *tableViewController = [[UITableViewController alloc] init];
tableViewController.tableView = self.myTableView;
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:#selector(getConnections) forControlEvents:UIControlEventValueChanged];
tableViewController.refreshControl = self.refreshControl;
To eliminate the stutter that is caused by the accepted answer, you can assign your UITableView to a UITableViewController.
_tableViewController = [[UITableViewController alloc]initWithStyle:UITableViewStylePlain];
[self addChildViewController:_tableViewController];
_tableViewController.refreshControl = [UIRefreshControl new];
[_tableViewController.refreshControl addTarget:self action:#selector(loadStream) forControlEvents:UIControlEventValueChanged];
_theTableView = _tableViewController.tableView;
EDIT:
A way to add a UIRefreshControl with no UITableViewController with no stutter and retain the nice animation after refreshing data on the tableview.
UIRefreshControl *refreshControl = [UIRefreshControl new];
[refreshControl addTarget:self action:#selector(handleRefresh:) forControlEvents:UIControlEventValueChanged];
[self.theTableView addSubview:refreshControl];
[self.theTableView sendSubviewToBack:refreshControl];
Later when handling the refreshed data...
- (void)handleRefresh:(UIRefreshControl *)refreshControl {
[self.theTableView reloadData];
[self.theTableView layoutIfNeeded];
[refreshControl endRefreshing];
}
What you would try is use container view inside ViewController you are using. you can define clean UITableViewController subclass with dedicated tableview and place that in the ViewController.
Well UIRefreshControl is a UIView subclass, so you can use it on it's own. I'm not sure though how it renders itself. The rendering could simply depend on the frame, but it also could rely on a UIScrollView or the UITableViewController.
Either way, it's going to be more of a hack than an elegant solution. I recommend you look into one of the available 3rd party clones or write your own.
ODRefreshControl
SlimeRefresh
Try delaying the call to the refreshControl -endRefresh method by a fraction of a second after the tableView reloads its contents, either by using NSObject's -performSelector:withObject:afterDelay: or GCD's dispatch_after.
I created a category on UIRefreshControl for this:
#implementation UIRefreshControl (Delay)
- (void)endRefreshingAfterDelay:(NSTimeInterval)delay {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self endRefreshing];
});
}
#end
I tested it and this also works on Collection Views. I've noticed that a delay as small as
0.01 seconds is enough:
// My data refresh process here while the refresh control 'isRefreshing'
[self.tableView reloadData];
[self.refreshControl endRefreshingAfterDelay:.01];
IOS 10 Swift 3.0
it's simple
import UIKit
class ViewControllerA: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var myTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
myTableView.delegate = self
myTableView.dataSource = self
if #available(iOS 10.0, *) {
let refreshControl = UIRefreshControl()
let title = NSLocalizedString("PullToRefresh", comment: "Pull to refresh")
refreshControl.attributedTitle = NSAttributedString(string: title)
refreshControl.addTarget(self,
action: #selector(refreshOptions(sender:)),
for: .valueChanged)
myTableView.refreshControl = refreshControl
}
}
#objc private func refreshOptions(sender: UIRefreshControl) {
// Perform actions to refresh the content
// ...
// and then dismiss the control
sender.endRefreshing()
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 12
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
cell.textLabel?.text = "Cell \(String(indexPath.row))"
return cell
}
}
If you want to learn about iOS 10 UIRefreshControl read here.
Adding the refresh control as a subview creates an empty space above section headers.
Instead, I embedded a UITableViewController into my UIViewController, then changed my tableView property to point towards the embedded one, and viola! Minimal code changes. :-)
Steps:
Create a new UITableViewController in Storyboard and embed it into your original UIViewController
Replace #IBOutlet weak var tableView: UITableView! with the one from the newly embedded UITableViewController, as shown below
class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let tableViewController = self.childViewControllers.first as! UITableViewController
tableView = tableViewController.tableView
tableView.dataSource = self
tableView.delegate = self
// Now we can (properly) add the refresh control
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: "handleRefresh:", forControlEvents: .ValueChanged)
tableViewController.refreshControl = refreshControl
}
...
}
For Swift 2.2 .
First make UIRefreshControl() .
var refreshControl : UIRefreshControl!
In your viewDidLoad() method add:
refreshControl = UIRefreshControl()
refreshControl.attributedTitle = NSAttributedString(string: "Refreshing..")
refreshControl.addTarget(self, action: #selector(YourUIViewController.refresh(_:)), forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refreshControl)
And make refresh function
func refresh(refreshControl: UIRefreshControl) {
// do something ...
// reload tableView
self.tableView.reloadData()
// End refreshing
refreshControl.endRefreshing()
}
Here's another solution which is a little different.
I had to use it because of some view hierarchy issues I had: I was creating some functionality that required passing views around to different places in the view hierarchy, which broken when using a UITableViewController's tableview b/c the tableView is the UITableViewController's root view (self.view) and not just a regular view, it created inconsistent controller / view hierarchies and caused a crash.
Basically create your own subclass of UITableViewController and override loadView to assign self.view a different view, and override the tableView property to return a separate tableview.
for example:
#interface MyTableVC : UITableViewController
#end
#interface MyTableVC ()
#property (nonatomic, strong) UITableView *separateTableView;
#end
#implementation MyTableVC
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:CGRectZero];
}
- (UITableView *)tableView {
return self.separateTableView;
}
- (void)setTableView:(UITableView *)tableView {
self.separateTableView = tableView;
}
#end
When combined with Keller's solution this will more robust in the sense that the tableView is now a regular view, not a VC's root view, and be more robust against changing view hierarchies. Example of using it this way:
MyTableVC *tableViewController = [[MyTableVC alloc] init];
tableViewController.tableView = self.myTableView;
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:#selector(getConnections) forControlEvents:UIControlEventValueChanged];
tableViewController.refreshControl = self.refreshControl;
There is another possible use for this:
Since subclassing this way separates self.view from self.tableView, it's possible now to use this UITableViewController as more of a regular controller, and add other subviews to self.view without the oddities of adding subviews to UITableView, so one may considering making their view controllers directly a subclass of UITableViewController instead of having UITableViewController children.
Some things to watch out for:
Since we're overriding the tableView property without calling super, there may be some things to watch out for and should handle where necessary. For example, setting the tableview in my above example will not add the tableview to self.view and not set the frame which you may want to do. Also, in this implementation there is no default tableView given to you when the class is instantiated, which is also something you may consider adding. I don't include it here because that is case by case, and this solution actually fits well with Keller's solution.
Try this,
The Above solutions are fine but tableView.refreshControl is available for UITableViewController only, till iOS 9.x and comes in UITableView from iOS 10.x onwards.
Written in Swift 3 -
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(FeedViewController.loadNewData), for: UIControlEvents.valueChanged)
// Fix for the RefreshControl Not appearing in background
tableView?.addSubview(refreshControl)
tableView.sendSubview(toBack: refreshControl)
Keller's first suggestion causes a strange bug in iOS 7 where the inset of the table is increased after the view controller reappears. Changing to the second answer, using the uitableviewcontroller, fixed things for me.
It turns out you can use the following when you use a UIViewController with a UITableView subview and conform to UITableViewDataSource and UITableViewDelegate:
self.refreshControl = [[UIRefreshControl alloc]init];
[self.refreshControl addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventValueChanged];

How do I use a CALayer to custom draw in a UIButton

I'm trying to do draw a fancy UIButton using Quartz. I've declared my own button class like this:
#interface MyButton : UIButton
#end
In the .m file I'm constructing the button:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code
CALayer *buttonLayer = self.layer;
buttonLayer.masksToBounds = YES;
CALayer *customDrawn = [CALayer layer];
customDrawn.delegate = self;
customDrawn.masksToBounds = YES;
[buttonLayer insertSublayer:customDrawn atIndex:0];
[customDrawn setNeedsDisplay];
}
return self;
}
But this results in some kind of recursion and finally fails with a EXC_BAD_ACCESS.
I've implemented a method drawLayer: inContext:, but it still crashes. The only way I can avoid the crash is by removing the delegate-assignment, but then I can't do any of the custom drawing I want to implement.
How can I make this work?
As described in the question mentioned by Kurt Revis, a UIView (such as UIButton) can't be used as a delegate for a sublayer. (It is already the delegate for the "main" layer of the UIView and can't be used as a delegate for another layer.) The solution is to use another object as a delegate which can be a "private" class just used to implement the drawLayer: inContext: method.

Objective-c How can add one UIview at the main view directly from a class?

I have this class
- (void) initWithFrame:(CGRect)frame withString:(NSString *)html{
//CGRect rectFrame = [UIScreen mainScreen].applicationFrame;
news = [[UIView alloc] initWithFrame:frame];
web = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 200, 300)];
web.scalesPageToFit = YES;
//web.delegate = self;
web.scalesPageToFit = YES;
[web loadHTMLString:html baseURL:nil];
[news addSubview:web];
[[self view] addSubview:news];
}
I receive Sigabrt at line [[self view] addSubview:news];
How can add the UIview at the main view directly from a class? is it possibile?
1) init<...> methods should return initialized object - declare it returning value of id type. Return self in implementation or nil if it's failed.
2) init<...> methods have to initialize this object. Read about initializing objects here - The Objective-C Programming Language - Allocating and Initializing Objects. Basically you have to call one of the initialization methods of superclass, assign self to returned value (self = [super init]) and check if this value is nil.
Is this class a controller? If so:
3) You have to load it's view - either by loading it from nib on initialization (init or initWithNibName:bundle: methods) or by implementing method loadView. See View Controller Programming Guide for iOS
4) You can be sure your view is available only when viewDidLoad is called on your controller. Implement this method if you want to add something to controller's view.
The reason you are receiving the abort signal is that your view is not initialized. Instead try adding to awakeFromNib call.

Subviews not showing up in UIView class

I'm trying to lay out some images in code using addSubview, and they are not showing up.
I created a class (myUIView) that subclasses UIView, and then changed the class of the nib file in IB to be myUIView.
Then I put in the following code, but am still getting a blank grey screen.
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Initialization code
[self setupSubviews];
}
return self;
}
- (void)setupSubviews
{
self.backgroundColor = [UIColor blackColor];
UIImageView *black = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"black.png"]];
black.center = self.center;
black.opaque = YES;
[self addSubview:black];
[black release];
}
yes, just implement initWithCoder.
initWithFrame is called when a UIView is created dynamically, from code.
a view that is loaded from a .nib file is always instantiated using initWithCoder, the coder takes care of reading the settings from the .nib file
i took the habit to do the initialization in a separate method, implementing both initWithCode and initWithFrame (and my own initialization methods when required)
try implementing initWithCoder: sometimes I've had trouble with IB and initWithFrame:
or at least add a logging call to see if your init method is executed