-
Comment on "Clean table view code" from objc.io #1
Finally I get to read all the great articles of objc.io. Recently I read [Clean table view code](http://www.objc.io/issue-1/table-views.html) (because I want to improve my `UITableView` code) and I have to comment on it, because for me the approach doesn't look that clean.
In **Bridging the Gap Between Model Objects and Cells** [@floriankugler](https://twitter.com/floriankugler) recommends to put the code, populating the `UITableViewCell` subclass into a category of the cell.
[code language="objc"]
@implementation PhotoCell (ConfigureForPhoto)- (void)configureForPhoto:(Photo *)photo
{
self.photoTitleLabel.text = photo.name;
NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
self.photoDateLabel.text = date;
}@end
[/code]I think this is not a good idea because it puts information about the model into the cell, which belongs to the view layer. Here an example where the problem about this becomes more obvious:
Let's say we are going to build a client for App.net. Posts in the client are represented in the model layer by `DDHPost` objects. If we use the suggested category approach this would result in something like this:
[code language="objc"]
#import "DDHPost.h"@implementation DDHPostCell (ConfigureForPost)
- (void)configureForPost:(DDHPost*)post {
self.nameLabel.text = post.userName;
self.postLabel.attributedText = [self attributedTextForPost:post];
self.postDateLabel.text = post.dateString;[self loadAvatarFromURL:post.avatarURL];
}@end
[/code]Later in the development we realize that it would be better to generate the attributed text for the post after we have downloaded the posts from the API. Therefore we add an `attributedText` property in the model layer. Now we have to change the `DDHPostCell` category (which belongs to the view layer) to reflect this change even though the cell looks exactly the same after this change. **(The advantage in this case is, that we don't have to change the data source of the table view.)**
I think this conflicts with the Model-View-Controller pattern. I prefer to have a smart table view data source which knows how to deal with the data an the presentation. In fact it already needs to know about the cell and the data object.
Did I miss something? Please let me know what you think! Get in touch with me at [App.net](https://alpha.app.net/dasdom) or [Twitter](https://twitter.com/dasdom).
Read more...
-
Test networking code
Clearly we all write unit test to test our code, aren't we. ;)
Here is how I test my networking code.To test networking code I use the open source framework [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs) by [AliSoftware](https://github.com/AliSoftware). I've added it to my project with [cocoapods](http://cocoapods.org). Search the web how to get cocoapods to work with a test target.
In Xcode go to `File > New > File...` and select `Objective-C test case class`. Name the class `DataFetcher`, click next and add it to the Test Target.
Import the header files `OHHTTPStubs.h` and `DataFetcher.h` into the test case class. We need a property of the class we would like to test. In the case of this example this is a `DataFetcher`. As we are testing asynchronous requests we also need a `BOOL` property to indicate if the API request is done.
[code language="objc"]
#import <XCTest/XCTest.h>
#import "OHHTTPStubs.h"
#import "DataFetcher.h"@interface DataFetcherTests : XCTestCase
@property (nonatomic, strong) DataFetcher *dataFetcher;
@property (nonatomic, assign) BOOL done;
@end
[/code]Now we edit the `setUp` and the `tearDown` methods to look like this:
[code language="objc"]
- (void)setUp {
[super setUp];
self.dataFetcher = [[DataFetcher alloc] init];
self.done = NO;
}- (void)tearDown{
self.dataFetcher = nil;
[OHHTTPStubs removeAllStubs];
[super tearDown];
}
[/code]We have to add a method which helps us to wait for the completion of the asynchronous requests.
[code language="objc"]
- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if([timeoutDate timeIntervalSinceNow] < 0.0)
break;
} while (!self.done);return self.done;
}
[/code]
I've copied the code from the [Infinite Loop Blog](http://www.infinite-loop.dk/blog/2011/04/unittesting-asynchronous-network-access/).Let's write the first test. This test simply tests if the expected API endpoint is called.
[code language="objc"]
- (void)testThatFetchGlobalStreamCallsAPI {
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
NSLog(@"request.URL.absoluteString: %@", request.URL.absoluteString);
BOOL adnAPICall = [request.URL.host isEqualToString:@"alpha-api.app.net"];
if (adnAPICall) {
XCTAssertEqualObjects(request.URL.absoluteString, @"https://alpha-api.app.net/stream/0/posts/stream/global");
}
return adnAPICall;
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
return nil;
}];typeof(self) __weak weakSelf = self;
[self.dataFetcher fetchGlobalStreamWithCompletion:^(NSArray *globalPostArray, NSError *error) {
typeof(self) __strong strongSelf = weakSelf;
strongSelf.done = YES;
}];XCTAssertTrue([self waitForCompletion:5.0f], @"Didn't complete in expected time.");
}
[/code]Done! To check whether we really testing our code, change `fetchGlobalStreamWithCompletion:` to use another API endpoint and run the test (Cmd u). It should fail. Change the test back to the expected API endpoint and run the test again. Now the test should succeed.
Ok, our code calls the expected API endpoint. What about the response? Does our code proceed the response in a way we would expect? Let's add a test:
First we need a file with a response which looks like what we expect. Open the Terminal, navigate to the test folder of your project and put into the prompt:[code]
echo '{"meta":{"min_id":"0","code":200,"max_id":"0","more":true},"data":[{"text":"This is a test response."}]}' > globalStreamResponse.json
[/code]Add the file `globalStreamResponse.json` to the test target. The response from the real API is way more complicated. But we only want to test here whether the response is correctly put into the array and passed to the block. (Another test would be whether a more realistic API response is correctly transformed into objects.)
[code language="objc" highlight="10,12,19,24,25,27"]
- (void)testThatFetchGlobalStreamReturnsExpectedResponse {
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
NSLog(@"request.URL.absoluteString: %@", request.URL.absoluteString);
BOOL adnAPICall = [request.URL.host isEqualToString:@"alpha-api.app.net"];
if (adnAPICall) {
XCTAssertEqualObjects(request.URL.absoluteString, @"https://alpha-api.app.net/stream/0/posts/stream/global");
}
return adnAPICall;
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"globalStreamResponse" ofType:@"json"];
NSLog(@"path: %@", path);
return [OHHTTPStubsResponse responseWithFileAtPath:path statusCode:200 headers:@{@"Content-Type":@"text/json"}];
}];typeof(self) __weak weakSelf = self;
[self.dataFetcher fetchGlobalWithCompletion:^(NSArray *globalPostArray, NSError *error) {
typeof(self) __strong strongSelf = weakSelf;XCTAssertEqual([globalPostArray count], 1);
NSDictionary *expecedResponse = @{@"text" : @"This is an easy test response."};
id firstResponseElement = [globalPostArray firstObject];XCTAssertTrue([firstResponseElement isKindOfClass:[NSDictionary class]]);
XCTAssertEqual([(NSDictionary*)firstResponseElement count], [expecedResponse count]);
for (NSString *keyStrings in expecedResponse) {
XCTAssertEqualObjects(expecedResponse[keyStrings], firstResponseElement[keyStrings]);
}
strongSelf.done = YES;
}];XCTAssertTrue([self waitForCompletion:5.0f], @"Didn't complete in expected time.");
}
[/code]In line 10 the file we created is loaded and this is used to create the response in line 12. The tests are in the lines 19, 24, 25 and 27.
In line 19 we are testing the number of the elements in the returned array.
In line 24 we test whether the first element in the array is of kind `NSDictionary`. This isn't really needed because if it wouldn't be an `NSDictionary` one of the later tests would fail. But the test doesn't hurt and it reminds us about what we are expecting.
The next test (line 25) tests the number of elements in the dictionary.
And finally we test all the elements in the dictionary (line 27). It's kind of worthless to have a loop here. But if we decide to change the test response to have more elements (for example we could use a real response from the App.net API) we would not have to change the test. Remove the loop if it bothers you.
This is how I test my networking code.
You can find the project on [github](https://github.com/dasdom/SimpleTableView). If you have any comments, feel free to contact me at App.net [@dasdom](https://alpha.app.net/dasdom) or at Twitter [@dasdom](https://twitter.com/dasdom).
Read more...
-
How I do basic networking
I know the awesome [AFNetworking framework](http://afnetworking.com) by [Mattt Thompson](http://mattt.me) even though I haven't used it yet. The reason for not using it is, that in all the projects I did so far I only needed basic requests to easy to use APIs. Using AFNetworking would have been an overkill. In additions I am still learning and needed first to develop my own style of using the networking frameworks that Apple provides.
Here is how I do network requests these days.
To have all the definitions of the API endpoints in one spot, I define static functions at the top of my networking class. (I'm using the App.net-API as an example.)
[code language="objc"]
static NSString *baseURLString = @"https://alpha-api.app.net/stream/0/";static NSURL *globalStreamURL(void) { return [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", baseURLString, @"posts/stream/global"]]; }
[/code]As I create several `NSURLRequests` I have helper methods to construct those.
[code language="objc"]
- (NSURLRequest*)requestWithURL:(NSURL*)url method:(NSString*)restMethod body:(NSDictionary*)body
{
NSArray *allowedMethodNames = @[@"GET", @"POST", @"DELETE"];
NSAssert([allowedMethodNames containsObject:restMethod], @"Only GET, POST and DELETE are allowed as rest methods at the moment.");NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:url];
[urlRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
urlRequest.HTTPMethod = restMethod;if (body) {
NSError *jsonError = nil;
NSData *bodyData = [NSJSONSerialization dataWithJSONObject:body options:kNilOptions error:&jsonError];
if (jsonError) {
return nil;
}
urlRequest.HTTPBody = bodyData;
}return urlRequest;
}- (NSURLRequest*)postRequestWithURL:(NSURL*)url body:(NSDictionary*)body {
return [self requestWithURL:url method:@"POST" body:body];
}- (NSURLRequest*)getRequestWithURL:(NSURL*)url {
return [self requestWithURL:url method:@"GET" body:nil];
}
[/code]The actual API call is a public method with a completion block.
[code language="objc"]
- (void)fetchGlobalStreamWithCompletion:(void(^)(NSArray *globalPostsArray, NSError *error))completion {
NSURLRequest *urlRequest = [self getRequestWithURL:globalStreamURL()];NSURLSession *session = [self sessionWithDefaultConfig];
NSURLSessionTask *sessionTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {NSError *jsonError = nil;
NSDictionary *responseDictionary = nil;
if (!error) {
responseDictionary = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
NSLog(@"responseString: %@", [data stringValue]);
globalPostsArray = responseDictionary[kDataKey];
error = jsonError;
}
if (completion) completion(globalPostsArray, error);
}];
[sessionTask resume];
}
[/code]As I'm only using `NSURLSessions` with default config I have a helper method to construct those.
[code language="objc"]
#pragma mark - Session with default config
- (NSURLSession*)sessionWithDefaultConfig {
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
return [NSURLSession sessionWithConfiguration:sessionConfiguration];
}
[/code]In the next post I will share how I test this code.
Read more...
-
Strange debugging problem
Recently I had to take over a project a ex-collegue had nearly finished before he left. During debugging I came across a strange problem. The debugger only stopped at some of my breakpoints. In some classes the breakpoints have been useless. First I thought that the code at these breakpoints did not get executed. But when I added NSLog statements in the line before or after the breakpoint the log worked.
It took a while until I figured out that the implementation file was not added to the target I was debugging. But why did this code then even got executed (as I knew because of the NSLog statements)? And why did it even compile.
After I added the file to the target I got the compiler error
`duplicate symbol _OBJC_IVAR_$...`.What the...?!?
A little internet search helped me to find the underlying problem. The ex-collegue had five times imported the implementation file instead of the interface file. So in five classes there where
[code language="objc"]
#import "MyAwesomeClasse.m"
[/code]instead of
[code language="objc"]
#import "MyAwesomeClasse.h"
[/code]In total it took me nearly two hours to find and fix the bug. I hope this post will save you from wasting this time.
Read more...
-
Behaviors
Xcode has a feature called Behaviors. You can find it `Xcode>Behaviors>Edit Behaviors`. The Behaviors manages the different contexts of Xcode. I use them to switch editors and navigators for different stages of Xcode. Here are my Behaviors (for some of them you need to name you tabs [like I did](http://dasdev.de/2014/01/18/xcode-tabs/)):
- Build
- Starts
- Show tab named Log in active window
- Show navigator: Log Navigator
- Navigate to current log
- Generates new issue
- Show navigator: Issue Navigator
- navigate to first new issue
- Testing
- Starts
- Show tab named Testing in active window
- Generates new issue
- Show tab named Testing in active window
- Show navigator: Issue Navigator
- navigate to first new issue
- Running
- Starts
- Show tab named Debug in active window
- Pauses
- Show navigator: Debug Navigator
- Show debugger with Current Views
- Generates output
- Show debugger with Current Views
- Completes
- Show tab names Main in active window
- If no output, hide debuggerComments about this post? I am [@dasdom](https://alpha.app.net/dasdom).
Read more...
-
Xcode Tabs
I use Xcode tabs to be faster in switching contexts. Right now I have in all my projects 4 tabs (plus one tab when I'm using Interface Builder in the project).
The tabs are
- Main: This is used for coding. I try to hide the project navigator and use Cmd-⇧-o to open files I want to edit directly without using the trackpad.
- Tests: Here I write the tests. Like in Main, the project navigator is hidden.
- Testing: In this tab I run the tests. On the left side the test navigator is shown.
- Log: This tab shows the log during building. I anything goes wrong I can just go there and figure out what happened.
- Debug: During debug I use this tab to see the debug logs and the breakpoints.
- IB: In this tab I do the interface building stuff. On the left side the project navigator is open with a filter to only show Interface Builder files (.storyboard, .xib).In addition I have added Behaviors to switch to the different tabs in different contexts. How this can be achieved will be shown in another post.
Comments about this post? I am [@dasdom](https://alpha.app.net/dasdom).
Read more...
-
The ?: operator
You sure know all the operator `?:`. With this operator the code snippet
[code language="objc"]
int x;
if (y) {
x = y;
} else {
x = z;
}
[/code]is equivalent to
[code language="objc"]
int x = y ? y : z;
[/code]But did you know that you can write that even shorter? When you look at the last code snippet the condition (`y`) is the same as the value we want to assign to `x`. In this case this can be written as
[code language="objc"]
int x = y ?: z;
[/code]And this is good because as a software developer you Don't want to Repeat Yourself (the DRY principle).
Any comments about this post? I am [@dasdom](https://alpha.app.net/dasdom).
Read more...
-
Improvements to UITextView (using an inputAccessoryView)
During the last year I have developed hAppy, a microblogging client for App.net. You can create posts for App.net within the client. One thing which bothered me a lot when using the first betas of it was the positioning of the cursor. The solution which is build into iOS, the tiny magnifying glass, is bad. It was cool in 2007. But today there are better solutions.
The solution I use in hAppy is the positioning of the cursor by panning in a field above the keyboard. The used code is quite simple.
I have created a subclass of
UITextViewand called itDDHTextView. In the initialization of the class (or inawakeFromNibin the case of the use of Interface Builder) the view for the pan gesture is initialized and assigned to theinputAccessoryViewof theDDHTextView.UIView *inputAccessoryView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, 0.0f, [[UIScreen mainScreen] applicationFrame].size.width, 40.0f)]; inputAccessoryView.backgroundColor = [UIColor colorWithWhite:0.90f alpha:1.0f]; self.inputAccessoryView = inputAccessoryView;Then the gesture recognizer are attached to the
inputAccessoryView._singleFingerPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(singleFingerPanHappend:)]; _singleFingerPanRecognizer.maximumNumberOfTouches = 1; [inputAccessoryView addGestureRecognizer:_singleFingerPanRecognizer];The last thing we need to do is to react on the pan gesture.
- (void)singleFingerPanHappend:(UIPanGestureRecognizer*)sender { if (sender.state == UIGestureRecognizerStateBegan) { self.startRange = self.selectedRange; } CGPoint translation = [sender translationInView:self]; CGFloat cursorLocation = MAX(self.startRange.location+(NSInteger)(translation.x*DDHCursorVelocity), 0); NSRange selectedRange = {cursorLocation, 0}; self.selectedRange = selectedRange; }In addition I have added a few other gesture recognizer for selecting text or moving the cursor fast to the beginning or end of the text. You can get
DDHTextViewon my github page or on cocoacontrols.com.If you have any comments about this post? I am @dasdom.
Read more...
-
Code-Snippets in Xcode
Some people may not know, there is a code snippet library build into Xcode. Recently I started to use it more often and figured out that it saves a lot of time while coding.
You can find the snippet library at
View > Utilities > Show Code Snippet Library.There are already some code snippets provided by Apple. But the real power comes with the possibility to add your own. To add a code snippet you select code and drag in onto the code snippet library. Xcode opens a popup which lets you edit your code snippet.
Let's say you type the following line a lot (as I do):
@property (nonatomic, strong)
To add this as a code snippet select the code and drag it to the snippet library. Enter in the popup
Title: Property Nonatomic Strong
Plattform: All
Language: Objective-C
Completion Shortcut: propertynonatomicstrong
Completion Scopes: All
and change the snippet to
@property (nonatomic, strong) <#type#> *<#name#>
The <#type#> tells the snippet library that there should be a placeholder. This mean that, after you have included this snippet into your code, you only have to type a tab to jump to the <#type#> to change it to the type you need.
I have put the snippets I use most onto github.
Read more...
-
Delegate and Data Source of Collection Views
How I split delegate/data source and view controller of
UICollectionViews.Recently I started to split the view controller of a
UICollectionViewand its delegate and data source. The delegate and data source are put into the same class which is a subclass ofNSObject. In the view controller the collection view is defined withinloadView:(I don't like Interface Builder and therefore construct the views in code) and the delegate and data source is set:
[code language="objc" light="true"]
- (void)loadView {
CGRect frame = [[UIScreen mainScreen] applicationFrame];
UIView *contentView = [[UIView alloc] initWithFrame:frame];UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; _collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:collectionViewFlowLayout]; _collectionView.delegate = self.collectionViewDelegateAndDataSource; _collectionView.dataSource = self.collectionViewDelegateAndDataSource; [contentView addSubview:_collectionView]; self.view = contentView;}
[/code](When using
UICollectionViews one has to register the classes for the cells before any cell can be created. This is needed because within the cell creationdequeueReusableCellWithReuseIdentifier:forIndexPath:is called. This method dequeues a cell for the given identifier if possible and creates a new cell otherwise. To create a cell the collection view needs to know the class of the cell. That's why one has to register the classes for the cells. I do this normally in loadView.)But here comes the drawback of putting the delegate/data source of the collection view into its own class:
When the delegate and data source isn't the view controller, both, the delegate/data source and the view controller need to know the classes which are used for the cells (the view controller needs to register the cells and the data source needs to populate the cells). This is not clean code because the information is doubled.
To remove this code doubling I have defined a protocol with one method@protocol DDHRegisterCellsProtocol <NSObject> - (void)registerCellsForCollectionView:(UICollectionView*)collectionView; @end
and require the delegate/data source to conform to this protocol. With this protocol the delegate/data source can register the classes for the cells without the need of a property holding a reference to the collection view. And the view controller doesn't need to know how the cells look like and how they are created. The only thing the view controller has to do is to call the protocol method.
This looks much better.
A project demonstrating this can be found on github. In this demo you can also see how I construct user interfaces without using Interface Builder.
Read more...
subscribe via RSS
