Swift小仿微博列表

前言

    鑑於目前Swift的ABI(應用程序二進制接口)、API(應用程序編程接口) 基本穩定,對於Swift的學習有必要提上日程了,這個Swift仿微博列表的效果是我最近一邊學習《Swift入門到精通-李明傑》 一邊練手的Demo,Swift新手還請關照~🤝
    這個示例的主要內容有三個方面:
    1、UITextView富文本的實現
    2、圖片轉場和瀏覽動畫
    3、界面流暢度優化html

富文本點擊效果
圖集瀏覽效果

1、UITextView富文本的實現

  • 標題的富文本顯示樣式我是參考微博的:@用戶暱稱、#話題#、圖標+描述、[表情]、全文:限制顯示字數,點擊連接跳轉或查看圖片

好比第一條數據的標題原始字符串爲@wsl2ls: 不要迷戀哥,哥只是一個傳說 github.com/wsl2ls, 是終將要成爲#海賊王#的男人!// @蜜桃君🏀: 🦆你真的太帥了[愛你] github.com/wsl2ls // @且行且珍惜_iOS: 發起了話題#我是一隻帥哥#不信點我看看 www.jianshu.com/u/e15d1f644… , 相信我,不會讓你失望滴O(∩_∩)O哈! ——> 正則匹配後富文本顯示爲@wsl2ls: 不要迷戀哥,哥只是一個傳說 查看圖片, 是終將要成爲#海賊王#的男人!// @蜜桃君🏀: 🦆你真的太帥了 查看圖片 // @且行且珍惜_iOS: 發起了話題#我是一隻帥哥#不信點我看看 查看圖片 , 相信我,不會讓你失望滴O(∩_∩)O哈!ios

//正則匹配規則
let KRegularMatcheHttpUrl = "((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)" // 圖標+描述 替換HTTP連接
let KRegularMatcheTopic = "#[^#]+#"    // 話題匹配 #話題#
let KRegularMatcheUser = "@[\\u4e00-\\u9fa5a-zA-Z0-9_-]*"  // @用戶匹配
let KRegularMatcheEmotion = "\\[[^ \\[\\]]+?\\]"   //表情匹配 [愛心]
複製代碼
  • 富文本是由原始字符串通過一系列的正則匹配到目標字符串後,再通過一系列的字符串高亮、刪除、替換等處理獲得的

注意:每個匹配項完成字符串處理後可能會改變原有字符串的NSRange,進而致使另外一個匹配項的Range在處理字符串時出現越界的崩潰問題!git

//標題正則匹配結果
    func matchesResultOfTitle(title: String, expan: Bool) -> (attributedString : NSMutableAttributedString , height : CGFloat) {
        //原富文本標題
        var attributedString:NSMutableAttributedString = NSMutableAttributedString(string:title)
        //原富文本的範圍
        let titleRange = NSRange(location: 0, length:attributedString.length)
        //最大字符 截取位置
        var cutoffLocation = KTitleLengthMax
        //圖標+描述 替換HTTP連接
        let urlRanges:[NSRange] = getRangesFromResult(regexStr:KRegularMatcheHttpUrl, title: title)
        for range in urlRanges {
            let attchimage:NSTextAttachment = NSTextAttachment()
            attchimage.image = UIImage.init(named: "photo")
            attchimage.bounds = CGRect.init(x: 0, y: -2, width: 16, height: 16)
            let replaceStr : NSMutableAttributedString = NSMutableAttributedString(attachment: attchimage)
            replaceStr.append(NSAttributedString.init(string: "查看圖片")) //添加描述
            replaceStr.addAttributes([NSAttributedString.Key.link :"http://img.wxcha.com/file/201811/21/afe8559b5e.gif"], range: NSRange(location: 0, length:replaceStr.length ))
            //注意:涉及到文本替換的 ,每替換一次,原有的富文本位置發生改變,下一輪替換的起點須要從新計算!
            let newLocation = range.location - (titleRange.length - attributedString.length)
            //圖標+描述 替換HTTP連接字符
            attributedString.replaceCharacters(in: NSRange(location: newLocation, length: range.length), with: replaceStr)
            //若是最多字符個數會截斷高亮字符,則捨去高亮字符
            if cutoffLocation >= newLocation && cutoffLocation <= newLocation + range.length {
                cutoffLocation = newLocation
            }
        }
        //話題匹配
        let topicRanges:[NSRange] = getRangesFromResult(regexStr: KRegularMatcheTopic, title: attributedString.string)
        for range in topicRanges {
        attributedString.addAttributes([NSAttributedString.Key.link :"https://github.com/wsl2ls"], range: range)
            //若是最多字符個數會截斷高亮字符,則捨去高亮字符
            if cutoffLocation >= range.location && cutoffLocation <= range.location + range.length {
                cutoffLocation = range.location
            }
        }
        //@用戶匹配
        let userRanges:[NSRange] = getRangesFromResult(regexStr: KRegularMatcheUser,title: attributedString.string)
        for range in userRanges {
   attributedString.addAttributes([NSAttributedString.Key.link :"https://www.jianshu.com/u/e15d1f644bea"], range: range)
            //若是最多字符個數會截斷高亮字符,則捨去高亮字符
            if cutoffLocation >= range.location && cutoffLocation <= range.location + range.length {
                cutoffLocation = range.location
            }
        }
        //表情匹配
        let emotionRanges:[NSRange] = getRangesFromResult(regexStr: KRegularMatcheEmotion,title: attributedString.string)
        //通過上述的匹配替換後,此時富文本的範圍
        let currentTitleRange = NSRange(location: 0, length:attributedString.length)
        for range in emotionRanges {
            //表情附件
            let attchimage:NSTextAttachment = NSTextAttachment()
            attchimage.image = UIImage.init(named: "愛你")
            attchimage.bounds = CGRect.init(x: 0, y: -2, width: 16, height: 16)
            let stringImage : NSAttributedString = NSAttributedString(attachment: attchimage)
            //注意:每替換一次,原有的位置發生改變,下一輪替換的起點須要從新計算!
            let newLocation = range.location - (currentTitleRange.length - attributedString.length)
            //圖片替換表情文字
            attributedString.replaceCharacters(in: NSRange(location: newLocation, length: range.length), with: stringImage)
            //若是最多字符個數會截斷高亮字符,則捨去高亮字符
            //字符替換以後,截取位置也要更新
            cutoffLocation -= (currentTitleRange.length - attributedString.length)
            if cutoffLocation >= newLocation && cutoffLocation <= newLocation + range.length {
                cutoffLocation = newLocation
            }
        }
        //超出字符個數限制,顯示全文
        if attributedString.length > cutoffLocation {
            var fullText: NSMutableAttributedString
            if expan {
attributedString.append(NSAttributedString(string:"\n"))
                fullText = NSMutableAttributedString(string:"收起")
                fullText.addAttributes([NSAttributedString.Key.link :"FullText"], range: NSRange(location:0, length:fullText.length ))
            }else {
                attributedString = attributedString.attributedSubstring(from: NSRange(location: 0, length: cutoffLocation)) as! NSMutableAttributedString
                fullText = NSMutableAttributedString(string:"...全文")
                fullText.addAttributes([NSAttributedString.Key.link :"FullText"], range: NSRange(location:3, length:fullText.length - 3))
            }
            attributedString.append(fullText)
        }
        //段落
        let paragraphStyle : NSMutableParagraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineSpacing = 4 //行間距   attributedString.addAttributes([NSAttributedString.Key.paragraphStyle :paragraphStyle, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)], range: NSRange(location:0, length:attributedString.length))
        //元組
        let attributedStringHeight = (attributedString, heightOfAttributedString(attributedString))
        return attributedStringHeight
    }
    //根據匹配規則返回全部的匹配結果
    fileprivate func getRangesFromResult(regexStr : String, title: String) -> [NSRange] {
        // 0.匹配規則
        let regex = try? NSRegularExpression(pattern:regexStr, options: [])
        // 1.匹配結果
        let results = regex?.matches(in:title, options:[], range: NSRange(location: 0, length: NSAttributedString(string: title).length))
        // 2.遍歷結果 數組
        var ranges = [NSRange]()
        for res in results! {
            ranges.append(res.range)
        }
        return ranges
    }
    //計算富文本的高度
    func heightOfAttributedString(_ attributedString: NSAttributedString) -> CGFloat {
        let height : CGFloat =  attributedString.boundingRect(with: CGSize(width: UIScreen.main.bounds.size.width - 15 * 2, height: CGFloat(MAXFLOAT)), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).height
        return ceil(height)
    }  
 }

複製代碼

2、圖片轉場和瀏覽動畫

  • 圖片的轉場動畫以及捏合放大縮小、觸摸點雙擊放大縮小、拖拽過渡轉場等圖集瀏覽動畫 是參考微信的效果來實現的,通過不斷反覆的去用和觀察微信的動畫,逐漸完善代碼邏輯和動畫效果。

自定義轉場動畫的實現能夠看下我以前的文章iOS 自定義轉場動畫,這裏我說一下動畫視圖的構造和圖集瀏覽手勢動畫。github

  • 一、列表頁cell中的imageView的大小是固定平均分配的,而每張圖片的大小和比例都是不同的,爲了保證圖片不變形,按比例只展現圖片的中心部分,怎麼作哪? 截取image的中心部分賦給ImageView ? 給imageView包一層View,而後設置view.clipsToBounds=true? NO!!!能夠經過設置imageView.layer.contentsRect 來實現,這個也是以下所示的慢放漸變更畫效果的關鍵。
if (image.size.height/image.size.width > 3) {
    //大長圖 僅展現頂部部份內容
     let proportion: CGFloat = height/(width * image.size.height/image.size.width)
    imageView.layer.contentsRect = CGRect(x: 0, y: 0, width: 1, height: proportion)
    } else if image.size.width >= image.size.height {
     // 寬>高
     let proportion: CGFloat = width/(height * image.size.width/image.size.height)
    imageView.layer.contentsRect = CGRect(x: (1 - proportion)/2, y: 0, width: proportion, height: 1)
    }else if image.size.width < image.size.height {
    //寬<高
let proportion: CGFloat = height/(width * image.size.height/image.size.width)
  imageView.layer.contentsRect = CGRect(x: 0, y: (1 - proportion)/2, width: 1, height: proportion)
}

複製代碼

轉場漸變更畫.gif

//漸變更畫
        UIView.animate(withDuration: self.transitionDuration(using: transitionContext), animations: {
            if(self.toAnimatonView!.frame != CGRect.zero) {
                self.fromAnimatonView?.frame = self.toAnimatonView!.frame
                self.fromAnimatonView?.layer.contentsRect = self.toAnimatonView!.layer.contentsRect
            }else {   
            }
        }) { (finished) in
            toView.isHidden = false
            bgView.removeFromSuperview()
            self.fromAnimatonView?.removeFromSuperview()
            transitionContext.completeTransition(true)
        }
    }

複製代碼
  • 二、圖集瀏覽頁面的動畫包括: 捏合放大縮小、觸摸點雙擊放大縮小、拖拽過渡轉場。 捏合放大縮小動畫是由繼承於UIScrollView的子類SLPictureZoomView完成;觸摸點雙擊放大是根據觸摸點在圖片的位置和屏幕上的位置獲得放大後的觸摸點相對位置來實現的;拖拽過渡轉場是根據手指在屏幕上的移動距離來調整SLPictureZoomView的大小和中心點位置的,詳情看代碼。

注意手勢衝突:編程

//解決 self.pictureZoomView 和UICollectionView 手勢衝突
  self.pictureZoomView.isUserInteractionEnabled = false;  
  self.contentView.addGestureRecognizer(self.pictureZoomView.panGestureRecognizer)     
  self.contentView.addGestureRecognizer(self.pictureZoomView.pinchGestureRecognizer!)
複製代碼

3、界面流暢度優化

網上關於界面流暢度優化的好文章仍是挺多的,我在這裏只記錄下本文示例中用到的部分優化策略,基本上FPS在60左右, 詳情能夠看代碼: 一、cell高度異步計算和緩存 二、富文本異步正則匹配和結果緩存 三、數組緩存九宮格圖片視圖以複用 四、圖片降採樣和預加載 五、減小視圖層級 六、減小沒必要要的數據請求數組

👁代碼傳送門 ——> Swift仿微博列表緩存

推薦閱讀
YYKit - iOS 保持界面流暢的技巧
iOS 自定義轉場動畫
iOS 圖片瀏覽的放大縮小
UIScrollView視覺差動畫bash

若是須要跟我交流的話:
※ Github: github.com/wsl2ls 
※ 簡書:www.jianshu.com/u/e15d1f644… 
※ 微信公衆號:iOS2679114653
※ QQ:1685527540微信

相關文章
相關標籤/搜索