📜 ⬆️ ⬇️

OOP patterns in the examples for iOS

From translator


We were looking for two Russian-speaking developers here - on iOS and on C ++ under Windows. Saw dozens of completed tests. The difference in OOP knowledge between the representatives of the two platforms is huge. In C ++, usually beautiful extensible code, as a matter of course. Objective C picture is depressing. Almost all iOS candidates did not know OOP beyond NSString 's nose and AppDelegate ' s nose .

It is clear that the pros are taught in the Stroustrup and the “gang of four”, and the Objective-C teaches more in the tutorials and the Stack Overflow. Fast food training leaves no room for fundamental questions ... But I did not expect such a difference.

Therefore, I translated the post, which gave initial information about the design patterns with examples on iOS ... "initial"? Yeah, then there will be a sequel? No, it will not. Further information you will receive from experience, from attempts to organize the process of writing code using patterns. At first it will not be possible, probably, the facade of the building will stick out of the chimney, but then an understanding will come where some techniques really help.
')
High-quality software development - a creative process that is unique to each specific head. Therefore, there is no general instruction: if (A and (B or C)) then use Pattern_N;

Just ask yourself more often: what I wrote is beautiful?

Disclaimer
Patterns are not a panacea, no cure for crooked hands, and no substitute for brains. Here is the stencil:



He does not draw! Draw - you.

What is a pattern


The idea of ​​patterns came 40 years ago from architect Christopher Alexander :
Any pattern describes a task that arises again and again in our work, as well as the principle of its solution, and in such a way that this solution can then be used a million times without reinventing anything.

Programmers saw this and said :
Although Alexander was referring to the patterns that arise in the design of buildings and cities, but his words are true in relation to the patterns of object-oriented design. Our solutions are expressed in terms of objects and interfaces, not walls and doors, but in both cases the meaning of the pattern is to offer a solution to a specific task in a specific context.



Unlike wikipedia , I will not call a pattern a “template” so as not to be confused with C ++ templates , which also have the right to life in Objective C.

This concludes the introduction and finally give the floor to the author of the article. - Approx. per. :)

Although not, another lyrical digression about the importance of patterns.
... Travelers went along the lane and found themselves in a quarter that was built up with houses with columns. There were columns and straight, and curves, and twisted, and twisted, and spiral, and inclined, and flattened, and kosopuzye, and pancake, and even those who can not pick a name. The eaves of the houses were also straight, and oblique, and curves, and broken, and zigzag. In some houses the columns were not at the bottom, as it should be, but on the top, on the roofs; the other houses had columns below, but the houses themselves stood above, above the columns; the third columns were suspended from the eaves and dangling over the heads of passersby. There was a house with a cornice at the bottom, and the columns were upside down and, in addition, were slanted to one side. There was also a house in which the columns stood straight, but the house itself stood aslant, as if it were about to collapse on the heads of passersby. There was also a house whose columns leaned in one direction, and the house itself leaned in another, so that it seemed as if all this would fall to the ground and crumble to dust.

“You are not looking at these slanting houses,” said the architect Kubik. - Once upon a time we had a fashion to get involved in the construction of houses that do not look like anything. That made such a disgrace that now even look ashamed!

N. Nosov. "Dunno in the Sunny City"




Although many developers agree that the theme of patterns is very important - not too many articles are devoted to it, and we often don’t devote to patterns worthy of attention when writing code.

A pattern is an example of a solution to a problem. A solution that can be repeated in another project. Patterns make code easier to understand. They help to write loosely coupled code — a code in which you can easily modify components, or replace an entire component with its counterpart, almost without touching the rest of the project.

If you are new to the topic of design patterns, then I have good news for you! First: you already use a huge number of patterns, thanks to the principles on which Cocoa is arranged. (However, this does not interfere with the bydlokodit in the examples on developer.apple.com - Note. Lane. ) Second: this article will introduce you to the main (and not just the main) design patterns that are commonly used in Cocoa.

For each pattern we consider:

Since patterns cannot be studied without practice, we will write a test application - a music library that will show your albums and information on them. During the development process, you will learn about some of the patterns:

Creational patterns make the system independent of the way objects are created:

Structural patterns look for simple ways to visualize the connections between objects:

Behavioral patterns (behavioral) define the process of interaction, “communication” between objects:

By the end of the article, our application will look something like this:



Getting started


Open the BlueLibrary project in your favorite IDE or in Xcode.



There is a common ViewController and a simple HTTP client with an empty implementation.

Did you know? As soon as you create a new project in Xcode or AppCode, your code is already full of patterns! MVC, “delegate”, “protocol”, “singleton” - you get them for free! :)

Before diving into the first pattern, we need to create two classes for storing and displaying information about albums.

Xcode : File> New> File ... or press ⌘N .
AppCode : ⌘N > File from Xcode Template ...

In the dialog that opens, select from the list: iOS> Cocoa Touch> Objective C class , click Next . Let the class be called Album , and it will be a subclass of NSObject .

Open Album.h and add several properties and one prototype of the method between @interface and @end :

 @property (nonatomic, copy, readonly) NSString * title; @property (nonatomic, copy, readonly) NSString * artist; @property (nonatomic, copy, readonly) NSString * genre; @property (nonatomic, copy, readonly) NSString * coverUrl; @property (nonatomic, copy, readonly) NSString * year; - (id)initWithTitle:(NSString *)title artist:(NSString *)artist coverUrl:(NSString *)coverUrl year:(NSString *)year; 

Please note: all properties have a readonly flag, since we do not need to change them after creating the Album object.

This method is an object initializer. Creating a new album, we pass here the name of the album, artist, cover URL and year of release.

Now open Album.m and insert the following code between @implementation and @end :

 - (id)initWithTitle:(NSString *)title artist:(NSString *)artist coverUrl:(NSString *)coverUrl year:(NSString *)year { self = [super init]; if (self) { _title = title; _artist = artist; _coverUrl = coverUrl; _year = year; _genre = @"Pop"; } return self; } 

The most common initialization.

Create another AlbumView class - a subclass of UIView .

In AlbumView.h, add a prototype of the method between @interface and @end :

 - (id)initWithFrame:(CGRect)frame albumCover:(NSString *)albumCover; 

And in AlbumView.m, replace the code between @implementation and @end with this one:

 @implementation AlbumView { UIImageView * coverImage; UIActivityIndicatorView * indicator; } - (id)initWithFrame:(CGRect)frame albumCover:(NSString *)albumCover { self = [super initWithFrame:frame]; if (self) { //   : self.backgroundColor = [UIColor blackColor]; //      - 5   : coverImage = [[UIImageView alloc] initWithFrame:CGRectMake(5, 5, frame.size.width-10, frame.size.height-10)]; [self addSubview:coverImage]; //   : indicator = [[UIActivityIndicatorView alloc] init]; indicator.center = self.center; indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge; [indicator startAnimating]; [self addSubview:indicator]; } return self; } @end 

The first thing you should notice is that there is an instance variable: coverImage . This variable is an image from a cover album. The second variable indicator is an indicator that spins, depicting activity while the cover is loading.

Why are these variables declared in the implementation file ( *.m ) and not in the header file ( *.h )? Because other classes (outside AlbumView ) do not need to know about the existence of these variables, since they are used only inside the class. This moment is extremely important if you create a library (or framework) for other developers.

Build the project (⌘B) - just check. Everything is good? Then get ready: your first pattern!

MVC - the king of patterns




Model — View — Controller (Model — View — Controller or simply MVC) is one of the foundations of Cocoa and undoubtedly the most commonly used pattern. It classifies objects according to their role in the application and helps to clean code separation (what it is, we discuss below).

Three roles:


Proper implementation of the MVC pattern means that in your application, each object falls into only one of these groups.



Why is this all about? Why not throw out the controller and combine the model with the presentation in one class? It will be much easier!

The question boils down to two (interconnected) things:
  1. Code separation;
  2. The prospect of code reuse.

Ideally, the presentation is completely separate from the model. If the view does not depend on the specific implementation of the model, it can be used in conjunction with another model to display other data.

For example, in the future we may want to add films or books to our library. We can still use AlbumView to display movie and book objects! And if we want to create a completely different project that will deal with albums, we can use the Album class in it - a model that does not depend on the presentation. This is the power of MVC!

How to use MVC pattern


1. Ensure that each class in the project is a model, controller, or view. One class cannot combine two roles! We have already taken the first step by creating two classes: Album and AlbumView .

2. Forget the horror that you saw on developer.apple.com! Yes, yes, those examples where ViewController 's play two or three roles at the same time. - Approx. per.

3. Create three groups in the project - Model , View and Controller :



How to do it:

  1. File> New> Group (or keyboard: in Xcode ⌘⌥N , in AppCode N> Group ), name the group Model . Do the same for Controller and View .
  2. Drag Album.h and Album.m to the Model group.
    Drag AlbumView.h and AlbumView.m to the View group.
    Drag ViewController.h and ViewController.m to the Controller group.

Now our project looks much better without these ... files floating around. Of course, you can create other groups and classes, but the main thing is these three categories.

Now that your components are organized, we need somewhere to get the data for the albums. We will create an API class through which it will be possible to access data from anywhere in the project. To do this, we approach the following design pattern:

Single (Singleton)


The loner pattern ensures that there is only one instance of this class in the entire application. There is a global access point to this instance. Delayed initialization is usually used: this single instance is created when it is needed for the first time.

Did you know? Apple makes extensive use of this approach. For example: [NSUserDefaults standardUserDefaults] , [UIApplication sharedApplication] , [UIScreen mainScreen] , [NSFileManager defaultManager] - each of these methods returns a singleton object.

You ask why worry about the fact that somewhere there are two or more instances of a class. Why not more than one copy? Memory is cheap now, isn't it?

There are cases where you need to have exactly one instance of a class. For example, you do not need to keep several instances of the class Logger (only if you do not write several different logs at the same time). Or the global configuration reference class: it is much better to provide thread-safe access to a certain shared resource (for example, to the settings file) than to have many classes that modify the settings file, possibly at the same time.

Using the loner pattern


Consider the scheme:

The Logger class has one instance property (a pointer to a single instance) and two methods: sharedInstance() and init() .

When the client first calls the sharedInstance() method, the instance property has not yet been initialized, then a new instance of the class is created and a pointer to it is returned.

The next time we call sharedInstance() , we are immediately returned to the instance without initialization. Such a scheme guarantees the existence of only one instance for the entire duration of the program.

We implement this pattern: create a singleton to manage all the data of the album.

Please note that the project has a group called API . There we will add classes that provide services for our application. In this group, create a class from the template iOS> Cocoa Touch> Objective-C class , a subclass of NSObject , and name it LibraryAPI .

Open LibraryAPI.h and replace its contents with the following:

 @interface LibraryAPI : NSObject + (LibraryAPI *)sharedInstance; @end 

Go to LibraryAPI.m and insert the following method after the @implentation line:

 + (LibraryAPI *)sharedInstance { // 1 static LibraryAPI * _sharedInstance = nil; // 2 static dispatch_once_t oncePredicate; // 3 dispatch_once(&oncePredicate, ^{ _sharedInstance = [[LibraryAPI alloc] init]; }); return _sharedInstance; } 

Many interesting things happen in this short method:

  1. We declare a static variable to store a pointer to a class instance (its value will be available globally from our class).
  2. We declare a static variable dispatch_once_t, which ensures that the initialization code will be executed only once.
  3. Using Grand Central Dispatch (GCD), we initialize the LibraryAPI instance. This is the essence of the “loner” pattern: the initialization block will never be executed again.

When you call sharedInstance() again, the code inside the dispatch_once block will not be executed (because it has already been executed before), and you will receive a pointer to the previous created instance of the LibraryAPI .

Note. To learn more about GCD and its application, the author of the article advises tutorials in English: Multithreading and Grand Central Dispatch and How To Use Blocks .

So, we have a singleton - the starting point for managing albums. Take the next step: create a class to store our data.

In the API group, create a new class: iOS> Cocoa Touch> Objective-C class , a subclass of NSObject , and call it PersistencyManager .

Open PersistencyManager.h and add a line to the beginning of the file:

 #import "Album.h" 

Next, add the following code after @interface :

 - (NSArray *)albums; - (void)addAlbum:(Album *)album atIndex:(NSUInteger)index; - (void)deleteAlbumAtIndex:(NSUInteger)index; 

These are prototypes of the three methods that will work with album data.

Open PersistencyManager.m and add the code just before the @implementation line:

 @interface PersistencyManager () { NSMutableArray * albums; //    } @end 

This class extension is another way to add private methods and variables to a class so that external classes are not aware of them. Here we declare an array that will contain the album data. This array is mutable, so we can easily add and remove albums.

Now add the implementation of the PersistencyManager class after the @implementation line:

 - (id)init { self = [super init]; if (self) { albums = [NSMutableArray arrayWithArray: @[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_david_bowie_best_of_bowie.png" year:@"1992"], [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_no_doubt_its_my_life_bathwater.png" year:@"2003"], [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_sting_nothing_like_the_sun.png" year:@"1999"], [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_u2_staring_at_the_sun.png" year:@"2000"], [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_madonna_american_pie.png" year:@"2000"]]]; } return self; } 

In the init method, we collect an array of five albums for example (if you have nothing to do, you can replace them with your favorite music).

Now add three methods to PersistencyManager.m :

 - (NSArray *)albums { return albums; } - (void)addAlbum:(Album *)album atIndex:(NSUInteger)index { if (albums.count >= index) [albums insertObject:album atIndex:index]; else [albums addObject:album]; } - (void)deleteAlbumAtIndex:(NSUInteger)index { [albums removeObjectAtIndex:index]; } 

These simple methods allow you to get, add and delete albums.

Build the project (⌘B), just to make sure that it is compiled.

You may wonder: what does PersistencyManager do in the singleton chapter? The relationship between LibraryAPI and PersistencyManager will be shown in the next chapter.

Facade




The “Facade” pattern provides a single interface to a complex subsystem. In order not to shock users with a variety of classes with different interfaces, we provide one simple API:



The user of this API does not care about the internal complexity of the system. This pattern is ideal when working with a large number of classes that are difficult to use.

The Facade pattern separates the code (API) that accesses the subsystem from the classes that we want to hide. It reduces the dependence of external code on the internal kitchen of the subsystem. For example, this is useful when classes hidden behind a facade are highly likely to undergo modification. The facade will provide all the same API, while everything changes behind the scenes.



If when it comes time to replace your backend, you don’t have to rewrite code using the API, since API will not change.

Using the pattern "Facade"


Now we have PersistencyManager for local data storage and HTTPClient for network interaction.Other classes in your project should not think about these two things.

To implement this pattern, only one class - LibraryAPI- must refer to instances PersistencyManagerand HTTPClient. LibraryAPI- this is the facade that provides a simple interface for accessing services (storage and transmission of data).

Comment.Normally, a singleton object exists for the duration of the program. There is no need to keep too many “strong” pointers to other objects in singletons, since they will not be released from memory until the application closes.
What is a strong pointer?
Stack Overflow :

- , «» . «» (weak pointer) — , , «» .

Example
, — . «» ( ).



— . , . 5 5 (5 1 ) — , 5 .

— , : « , !» , (« ») . , , , .

:



LibraryAPI , HTTPClient PersistencyManager .

LibraryAPI.h :

 #import "Album.h" 

:

 - (NSArray *)albums; - (void)addAlbum:(Album *)album atIndex:(int)index; - (void)deleteAlbumAtIndex:(int)index; 

, .

LibraryAPI.m :

 #import "PersistencyManager.h" #import "HTTPClient.h" 

, ! API «» .

( @implementation ):

 @interface LibraryAPI () { PersistencyManager * persistencyManager; HTTPClient * httpClient; BOOL isOnline; } @end 

isOnline : , (, ) ?

. LibraryAPI.m :

 - (id)init { self = [super init]; if (self) { persistencyManager = [[PersistencyManager alloc] init]; httpClient = [[HTTPClient alloc] init]; isOnline = NO; } return self; } 

: HTTP- . «». isOnline NO .

LibraryAPI :

 - (NSArray *)albums { return [persistencyManager albums]; } - (void)addAlbum:(Album *)album atIndex:(int)index { [persistencyManager addAlbum:album atIndex:index]; if (isOnline) { [httpClient postRequest:@"/api/addAlbum" body:[album description]]; } } - (void)deleteAlbumAtIndex:(int)index { [persistencyManager deleteAlbumAtIndex:index]; if (isOnline) { [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]]; } } 

addAlbum:atIndex: . , , , . «»: - , — — , .

: «» , «» . « » , .

. :



iOS 6 - . iOS 7 . .

- . , …

Decorator


, «» (behaviors) (responsibilities) , . ( , ).

Objective-C : .

Categories


Category is a very powerful mechanism for adding methods to existing classes without inheritance. New methods are added at compilation and can be executed as usual methods of the extended class. This is slightly different from the classic definition of "Decorator", because "Category" does not contain an instance of the class that it extends.

Note: In addition to extending your own classes, you can also add methods to any Cocoa classes!

How to use categories


: Album , :



? Album — «», , . ( Album ) , Album , Album .

Album . , UITableView .

:



Album , Objective-C category ( Objective-C class!)
:
Category : TableRepresentation
Category on : Album

? Album+TableRepresentation , Album . , ( - ).

Album+TableRepresentation.h :

 - (NSDictionary *)tr_tableRepresentation; 

: tr_ — ( TableRepresentation ). , .

. , , ( ) — . Like this.It is not known which method will be executed. This is not a problem if you operate categories on your classes. But it can be very difficult if you use categories to extend the standard Cocoa or Cocoa Touch classes .

Go to Album + TableRepresentation.m and add the following method:

 - (NSDictionary *)tr_tableRepresentation { return @{@"titles":@[@"", @"", @"", @""], @"values":@[self.artist, self.title, self.genre, self.year]}; } 

Just think how powerful the pattern is:

Apple Foundation. , , NSString.h . @interface NSString — : NSStringExtensionMethods , NSExtendedStringPropertyListParsing , NSStringDeprecated . , .


«» — . , ( ). , .

: UITableView , , tableView:numberOfRowsInSection: ( ).

, UITableView , . , . UITableView « » . : UITableView .

, UITableView :



UITableView , . , . ( ), . , , ( ).

, ? : . Objective C . ( ) , .

: . Apple UIKit: UITableView , UITextView , UITextField , UIWebView , UIAlert , UIActionSheet , UICollectionView , UIPickerView , UIGestureRecognizer , UIScrollView , … , .


ViewController.m «»:

 #import "LibraryAPI.h" #import "Album+TableRepresentation.h" 

, @interface @end :

 @interface ViewController () { UITableView * dataTable; NSArray * allAlbums; NSDictionary * currentAlbumData; int currentAlbumIndex; } @end 

— , . :

 @interface ViewController () <UITableViewDataSource, UITableViewDelegate> 

UITableViewDataSource , UITableViewDelegate — . , . «» .

, UITableView , .

viewDidLoad :

 - (void)viewDidLoad { [super viewDidLoad]; // 1 self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1.f]; currentAlbumIndex = 0; // 2 allAlbums = [[LibraryAPI sharedInstance] albums]; // 3 // UITableView,     CGRect frame = CGRectMake(0.f, 120.f, self.view.frame.size.width, self.view.frame.size.height - 120.f); dataTable = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped]; dataTable.delegate = self; dataTable.dataSource = self; dataTable.backgroundView = nil; [self.view addSubview:dataTable]; } 

:
  1. -.
  2. API. PersistencyManager !
  3. UITableView . , ViewController — (data source) UITableView , , UITableView , ViewController '.

ViewController.m :

 - (void)showDataForAlbumAtIndex:(int)albumIndex { //   : ,       if (albumIndex < allAlbums.count) { //  : Album * album = allAlbums[albumIndex]; //   ,     TableView: currentAlbumData = [album tr_tableRepresentation]; } else { currentAlbumData = nil; } //    ,   .  TableView [dataTable reloadData]; } 

showDataForAlbumAtIndex: . , reloadData . , UITableView , , , , , .

viewDidLoad :

 [self showDataForAlbumAtIndex:currentAlbumIndex]; 

This line loads the current album when the application starts. Since the index of the current album currentAlbumIndexwas previously set to 0, we see the first zero album in the collection.

Running the application on the simulator, we get ... crash:



What's happening?We declared our ViewControllerdelegate and data source UITableView. But!Having done this, we must implement all the required methods (including tableView:numberOfRowsInSection:), but we have not done it yet.

Add these two methods to ViewController.m , anywhere between @implementationand @end:

 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [currentAlbumData[@"titles"] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"]; } cell.textLabel.text = currentAlbumData[@"titles"][indexPath.row]; cell.detailTextLabel.text = currentAlbumData[@"values"][indexPath.row]; return cell; } 

tableView:numberOfRowsInSection: , TableView , .

tableView:cellForRowAtIndexPath: ( title ) ( value ).

, :



. , .

«» , , , ? — . , . , UITableView . .

«»


. — , .



«» , , Apple ( MicroUSB). . , , UITableViewDelegate , UIScrollViewDelegate , NSCoding , NSCopying . , NSCopying copy .

«»


:



, «Objective-C class» ( , ?) View, HorizontalScroller UIView .

HorizontalScroller.h @end :

 @protocol HorizontalScrollerDelegate <NSObject> //     @end 

HorizontalScrollerDelegateNSObject ( , ). — NSObject , / NSObject . , NSObject , HorizontalScroller . , .

, , @protocol @end :

 @required //  ,        - (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller *)scroller; //       <index> - (UIView *)horizontalScroller:(HorizontalScroller *)scroller viewAtIndex:(int)index; //         <index> - (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index; @optional //  ,       // ( ,   0,     ) - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller; 

. — , , . (), , .

— . , HorizontalScroller .

HorizontalScroller . , .. HorizontalScroller . What to do?

The solution is a forward declaration of the protocol HorizontalScrollerDelegate. So that the compiler knows that we have such a protocol (but will be announced later). Add above line @interface:

 @protocol HorizontalScrollerDelegate; 

In the same HorizontalScroller.h file, add a couple more lines between @interfaceand @end:

 @property (weak) id<HorizontalScrollerDelegate> delegate; - (void)reload; 

, weak («») ( ) delegate . , «retain-». , , . .

( - , «» ).

id<HorizontalScrollerDelegate> , , HorizontalScrollerDelegate ( ).

reload reloadData UITableView : , .

HorizontalScroller.m ( ):

 #import "HorizontalScroller.h" // 1 #define VIEW_PADDING 10 #define VIEW_DIMENSIONS 100 #define VIEWS_OFFSET 100 // 2 @interface HorizontalScroller () <UIScrollViewDelegate> @end // 3 @implementation HorizontalScroller { UIScrollView * scroller; } @end 

, :
  1. . 100100 10 , .
    ( — ! iOS Drawing Concepts Points Versus Pixels ) .
  2. HorizontalScroller UIScrollViewDelegate. HorizontalScroller UIScrollView , , , .
  3. , scroll view.

:

 - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; scroller.delegate = self; [self addSubview:scroller]; UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollerTapped:)]; [scroller addGestureRecognizer:tapRecognizer]; } return self; } 

( scroller ) HorizontalScroller . UITapGestureRecognizer ScrollView , . , HorizontalScroller .

:

 - (void)scrollerTapped:(UITapGestureRecognizer *)gesture { CGPoint location = [gesture locationInView:gesture.view]; //   enumerator, ..      . //      subviews,   : for (int index = 0; index < [self.delegate numberOfViewsForHorizontalScroller:self]; index++) { UIView * view = scroller.subviews[index]; if (CGRectContainsPoint(view.frame, location)) { [self.delegate horizontalScroller:self clickedViewAtIndex:index]; CGPoint offset = CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0); [scroller setContentOffset:offset animated:YES]; break; } } } 

«» ( gesture ), , ( locationInView: ).

numberOfViewsForHorizontalScroller: . HorizontalScroller , , ( HorizontalScrollerDelegate ).

scroll view — , , CGRectContainsPoint . , horizontalScroller:clickedViewAtIndex: .

:

 - (void)reload { // 1 -  ,   : if (self.delegate == nil) return; // 2 -   subviews: [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) { [obj removeFromSuperview]; }]; // 3 - xValue -      : CGFloat xValue = VIEWS_OFFSET; for (int i = 0; i < [self.delegate numberOfViewsForHorizontalScroller:self]; i++) { // 4 -     : xValue += VIEW_PADDING; UIView * view = [self.delegate horizontalScroller:self viewAtIndex:i]; view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSIONS, VIEW_DIMENSIONS); [scroller addSubview:view]; xValue += VIEW_DIMENSIONS + VIEW_PADDING; } // 5 [scroller setContentSize:CGSizeMake(xValue + VIEWS_OFFSET, self.frame.size.height)]; // 6 -   initialView,    : if ([self.delegate respondsToSelector:@selector(initialViewIndexForHorizontalScroller:)]) { int initialView = [self.delegate initialViewIndexForHorizontalScroller:self]; CGPoint offset = CGPointMake(initialView * (VIEW_DIMENSIONS + (2 * VIEW_PADDING)), 0); [scroller setContentOffset:offset animated:YES]; } } 

:
  1. , . .
  2. (subviews), .
    (, «subview» ? «» . :) — . .)
  3. ( VIEWS_OFFSET ). 100 , #define .
  4. HorizontalScroller ( UIView ) , .
  5. , , .
  6. HorizontalScrollerLooks at whether the delegate responds to the message selector initialViewIndexForHorizontalScroller:. This check is necessary because this protocol method is optional. If the delegate does not implement this method, the default value is taken 0. This part of the code sets the scrolling view to the center of the view defined by the delegate ( initialView).

We perform reloadin the event that our data has changed. Also this method needs to be called when we add HorizontalScrollerto a new view. To do this, add this method to the class HorizontalScroller:

 - (void)didMoveToSuperview { [self reload]; } 

didMoveToSuperview , , : , - . .

HorizontalScroller — , scroll view. , .

( , , HorizontalScroller.m ):

 - (void)centerCurrentView { int xFinal = scroller.contentOffset.x + (VIEWS_OFFSET / 2) + VIEW_PADDING; int viewIndex = xFinal / (VIEW_DIMENSIONS + (2 * VIEW_PADDING)); xFinal = viewIndex * (VIEW_DIMENSIONS + (2 * VIEW_PADDING)); [scroller setContentOffset:CGPointMake(xFinal, 0) animated:YES]; [self.delegate horizontalScroller:self clickedViewAtIndex:viewIndex]; } 

(content offset), (dimensions) (padding). : , , .

, , UIScrollViewDelegate :

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) { [self centerCurrentView]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self centerCurrentView]; } 

scrollViewDidEndDragging:willDecelerate: , . decelerate («») true , , « ». , scrollViewDidEndDecelerating . ( centerCurrentView ), , , .

HorizontalScroller ! . Album AlbumView . Excellent!This means our new scroller has turned out really independent of the content, and it can be reused.

Build the project to make sure everything compiles.

Now that the horizontal scroll is ready, it's time to use it in our application! Open ViewController.m and include 2 header files:

 #import "HorizontalScroller.h" #import "AlbumView.h" 

Below add HorizontalScrollerDelegateto the list of protocols that implements ViewController:

 @interface ViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate> 

Add a scroller instance variable to the class extension (between curly braces):

 HorizontalScroller * scroller; 

Now you can implement delegate methods. You will be surprised how little code is required for a variety of functions!

Add code to ViewController.m :

 #pragma mark - HorizontalScrollerDelegate methods - (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index { currentAlbumIndex = index; [self showDataForAlbumAtIndex:index]; } 

, , showDataForAlbumAtIndex: .

Note. . #pragma mark . , IDE — . , AppCode ⌘F12 :



, , .

:

 - (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller *)scroller { return allAlbums.count; } 

Remember? , scroll view. , , .

:

 - (UIView *)horizontalScroller:(HorizontalScroller *)scroller viewAtIndex:(int)index { Album * album = allAlbums[index]; return [[AlbumView alloc] initWithFrame:CGRectMake(0.f, 0.f, 100.f, 100.f) albumCover:album.coverUrl]; } 

Here we create a new one AlbumViewand transfer it to HorizontalScroller.

Just! Three short methods to display a beautiful horizontal scroller!

Yes, and we still need to create (actually) a scroller and add it to the main view. But first, add the following method:

 - (void)reloadScroller { allAlbums = [[LibraryAPI sharedInstance] albums]; if (currentAlbumIndex < 0) currentAlbumIndex = 0; else if (currentAlbumIndex >= allAlbums.count) currentAlbumIndex = allAlbums.count - 1; [scroller reload]; [self showDataForAlbumAtIndex:currentAlbumIndex]; } 

This method loads album data through LibraryAPI, and then sets the current view. Just in case - check for going beyond the array.

Now we initialize the scroller by adding the following code in the viewDidLoadfront of the line[self showDataForAlbumAtIndex:currentAlbumIndex];

 scroller = [[HorizontalScroller alloc] initWithFrame:CGRectMake(0.f, 20.f, self.view.frame.size.width, 120.f)]; scroller.backgroundColor = [UIColor colorWithRed:0.24f green:0.35f blue:0.49f alpha:1]; scroller.delegate = self; [self.view addSubview:scroller]; [self reloadScroller]; 

This code creates a new instance HorizontalScroller, sets the background color, designates itself as a delegate. Then it adds a scroller to the main screen and updates the data in it.

Note.If the protocol grows heavily, consider breaking it into several smaller ones. For example, UITableViewDelegateand UITableViewDataSource. Both of them are protocols UITableView, i.e. technically could exist in the same protocol, but broken up for convenience. Try to develop protocols so that everyone is responsible for their own functional area.

Build and run the project. Wonderful horizontal scroll:



So, stop.There is a scroll. Where are the covers?

Oh, right! We have not yet written the code for downloading covers. We need to figure out how to upload images. Since all of our access to services goes through LibraryAPI, the new method will have to go there. But first you need to make out a few points:

1. AlbumViewshould not work directly with LibraryAPI. We don't want to mix UI code with network interaction, right?
2. For the same reason LibraryAPIshould not know about AlbumView.
3. LibraryAPImust report AlbumViewas soon as the covers are loaded so that AlbumViewthey are displayed.

Looks like a puzzle? Do not despair! We are in a hurry to help the next pattern -

Observer


«» . , , — - ( , ) . , « » .

«» . , - . push- Apple — .

MVC (: ) — , — ! «».

Cocoa : (Notifications) Key-Value Observing (KVO).

Notifications


( Push . .) «—». , «» (publisher) (subscribers / listeners). .

Apple. , iOS , : UIKeyboardWillShowNotification UIKeyboardWillHideNotification , . , UIApplicationDidEnterBackgroundNotification .

Note. UIApplication.h , , .
?
1 : :

 #import <UIKit/UIApplication.h> 

UIApplication.h .

2 : Frameworks ( ), UIKit.framework ( , ) UIApplication.h :


How to use notifications


Go to AlbumView.m and paste the following code in initWithFrame:albumCover:after[self addSubview:indicator];

 [[NSNotificationCenter defaultCenter] postNotificationName:@"BLDownloadImageNotification" object:self userInfo:@{@"coverUrl":albumCover, @"imageView":coverImage}]; 

This line sends the notification via singleton NSNotificationCenter. The notification contains the URL of the image you want to upload, and UIImageViewwhere you want to put this image. That's all that our “subscriber” needs to know, who will receive this notification in order to complete the download task.

Add the following line in LibraryAPI.m to the method initimmediately after isOnline = NO:

 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadImage:) name:@"BLDownloadImageNotification" object:nil]; 

— (). , AlbumView "BLDownloadImageNotification" , .. LibraryAPI , LibraryAPI . , LibraryAPI downloadImage: .

«BL» = BlueLibrary .

, : , . , , (deallocated), .

Objective C
, , .

Add the following method to LibraryAPI.m :

 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } 

When a class object is unloaded from memory, it unsubscribes from all notifications to which it was subscribed.

And one more thing. It would be nice to save the downloaded covers locally so that you don’t have to download them again and again.

Open PersistencyManager.h and add two prototype methods:

 - (void)saveImage:(UIImage *)image filename:(NSString *)filename; - (UIImage *)getImage:(NSString *)filename; 

And here is their implementation in PersistencyManager.m :

 - (void)saveImage:(UIImage *)image filename:(NSString *)filename { filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename]; NSData * data = UIImagePNGRepresentation(image); [data writeToFile:filename atomically:YES]; } - (UIImage *)getImage:(NSString *)filename { filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename]; NSData * data = [NSData dataWithContentsOfFile:filename]; return [UIImage imageWithData:data]; } 

, . Documents/ . getImage: nil , . ( , , UIImage , . — . . )

LibraryAPI.m :

 - (void)downloadImage:(NSNotification *)notification { // 1 NSString * coverUrl = notification.userInfo[@"coverUrl"]; UIImageView * imageView = notification.userInfo[@"imageView"]; // 2 imageView.image = [persistencyManager getImage:[coverUrl lastPathComponent]]; if (imageView.image == nil) { // 3 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ UIImage * image = [httpClient downloadImage:coverUrl]; // 4 dispatch_sync(dispatch_get_main_queue(), ^{ imageView.image = image; [persistencyManager saveImage:image filename:[coverUrl lastPathComponent]]; }); }); } } 

:

  1. downloadImage NSNotification . URL UIImageView .
  2. PersistencyManager , .
  3. , HTTPClient .
  4. , UIImageView .

«», . , : .

, HorizontalScroller :



iOS 9 : , ?
. HTTPS, . , .

. , , .. . .

: «» ! ?

: , , , . , . : KVO.

Key-Value Observing (KVO)


: - . : «-».

KVO , — . KVO Programming Guide Apple.

KVO


KVO . KVO, image UIImageView .

AlbumView.m initWithFrame:albumCover: [self addSubview:indicator];

 [coverImage addObserver:self forKeyPath:@"image" options:0 context:nil]; 

self ( AlbumView ) image coverImage .
« » , , AlbumView.m @end :

 - (void)dealloc { [coverImage removeObserver:self forKeyPath:@"image"]; } 

, :

 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"image"]) { [indicator stopAnimating]; } } 

, . , «» . , image . , , .

( , change , . , . nil , => . — . . )

Run the application. :



Note. dealloc . , !

, , : ( ) . , ? :)

«», :

(Memento)


«» … -. , .. (private) .

Memento


ViewController.m :

 - (void)saveCurrentState { //          , //      ,    .   //     .   , //    NSUserDefaults: [[NSUserDefaults standardUserDefaults] setInteger:currentAlbumIndex forKey:@"currentAlbumIndex"]; } - (void)loadPreviousState { currentAlbumIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"currentAlbumIndex"]; [self showDataForAlbumAtIndex:currentAlbumIndex]; } 

saveCurrentState . NSUserDefaults — , iOS.

loadPreviousState , . «», .

ViewController.m viewDidLoad :

 [self loadPreviousState]; 

. ? . iOS UIApplicationDidEnterBackgroundNotification , . , saveCurrentState . Conveniently? Yes.

viewDidLoad :

 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveCurrentState) name:UIApplicationDidEnterBackgroundNotification object:nil]; 

, ViewController , saveCurrentState .

: iOS 6, . , . Apple ? ? , iOS 7. , -, , . , iOS 7.0 …DidEnterBackground…, , . , .

:

 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } 

ViewController , .

, . , Home ( — ⌘⇧H ) . , , :



TableView , . Why?

initialViewIndexForHorizontalScroller: — . ( ViewController ). .

, ViewController.m :

 - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller { return currentAlbumIndex; } 

( , ) currentAlbumIndex . .

. Problem solved!



PersistencyManager init , , Album . , PersistencyManager . . ?

— , plist, Album . , :
  1. . Movie , .
  2. , .

Apple [citation needed] .


«» Apple — . , , .. . Ray Wenderlich Apple .


, Album . , NSCoding . Album.h @interface :

 @interface Album : NSObject <NSCoding> 

Album.m :

 - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.year forKey:@"year"]; [aCoder encodeObject:self.title forKey:@"album"]; [aCoder encodeObject:self.artist forKey:@"artist"]; [aCoder encodeObject:self.coverUrl forKey:@"cover_url"]; [aCoder encodeObject:self.genre forKey:@"genre"]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { _year = [aDecoder decodeObjectForKey:@"year"]; _title = [aDecoder decodeObjectForKey:@"album"]; _artist = [aDecoder decodeObjectForKey:@"artist"]; _coverUrl = [aDecoder decodeObjectForKey:@"cover_url"]; _genre = [aDecoder decodeObjectForKey:@"genre"]; } return self; } 

, encodeWithCoder: . initWithCoder: . , .

, Album , , / .

PersistencyManager.h :

 - (void)saveAlbums; 

PersistencyManager.m :

 - (void)saveAlbums { NSString * filename = [NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]; NSData * data = [NSKeyedArchiver archivedDataWithRootObject:albums]; [data writeToFile:filename atomically:YES]; } 

NSKeyedArchiver " albums.bin ".

, , . albums ( Album ). NSArray Album NSCoding , .

PersistencyManager.m init :

 - (id)init { self = [super init]; if (self) { NSData * data = [NSData dataWithContentsOfFile:[NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]]; albums = [NSKeyedUnarchiver unarchiveObjectWithData:data]; if (albums == nil) { albums = [NSMutableArray arrayWithArray: @[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_david_bowie_best_of_bowie.png" year:@"1992"], [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_no_doubt_its_my_life_bathwater.png" year:@"2003"], [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_sting_nothing_like_the_sun.png" year:@"1999"], [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_u2_staring_at_the_sun.png" year:@"2000"], [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"https://s3.amazonaws.com/CoverProject/album/album_madonna_american_pie.png" year:@"2000"]]]; [self saveAlbums]; } } return self; } 

NSKeyedUnarchiver , . , . , .

, . , , ? , .

LibraryAPI.h :

 - (void)saveAlbums; 

LibraryAPI , , . PersistencyManager ', .

LibraryAPI.m :

 - (void)saveAlbums { [persistencyManager saveAlbums]; } 

, ViewController.m saveCurrentState :

 [[LibraryAPI sharedInstance] saveAlbums]; 

, ViewController .

(Build), , .

, . Documents ( iExplorer). , . , .

, : . , .

: .

Team


— «», Command, Team.

«» . , « » :

 Forrest->Run(speed, distance); 

:

: , , , … , . Apple «-» (Target-Action) (Invocation).

Target-Action Apple . NSInvocation , ( ), . , , . «», ( => ). , .

«»


, , . UIToolbar NSMutableArray (undo stack).

ViewController.m ViewController , :

 UIToolbar * toolbar; NSMutableArray * undoStack; //   ,       push  pop 

, — . , .

viewDidLoad: " // 2 ":

 toolbar = [[UIToolbar alloc] init]; UIBarButtonItem * undoItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:self action:@selector(undoAction)]; undoItem.enabled = NO; UIBarButtonItem * space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIBarButtonItem * delete = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deleteAlbum)]; [toolbar setItems:@[undoItem, space, delete]]; [self.view addSubview:toolbar]; undoStack = [[NSMutableArray alloc] init]; 

( = ). , . Undo , .. .

, frame, .. . , viewDidLoad . , ViewController «». ViewController.m :

 - (void)viewWillLayoutSubviews { toolbar.frame = CGRectMake(0, self.view.frame.size.height - 44, self.view.frame.size.width, 44); dataTable.frame = CGRectMake(0, 130, self.view.frame.size.width, self.view.frame.size.height - 200); } 

ViewController.m : , .

— :

 - (void)addAlbum:(Album *)album atIndex:(int)index { [[LibraryAPI sharedInstance] addAlbum:album atIndex:index]; currentAlbumIndex = index; [self reloadScroller]; } 

, «» ( ) .

, :

 - (void)deleteAlbum { // 1 Album * deletedAlbum = allAlbums[currentAlbumIndex]; // 2 NSMethodSignature * sig = [self methodSignatureForSelector:@selector(addAlbum:atIndex:)]; NSInvocation * undoDeleteAction = [NSInvocation invocationWithMethodSignature:sig]; [undoDeleteAction setTarget:self]; [undoDeleteAction setSelector:@selector(addAlbum:atIndex:)]; [undoDeleteAction setArgument:&deletedAlbum atIndex:2]; [undoDeleteAction setArgument:&currentAlbumIndex atIndex:3]; [undoDeleteAction retainArguments]; // 3 [undoStack addObject:undoDeleteAction]; // 4 [[LibraryAPI sharedInstance] deleteAlbumAtIndex:currentAlbumIndex]; [self reloadScroller]; // 5 [toolbar.items[0] setEnabled:YES]; } 

, :

  1. , .
  2. ( NSMethodSignature ). NSInvocation , — undoDeleteAction .
    NSInvocation :
    • — , ;
    • — ;
    • .
    , «». — , .
  3. When we have created undoDeleteAction, add it to the imaginary "stack" ...
    Never do that! The author calls the array a stack. Like, "a person reading a code must imagine that an array is a stack" ... Do you want a stack? There must be two operations: pushand pop. Everything. - Approx. per.
  4. Remove the album from the data structure using LibraryAPIand update the scroller.
  5. We have an action that can be undone, so we need to allow the Undo button to be pressed .

Note.Using the scheme NSInvocation, you need to keep in mind the following:

, «»:

 - (void)undoAction { if (undoStack.count > 0) { NSInvocation * undoAction = [undoStack lastObject]; [undoStack removeLastObject]; [undoAction invoke]; } if (undoStack.count == 0) { [toolbar.items[0] setEnabled:NO]; } } 

: lastObject + removeLastObject . , ViewController . — .

, NSInvocation invoke . , , ( ) — .

, , . , . , . Undo ( ).

, , ​​ . - Undo, :

, . , , . , .

What's next


Objective C , : ( Abstract Factory ) ( Chain of Responsibility ). .

, : . , , MVC, , , , , .

, , . , , , .

, . , , , , , . , — !



( )


: « ». , :
  1. ;
  2. , .

, — , . «», «» . — ? ? ?

, . , , «, NSMutableArray — » . , (« , , »).

, , , , , , . « ? ?» — .

And further. IDE. , showDataForAlbumAtIndex: if (albumIndex < allAlbums.count) , (albumIndex >= 0) ? , NSArray , AppCode :



int NSUInteger , — — «by design».

PS


dev @ x128 . ru , - .

Cocoa Design Patterns Apple, :



Streamline Your App with Design Patterns , «Gang of Four». «Start Developing Mac Apps Today» - . .

. Objective C ****Script, ( -). , .

What to read?


Source: https://habr.com/ru/post/202960/


All Articles