swift实现一个小说阅读器(三)

96
butcheryl
0.1 2015.10.27 15:29* 字数 985

小说列表搞定,接下来就要弄小说的内容和目录了。因为之前的网络请求使用系统的NSData是同步请求,所以我们先改造一下我们的网络请求,和加一个Loading。


准备


Alamofire

在swift中替代AFNetworking的三方库,因为与Ono是一个作者并且基于Alamofire可拓展性所以可以无缝结合。

SVProgressHUD

因为是异步加载所以在等待过程中要有一个Loading,并且可以阻断用户接下来的操作。

SnapKit

虽然大部分界面是用AutoLayout来完成,但是不得不说有一些界面的布局还是手写的更方便!
Swift版本的Masonry,语法一模一样。关于Masonry看下面两篇博文入门起来应该很容易:

Masonry介绍与使用实践(快速上手Autolayout) | 里脊串的开发随笔 2014-09-28
如何使用Masonry设计复合型cell | 里脊串的开发随笔 2015-06-08

改造网络请求



新建一个Swift File,起个名字HTMLResponseSerializer.swift
在里面加入如下代码:

import Ono
import Alamofire
import Foundation

extension Request {
    public static func HTMLResponseSerializer() -> ResponseSerializer<ONOXMLDocument, NSError> {
        return ResponseSerializer { request, response, data, error in
            guard error == nil else {
                return .Failure(error!)
            }
            
            guard let validData = data else {
                let failureReason = "Data could not be serialized. Input data was nil."
                let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
                return .Failure(error)
            }

            // 将gbk编码的data转换成UTF-8的字符串
            let content = NSString(data: validData, encoding: kContentEncoding) as? String
            guard let validContent = content else {
                let failureReason = "Data could not be serialized. Convert data was nil."
                let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
                return .Failure(error)
            }
            
            do {
                // 创建 document
                let HTML = try ONOXMLDocument(string: validContent, encoding: NSUTF8StringEncoding)
                return .Success(HTML)
            } catch {
                return .Failure(error as NSError)
            }
        }
    }
    
    public func responseHTMLDocument(completionHandler: Response<ONOXMLDocument, NSError> -> Void) -> Self {
        return response(responseSerializer: Request.HTMLResponseSerializer(), completionHandler: completionHandler)
    }
}

更多的自定义 Response Serialization 可以看这个官方文档:Creating a Custom Response Serializer
ViewController.swift中把viewDidLoad()中把之前写的加载小说列表的代码替换成下面的代码

    SVProgressHUD.show()
    Alamofire.request(.GET, "http://m.ybdu.com/quanben/1").responseHTMLDocument { (response) -> Void in
        if let document = response.result.value {
            // 根据CSS规则检索节点并使用闭包遍历所有检索结果
            document.enumerateElementsWithCSS(".list p", usingBlock: { (element: ONOXMLElement!, _, _) -> Void in
                let bookElement = element.children.first as! ONOXMLElement
                let bookHref = (bookElement["href"] as! String).stringByReplacingOccurrencesOfString("/xiazai", withString: "")
                self.books.append(Book(uri: bookHref, name: bookElement.stringValue(), author: nil))
            })
            self.tableView.reloadData()
            SVProgressHUD.dismiss()
        }
    }

跳转到小说详情页



ViewController.swift中加入下面代码:

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
    let indexPath = self.tableView.indexPathForCell(sender as! UITableViewCell)!
    
    let vc = segue.destinationViewController as! ArticlesViewController
    vc.book = self.books[indexPath.row]
}

Main.storyboard拖出一个UIViewController,从ViewController上的UITableViewCell拉一根线到新建的UIViewController中在出现的黑框中选择
Selection Segue -> Show

新建一个类ArticlesViewController.swift,设置给刚才拖出来的UIViewController
在上面添加如下控件,并设置好约束。
给[换源][缓存][目录][设置]四个按钮设置tag按照顺序0-4
最后将 顶部工具条底部工具条 隐藏


将 顶部工具条 和 底部工具条 以及 章节名称拖到ArticlesViewController.swift中设置成为属性

@IBOutlet weak var topToolBar: UIView!
@IBOutlet weak var bottomToolBar: UIView!
@IBOutlet weak var header: UILabel!

将四个按钮拖到ArticlesViewController.swift中设置成为方法

@IBAction func items_click(sender: UIButton) {
    switch sender.tag {
    case 0:
        print("换源")
    case 1:
        print("缓存")
    case 2:
        print("目录")
    case 3:
        print("设置")
    default:
        break
    }
}

在这个界面使用UIPageViewController,每一章小说是一个由UIPageViewController管理的viewController。在代码中加入这个属性

lazy var pageViewController: UIPageViewController! = {
    var pageViewController = UIPageViewController(transitionStyle: .PageCurl, navigationOrientation: .Horizontal, options: nil)
    pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height)
    pageViewController.setViewControllers([UIViewController()], direction: .Forward, animated: true, completion: nil)
    pageViewController.delegate = self
    pageViewController.dataSource = self
    
    return pageViewController
}()

上面的属性使用了 lazy 关键字,具体看: Swift-属性-存储属性-延迟存储属性

并且要遵循协议

class ArticlesViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {}

新建一个类 ArticlesDetailViewController.swift:这个页面负责显示小说的内容
加入三个属性和一个便利构造器:

lazy var label: UILabel = {
    let label = UILabel()
    label.backgroundColor = UIColor.whiteColor()
    label.font = UIFont.systemFontOfSize(13)
    label.textColor = UIColor.lightGrayColor()
    self.view.addSubview(label)
    return label
}()

lazy var textView: UITextView = {
    let textView = UITextView()
    textView.frame = CGRectMake(0, 30, self.view.bounds.size.width, self.view.bounds.size.height - 30)
    textView.backgroundColor = UIColor.whiteColor()
    textView.editable = false
    textView.font = UIFont.systemFontOfSize(15)
    self.view.addSubview(textView)
    return textView
}()

var url: String!

convenience init(url: String!) {
    self.init()
    self.url = url
}

viewDidLoad()中加入下面代码:(SnapKit 添加约束的方法上面有说)

    self.label.snp_makeConstraints { (make) -> Void in
        make.top.left.right.equalTo(0)
        make.height.equalTo(30)
    }
    
    self.textView.snp_makeConstraints { (make) -> Void in
        make.top.equalTo(self.label.snp_bottom)
        make.left.right.bottom.equalTo(0)
    }
    
    // 请求当前章节的内容
    Alamofire.request(.GET, self.url).responseHTMLDocument({ (response) -> Void in
        if let document = response.result.value {
            document.enumerateElementsWithXPath(".//*[@id='nr_title']", usingBlock: { (element:ONOXMLElement!, _, _) -> Void in
                self.label.text = element.stringValue()
            })
            
            document.enumerateElementsWithXPath(".//*[@id='txt']", usingBlock: { (element:ONOXMLElement!, _, _) -> Void in
                self.textView.text = element.stringValue()
            })
            SVProgressHUD.dismiss()
        }
    })

回到ArticlesViewController.swift中,加入下面代码:

func load_catalogue() {
    var catalogues = [(uri: String, title: String)]()
    SVProgressHUD.show()
    // 获取当前小说的目录列表
    Alamofire.request(.GET, self.book.catalogueURL()).responseHTMLDocument { (response) -> Void in
        if let document = response.result.value {
            // 根据CSS规则检索节点并使用闭包遍历所有检索结果
            document.enumerateElementsWithCSS(".mulu_list", usingBlock: { (element: ONOXMLElement!, _, _) -> Void in
                for children in element.children as! [ONOXMLElement]! {
                    let aElement = children.firstChildWithTag("a")
                    if aElement != nil {
                        catalogues.append((uri: (aElement["href"] as! String), title: aElement.stringValue()))
                    }
                }
            })
            self.book.catalogues = catalogues
            
            // 加载第一章的内容
            let vc = ArticlesDetailViewController(url: self.book.chapterURL(0))
            
            // 显示到界面上
            self.pageViewController.setViewControllers([vc], direction: .Forward, animated: false, completion: nil)
        }
    }
}

在viewDidLoad()中加入下面代码:

    // 隐藏系统的导航栏
    self.navigationController?.navigationBarHidden = true
    
    self.addChildViewController(self.pageViewController)
    self.view.insertSubview(self.pageViewController.view, atIndex: 0)
    self.pageViewController.didMoveToParentViewController(self)
    self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action:"background_click:"))
    
    self.load_catalogue()

self.view添加一个点击事件,来控制隐藏和显示刚才我们加的章节名称和底部四个按钮。刚开始我是在self.view上添加的一个沾满屏幕的大的透明按钮来达到这样的目的,但是这样这个按钮会把pageViewContronller的滑动也拦截下来影响下面的pageViewController翻页。但是pageViewController没有拦截tap手势,直接将手势传递给下一个响应者了。所以直接在self.view中添加一个点击事件就行了。

func background_click(sender: AnyObject) {
    self.header.text = self.book.name
    
    self.topToolBar.hidden = !self.topToolBar.hidden
    self.bottomToolBar.hidden = !self.bottomToolBar.hidden
    // 使状态栏的文字颜色重新刷新
    self.setNeedsStatusBarAppearanceUpdate()
}

因为状态栏的文字颜色是黑色的,但是标题栏的背景也是黑色的所以在显示标题栏的时候要将状态栏的颜色改成白色

override func prefersStatusBarHidden() -> Bool {
    return self.topToolBar == nil ? true : self.topToolBar.hidden
}

下面是pageViewController翻页的协议方法

func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    if self.book.currentChapterNumber == 0 {
        return nil
    } else {
        self.book.currentChapterNumber--
    }
    
    return ArticlesDetailViewController(url: self.book.chapterURL())
}

func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    if self.book.currentChapterNumber == self.book.catalogues!.count - 1 {
        return nil
    } else {
        self.book.currentChapterNumber++
    }
    
    return ArticlesDetailViewController(url: self.book.chapterURL())
}

之前写的Model类Book已经不能满足我们了,要加2个属性和3个方法:

var catalogues: [(uri: String, title: String)]?
var currentChapterNumber: Int = 0

func catalogueURL() -> String! {
    return "http://www.ybdu.com/xiaoshuo" + self.uri
}

func chapterURL(index: Int = -1) -> String! {
    if index == -1 {
        return self.chapterURL(self.currentChapterNumber)
    }
    
    if index >= self.catalogues?.count {
        return ""
    }
    
    guard let catalogue = self.catalogues?[index] else {
        return ""
    }
    return "http://m.ybdu.com/xiaoshuo" + self.uri + catalogue.uri
}

func chapterHeader(index: Int = -1) -> String! {
    if index == -1 {
        return self.chapterHeader(self.currentChapterNumber)
    }
    
    if index >= self.catalogues?.count {
        return ""
    }
    
    guard let catalogue = self.catalogues?[index] else {
        return ""
    }
    return catalogue.title
}

这里用到了函数的默认参数值


小说目录

新建CatalogueListViewController.swift继承自UITableViewController
添加如下属性和方法:

var items: [(uri: String, title: String)]!

convenience init(catalogue: [(uri: String, title: String)]) {
    self.init()
    self.items = catalogue
}

在viewDidLoad()中给tableView注册一个cell,在左上角添加一个关闭按钮

override func viewDidLoad() {
    super.viewDidLoad()
    
    // Do any additional setup after loading the view.
    
    self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .Plain, target: self, action: "backButton_click:")
}

func backButton_click(sender: UIBarButtonItem) {
    self.dismissViewControllerAnimated(true) {}
}

重写tableView协议方法

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.items.count
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 44
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell")
    cell!.textLabel?.text = "\(self.items[indexPath.row].title)"
    return cell!
}

ArticlesViewController.swift中改写@IBAction func items_click(sender: UIButton) {}方法
print("目录")替换如下代码:

        guard let catalogues = self.book.catalogues where catalogues.count > 0 else {
            // 目录条数为空
            return
        }
        
        let vc = CatalogueListViewController(catalogue: catalogues)
        let navi = UINavigationController(rootViewController: vc)
        self.presentViewController(navi, animated: true, completion: { () -> Void in
            navi.title = self.book.name
        })

效果




源码



github: YYReader
master分支->tag 0.3

日记本