相信做App开发的同学,对于一些第三方的统计分析、错误收集等SDK应该都不陌生。就目前而言市面上也有许多相同功能的产品,眼花缭乱,让人无法抉择选哪一款SDK才是最靠谱的。那就随便先选一款试试用吧!

那么问题来了:如果项目都快做完了结果发现这款SDK实在坑爹,不仅扩展性差,还经常让App Crash,那你是不是会想到替换掉这个SDK?

OK,那我们就换另一个试试,下载SDK下来,一看,傻眼了,设计风格,封装模块完全不一样,于是乎我们就到项目中全局搜索找到之前的SDK代码干掉,然后重新再到各种地方用新的SDK来写新的逻辑来替换,关键的是,中间还不知道会产生多少bug,漏掉多少未修改的代码,总之始终会有一种不靠谱的感觉。

换一次还算好的,如果之后团队壮大了,这些数据分析之类的东西突然想自己做了,毕竟这些有价值的数据并不想这么拱手让给一个第三方的公司嘛~这个时候你是不是就只想说:『呵呵』

所以这个时候适配器模式就起到作用了~

何为适配器模式
GoF对于适配器模式的解释如下:

将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。
个人通俗理解:

适配器:顾名思义,将不兼容的转换为兼容,如电源适配器,将全世界各种不相同的电压转换成相同的电压输出给目标设备。

这里可以将目标设备理解为『接口』,世界各种电压可以理解为『产生相同功能的类』,电源适配器可以理解为『需要实现的适配器类』。

适配器模式产生的效果是:在不修改代码或者修改极少代码的情况下,快速的切换源(数据源、内容源等)。

就像电源适配器一样,去到不同国家,同一个设备只需要不同的电源适配器就可以使用当前国家的电源,而不需要取拆卸机器。

使用真实场景
如文章开头所讲,被某盟的SDK坑了之后(确实在某些状况下让App Crash,产生原因初步判断是滥用performSelector,不考虑对象被释放的情况而产生的Crash),产生替换念想而思考,如果将来替换岂不是又要苦逼我们自己?

于是乎为了将来的轻松就必须动动脑子去设计代码了,于是有了今天的适配器模式实战。

如何使用适配器模式
一个适配器允许接口不兼容的类在一起工作。它把它自己包裹成一个对象,公开一个与这个对象相互作用的标准接口。

如果你熟习适配器模式,你会注意到苹果实施它的时候有一点不同的习惯─苹果使用协议 (protocols)。你可能熟习像 UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying 这样的协议。例子,NSCopying 的协议 (protocol),任何类都可以提供这样一个标准的复制方法。

我们提到的滚动区域是这样的:

2016323171127008.png (320×120)

现在开始,在项目导航的 View 文件夹上右击鼠标,选择 New File…,用 iOS\Cocoa Touch\Object-C class 模板创建一个新类。新类的名字叫 HorizontalScroller,选择它的子类为 UIView。

打开 HorizontalScroller.h 文件在 @end 后面插入如下代码:

@protocol HorizontalScrollerDelegate 

// methods declaration goes in here

@end

这里定义一个 HorizontalScrollerDelegate 名字的协议,它继承于 NSObject 协议,同样的这是继承它父类的一个 Objective-C 类。符合 NSObject 协议,这是一个很好的做法─或者遵照 NSObject 协议。这能使你从定义的 NSObject 发送消息到 HorizontalScroller 的代理。你将会看到为什么这很重要。

定义个代理执行的方法,要在 @protocol 和 @end 之间,它们分为必要方法和可选方法。添加下面协议方法:

@required

// 询问 delegate 在滚动区域里有多少个视图要被显示

- (NSInteger)numberOfViewsForHorizontalScroller:    (HorizontalScroller*)scroller;
// 返回索引是 index 的视图

- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;
// 当索引是 index 的视图被点击了,通知 delegate 

- (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;
@optional

// 通知 delegate,显示初始化时索引是 Index 的视图。这个方法是可选的

// ask the delegate for the index of the initial view to display. this method is optional

// 如果没有被 delegate 执行,默认值是 0

- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;

这里我们必选的和可选的方法我们都定义了。必选方法一定要被代理执行,它通常包含一些类必须要执行的数据。这里,必选方法是获取视图的数量,当前显示视图的索引和当视图被点击的时候执行的操作。可选方法这里是初始化视图;如果没有执行 HorizontalScroller 将会显示第一个索引的视图。

接下来,你需要在 HorizontalScroller 内部定义你的新代理。但是协议的定义在类的定义下面,因此在这点上它是不可见的。你该怎么办?

解决办法就是在前面声明协议以便于编译器(和Xcode)知道这个协议是可用的。好了,在 @interface 上面加入下面代码:
[/ode]
@protocol HorizontalScrollerDelegate;
[/code]
还是 HorizontalScroller.h,在 @interface 和 @end 之间加入下面代码:

@property (weak) id delegate;

- (void)reload;

这个属性被定义成为一个 weak。这是为了防止循环 retain。如果一个类保持一个强指针(strong pointer)指向它的委托(delegate),同时委托也保持一个强指针指向这个类,在释放类所占用的内存时会造成 app 内存泄漏。

id 的意思是把这个代理指定给一个类,它遵照 HorizontalScrollerDelegate,给你一些类型安全。

reload 方法是模仿 UITableView 类的 relaodData;它重新加载所有数据用来创建一个水平移动视图。

用下面代码替换 HorizontalScroller.m 的内容:

#import “HorizontalScroller.m”
#define VIEW_PADDING 10

#define VIEW_DIMENSIONS 100

#define VIEW_OFFSET 100
@interface HorizontalScroller () 

@end

@implementation HorizontalScroller

{

    UIScrollView *scroller;

}

@end

来解释下每块代码:

常量定义,在设计时间可以方便修改布局。在滚动视图内,每个图片的大小在一个 100×100 内边距为 10 点(point) 的矩形内。
HorizontalScroller 遵照 UIScrollViewDelegate 协议。因为 HorizontalScroller 使用一个 UIScrollView 来滚动专辑封面,它需要知道用户什么时候停止滚动。
创建一个包含图片的滚动视图。
接下来你需要执行初始化。添加下面的方法:

- (id)initWithFrame:(CGRect)frame

{

    self = [super initWithFrame:frame];

    if (self) {

        scroller = [[UIScrollerView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];

        scroller.delegate = self;

        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarger:self action:@select(scrollerTapped:)];

        [scroller addGestureRecognizer:tapRecognizer];

    }

    return self;

}

HorizontalScroller 将被滚动视图整个填充。如果一个专辑封面被点击,UITapGestureRecognizer 将会监听它上面的事件。如果有,它会通知 HorizontalScroller 的代理。

现在添加下面方法:

- (void)scrollerTapped:(UITapGestureRecognizer*)gesture

{

    CGPoint location = [gesture locationInView:gesture.view];

    // we can't use an enumerator here, because we don't want to enumerate over ALL of the UIScrollView subviews.

    // we want to enumerate only the subview that we added

    for (int index=0; index

手势操作就如同传入的一个参数,可以从 locationInView: 获取定位信息。

接下来,调用委托的 numberOfViewForHorizontalScroller: 方法。它必须遵照 HorizontalScrollerDelegate 的协议安全发送消息,否则 HorizontalScroller 实例的代理是没法使用这些信息。

滚动视图里的每个视图,用 CGRectContainsPoint 执行一个点击测试,找到那个被点击的视图。当视图被找到,发送给委托一个消息 horizontalScroller:clickedViewAtIndex:。当你跳出这个循环后,设置被点击的视图滚动到视图中间。

现在添加下面的代码,用来刷新滚动视图(scroller):

- (void)reload

{

    // 1 - nothing to load if there's no delegate

    if (self.delegate == nil) return;
    // 2 - remover all subviews

    [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

        [obj removeFromSuperview];

    }
    // 3 - xValue is the starting point of the views inside the scroller

    CGFloat xValue = VIEWS_OFFSET;

    for (int i=0; i

能过代码一步步来讨论:

如果没有代理,这里什么事情也不做。
移除之前添加的所有的子视图。
给所有视图设置一个偏移(offset)位置。现在的是 100,但是通过顶部的 #define,它很容易修改。
HorizontalScroller 通过它的委托一次请求一个视图,用之前定义的 padding 值把它们依次的一个个放置下来。
当所有的视图都生成好,通过设置滚动视图内容的偏移量以达到用户能过滚动可以看到所有专辑封面的目的。
HorizontalScroller 的委托需要验证是否响应了 initialViewIndexForHorizontalScroller: 方法。这个验证是必需的,因为这个特别的协议方法是可选性的。如果代理没有执行这个方法,它的默认值会是 0。最终,通过委托,这块代码会在滚动视图中间设置一个初始化好的视图。
当数据发生改变的时候执行 reload 方法。当添加 HorizontalScroller 到别个一个视图时,你同样可以执行这个方法。在 HorizontalScroller.m 添加下面的代码替换后面的方案:

- (void)didMoveToSuperview

{

    [self reload];

}

当它要添加一个子视图的时候,didMoveToSuperview 会发送消息给视图。这时正好可以更新滚动视图的内容。

HorizontalScroller 的最后一个难题就是,如何设置你看到的专辑总是在滚动视图的中间。为了这些,当用户通过他们的手指拖动滚动视图的时候你就需要做一些计算了。

添加下面方法(同样在 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];

}

上面的代码通过滚动视图的当前偏移量,外观尺寸,内边距来计算当前视图离中心的距离。最后一行非常重要:当一个视图居中后,你需要通知委托你选择的视图改变了。

为了侦测用户在滚动视图内完成拖拽的动作,你需要添加 UIScrollViewDelegate 方法:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

{

    if (!decelerate)

    {

        [self centerCurrentView];

    }

}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

{

    [self centerCurrentView];

}

当用户完成拖拽的时候 scrollViewDidEndDragging:willDecelerate: 通知委托。如果滚动视图没有停止滚动, decelerate 参数会返回 true。当滚动结束,系统将会调用 scrollViewDidEndDecelerating。当用户拖动滚动当前视图后,两种情况,我们都需要调用一个新方法来使当前视图居中。

HorizontalScroller 现在可以使用了。浏览你刚刚写的代码;这里没有一处提到 Album 和 AlbumView 类。这非常棒,说明这个新的滚动视图是真正的完全独立的和可重用的。

Build 项目,确保所有的代码编译正确。

现在 HorizontalScroller 完成了,是时候在你的 APP 中使用了。打开 ViewController.m 添加如下引用:

#import “HorizontalScroller.h”

#import “AlbumView.h”

给 ViewController 添加 HorizontalScrollerDelegate:
@interface ViewController () 

在类的扩展里为水平滚动视图添加如下实例变量:
HorizontalScroller *scroller;

现在你可以执行代理方法了;你会惊奇的发现只需要几行代码你就能实现很多功能。

在 ViewController.m 添加如下代码:

#pragma mark - HorizontalScrollerDelegate methods

- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index

{

    currentAlbumIndex = index;

    [self showDataForAlbumAtIndex:index];

}

这里设置一个变量用来存储当前的专辑,然后调用 showDataForAlbumAtIndex: 显示一个新专辑的数据。

提示:一般在方法代码的前面放置 #pragma mark 指示符。编译器会忽略这一行,当你在使用 Xcode 的跳转工具栏(Xcode's jump bar)查看你的方法列表时,你会看到一个分隔符和个加粗的指示标题。在 Xcode 里,这可以帮助你很容易的组织代码。

下面,添加如下代码:

- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller *)scroller

{

    return allAlbums.count;

}

这里,协议方法返回滚动视图里的视图数量。因为滚动视图需要显示所有的专辑封面,这个 count 是所有专辑的数目。

现在,添加这些代码:

- (UIView *)horizontalScroller:(HorizontalScroller *)scroller viewAtIndex:(ini)index

{

    Album *album = allAlbums[index];

    return [[Album alloc] initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl];

}

这里你创建了一个新 AlbumView,然后交给 HorizontalScroller 使用。

就是这样,通过三个这么短的方法就可以显示一个漂亮的滚动视图。

实际上,你仍需要创建一个真正的滚动视图,然后添加到你的主视图上,但是在这之前,先添加下面的方法:

- (void)reloadScroller

{

    allAlbums = [[LibraryAPI sharedInstance] getAlbums];

    if (currentAlbumIndex =allAlbum.count) currentAlbumIndex = allAlbum.count - 1;

    [scroller reload];
    [self showDataFroAlbumAtIndex:currentAlbumIndex;

}

这个方法从 LibraryAPI 加载专辑数据,然后以当前视图的索引值为基础设置显示当前的图片。 如果当前视图的索引小于零,意味着当前没有选择视图,显示列表里的第一张专辑。否则显示最后一张专辑。

现在,在 viewDidLoad 里 [self showDataForAlbumIndex:0] 前面添加下面代码来初始化滚动视图:

scroller  = [[HorizontalScroller alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)];

scroller.backgroundColor = [UIColor colorWithRed:0.24f greed:0.35f blue:0.49f alpha:1];

scroller.delegate = self;

[self.view addSubview:scroller];
[self reloadScroller];

上面的代码创建了一个 HorizontalScroller 的实例,设置了它的背景颜色和委托,添加滚动视图到主视图上,在滚动视图的子视图上加载专辑数据。

提示:如果一个协议变得很大,里面有很多方法,你应该考虑把它们分散到几个小的协议里去。UITableViewDelegate 和 UITableViewDataSource 就是一个很好的例子,因为它们都是 UITablveView 的协议。设计协议的时候,最好一个名称引导一个功能。
构建和运行你的项目,你会看到一个新的很了不起的水平滚动视图:

2016323171231141.png (298×320)

啊嗯,等等。水平滚动的视图已经有了,可是专辑封面在哪里?

对了,你还没有代码来执行下载图片的功能。你需要添加一个下载图片的方法。查检 LibraryAPI 服务的所有接口,这里需要添加一个新的方法。不管怎样,现在还有几件事情需要考虑:

AlbumView 并没没有通过 LibraryAPI 立即工作。你没有给视图添加通信逻辑。
相同的原因,LibraryAPI 并不认识 AlbumView。
LibraryAPI 需要通知 AlbumView,一旦封面下载完成,AlbumView 就会显示它。

iOS App设计模式开发之适配器模式使用的实战演练的更多相关文章

  1. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  2. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  4. ios – Testflight无法安装应用程序

    我有几个测试人员注册了testflight并连接了他们的设备……他们有不同的ios型号……但是所有这些都有同样的问题.当他们从“safari”或“testflight”应用程序本身单击应用程序的安装按钮时……达到约90%并出现错误消息…

  5. ibm-mobilefirst – 在iOS 7.1上获取“无法安装应用程序,因为证书无效”错误

    当我的客户端将他们的设备更新到iOS7.1,然后尝试从AppCenter更新我们的应用程序时,我收到了上述错误.经过一番搜索,我找到了一个类似问题的帖子here.但是后来因为我在客户端使用AppCenter更新应用程序的环境中,我无法使用USB插件并为他们安装应用程序.在发布支持之前,是否有通过AppCenter进行下载的解决方法?

  6. ios – 视图的简单拖放?

    我正在学习iOS,但我找不到如何向UIView添加拖放行为.我试过了:它说“UIView没有可见的接口声明选择器addTarget”此外,我尝试添加平移手势识别器,但不确定这是否是我需要的它被称为,但不知道如何获得事件的坐标.在iOS中注册移动事件回调/拖放操作的标准简单方法是什么?

  7. ios – 什么控制iTunes中iPhone应用程序支持的语言列表?

    什么控制iPhone应用程序的iTunes页面中支持的语言?

  8. ios – 获得APNs响应BadDeviceToken或Unregistered的可能原因是什么?

    我知道设备令牌在某些时候是有效的.用户如何使其设备令牌变坏?从关于“未注册”的文档:Thedevicetokenisinactiveforthespecifiedtopic.这是否意味着应用程序已被删除?.您应该看到四种分发方法:如果您选择AppStore或Enterprise,您将在后面的对话框中看到Xcode将APNS权利更改为生产:如果选择AdHoc或Development,则aps-environment下的文本将是开发,然后应与后端的配置匹配.

  9. ios – 当我关闭应用程序时,我从调试器获得消息:由于信号15而终止

    我怎么能解决这个问题,我不知道这个链接MypreviousproblemaboutCoredata对我的问题有影响吗?当我cmd应用程序的Q时,将出现此消息.Messagefromdebugger:Terminatedduetosignal15如果谁知道我以前的问题的解决方案,请告诉我.解决方法>来自调试器的消息:每当用户通过CMD-Q(退出)或STOP手动终止应用程序(无论是在iOS模拟器中还是

  10. ios – NSUbiquityIdentityDidChangeNotification和SIGKILL

    当应用程序被发送到后台时,我们会删除观察者吗?我遇到的问题是,当UbiquityToken发生变化时,应用程序终止,因为用户已经更改了iCloud设置.你们如何设法订阅这个通知,如果你不这样做,你会做什么来跟踪当前登录的iCloud用户?

随机推荐

  1. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  2. iOS – genstrings:无法连接到输出目录en.lproj

    使用我桌面上的项目文件夹,我启动终端输入:cd然后将我的项目文件夹拖到终端,它给了我路径.然后我将这行代码粘贴到终端中找.-name*.m|xargsgenstrings-oen.lproj我在终端中收到此错误消息:genstrings:无法连接到输出目录en.lproj它多次打印这行,然后说我的项目是一个目录的路径?没有.strings文件.对我做错了什么的想法?

  3. iOS 7 UIButtonBarItem图像没有色调

    如何确保按钮图标采用全局色调?解决方法只是想将其转换为根注释,以便为“回答”复选标记提供更好的上下文,并提供更好的格式.我能想出这个!

  4. ios – 在自定义相机层的AVFoundation中自动对焦和自动曝光

    为AVFoundation定制图层相机创建精确的自动对焦和曝光的最佳方法是什么?

  5. ios – Xcode找不到Alamofire,错误:没有这样的模块’Alamofire’

    我正在尝试按照github(https://github.com/Alamofire/Alamofire#cocoapods)指令将Alamofire包含在我的Swift项目中.我创建了一个新项目,导航到项目目录并运行此命令sudogeminstallcocoapods.然后我面临以下错误:搜索后我设法通过运行此命令安装cocoapodssudogeminstall-n/usr/local/bin

  6. ios – 在没有iPhone6s或更新的情况下测试ARKit

    我在决定下载Xcode9之前.我想玩新的框架–ARKit.我知道要用ARKit运行app我需要一个带有A9芯片或更新版本的设备.不幸的是我有一个较旧的.我的问题是已经下载了新Xcode的人.在我的情况下有可能运行ARKit应用程序吗?那个或其他任何模拟器?任何想法或我将不得不购买新设备?解决方法任何iOS11设备都可以使用ARKit,但是具有高质量AR体验的全球跟踪功能需要使用A9或更高版本处理器的设备.使用iOS11测试版更新您的设备是必要的.

  7. 将iOS应用移植到Android

    我们制作了一个具有2000个目标c类的退出大型iOS应用程序.我想知道有一个最佳实践指南将其移植到Android?此外,由于我们的应用程序大量使用UINavigation和UIView控制器,我想知道在Android上有类似的模型和实现.谢谢到目前为止,guenter解决方法老实说,我认为你正在计划的只是制作难以维护的糟糕代码.我意识到这听起来像很多工作,但从长远来看它会更容易,我只是将应用程序的概念“移植”到android并从头开始编写.

  8. ios – 在Swift中覆盖Objective C类方法

    我是Swift的初学者,我正在尝试在Swift项目中使用JSONModel.我想从JSONModel覆盖方法keyMapper,但我没有找到如何覆盖模型类中的Objective-C类方法.该方法的签名是:我怎样才能做到这一点?解决方法您可以像覆盖实例方法一样执行此操作,但使用class关键字除外:

  9. ios – 在WKWebView中获取链接URL

    我想在WKWebView中获取tapped链接的url.链接采用自定义格式,可触发应用中的某些操作.例如HTTP://我的网站/帮助#深层链接对讲.我这样使用KVO:这在第一次点击链接时效果很好.但是,如果我连续两次点击相同的链接,它将不报告链接点击.是否有解决方法来解决这个问题,以便我可以检测每个点击并获取链接?任何关于这个的指针都会很棒!解决方法像这样更改addobserver在observeValue函数中,您可以获得两个值

  10. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

返回
顶部