【译】使用UIKit进行面向对象的编程

在WWDC 2015上,Apple谈了Swift中面向协议编程的话题,令人深思。在那之后,好像每个人都在讨论关于协议扩展的话题,这个新的语言特性使每个人都有所困惑。

我阅读了许多关于Swift中协议的文章,了解过了协议扩展(protocol extensions)的详情。毫无疑问,协议扩展将是Swift这道菜中的一位重要调料。Apple甚至建议尽可能的使用协议(protocol)来替换类(class)--这是面向协议编程的关键。

我读过许多文章,其中对协议扩展的定义讲的很清晰。但都没有说明面向协议编程真正能为UI开发带来些什么。当前可用的一些示例代码并不是基于一些实际场景的,而且没用应用任何框架。

我想要知道面向协议编程是如何影响已有的应用,以及该如何在一个最常用的iOS库(例如UIKit)中最大化的发挥它的作用。

既然我们已经有了协议扩展,基于协议的方法是否在UIkit这个“类重地”上有更大的价值?这篇文章中我将尝试在真实的UI使用场景中讲述Swift协议扩展。通过研究的过程来说明协议扩展并不是我之前所想的样子。

协议的好处

协议并不是什么新事物,但使用内置功能、共享逻辑,甚至“魔法能力”来扩展协议的想法很迷人。更多的协议意味着更大的灵活性。协议扩展是模块化功能的一部分,它可以被采用(adopted),被覆盖(overriden),也可以通过where语句进行指定类型的访问。

从编译角度来说,协议本身只能迎合编译器。但是协议扩展却是实际的代码块,可以被整个代码库使用。

不同于从父类继承子类,我们可以使用任意多个协议。使用扩展协议就像是在Angular.js中为一个元素添加一条指令--我们插入一段代码逻辑来替换对象的行为。这里,协议已经不单单是一种约定,通过扩展的方式我们可以使用实际的功能。

如何使用扩展协议

方法很简单。本文不会介绍如何使用,而会讨论在UIKit中的实际应用。如果你需要尽快了解协议是如何工作的,请参考:Official Swift Documentation on Procotol Extensions.

协议扩展的局限

开始之前,让我们先搞清楚协议不能做什么。许多协议不能做的事情是出于设计考虑。不过我也很希望看到Apple在未来的Swift版本中处理这些限制。

在Objective-C中不能调用扩展协议的成员。

  • 不能对struct类型使用where语句
  • 不能在一个if let语句中定义多个逗号分隔的where语句
  • 不能在协议扩展中存储动态变量

1.这条对非泛型扩展也同样使用

2.静态变量理论上是支持的,但是在Xcode 7.0上使用会报错:“static stored properties not yet supported in generic types”

  • 不能在扩展协议中调用super(这点不同于非泛型扩展) @ketzusaka

基于这个原因,没有真正意义上的协议扩展继承。

  • 不能使用多个协议扩展中同名的成员。

1.Swift运行时环境会选择最后一个协议中的成员并且忽略其他的。

2.例如:如果我们使用两个扩展协议,其中实现了两个同名方法,当调用该方法时,只有最后一个协议中的方法会被调用。其他扩展中的方法调用不到。

  • 不能扩展可选(optional)的协议方法。

1.可选协议方法需要@objc的标记,这样就无法同时使用协议扩展。

  • 无法同时声明协议和它的扩展。

1.最好声明extension protocol SomeProtocol {},这样就同时声明了协议并且实现了扩展。

Part 1:扩展现有UIKit协议

刚开始研究协议扩展时,第一个想到的是UITableViewDataSource,它或许是iOS平台上使用最广的协议。如果可以为UITableViewDataSource协议添加一个默认的实现,这不是很有意思吗?

如果应用中每个UITableView都有固定的若干个section,为什么不扩展UITableViewDataSource并且在其中实现numberOfSectionsInTableView: 方法?如果所有的table都有滑动删除的功能,扩展UITableViewDelegate协议并实现相应方法就完美多了。

泼盆冷水吧,这些都是不可能的。

  • 不可能任务:

为Objective-C协议提供默认实现。

UIKit仍然使用Objective-C编译,而Objective-C中并没有协议扩展的概念。在实际使用中,这意味着即使我们可以声明UIKit协议的扩展,对于UIKit对象来说,扩展协议中的方法仍然是不可见的。

例如:如果我们扩展UICollectionViewDelegate 并实现collectionView:didSelectItemAtIndexPath:方法。在我们点击cell的时候,这个方法并不会被调用。因为UICollectionView在Objective-C上下文中查找不到这个扩展方法。如果我们把如collectionView:cellForItemAtIndexPath:此类必要(required)方法放在协议扩展中,编译器还是会提示使用该协议的类没有遵循UICollectionViewDelegate协议。

Xcode尝试通过添加@objc标签来解决这个问题,但是这是徒劳的,会有一个新的错误:"协议扩展中的方法不能用Objective-C实现"。这是个隐藏错误:协议扩展只能在Swift 2以上代码中使用。

  • 我们能做的:

为现有的Objective-C协议添加新的方法

我们可以通过Swift直接调用UIKit协议的扩展方法,即使对于UIKit来说它们是不可见的。这意味着我们不能覆盖已有的协议方法,但是可以为协议添加新的方法。

这并没有什么惊喜之处,因为Objective-C代码依然不能访问这些方法。但还是带来了一些机会。以下是一些组合使用协议扩展和现有UIKit协议的可能方式。

UIKit协议扩展示例:

扩展UICoordinateSpace

你以前一定尝试过UIKit和Core Graphics坐标之间的相互转换(左上坐标系->左上坐标系)。我们可以为UICoordinateSpace(一个UIView使用的协议)添加一些便利方法。


1

2

3

4

5

6

7

extension UICoordinateSpace {

    func invertedRect(rect: CGRect) -> CGRect {

        var transform = CGAffineTransformMakeScale(1, -1)

        transform = CGAffineTransformTranslate(transform, 0, -self.bounds.size.height)

        return CGRectApplyAffineTransform(rect, transform)

    }

}

现在我们的invertedRect方法可以被所有使用UICoordinateSpace的对象调用。我们可以在绘制代码中这样使用:


1

2

3

4

5

6

7

class DrawingView : UIView {

    // Example -- Referencing custom UICoordinateSpace method inside UIView drawRect.

    override func drawRect(rect: CGRect) {

        let invertedRect = self.invertedRect(CGRectMake(50.0, 50.0, 200.0, 100.0))

        print(NSStringFromCGRect(invertedRect)) // 50.0, -150.0, 200.0, 100.0

    }

}

扩展UITableViewDataSource协议

虽然不能修改UITableViewDataSource 的默认实现,我们还是可以添加一些公用代码到UITableViewDataSource 中。


1

2

3

4

5

6

7

8

9

10

11

12

extension UITableViewDataSource {

    // Returns the total # of rows in a table view.

    func totalRows(tableView: UITableView) -> Int {

        let totalSections = self.numberOfSectionsInTableView?(tableView) ?? 1

        var s = 0, t = 0

        while s < totalSections {

            t += self.tableView(tableView, numberOfRowsInSection: s)

            s++

        }

        return t

    }

}

totalRows:方法可以快速计算table view中所有条目的数量。如果有个label显示条目数量,而我们的数据都分散在各个section中的时候,这个方法格外有用。比如在tableView:titleForFooterInSection:方法中:


1

2

3

4

5

6

7

8

9

class ItemsController: UITableViewController {

    // Example -- displaying total # of items as a footer label.

    override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {

        if section == self.numberOfSectionsInTableView(tableView)-1 {

            return String("Viewing %f Items", self.totalRows(tableView))

        }

        return ""

    }

}

扩展UIViewControllerContextTransitioning协议

如果读过我针对iOS 7写的文章 Custom Navigation Transitions & More,并使用其中的方法自定义navigation的过渡。以下就有一组我使用过的方法,通过扩展UIViewControllerContextTransitioning 协议来实现。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

extension UIViewControllerContextTransitioning {

    // Mock the indicated view by replacing it with its own snapshot. Useful when we don‘t want to render a view‘s subviews during animation, such as when applying transforms.

    func mockViewWithKey(key: String) -> UIView? {

        if let view = self.viewForKey(key), container = self.containerView() {

            let snapshot = view.snapshotViewAfterScreenUpdates(false)

            snapshot.frame = view.frame

            

            container.insertSubview(snapshot, aboveSubview: view)

            view.removeFromSuperview()

            return snapshot

        }

        

        return nil

    }

    

    // Add a background to the container view. Useful for modal presentations, such as showing a partially translucent background behind our modal content.

    func addBackgroundView(color: UIColor) -> UIView? {

        if let container = self.containerView() {

            let bg = UIView(frame: container.bounds)

            bg.backgroundColor = color

            

            container.addSubview(bg)

            container.sendSubviewToBack(bg)

            return bg

        }

        return nil

    }

}

我们可以在传递到animation coordinator的transitionContext对象调用这些方法


1

2

3

4

5

6

7

8

9

10

11

class AnimationCoordinator : NSObject, UIViewControllerAnimatedTransitioning {    // Example -- using helper methods during a view controller transition.

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {        // Add a background

        transitionContext.addBackgroundView(UIColor(white: 0.0, alpha: 0.5))        

        // Swap out the "from" view

        transitionContext.mockViewWithKey(UITransitionContextFromViewKey)        

        // Animate using awesome 3D animation...

    }

    

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {        return 5.0

    }

}

扩展UIScrollViewDelegate协议

假设我们有许多个UIPageControl实例,我们需要拷贝粘贴UIScrollViewDelegate中的实现。使用协议扩展的方法我们可以全局访问这段代码,只需要简单的使用self调用。


1

2

3

4

5

6

extension UIScrollViewDelegate {

    // Convenience method to update a UIPageControl with the correct page.

    func updatePageControl(pageControl: UIPageControl, scrollView: UIScrollView) {

        pageControl.currentPage = lroundf(Float(scrollView.contentOffset.x / (scrollView.contentSize.width / CGFloat(pageControl.numberOfPages))));

    }

}

另外,如果我们在使用UICollectionViewController,就可以去掉scrollView参数:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

extension UIScrollViewDelegate where Self: UICollectionViewController {

    func updatePageControl(pageControl: UIPageControl) {

        pageControl.currentPage = lroundf(Float(self.collectionView!.contentOffset.x / (self.collectionView!.contentSize.width / CGFloat(pageControl.numberOfPages))));

    }

}

// Example -- Page control updates from a UICollectionViewController using a protocol extension.

class PagedCollectionView : UICollectionViewController {

    let pageControl = UIPageControl()

    

    override func scrollViewDidScroll(scrollView: UIScrollView) {

        self.updatePageControl(self.pageControl)

    }

}

不得不承认,以上例子都有些牵强。这说明了扩展现有UIKit协议并没有太大的空间,而其价值并不明显。不过,我们还是希望探索如何利用UIKit的设计模式扩展自定义协议。

Part 2:扩展自定义协议

MVC中使用面向协议编程

iOS程序内部通常包含3个重要部分。通常被描述为MVC(Model-View-Controller)模式。在App中使用这种模式来计算数据并展示出来。

下面的三个例子中,我将展示一些有协议扩展特色的面向协议设计模式,依次用到Model->Controller->View组件。

Model管理中的协议(M)

假设我们有一个音乐类应用,叫Pear Music,里面用到的model对象有Artists,Albums, Songs 和Playlists。我们需要通过某种标识,从网络端加载这些model对象。

设计协议时,最好从顶端的抽象开始。基本思路是:有一个远程资源,可以通过一个API来创建。我们这样来定义协议:


1

2

// Any entity which represents data which can be loaded from a remote source.

protocol RemoteResource {}

等等,这只是个空协议。RemoteResource并未被显式的使用。我们并不是需要一个约定,而是需要一系列设计网络请求的功能。这样说来,它真正的价值在于扩展:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

extension RemoteResource {

    func load(url: String, completion: ((success: Bool)->())?) {

        print("Performing request: ", url)

        

        let task = NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) -> Void in

            if let httpResponse = response as? NSHTTPURLResponse where error == nil && data != nil {

                print("Response Code: %d", httpResponse.statusCode)

                

                dataCache[url] = data

                if let c = completion {

                    c(success: true)

                }

            else {

                print("Request Error")

                if let c = completion {

                    c(success: false)

                }

            }

        }

        task.resume()

    }

    

    func dataForURL(url: String) -> NSData? {

        // A real app would require a more robust caching solution.

        return dataCache[url]

    }

}

public var dataCache: [String : NSData] = [:]

现在我们的协议有了内置的功能,可以加载并获取远程数据。所有应用该协议的对象都可以直接访问这些方法。

假定还有两个API需要调用,一个从"api.pearmusic.com"返回JSON类型数据; 另外一个从"media.pearmusic.com"返回media数据.要处理这些,我们为RemoteResource 协议创建子协议:


1

2

3

4

5

6

7

8

9

10

protocol JSONResource : RemoteResource {

    var jsonHost: String { get }

    var jsonPath: String { get }

    func processJSON(success: Bool)

}

protocol MediaResource : RemoteResource {

    var mediaHost: String { get }

    var mediaPath: String { get }

}

接下来是子协议(扩展)的实现:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

extension JSONResource {

    // Default host value for REST resources

    var jsonHost: String { return "api.pearmusic.com" }

    

    // Generate the fully qualified URL

    var jsonURL: String { return String(format: "http://%@%@", self.jsonHost, self.jsonPath) }

    

    // Main loading method.

    func loadJSON(completion: (()->())?) {

        self.load(self.jsonURL) { (success) -> () in

            // Call adopter to process the result

            self.processJSON(success)

            

            // Execute completion block on the main queue

            if let c = completion {

                dispatch_async(dispatch_get_main_queue(), c)

            }

        }

    }

}

我们提供了默认的host名称、创建完整URL的方法,还有加载资源的方法。接下来需要协议的使用者提供正确的jsonPath。

MediaResource使用同样的模式:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

extension MediaResource {

    // Default host value for media resources

    var mediaHost: String { return "media.pearmusic.com" }

    

    // Generate the fully qualified URL

    var mediaURL: String { return String(format: "http://%@%@", self.mediaHost, self.mediaPath) }

    

    // Main loading method

    func loadMedia(completion: (()->())?) {

        self.load(self.mediaURL) { (success) -> () in

            // Execute completion block on the main queue

            if let c = completion {

                dispatch_async(dispatch_get_main_queue(), c)

            }

        }

    }

}

如你所见,以上实现都很类似。事实上,将以上子协议中的代码提到RemoteResource中会更合理,这样子协议只需要返回正确的host名称即可。

一个麻烦之处在于:这些协议之间并不互斥。也就是说,我们可能需要一个对象既是JSONResource,同时又是MediaResource。记住之前我们说过的,协议本身是会覆盖的。只有最后一个协议中的方法会被调用,除非我们使用不同的属性或方法。

让我们来专门说说数据访问方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

extension JSONResource {

    var jsonValue: [String : AnyObject]? {

        do {

            if let d = self.dataForURL(self.jsonURL), result = try NSJSONSerialization.JSONObjectWithData(d, options: NSJSONReadingOptions.MutableContainers) as? [String : AnyObject] {

                return result

            }

        catch {}

        return nil

    }

}

extension MediaResource {

    var imageValue: UIImage? {

        if let d = self.dataForURL(self.mediaURL) {

            return UIImage(data: d)

        }

        return nil

    }

}

这是用来说明协议扩展内涵的一个典型例子。传统意义上的协议像是在说:“我有这些功能,因此我承诺我是这种类型”。一个扩展协议会说:“因为我有这些功能,我能做这些特别的事情”。因为MediaResource有image数据的访问权限,因此应用MediaResource协议的对象可以提供imageValue,而不管它本身是什么类型的,也不需要考虑上下文环境。

之前提到我们可以通过已知的标识符加载model对象。因此我们创建一个描述唯一标识的协议:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

protocol Unique {

    var id: String! { get set }

}

extension Unique where Self: NSObject {

    // Built-in init method from a protocol!

    init(id: String?) {

        self.init()

        if let identifier = id {

            self.id = identifier

        else {

            self.id = NSUUID().UUIDString

        }

    }

}

// Bonus: Make sure Unique adopters are comparable.

func ==(lhs: Unique, rhs: Unique) -> Bool {

    return lhs.id == rhs.id

}

extension NSObjectProtocol where Self: Unique {

    func isEqual(object: AnyObject?) -> Bool {

        if let o = object as? Unique {

            return o.id == self.id

        }

        return false

    }

}

这段代码中,我们还是需要依赖于协议采用者提供“id”属性,因为在协议扩展中我们不能存储属性。另外需要注意的一点是:这里用where Self:NSObject语句限定只有在类型为NSObject时才可使用该扩展。不这样做的话,就没办法调用self.init()方法,因为根本没有它的声明。一个替代方案是在该协议中自己声明init()方法,但是这样做的话,协议的采用者就必须显式的实现它。因为所有的model对象都是NSObject的子类,因此这并不是问题。

OK,现在我们有了一个获取网络资源的基本方案。下来我们来创建遵循这些协议的model类型。首先是Song model类:


1

2

3

4

5

6

7

8

9

10

11

class Song : NSObject, JSONResource, Unique {

    // MARK: - Metadata

    var title: String?

    var artist: String?

    var streamURL: String?

    var duration: NSNumber?

    var imageURL: String?

    

    // MARK: - Unique

    var id: String!

}

等一下,JSONResource的(扩展)实现在哪里?

比起直接在类中实现JSONResource的方法,使用条件控制的协议扩展更方便。这样使我们可以将所有基于RemoteResource的代码逻辑整合在一起,便于调整。另外,也使model类的实现更加整洁。添加如下代码到RemoteResource.swift文件:


1

2

3

4

5

6

7

8

9

10

11

12

extension JSONResource where Self: Song {

    var jsonPath: String { return String(format: "/songs/%@", self.id) }

    

    func processJSON(success: Bool) {

        if let json = self.jsonValue where success {

            self.title = json["title"] as? String ?? ""

            self.artist = json["artist"] as? String ?? ""

            self.streamURL = json["url"] as? String ?? ""

            self.duration = json["duration"] as? NSNumber ?? 0

        }

    }

}

将这些内容都和RemoteResource关联在一个位置,在组织上有很多好处。在一个位置编写协议的实现方法,这里扩展的作用范围是清晰的。当声明一个协议,且需要扩展时,我建议将扩展写在同一个文件中。

有了JSONResource和Unique协议扩展,我们加载Song对象的代码会像这样:


1

2

3

4

5

6

let s = Song(id: "abcd12345")

let artistLabel = UILabel()

s.loadJSON { (success) -> () in

  artistLabel.text = s.artist

}

Duang!我们的Song对象就成了元数据的一个包装,它本该如此。我们的协议扩展是真正的幕后英雄。

以下是Playlist对象的一个例子,它同时遵循JSONResource和MediaResource协议。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class Playlist: NSObject, JSONResource, MediaResource, Unique {

    // MARK: - Metadata

    var title: String?

    var createdBy: String?

    var songs: [Song]?

    

    // MARK: - Unique

    var id: String!

}

extension JSONResource where Self: Playlist {

    var jsonPath: String { return String(format: "/playlists/%@", self.id) }

    

    func processJSON(success: Bool) {

        if let json = self.jsonValue where success {

            self.title = json["title"] as? String ?? ""

            self.createdBy = json["createdBy"] as? String ?? ""

            // etc...

        }

    }

}

在我们摸索着为Playlist实现MediaResource协议之前,先稍稍退一步。我们意识到media API只需要identifier,而不需要考虑协议应用者的类型。这意味着,只要知道了identifier,就可以创建出mediaPath。用where语句可使MediaResource更智能的处理Unique协议。


1

2

3

extension MediaResource where Self: Unique {

   var mediaPath: String { return String(format: "/images/%@", self.id) }

}

因为我们的Playlist类已经遵循了Unique协议,因此不需要显式的处理,它就可以和MediaResource搭配使用。对于所有MediaResource的使用者来说(它们也必然适配于Unique协议)也是一样的:只要对象的identifier对应media API中的一张图片,就可以通过这种方式创建mediaPath。

以下是加载Playlist图片的方法:


1

2

3

4

5

6

let p = Playlist(id: "abcd12345")

let playlistImageView = UIImageView(frame: CGRectMake(0.0, 0.0, 200.0, 200.0))

p.loadMedia { () -> () in

  playlistImageView.image = p.imageValue

}

现在,我们已经有了一种定义远程资源的通用方式,对于程序中任何实体都使用,而不局限于这些model对象。我们可以通过简单的方式扩展RemoteResource,使其支持各种REST操作,另外,也可以针对其他数据类型创建子协议。

处理数据格式化的协议(C)

上文中我们创建了一种加载model对象的方法,继续下一步:我们需要格式化对象中的元数据,并协调的显示出来。

Peer Music是一个大应用,其中有许多不同类型的model。每个model都可能在不同的地方显示。例如:作为view controller的title时,我们可能只显示“name”。而如果有更多显示空间的话,如UITableViewCell中,则显示为“name instrument”。空间再多点的话,还可以显示为“name instrument bio”。

当然,在controllers中,cell中,或者label中实现这些格式化方法没有问题。但是如果能够提取出这部分代码逻辑,给整个app使用,会大大减少维护成本。

我们也可以将字符串格式化的代码放到model对象中,但这样在显示字符串的时候,就必须确定model的类型。

也可以在基类中实现某些便利方法,由各model子类提供各自的格式化方式。由于我们正在讨论面向协议编程,这里就考虑的更通用一些。

考虑一下这样的需求:将某些实体按字符串方式展现出来。上面的方法就可以推广使用。针对不同的UI场景,可以提供出不同长度的字符串。


1

2

3

4

5

6

7

8

9

10

11

// Any entity which can be represented as a string of varying lengths.

protocol StringRepresentable {

    var shortString: String { get }

    var mediumString: String { get }

    var longString: String { get }

}

// Bonus: Make sure StringRepresentable adopters are printed descriptively to the console.

extension NSObjectProtocol where Self: StringRepresentable {

    var description: String { return self.longString }

}

简单吧。以下是model对象使用StringRepresentable的例子:


1

2

3

4

5

6

7

8

9

10

11

class Artist : NSObject, StringRepresentable {

    var name: String!

    var instrument: String!

    var bio: String!

}

class Album : NSObject, StringRepresentable {

    var title: String!

    var artist: Artist!

    var tracks: Int!

}

和实现RemoteResource的方式类似,我们也将所有格式化字符串的逻辑放到StringRepresentable.swift文件中(这里同样有协议的声明)。


1

2

3

4

5

6

7

8

9

10

extension StringRepresentable where Self: Artist {

    var shortString: String { return self.name }

    var mediumString: String { return String(format: "%@ (%@)", self.name, self.instrument) }

    var longString: String { return String(format: "%@ (%@), %@", self.name, self.instrument, self.bio) }

}

extension StringRepresentable where Self: Album {

    var shortString: String { return self.title }

    var mediumString: String { return String(format: "%@ (%d Tracks)", self.title, self.tracks) }

    var longString: String { return String(format: "%@, an Album by %@ (%d Tracks)", self.title, self.artist.name, self.tracks) }

}

现在,所有格式化功能都搞定了,现在可以考虑将其作用到不同的UI场景中。基于通用考虑,我们的设计用于显示所有StringRepresentable的应用者,只要给出containerSize和containerFont用来计算即可。


1

2

3

4

5

protocol StringDisplay {

  var containerSize: CGSize { get }

  var containerFont: UIFont { get }

  func assignString(str: String)

}

建议只将方法声明放置到协议中,协议的应用者(adopter)会实现这些方法。而对协议扩展来说,我们会添加真正的实现代码。displayStringValue: 方法会决定使用哪个字符串,它会用assignString:将该字符串传递出去,而assignString:方法可以由不同的类实现。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

extension StringDisplay {

    func displayStringValue(obj: StringRepresentable) {

        // Determine the longest string which can fit within the containerSize, then assign it.

        if self.stringWithin(obj.longString) {

            self.assignString(obj.longString)

        else if self.stringWithin(obj.mediumString) {

            self.assignString(obj.mediumString)

        else {

            self.assignString(obj.shortString)

        }

    }

    

#pragma mark - Helper Methods

    

    func sizeWithString(str: String) -> CGSize {

        return (str as NSString).boundingRectWithSize(CGSizeMake(self.containerSize.width, .max),

            options: .UsesLineFragmentOrigin,

            attributes:  [NSFontAttributeName: self.containerFont],

            context: nil).size

    }

    

    private func stringWithin(str: String) -> Bool {

        return self.sizeWithString(str).height <= self.containerSize.height

    }

}

现在我们的model对象已经遵循了StringRepresentable协议,另外,我们还有了可以自动选择字符串的协议。下面看看如何在UIKit中使用。

从最简单的UILabel开始吧。传统做法是:继承UILabel类,应用协议,然后在需要使用StringRepresentable来显示的时候调用这个自定义的UILabel。而更好的方案(假定我们不需要继承),就是使用指定类型的扩展(当然这里指定的是UILabel类),让所有的UILabel类自动适应StringDisplay协议。


1

2

3

4

5

6

7

extension UILabel : StringDisplay {

    var containerSize: CGSize { return self.frame.size }

    var containerFont: UIFont { return self.font }

    func assignString(str: String) {

        self.text = str

    }

}

只需要这么多代码。对于其他的UIKit类,都可以这么做。只需要返回StringDisplay协议需要的数据,剩下的全由它帮忙搞定。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

extension UITableViewCell : StringDisplay {

    var containerSize: CGSize { return self.textLabel!.frame.size }

    var containerFont: UIFont { return self.textLabel!.font }

    func assignString(str: String) {

        self.textLabel!.text = str

    }

}

extension UIButton : StringDisplay {

    var containerSize: CGSize { return self.frame.size}

    var containerFont: UIFont { return self.titleLabel!.font }

    func assignString(str: String) {

        self.setTitle(str, forState: .Normal)

    }

}

extension UIViewController : StringDisplay {

    var containerSize: CGSize { return self.navigationController!.navigationBar.frame.size }

    var containerFont: UIFont { return UIFont(name: "HelveticaNeue-Medium", size: 34.0)! } // default UINavigationBar title font

    func assignString(str: String) {

        self.title = str

    }

}

使用起来效果如何?接下来我们声明一个Artist,它也会用StringRepresentable协议。


1

2

3

4

let a = Artist()

a.name = "Bob Marley"

a.instrument = "Guitar / Vocals"

a.bio = "Every little thing‘s gonna be alright."

因为所有的UIButton被扩展为适配StringDisplay协议,我们可以直接调用UIButton对象的displayStringValue:方法。


1

2

3

4

5

6

7

8

9

let smallButton = UIButton(frame: CGRectMake(0.0, 0.0, 120.0, 40.0))

smallButton.displayStringValue(a)

print(smallButton.titleLabel!.text) // ‘Bob Marley‘

let mediumButton = UIButton(frame: CGRectMake(0.0, 0.0, 300.0, 40.0))

mediumButton.displayStringValue(a)

print(mediumButton.titleLabel!.text) // ‘Bob Marley (Guitar / Vocals)‘

现在button会根据frame的大小自动选择title来显示。

若我们点击一个Album,进入AlbumDetailsViewController的页面,协议可以帮助我们找到一个合适的字符串作为navigation的标题。有了StringDisplay协议,UINavigationBar在iPad上会显示长标题,而在iPhone上显示短标题。


1

2

3

4

5

6

7

8

9

10

class AlbumDetailsViewController : UIViewController {

    var album: Album!

    

    override func viewWillAppear(animated: Bool) {

        super.viewWillAppear(animated)

        

        // Display the right string based on the nav bar width.

        self.displayStringValue(self.album)

    }

}

现在我们可以相信,格式化model的工作可以由协议扩展单独完成,并且能够根据不同的UI元素灵活显示。这种模式可以在以后的model对象上重复使用,适应于不同的UI元素。因为协议的这种可扩展性,它甚至可以用在许多非UI环境中。

样式中使用协议(V)

我们已经了解了如何在model类和格式化字符串中使用协议扩展,现在,让我们看看单纯的前段实例,看一下协议扩展是如何使UI开发更加快捷。

我们把协议看作是类似于css类的东西,使用协议来定义UIKit对象的样式,之后,应用样式协议的对象可以自动改变显示外观。

首先,我们定义一个基础协议,用来表示样式处理的实体,在其中声明一个最终用于处理样式的方法。


1

2

3

4

// Any entity which supports protocol-based styling.

protocol Styled {

  func updateStyles()

}

接下来,我们创建一些子协议,定义具体需要的样式。


1

2

3

4

5

6

7

8

protocol BackgroundColor : Styled {

  var color: UIColor { get }

}

protocol FontWeight : Styled {

  var size: CGFloat { get }

  var bold: Bool { get }

}

这样,协议使用者就不需要进行显式调用。

接着,我们定义各种特定样式,在协议扩展的实现中返回需要的值。


1

2

3

4

5

6

7

8

9

10

protocol BackgroundColor_Purple : BackgroundColor {}

extension BackgroundColor_Purple {

    var color: UIColor { return UIColor.purpleColor() }

}

protocol FontWeight_H1 : FontWeight {}

extension FontWeight_H1 {

    var size: CGFloat { return 24.0 }

    var bold: Bool { return true }

}

最后,只需要根据不同的UIKit对象类型,实现updateStyles即可。用指定类型的扩展让所有UITableViewCell的实例都遵循Styled协议。


1

2

3

4

5

6

7

8

9

10

11

12

extension UITableViewCell : Styled {

    func updateStyles() {

        if let s = self as? BackgroundColor {

            self.backgroundColor = s.color

            self.textLabel?.textColor = .whiteColor()

        }

        

        if let s = self as? FontWeight {

            self.textLabel?.font = (s.bold) ? UIFont.boldSystemFontOfSize(s.size) : UIFont.systemFontOfSize(s.size)

        }

    }

}

为保证updateStyles被自动调用,我们在扩展中重写awakeFromNib方法。这里你可能有点疑问,实际上,重写的awakeFromNib方法被插入到了继承链中,就好像是继承自UITableViewCell类本身。这样,在UITableViewCell子类中调用super,就会直接调用到这个方法。


1

2

3

4

5

public override func awakeFromNib() {

     super.awakeFromNib()

     self.updateStyles()

  }

}

现在,我们创建子类,然后通过应用协议来加载需要的样式:


1

class PurpleHeaderCell : UITableViewCell, BackgroundColor_Purple, FontWeight_H1 {}

我们已经为UIKit的元素创建了类似css的样式声明。使用协议扩展,甚至可以为UIKit添加如Bootstrap的功能。这种方案在不同的方面都可以有所作为,特别是当程序中的样式动态成都高、显示元素较多时,更能发挥价值。

假定我们程序中有20+的view controller,每个都使用了2-3中显示样式。之前的我们只能被迫创建基类或者写一堆用来定义样式的全局函数;现在只需要实现并使用样式协议就可以了。

我们得到了什么?

到此为止我们已经尝试了不少东西,它们都很有趣。但是思考一下:我们到底能从协议和协议扩展中获得什么?有人会认为根本没有必要创建协议。

面向协议编程并不能完美适配于所有UI场景。

通常,当添加共享代码或通用方法时,协议和协议扩展好处颇多。而且,代码的组织性和函数相比更好。

数据类型越多,协议越能发挥用武之地。在UI需要显示多种信息格式时,使用协议会得心应手。但这并不意味着,我们要添加6种协议和一打协议扩展来创建一个显示artist名称的紫色背景cell。

让我们来补充Pear Music软件的使用场景,来看看面向协议编程是否真的物有所值。

添加复杂度

假定我们已经维护了Pear Music一段时间,这个软件可以显示albums、artists和songs,有着友好的界面。我们又有巧妙的协议和扩展来维持MVC的结构。现在Pear的CEO要求我们创建Pear Music的2.0版本。我们需要和一个叫Apple Music的软件进行竞争。

我们需要一项酷炫的新功能来证明自己,经过研究,决定添加“长按预览”功能。这项功能创意新颖、独到。公司里长的像Jony Ive的哥们已经坐在镜头前侃侃而谈。让我们赶紧开始干活,用面向协议编程的方法来搞定它。

创建Modal Page

流程如下:用户长按artist,album,song或者playlist,这时一个模态窗口(modal view)在屏幕上显示出来,从网络上加载条目的图片,并显示其描述,就像Facebook的分享按钮做的那样。

我们先来创建一个UIViewController,它将用来做模态显示。从一开始,我们就考虑让初始化方式更加通用,只需要一些遵循StringRepresentable和MediaResource协议的对象。


1

2

3

4

5

6

7

8

9

10

11

12

13

class PreviewController: UIViewController {

    @IBOutlet weak var descriptionLabel: UILabel!

    @IBOutlet weak var imageView: UIImageView!

    

    // The main model object which we‘re displaying

    var modelObject: protocol!

    

    init(previewObject: protocol) {

        self.modelObject = previewObject

    

        super.init(nibName: "PreviewController", bundle: NSBundle.mainBundle())

    }

}

接下来我们使用内置的协议扩展方法来给descriptionLabel和imageView传递数据:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

override func viewDidLoad() {

        super.viewDidLoad()

        

        // Apply string representations to our label. Will use the string which fits into our descLabel.

        self.descriptionLabel.displayStringValue(self.modelObject)

        

        // Load MediaResource image from the network if needed

        if self.modelObject.imageValue == nil {

            self.modelObject.loadMedia { () -> () in

                self.imageView.image = self.modelObject.imageValue

            }

        else {

            self.imageView.image = self.modelObject.imageValue

        }

    }

最后,通过同样的方法获取metadata,就像我们在Facebook例子中做的那样。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// Called when tapping the Facebook share button.

    @IBAction func tapShareButton(sender: UIButton) {

        if SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook) {

            let vc = SLComposeViewController(forServiceType: SLServiceTypeFacebook)

            

            // Use StringRepresentable.shortString in the title

            let post = String(format: "Check out %@ on Pear Music 2.0!", self.modelObject.shortString)

            vc.setInitialText(post)

            

            // Use the MediaResource url to link to

            let url = String(self.modelObject.mediaURL)

            vc.addURL(NSURL(string: url))

            

            // Add the entity‘s image

            vc.addImage(self.modelObject.imageValue!);

            

            self.presentViewController(vc, animated: true, completion: nil)

        }

    }

}

通过协议,我们获得了很多便利,如果没有它们,我们需要根据不同的数据类型,分别创建PreviewController的初始化方法。通过基于协议的方式,既可以保证view controller的简洁性,又可以保证其扩展性。

按照这种方式,PreviewController不用分别处理Artist,Album,Song,Playlist等不同的数据类型,变得更加简洁和轻量级。它甚至不用些一行数据类型相关的代码。

集成第三方代码

以下是本教程中最后一个酷炫的示例。同样,用PreviewController展示。这里我们需要集成一个新的框架,来展示Twitter上音乐家的信息。在主页面上显示推文列表,有一下的model类可以使用:


1

2

3

4

5

6

7

class TweetObject {

  var favorite_count: Int!

  var retweet_count: Int!

  var text: String!

  var user_name: String!

  var profile_image_id: String!

}

我们没有这个框架的代码,也无法修改TweetObject类,但是还是希望用户能通过长按的方法在PreviewController的UI上显示推文。这里只需要通过应用现有协议来扩展它,就这么简单。


1

2

3

4

5

6

7

8

9

10

extension TweetObject : StringRepresentable, MediaResource {

    // MARK: - MediaResource

    var mediaHost: String { return "api.twitter.com" }

    var mediaPath: String { return String(format: "/images/%@", self.profile_image_id) }

    

    // MARK: - StringRepresentable

    var shortString: String { return self.user_name }

    var mediumString: String { return String(format: "%@ (%d Retweets)", self.user_name, self.retweet_count) }

    var longString: String { return String(format: "%@ Wrote: %@", self.user_name, self.text) }

}

这样,我们就可以直接传递TweetObject的对象给PreviewController了。对于PreviewController来说,它甚至不需要知道现在正在和一个外部框架打交道。


1

2

let tweet = TweetObject()

let vc = PreviewController(previewObject: tweet)

课程总结

在WWDC2015上Apple建议创建协议,而不是类。但是我对这个观点持怀疑态度,因为它忽略了在使用UIKit这个已类为重的框架时,协议扩展微妙的限制。只有当协议扩展被广泛应用,而且不需要考虑旧代码的时候,才能发挥它的威力。虽然在一开始我提到的例子看起来都很琐碎,这种通用的设计在程序扩展、复杂度不断提升时,还是非常有效。

在代码解释性和成本之间,需要综合考虑。协议和扩展在大多数基于UI的程序中并不怎么实用。如果你的app只有一个单view,显示一种类型的数据,而且永远不改变,就不用过分考虑实用协议。但是如果你的app要让核心数据在不同的显示状态下切换,显示样式和展现方式多种多样。这时,协议和协议扩展将成为数据和显示层的桥梁,你会在后期使用中受益匪浅。

最后,我不想把协议看做是万用灵药,而是将其当做在某种开发场景中,一种创造性的工具。当然,我认为开发者尝试一下面向协议技术是很有好处的,按照协议的方式,重新审视自己的代码,你会发现很多不一样的东西。聪明的使用它们。

时间: 2024-08-10 15:02:48

【译】使用UIKit进行面向对象的编程的相关文章

如何培养面向对象的编程思想(转)

1.什么是面向对象? 面向对象是专指在程序设计中采用封装,继承,多态和抽象的设计方法.面向对象的程序设计语言必须有描述对象及其相互之间关系的语言成分.这些程序设计语言可分为以下几类:系统中一切事物皆为对象:对象是属性及其操作的封装体:对象可按其性质分类,对象成为类的实例:实例关系和继承关系是对象之间的静态关系:消息传递是对象之间动态联系的唯一方式,也是计算的唯一形式:方法是消息的序列. 2.如何理解面向对象? 从世界观的角度可以认为:面向对象的基本哲学是认为世界是由各种各样具有自己的运动规律和内

Python3 面向对象 高级编程

正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性.  class Student(object): pass 然后,尝试给实例绑定一个属性: >>> s = Student() >>> s.name = 'Michael' # 动态给实例绑定一个属性 还可以尝试给实例绑定一个方法: >>> def set_age(self, age): # 定义一个函数作为实例方法 ...

Python 2.7 学习笔记 面向对象的编程

python是一种面向对象的语言.本文介绍如何用python进行面向对象的编程. 首先我们说下对象和类,类是一种类型的定义,对象是类的实例. 一.内置对象类型(内置类) 其实我们前面已经大量用到了对象,如字符串.列表.字典等,这些对象的类型是python的内建对象类型. 比如: a=[] 这其实就是创建了一个空的列表对象,并将它赋值给变量a,变量a就指向了一个列表对象. 那列表对象对应的类是什么呢?其实列表对象对应的类名是list. 我们还可以通过类来创建对象,看下例子: >>> pri

C++面向对象的编程思想机器人

C++的面向对象的编程思想如下,一般情况为一个类中包含了这个对象的所有属性与函数,直接调用这个对象就可以对这个对象执行它可以使用的任何操作. #include <iostream> class Robot { public: Robot() :Battery(100){}//构造一个机器人的时候让机器人的电量值为满格 void speak();//发言函数 void charge();//充电函数 void function(int i);//选择功能函数 private: unsigned

【PHP面向对象(OOP)编程入门教程】2.什么是类,什么是对象,类和对象之间的关系

类的概念:类是具有相同属性和服务的一组对象的集合.它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和服务两个主要部分.在面向对象的编程语言中,类是一个独立的程序单位,它应该有一个类名并包括属性说明和服务说明两个主要部分. 对象的概念:对象是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位.一个对象由一组属性和对这组属性进行操作的一组服务组成.从更抽象 的角度来说,对象是问题域或实现域中某些事物的一个抽象,它反映该事物在系统中需要保存的信息和发挥的作用:它是一组属性和有权对这

&#8203;Python中面向对象的编程

Python面向对象的编程 1概述 (1)面向对象编程 面向对象的编程是利用"类"和"对象"来创建各种模型来实现对真实世界的描述,使用面向对象编程的原因一方面是因为它可以使程序的维护和扩展变得更简单,并且可以大大提高程序开发效率,另外,基于面向对象的程序可以使它人更加容易理解你的代码逻辑,从而使团队开发变得更从容. (2)面向对象的特征 1)类(Class):一个类即是对一类拥有相同属性的对象的抽象.蓝图.原型.在类中定义了这些对象的都具备的属性(variables

PHP面向对象(OOP)编程完全教程

转自:http://blog.snsgou.com/post-41.html 面向对象编程(OOP)是我们编程的一项基本技能,PHP5对OOP提供了良好的支持.如何使用OOP的思想来进行PHP的高级编程,对于提高PHP编程能力和规划好Web开发构架都是非常有意义的.下面我们就通过实例来说明使用PHP的OOP进行编程的实际意义和应用方法. 我们通常在做一个有数据库后台的网站的时候,都会考虑到程序需要适用于不同的应用环境.和其他编程语言有所不同的是,在PHP中,操作数据库的是一系列的具体功能函数(如

Java面向对象的编程

类的多态性: Java语言中含有方法重载与成员覆盖两种形式的多态:(区别于c++) 方法重载:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同. 成员覆盖:子类与父类允许具有相同的变量名称,但数据类型不同,允许具有相同的方法名称,但完成的功能不同. 类:class 对象:object   实例:Instance 创建属于某类的对象,需要通过下面两个步骤来实现: 1.  声明指向"由类所创建的对象"的变量 2.  利用new创建新的对象,并指派给先前所创建的变量

C++面向对象高级编程(九)Reference与重载operator new和operator delete

摘要: 技术在于交流.沟通,转载请注明出处并保持作品的完整性. 一 Reference 引用:之前提及过,他的主要作用就是取别名,与指针很相似,实现也是基于指针. 1.引用必须有初值,且不能引用nullptr 2.引用之后不能再引用别人 3.引用通常不用于声明变量,多用于参数类型,和返回值类型 见下面代码 int main(int argc, const char * argv[]) { int x=0; //p is a pointer to x int* p = &x; // r is a