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 } }
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 } }
我们能在传入我们动画协调者的 transitionContext 对象中调用这些方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
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 } }
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)))); } } Additionally, if we know Self is a UICollectionViewController we can eliminate the method's scrollView parameter.
// 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) } }
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) } } } }
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) } } } }
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 } }
由于我们不能在扩展中存储属性,我们还是不得不去依赖我们 Unique 的接受者声明 id 类型的属性。另外,你可能注意到了,我只为那些那些继承自 NSObject 类型的对象扩展了 Unique 协议。否则我们就不能调用 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 } } }
// 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 } }
足够简单。这里有几个我们将要构建成符合 StringRepresentable 协议的 model 对象。
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! }
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) } }
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 5 6 7 8 9 10 11 12 13 14 15
let a = Artist() a.name = "Bob Marley" a.instrument = "Guitar / Vocals" a.bio = "Every little thing's gonna be alright." Since all UIButton instances have been extended to adopt StringDisplay, we can call the displayStringValue: method on them.
let smallButton = UIButton(frame: CGRectMake(0.0, 0.0, 120.0, 40.0)) smallButton.displayStringValue(a)
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 的环境中去。
// Any entity which supports protocol-based styling. protocol Styled { func updateStyles() } Then we'll make some sub-protocols which define different types of styles we'd like to use.
protocol BackgroundColor : Styled { var color: UIColor { get } }
protocol FontWeight : Styled { var size: CGFloat { get } var bold: Bool { get } }
我们已经在 Pear Music 上工作了一段时间,而且我们的唱片,艺人,歌曲和播放列表有着很棒的 UI。我们仍然用着美妙的协议和扩展,让它们为 MVC 的所有层面上都提供着便捷。现在 Pear 的 CEO 已经要求我们着手构建2.0版本了…我们面临一个新奇的竞争对手的挑战,它叫 Apple Music。
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()) } }
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 } }
// 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) } } }