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


小说列表搞定,接下来就要弄小说的内容和目录了。因为之前的网络请求使用系统的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,966评论 4 60
  • 01 八十年代初期,农村实行了生产责任制。农民劳动的积极性很高,在自己的责任田里精耕细作。彻底改变了在生产队集体劳...
    欣然_bd23阅读 424评论 6 12
  • 门外的大雪,漆黑的夜色。 1996年12月03日晚十点二十,一个小柴房的门被打开,一个中年男子掺着一个微胖的大着肚...
    海绵O我是宝宝阅读 233评论 0 0