斷斷續續的已經學習Swift
一年多了, 從1.2
到如今的2.1
, 一直在語法之間徘徊, 學一段時間, 工做一忙, 再撿起來隔段時間又忘了.思來想去, 趁着這兩個月加班不是特別多, 就決定用swift
仿寫一個完整項目.ios
花田小憩:是一個植物美學生活平臺, 以天然生活爲主導, 提倡植物學生活方法, 倡導美學標準的生活態度的一個APP.
我的文字功底有限, 就我而言, 這款APP作的挺惟美
的...git
此花田小憩
項目裏面的都是真實接口, 真實數據, 僅供學習, 毋做其餘用途!!!github
簡書web
因爲項目的大致功能都已經實現了的, 因此整個項目仍是比較龐大的.因此, 下面羅列部分功能的截圖.
因爲gif錄製的時候, 會從新渲染一遍圖片, 因此致使項目中用到高斯模糊
的地方, 看起來感受比較亂, 實際效果仍是不錯的.swift
編譯器 : Xcode7.2數組
語言 : Swift2.1瀏覽器
整個項目都是採用純代碼
開發模式網絡
use_frameworks! platform :ios, "8.0" target 'Floral' do pod 'SnapKit', '~> 0.20.0' ## 自動佈局 pod 'Alamofire', '~> 3.3.1' ## 網絡請求, swift版的AFN pod 'Kingfisher', '~> 2.3.1' ## 輕量級的SDWebImage end
還用到了MBProgressHUD
.
除此以外,幾乎所有都是本身造的小輪子
...app
Classes
下包含7個功能目錄:框架
①Resources
: 項目用到的資源,包含plist文件
, js文件
和字體
②Network
: 網絡請求, 全部的網絡請求都在這裏面, 接口
和參數
都有詳細的註釋
③Tool
: 包含tools(工具類)
, 3rdLib(第三方:友盟分享, MBProgressHUD )
, Category(全部項目用到的分類)
④Home
: 首頁(專題), 包含專題分類
, 詳情
, 每週Top10
, 評論
, 分享
等等功能模塊
⑤Main
: UITabBarController
, UINavigationController
設置以及新特性
⑥Malls
: 商城, 包含商城分類
, 商品搜索
, 詳情
, 購物車
, 購買
, 訂單
, 地址管理
, 支付
等等功能模塊
⑦Profile
: 我的中心, 專欄做者, 登陸/註冊/忘記密碼, 設置等功能模塊
你們能夠下載項目, 對照這個目錄結構進行查看, 很典型的MVC
文件結構, 仍是很方便的.
NewFeatureViewController
: 這個功能模塊仍是比較簡單的, 用到了UICollectionViewController
, 而後本身添加了UIPageControl
, 只須要監聽最後一個cell的點擊便可.這兒有一個注意點是: 咱們須要根據版本號來判斷是進入新特性界面, 廣告頁仍是首頁.
private let SLBundleShortVersionString = "SLBundleShortVersionString" // MARK: - 判斷版本號 private func toNewFeature() -> Bool { // 根據版本號來肯定是否進入新特性界面 let currentVersion = NSBundle.mainBundle().infoDictionary!["CFBundleShortVersionString"] as! String let oldVersion = NSUserDefaults.standardUserDefaults().objectForKey(SLBundleShortVersionString) ?? "" // 若是當前的版本號和本地保存的版本比較是降序, 則須要顯示新特性 if (currentVersion.compare(oldVersion as! String)) == .OrderedDescending{ // 保存當前的版本 NSUserDefaults.standardUserDefaults().setObject(currentVersion, forKey: SLBundleShortVersionString) return true } return false }
RefreshControl
: 在這個項目中, 沒有用第三方的下拉刷新控件, 而是本身實現了一個簡單的下拉刷新輪子, 而後賦值給UITableViewController
的public var refreshControl: UIRefreshControl?
屬性. 主要原理就是判斷下拉時的frame
變化:// 監聽frame的變化 addObserver(self, forKeyPath: "frame", options:.New, context: nil)
// 刷新的時候, 再也不進行其餘操做 private var isLoading = false override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { let y = frame.origin.y // 1. 最開始一進來的時候, 刷新按鈕是隱藏的, y就是-64, 須要先判斷掉, y>=0 , 說明刷新控件已經徹底縮回去了... if y >= 0 || y == -64 { return } // 2. 判斷是否一進來就進行刷新 if beginAnimFlag && (y == -60.0 || y == -124.0){ if !isLoading { isLoading = true animtoringFlag = true tipView.beginLoadingAnimator() } return } // 3. 釋放已經觸發了刷新事件, 若是觸發了, 須要進行旋轉 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) } }
高斯模糊
: 使用的是系統自帶的高斯模糊控件UIVisualEffectView
, 它是@available(iOS 8.0, *)
, 附一段簡單的使用代碼private lazy var blurView : BlurView = { let blur = BlurView(effect: UIBlurEffect(style: .Light)) blur.categories = self.categories blur.delegate = self return blur }()
能夠根據alpha = 0.5
, 調整alpha
來調整模糊效果, gif圖中的高斯模糊效果不是很明顯, 實際效果特別好.
商城購物車動畫
:這組動畫仍是比較簡單的, 直接附代碼, 若是有什麼疑惑, 能夠留言或者私信我// MARK : - 動畫相關懶加載 /// 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() /// 動畫組 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") // 先執行上面的, 而後再開始 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: - 點擊事件處理 private var num = 0 func gotoShopCar() { if num >= 99 { self.showErrorMessage("親, 企業採購請聯繫咱們客服") return } addtoCar.userInteractionEnabled = false // 設置layer // 貝塞爾弧線的起點 animLayer.position = addtoCar.center layer.addSublayer(animLayer) // 設置path animPath.moveToPoint(animLayer.position) let controlPointX = CGRectGetMaxX(addtoCar.frame) * 0.5 // 弧線, controlPoint基準點, endPoint結束點 animPath.addQuadCurveToPoint(shopCarBtn.center, controlPoint: CGPointMake(controlPointX, -frame.size.height * 5)) // 添加並開始動畫 animLayer.addAnimation(groupAnim, forKey: "groups") } // MARK: - 動畫的代理 // 動畫中止的代理 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 } }
商城詳情頁
的作法也是差很少的, 不過更簡單一點.關鍵一點在於, 詳情頁的展現主要依靠於H5
頁面. 而咱們須要根據webview
的高度來肯定webviewCell
的高度.個人作法是監聽UIWebView
的webViewDidFinishLoad
, 取出webView.scrollView.contentSize.height
而後給詳情頁發送一個通知, 讓其刷新界面. 暫時沒有想到更好的方法, 若是您有更好的作法, 請務必告訴我, 謝謝...
UIWebView
中圖片的點擊第①步: 咱們建立一個image.js
文件, 代碼以下:
//setImage的做用是爲頁面的中img元素添加onClick事件,即設置點擊時調用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時觸發的方法,document.location = url;的做用是使調用 //webView: shouldStartLoadWithRequest: navigationType:方法,在該方法中咱們真正處理圖片的點擊 function imageClick(imagesrc){ var url="imageClick::"+imagesrc; document.location = url; }
第②步:在UIWebView
的代理方法webViewDidFinishLoad
中, 加載JS文件, 並給圖片綁定綁定點擊事件
// 加載js文件 webView.stringByEvaluatingJavaScriptFromString(try! String(contentsOfURL: NSBundle.mainBundle().URLForResource("image", withExtension: "js")!, encoding: NSUTF8StringEncoding)) // 給圖片綁定點擊事件 webView.stringByEvaluatingJavaScriptFromString("setImageClick()")
第③步:在UIWebView
的代理方法-webView:shouldStartLoadWithRequest:navigationType:
中判斷圖片的點擊
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { let urlstr = request.URL?.absoluteString let components : [String] = urlstr!.componentsSeparatedByString("::") if (components.count >= 1) { //判斷是否是圖片點擊 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 }
花田小憩
中的登陸/註冊/忘記密碼界面幾乎是同樣的, 個人作法是用一個控制器LoginViewController
來表明登陸/註冊/忘記密碼
三個功能模塊, 經過兩個變量isRegister
和isRevPwd
來判斷是哪一個功能, 顯示哪些界面, 咱們點擊註冊
和忘記密碼
的時候, 會執行代理方法:// 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) }
倒計時
功能/// 點擊"發送驗證碼"按鈕 func clickSafeNum(btn: UIButton) { var seconds = 10 //倒計時時間 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); //每秒執行 dispatch_source_set_event_handler(timer) { if(seconds<=0){ //倒計時結束,關閉 dispatch_source_cancel(timer); dispatch_async(dispatch_get_main_queue(), { //設置界面的按鈕顯示 根據本身需求設置 btn.setTitleColor(UIColor.blackColor(), forState:.Normal) btn.setTitle("獲取驗證碼", 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(), { //設置界面的按鈕顯示 根據本身需求設置 UIView.beginAnimations(nil, context: nil) UIView.setAnimationDuration(1) btn.setTitleColor(UIColor.orangeColor(), forState:.Normal) btn.setTitle("\(seconds)秒後從新發送", forState:.Normal) btn.titleLabel?.font = UIFont.systemFontOfSize(11) UIView.commitAnimations() btn.userInteractionEnabled = false }) seconds -= 1 } } dispatch_resume(timer) }
設置
模塊中給咱們評分
這個功能在實際開發中特別常見:
代碼以下, 很簡單:
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: 此功能測試的時候, 必須用真機!!!
咱們能夠經過NSHTTPCookieStorage中的NSHTTPCookie來判斷登陸狀態.也能夠自定義一個字段來保存. 根據我抓包得知, 花田小憩
APP的作法是第一次登陸後保存用戶名和密碼(MD5
加密的, 我測試過), 而後每次啓動應用程序的時候, 會首前後臺自動登陸, 而後在進行評論/點贊等操做的時候呢, 參數中會帶上用戶的id.因爲涉及到花田小憩
的帳號密碼的一些隱私, 因此登陸/註冊模塊, 我就沒有沒有完整的寫出來. 有興趣的朋友能夠私信我, 我能夠把接口給你, 在此聲明: 僅供學習, 毋作傷天害理之事
`tip: 我在AppDelegate.swift中給你們留了一個開關, 能夠快速的進行登陸狀態的切換...
我的/專欄
中心: 這兩個功能是同一個控制器, 是UICollectionViewController
而不是UITableViewController
你們對UITableViewController
的header
應該很熟悉吧, 向上滑動的時候, 會停留在navigationBar的下面, 雖然UICollectionViewController
也能夠設置header
, 可是在iOS9之前, 他是不能直接設置停留的.在iOS9以後, 能夠一行代碼設置header的停留
sectionHeadersPinToVisibleBounds = true
可是在iOS9以前, 咱們須要本身實現這個功能:
// // 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() // 即便界面內容沒有超過界面大小,也要豎直方向滑動 collectionView?.alwaysBounceVertical = true // sectionHeader停留 if #available(iOS 9.0, *) { sectionHeadersPinToVisibleBounds = true } } override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // 1. 獲取父類返回的UICollectionViewLayoutAttributes數組 var answer = super.layoutAttributesForElementsInRect(rect)! // 2. 若是是iOS9.0以上, 直接返回父類的便可. 不用執行下面的操做了. 由於咱們直接設置sectionHeadersPinToVisibleBounds = true便可 if #available(iOS 9.0, *) { return answer } // 3. 若是是iOS9.0如下的系統 // 如下代碼來源: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同樣, 能夠停留 // 建立一個索引集.(NSIndexSet:惟一的,有序的,無符號整數的集合) let missingSections = NSMutableIndexSet() // 遍歷, 獲取當前屏幕上的全部section for layoutAttributes in answer { // 若是是cell類型, 就加入索引集裏面 if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) { missingSections.addIndex(layoutAttributes.indexPath.section) } } // 遍歷, 將屏幕中擁有header的section從索引集中移除 for layoutAttributes in answer { // 若是是header, 移掉所在的數組 if (layoutAttributes.representedElementKind == UICollectionElementKindSectionHeader) { missingSections .removeIndex(layoutAttributes.indexPath.section) } } // 遍歷當前屏幕沒有header的索引集 missingSections.enumerateIndexesUsingBlock { (idx, _) in // 獲取section中第一個indexpath let indexPath = NSIndexPath(forItem: 0, inSection: idx) // 獲取其UICollectionViewLayoutAttributes let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) // 若是有值, 就添加到UICollectionViewLayoutAttributes數組中去 if let _ = layoutAttributes{ answer.append(layoutAttributes!) } } // 遍歷UICollectionViewLayoutAttributes數組, 更改header的值 for layoutAttributes in answer { // 若是是header, 改變其參數 if (layoutAttributes.representedElementKind==UICollectionElementKindSectionHeader) { // 獲取header所在的section let section = layoutAttributes.indexPath.section // 獲取section中cell總數 let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section) // 獲取第一個item的IndexPath let firstObjectIndexPath = NSIndexPath(forItem: 0, inSection: section) // 獲取最後一個item的IndexPath let lastObjectIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section) // 定義兩個變量來保存第一個和最後一個item的layoutAttributes屬性 var firstObjectAttrs : UICollectionViewLayoutAttributes var lastObjectAttrs : UICollectionViewLayoutAttributes // 若是當前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)! } // 獲取當前header的高和origin let headerHeight = CGRectGetHeight(layoutAttributes.frame) var origin = layoutAttributes.frame.origin origin.y = min(// 2. 要保證在即將消失的臨界點跟着消失 max( // 1. 須要保證header懸停, 因此取最大值 collectionView!.contentOffset.y + collectionView!.contentInset.top, (CGRectGetMinY(firstObjectAttrs.frame) - headerHeight) ), (CGRectGetMaxY(lastObjectAttrs.frame) - headerHeight) ) // 默認的層次關係是0. 這兒設置大於0便可.爲何設置成1024呢?由於咱們是程序猿... layoutAttributes.zIndex = 1024 layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size) } } return answer; } override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { // 返回true, 表示一旦進行滑動, 就實時調用上面的-layoutAttributesForElementsInRect:方法 return true } }
⑩+@end: 整個項目, 東西仍是蠻多的, 也不是僅僅幾百上千字能說清楚的, 幾乎每個頁面, 每個文件, 我都有詳細的中文註釋. 但願你們一塊兒進步. 這也是個人第一個開源的完整的Swift
項目, 有什麼不足或者錯誤的地方, 但願你們指出來, 萬分感激!!!
若是對您有些許幫助, 請☆star
可能有些功能模塊存在bug, 後續我都會一一進行修復和完善的, 並更新在github
上.
若是您有任何疑問,或者發現bug以及不足的地方, 能夠在下面給我留言, 或者關注個人新浪微博, 給我私信.