前言
斷斷續(xù)續(xù)的已經(jīng)學(xué)習(xí)Swift一年多了, 從1.2到現(xiàn)在的2.2, 一直在語法之間徘徊, 學(xué)一段時(shí)間, 工作一忙, 再撿起來隔段時(shí)間又忘了.思來想去, 趁著這兩個(gè)月加班不是特別多, 就決定用swift仿寫一個(gè)完整項(xiàng)目.
花田小憩:是一個(gè)植物美學(xué)生活平臺(tái),
以自然生活為主導(dǎo),
提倡植物學(xué)生活方法,
倡導(dǎo)美學(xué)標(biāo)準(zhǔn)的生活態(tài)度的一個(gè)APP.
個(gè)人文字功底有限, 就我而言, 這款A(yù)PP做的挺唯美的...
github地址
<a >github地址</a>
聲明
此花田小憩項(xiàng)目里面的都是真實(shí)接口, 真實(shí)數(shù)據(jù), 僅供學(xué)習(xí), 毋作其他用途!!!
項(xiàng)目部分截圖
由于項(xiàng)目的大體功能都已經(jīng)實(shí)現(xiàn)了的, 所以整個(gè)項(xiàng)目還是比較龐大的.所以, 下面羅列部分功能的截圖.
由于gif錄制的時(shí)候, 會(huì)重新渲染一遍圖片, 所以導(dǎo)致項(xiàng)目中用到高斯模糊的地方, 看起來感覺比較亂, 實(shí)際效果還是不錯(cuò)的.













項(xiàng)目環(huán)境
編譯器 : Xcode7.3及以上
語言 : Swift2.2
整個(gè)項(xiàng)目都是采用純代碼開發(fā)模式
tip: 之前編譯環(huán)境這兒有點(diǎn)錯(cuò)誤, 因?yàn)槲翼?xiàng)目中用了Swift2.2的特性, 2.2之后方法名需要寫成#selector(AddAddressViewController.save), 不再使用雙引號(hào)了
第三方框架
use_frameworks!
platform :ios, "8.0"
target 'Floral' do
pod 'SnapKit', '~> 0.20.0' ## 自動(dòng)布局
pod 'Alamofire', '~> 3.3.1' ## 網(wǎng)絡(luò)請(qǐng)求, swift版的AFN
pod 'Kingfisher', '~> 2.3.1' ## 輕量級(jí)的SDWebImage
end
還用到了MBProgressHUD.
除此之外,幾乎全部都是自己造的小輪子...
目錄結(jié)構(gòu)詳解
Classes下包含7個(gè)功能目錄:
①Resources : 項(xiàng)目用到的資源,包含plist文件, js文件和字體
②Network : 網(wǎng)絡(luò)請(qǐng)求, 所有的網(wǎng)絡(luò)請(qǐng)求都在這里面, 接口和參數(shù)都有詳細(xì)的注釋
③Tool : 包含tools(工具類), 3rdLib(第三方:友盟分享, MBProgressHUD ), Category(所有項(xiàng)目用到的分類)
④Home : 首頁(專題), 包含專題分類, 詳情, 每周Top10, 評(píng)論, 分享等等功能模塊
⑤Main : UITabBarController, UINavigationController設(shè)置以及新特性
⑥Malls : 商城, 包含商城分類, 商品搜索, 詳情, 購(gòu)物車, 購(gòu)買, 訂單, 地址管理, 支付等等功能模塊
⑦Profile : 個(gè)人中心, 專欄作者, 登錄/注冊(cè)/忘記密碼, 設(shè)置等功能模塊
大家可以下載項(xiàng)目, 對(duì)照這個(gè)目錄結(jié)構(gòu)進(jìn)行查看, 很典型的MVC文件結(jié)構(gòu), 還是很方便的.
項(xiàng)目部分功能模塊詳解
① 新特性NewFeatureViewController : 這個(gè)功能模塊還是比較簡(jiǎn)單的, 用到了UICollectionViewController, 然后自己添加了UIPageControl, 只需要監(jiān)聽最后一個(gè)cell的點(diǎn)擊即可.
這兒有一個(gè)注意點(diǎn)是: 我們需要根據(jù)版本號(hào)來判斷是進(jìn)入新特性界面, 廣告頁還是首頁.
private let SLBundleShortVersionString = "SLBundleShortVersionString"
// MARK: - 判斷版本號(hào)
private func toNewFeature() -> Bool
{
// 根據(jù)版本號(hào)來確定是否進(jìn)入新特性界面
let currentVersion = NSBundle.mainBundle().infoDictionary!["CFBundleShortVersionString"] as! String
let oldVersion = NSUserDefaults.standardUserDefaults().objectForKey(SLBundleShortVersionString) ?? ""
// 如果當(dāng)前的版本號(hào)和本地保存的版本比較是降序, 則需要顯示新特性
if (currentVersion.compare(oldVersion as! String)) == .OrderedDescending{
// 保存當(dāng)前的版本
NSUserDefaults.standardUserDefaults().setObject(currentVersion, forKey: SLBundleShortVersionString)
return true
}
return false
}
② 下拉刷新RefreshControl : 在這個(gè)項(xiàng)目中, 沒有用第三方的下拉刷新控件, 而是自己實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的下拉刷新輪子, 然后賦值給UITableViewController的public var refreshControl: UIRefreshControl?屬性. 主要原理就是判斷下拉時(shí)的frame變化:
// 監(jiān)聽frame的變化
addObserver(self, forKeyPath: "frame", options:.New, context: nil)
// 刷新的時(shí)候, 不再進(jìn)行其他操作
private var isLoading = false
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
let y = frame.origin.y
// 1. 最開始一進(jìn)來的時(shí)候, 刷新按鈕是隱藏的, y就是-64, 需要先判斷掉, y>=0 , 說明刷新控件已經(jīng)完全縮回去了...
if y >= 0 || y == -64
{
return
}
// 2. 判斷是否一進(jìn)來就進(jìn)行刷新
if beginAnimFlag && (y == -60.0 || y == -124.0){
if !isLoading {
isLoading = true
animtoringFlag = true
tipView.beginLoadingAnimator()
}
return
}
// 3. 釋放已經(jīng)觸發(fā)了刷新事件, 如果觸發(fā)了, 需要進(jìn)行旋轉(zhuǎn)
if refreshing && !animtoringFlag
{
animtoringFlag = true
tipView.beginLoadingAnimator()
return
}
if y <= -50 && !rotationFlag
{
rotationFlag = true
tipView.rotationRefresh(rotationFlag)
}else if(y > -50 && rotationFlag){
rotationFlag = false
tipView.rotationRefresh(rotationFlag)
}
}
③ 高斯模糊: 使用的是系統(tǒng)自帶的高斯模糊控件UIVisualEffectView, 它是@available(iOS 8.0, *), 附一段簡(jiǎn)單的使用代碼
private lazy var blurView : BlurView = {
let blur = BlurView(effect: UIBlurEffect(style: .Light))
blur.categories = self.categories
blur.delegate = self
return blur
}()
可以根據(jù)alpha = 0.5, 調(diào)整alpha來調(diào)整模糊效果, gif圖中的高斯模糊效果不是很明顯, 實(shí)際效果特別好.

④ 商城購(gòu)物車動(dòng)畫:這組動(dòng)畫還是比較簡(jiǎn)單的, 直接附代碼, 如果有什么疑惑, 可以留言或者私信我
// MARK : - 動(dòng)畫相關(guān)懶加載
/// layer
private lazy var animLayer : CALayer = {
let layer = CALayer()
layer.contentsGravity = kCAGravityResizeAspectFill;
layer.bounds = CGRectMake(0, 0, 50, 50);
layer.cornerRadius = CGRectGetHeight(layer.bounds) / 2
layer.masksToBounds = true;
return layer
}()
/// 貝塞爾路徑
private lazy var animPath = UIBezierPath()
/// 動(dòng)畫組
private lazy var groupAnim : CAAnimationGroup = {
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = self.animPath.CGPath
animation.rotationMode = kCAAnimationRotateAuto
let expandAnimation = CABasicAnimation(keyPath: "transform.scale")
expandAnimation.duration = 1
expandAnimation.fromValue = 0.5
expandAnimation.toValue = 2
expandAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
let narrowAnimation = CABasicAnimation(keyPath: "transform.scale")
// 先執(zhí)行上面的, 然后再開始
narrowAnimation.beginTime = 1
narrowAnimation.duration = 0.5
narrowAnimation.fromValue = 2
narrowAnimation.toValue = 0.5
narrowAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
let groups = CAAnimationGroup()
groups.animations = [animation,expandAnimation,narrowAnimation]
groups.duration = 1.5
groups.removedOnCompletion = false
groups.fillMode = kCAFillModeForwards
groups.delegate = self
return groups
}()
// MARK: - 點(diǎn)擊事件處理
private var num = 0
func gotoShopCar() {
if num >= 99 {
self.showErrorMessage("親, 企業(yè)采購(gòu)請(qǐng)聯(lián)系我們客服")
return
}
addtoCar.userInteractionEnabled = false
// 設(shè)置layer
// 貝塞爾弧線的起點(diǎn)
animLayer.position = addtoCar.center
layer.addSublayer(animLayer)
// 設(shè)置path
animPath.moveToPoint(animLayer.position)
let controlPointX = CGRectGetMaxX(addtoCar.frame) * 0.5
// 弧線, controlPoint基準(zhǔn)點(diǎn), endPoint結(jié)束點(diǎn)
animPath.addQuadCurveToPoint(shopCarBtn.center, controlPoint: CGPointMake(controlPointX, -frame.size.height * 5))
// 添加并開始動(dòng)畫
animLayer.addAnimation(groupAnim, forKey: "groups")
}
// MARK: - 動(dòng)畫的代理
// 動(dòng)畫停止的代理
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if anim == animLayer.animationForKey("groups")!{
animLayer.removeFromSuperlayer()
animLayer.removeAllAnimations()
num += 1
shopCarBtn.num = num
let animation = CATransition()
animation.duration = 0.25
shopCarBtn.layer.addAnimation(animation, forKey: nil)
let shakeAnimation = CABasicAnimation(keyPath: "transform.translation.y")
shakeAnimation.duration = 0.25
shakeAnimation.fromValue = -5
shakeAnimation.toValue = 5
shakeAnimation.autoreverses = true
shopCarBtn.layer .addAnimation(shakeAnimation, forKey: nil)
addtoCar.userInteractionEnabled = true
}
}
⑤ 主題詳情頁:商城詳情頁的做法也是差不多的, 不過更簡(jiǎn)單一點(diǎn).

關(guān)鍵一點(diǎn)在于, 詳情頁的展示主要依靠于
H5頁面. 而我們需要根據(jù)webview的高度來確定webviewCell的高度.我的做法是監(jiān)聽UIWebView的webViewDidFinishLoad, 取出webView.scrollView.contentSize.height然后給詳情頁發(fā)送一個(gè)通知, 讓其刷新界面. 暫時(shí)沒有想到更好的方法, 如果您有更好的做法, 請(qǐng)務(wù)必告訴我, 謝謝...
⑥ UIWebView中圖片的點(diǎn)擊
第①步: 我們創(chuàng)建一個(gè)image.js文件, 代碼如下:
//setImage的作用是為頁面的中img元素添加onClick事件,即設(shè)置點(diǎn)擊時(shí)調(diào)用imageClick
function setImageClick(){
var imgs = document.getElementsByTagName("img");
for (var i=0;i<imgs.length;i++){
var src = imgs[i].src;
imgs[i].setAttribute("onClick","imageClick(src)");
}
document.location = imageurls;
}
//imageClick即圖片 onClick時(shí)觸發(fā)的方法,document.location = url;的作用是使調(diào)用
//webView: shouldStartLoadWithRequest: navigationType:方法,在該方法中我們真正處理圖片的點(diǎn)擊
function imageClick(imagesrc){
var url="imageClick::"+imagesrc;
document.location = url;
}
第②步:在UIWebView的代理方法webViewDidFinishLoad中, 加載JS文件, 并給圖片綁定綁定點(diǎn)擊事件
// 加載js文件
webView.stringByEvaluatingJavaScriptFromString(try! String(contentsOfURL: NSBundle.mainBundle().URLForResource("image", withExtension: "js")!, encoding: NSUTF8StringEncoding))
// 給圖片綁定點(diǎn)擊事件
webView.stringByEvaluatingJavaScriptFromString("setImageClick()")
第③步:在UIWebView的代理方法-webView:shouldStartLoadWithRequest:navigationType:中判斷圖片的點(diǎn)擊
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let urlstr = request.URL?.absoluteString
let components : [String] = urlstr!.componentsSeparatedByString("::")
if (components.count >= 1) {
//判斷是不是圖片點(diǎn)擊
if (components[0] == "imageclick") {
parentViewController?.presentViewController(ImageBrowserViewController(urls: [NSURL(string: components.last!)!], index: NSIndexPath(forItem: 0, inSection: 0)), animated: true, completion: nil)
return false;
}
return true;
}
return true
}
⑦ 登錄/注冊(cè)/忘記密碼: 眼尖一點(diǎn)的朋友可能在上面的gif中已經(jīng)發(fā)現(xiàn), 花田小憩中的登錄/注冊(cè)/忘記密碼界面幾乎是一樣的, 我的做法是用一個(gè)控制器LoginViewController來代表登錄/注冊(cè)/忘記密碼三個(gè)功能模塊, 通過兩個(gè)變量isRegister和isRevPwd來判斷是哪個(gè)功能, 顯示哪些界面, 我們點(diǎn)擊注冊(cè)和忘記密碼的時(shí)候, 會(huì)執(zhí)行代理方法:
// MARK: - LoginHeaderViewDelegate
func loginHeaderView(loginHeaderView : LoginHeaderView, clickRevpwd pwdBtn: UIButton) {
let login = LoginViewController()
login.isRevPwd = true
navigationController?.pushViewController(login, animated: true)
}
func loginHeaderView(loginHeaderView : LoginHeaderView, clickRegister registerbtn: UIButton) {
let login = LoginViewController()
login.isRegister = true
navigationController?.pushViewController(login, animated: true)
}
⑧ 驗(yàn)證碼的倒計(jì)時(shí)功能

/// 點(diǎn)擊"發(fā)送驗(yàn)證碼"按鈕
func clickSafeNum(btn: UIButton) {
var seconds = 10 //倒計(jì)時(shí)時(shí)間
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
dispatch_source_set_timer(timer,dispatch_walltime(nil, 0),1 * NSEC_PER_SEC, 0); //每秒執(zhí)行
dispatch_source_set_event_handler(timer) {
if(seconds<=0){ //倒計(jì)時(shí)結(jié)束,關(guān)閉
dispatch_source_cancel(timer);
dispatch_async(dispatch_get_main_queue(), {
//設(shè)置界面的按鈕顯示 根據(jù)自己需求設(shè)置
btn.setTitleColor(UIColor.blackColor(), forState:.Normal)
btn.setTitle("獲取驗(yàn)證碼", forState:.Normal)
btn.titleLabel?.font = defaultFont14
btn.userInteractionEnabled = true
});
}else{
dispatch_async(dispatch_get_main_queue(), {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(1)
})
dispatch_async(dispatch_get_main_queue(), {
//設(shè)置界面的按鈕顯示 根據(jù)自己需求設(shè)置
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(1)
btn.setTitleColor(UIColor.orangeColor(), forState:.Normal)
btn.setTitle("\(seconds)秒后重新發(fā)送", forState:.Normal)
btn.titleLabel?.font = UIFont.systemFontOfSize(11)
UIView.commitAnimations()
btn.userInteractionEnabled = false
})
seconds -= 1
}
}
dispatch_resume(timer)
}
⑨ 設(shè)置模塊中給我們?cè)u(píng)分
這個(gè)功能在實(shí)際開發(fā)中特別常見:

代碼如下, 很簡(jiǎn)單:
UIApplication.sharedApplication().openURL(NSURL(string: "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=998252000")!)
其中最后的id需要填寫你自己的APP在AppStore中的id, 打開iTunes找到你自己的APP或者你想要的APP, 就能查看到id.
tip: 此功能測(cè)試的時(shí)候, 必須用真機(jī)!!!
⑩ 登錄狀態(tài).
我們可以通過NSHTTPCookieStorage中的NSHTTPCookie來判斷登錄狀態(tài).也可以自定義一個(gè)字段來保存. 根據(jù)我抓包得知, 花田小憩APP的做法是第一次登錄后保存用戶名和密碼(MD5加密的, 我測(cè)試過), 然后每次啟動(dòng)應(yīng)用程序的時(shí)候, 會(huì)首先后臺(tái)自動(dòng)登錄, 然后在進(jìn)行評(píng)論/點(diǎn)贊等操作的時(shí)候呢, 參數(shù)中會(huì)帶上用戶的id.由于涉及到花田小憩的賬號(hào)密碼的一些隱私, 所以登錄/注冊(cè)模塊, 我就沒有沒有完整的寫出來. 有興趣的朋友可以私信我, 我可以把接口給你, 在此聲明: 僅供學(xué)習(xí), 毋做傷天害理之事
`tip: 我在AppDelegate.swift中給大家留了一個(gè)開關(guān), 可以快速的進(jìn)行登錄狀態(tài)的切換...
⑩+①: 個(gè)人/專欄中心: 這兩個(gè)功能是同一個(gè)控制器, 是UICollectionViewController而不是UITableViewController

大家對(duì)UITableViewController的header應(yīng)該很熟悉吧, 向上滑動(dòng)的時(shí)候, 會(huì)停留在navigationBar的下面, 雖然UICollectionViewController也可以設(shè)置header, 但是在iOS9以前, 他是不能直接設(shè)置停留的.在iOS9之后, 可以一行代碼設(shè)置header的停留
sectionHeadersPinToVisibleBounds = true
但是在iOS9之前, 我們需要自己實(shí)現(xiàn)這個(gè)功能:
//
// LevitateHeaderFlowLayout.swift
// Floral
//
// Created by ALin on 16/5/20.
// Copyright ? 2016年 ALin. All rights reserved.
// 可以讓header懸浮的流水布局
import UIKit
class LevitateHeaderFlowLayout: UICollectionViewFlowLayout {
override func prepareLayout() {
super.prepareLayout()
// 即使界面內(nèi)容沒有超過界面大小,也要豎直方向滑動(dòng)
collectionView?.alwaysBounceVertical = true
// sectionHeader停留
if #available(iOS 9.0, *) {
sectionHeadersPinToVisibleBounds = true
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// 1. 獲取父類返回的UICollectionViewLayoutAttributes數(shù)組
var answer = super.layoutAttributesForElementsInRect(rect)!
// 2. 如果是iOS9.0以上, 直接返回父類的即可. 不用執(zhí)行下面的操作了. 因?yàn)槲覀冎苯釉O(shè)置sectionHeadersPinToVisibleBounds = true即可
if #available(iOS 9.0, *) {
return answer
}
// 3. 如果是iOS9.0以下的系統(tǒng)
// 以下代碼來源:http://stackoverflow.com/questions/13511733/how-to-make-supplementary-view-float-in-uicollectionview-as-section-headers-do-i%3C/p%3E
// 目的是讓collectionview的header可以像tableview的header一樣, 可以停留
// 創(chuàng)建一個(gè)索引集.(NSIndexSet:唯一的,有序的,無符號(hào)整數(shù)的集合)
let missingSections = NSMutableIndexSet()
// 遍歷, 獲取當(dāng)前屏幕上的所有section
for layoutAttributes in answer {
// 如果是cell類型, 就加入索引集里面
if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) {
missingSections.addIndex(layoutAttributes.indexPath.section)
}
}
// 遍歷, 將屏幕中擁有header的section從索引集中移除
for layoutAttributes in answer {
// 如果是header, 移掉所在的數(shù)組
if (layoutAttributes.representedElementKind == UICollectionElementKindSectionHeader) {
missingSections .removeIndex(layoutAttributes.indexPath.section)
}
}
// 遍歷當(dāng)前屏幕沒有header的索引集
missingSections.enumerateIndexesUsingBlock { (idx, _) in
// 獲取section中第一個(gè)indexpath
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
// 獲取其UICollectionViewLayoutAttributes
let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
// 如果有值, 就添加到UICollectionViewLayoutAttributes數(shù)組中去
if let _ = layoutAttributes{
answer.append(layoutAttributes!)
}
}
// 遍歷UICollectionViewLayoutAttributes數(shù)組, 更改header的值
for layoutAttributes in answer {
// 如果是header, 改變其參數(shù)
if (layoutAttributes.representedElementKind==UICollectionElementKindSectionHeader) {
// 獲取header所在的section
let section = layoutAttributes.indexPath.section
// 獲取section中cell總數(shù)
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)
// 獲取第一個(gè)item的IndexPath
let firstObjectIndexPath = NSIndexPath(forItem: 0, inSection: section)
// 獲取最后一個(gè)item的IndexPath
let lastObjectIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)
// 定義兩個(gè)變量來保存第一個(gè)和最后一個(gè)item的layoutAttributes屬性
var firstObjectAttrs : UICollectionViewLayoutAttributes
var lastObjectAttrs : UICollectionViewLayoutAttributes
// 如果當(dāng)前section中cell有值, 直接取出來即可
if (numberOfItemsInSection > 0) {
firstObjectAttrs =
self.layoutAttributesForItemAtIndexPath(firstObjectIndexPath)!
lastObjectAttrs = self.layoutAttributesForItemAtIndexPath(lastObjectIndexPath)!
} else { // 反之, 直接取header和footer的layoutAttributes屬性
firstObjectAttrs = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstObjectIndexPath)!
lastObjectAttrs = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastObjectIndexPath)!
}
// 獲取當(dāng)前header的高和origin
let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin
origin.y = min(// 2. 要保證在即將消失的臨界點(diǎn)跟著消失
max( // 1. 需要保證header懸停, 所以取最大值
collectionView!.contentOffset.y + collectionView!.contentInset.top,
(CGRectGetMinY(firstObjectAttrs.frame) - headerHeight)
),
(CGRectGetMaxY(lastObjectAttrs.frame) - headerHeight)
)
// 默認(rèn)的層次關(guān)系是0. 這兒設(shè)置大于0即可.為什么設(shè)置成1024呢?因?yàn)槲覀兪浅绦蛟?..
layoutAttributes.zIndex = 1024
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
}
}
return answer;
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
// 返回true, 表示一旦進(jìn)行滑動(dòng), 就實(shí)時(shí)調(diào)用上面的-layoutAttributesForElementsInRect:方法
return true
}
}
⑩+@end: 整個(gè)項(xiàng)目, 東西還是蠻多的, 也不是僅僅幾百上千字能說清楚的, 幾乎每一個(gè)頁面, 每一個(gè)文件, 我都有詳細(xì)的中文注釋. 希望大家一起進(jìn)步. 這也是我的第一個(gè)開源的完整的Swift 項(xiàng)目, 有什么不足或者錯(cuò)誤的地方, 希望大家指出來, 萬分感激!!!
下載地址
<a >github地址</a>
如果對(duì)您有些許幫助, 請(qǐng)<a >☆star</a>
后續(xù)
可能有些功能模塊存在bug, 后續(xù)我都會(huì)一一進(jìn)行修復(fù)和完善的, 并更新在github上.
如果您有任何疑問,或者發(fā)現(xiàn)bug以及不足的地方, 可以在下面給我留言, 或者關(guān)注我的新浪微博, 給我私信.
聯(lián)系我
<a >github</a>
<a >微博</a>
<a href="http://www.itdecent.cn/users/9723687edfb5/latest_articles">簡(jiǎn)書</a>