抓住iOS的未來 - 30天學習編寫30個Swift小程序


更新:所有代碼已經更新到Swift4.1,請移步github下載

=======================================================

iOS開發已經做了快4年了,聽說Swift也已經有兩年多,但是一直都只是把學習停留在表面。無意中聽說了有一個叫Sam Lu在Twitter上發起了一個100天做40個Swift小程序的活動,再加上國內看到了Allen_朝輝寫的Swift學習的文章,心里暗自下了一個決定:30天寫30個Swift小程序,希望能推動自己學習Swift的計劃。這30個小程序難度不同,有的一個晚上就能寫完,有的要占用周末大部分時間來細研究。大部分不會的東西Google都能找到,就算Swift版本沒有找到Objective-C版本然后用Swift重寫就好,好在他們對應關系比較明確。

用例方面,既參考了Sam Lu的40個小項目,也參考了Allen_朝輝的項目,還有的是我自己仿寫的知名App。

其實我并不是唯一在國內發起這個30天30個Swift小程序并且將其開源的作者,但是我可能是唯一一個從頭到尾用XCode 8 + Swift3環境編寫的作者。而且,為了讓代碼更加可讀,所有代碼完全手寫,而非用Storyboard(除了只能用Storyboard的,例如apple watch app)。實際上多人協作的項目中我們盡可能少用Storyboard,因為很容易出現沖突問題。況且從學習的角度,storyboard很難說清楚操作步驟是什么。在這上面我其實花了不少時間,但是我認為很值得。

希望能有更多對Swift感興趣的開發者加入這項#30天30個Swift小程序 的活動里面來。以下為Github鏈接:
https://github.com/nimomeng/30-swift-projects-in-30-days

Project 30 - Google Now App

GoogleNow.gif

我學到了

  • 這次Project演示了Present/Dismissd如何做Transition動畫,這和做Push/Pop的轉場動畫的基本原理都是一樣的
  • 這次的動畫參考了BubbleTransition的動畫效果,在它之上加了修改,支持傳入自定義的UI屬性,方便做組合型動畫(例如本例中按鈕不僅放大而且上下移動)
  • 動畫變化的原理是將相應的ViewController進行Scale變換,再通過一個Bubble的蒙版看起來像是氣泡效果
  • 其它的細節知識如下:
    • 畫圓形按鈕的方法,必須要cornerRadius屬性為邊長的1/2,具體代碼如下:
        triggerButton.layer.cornerRadius = triggerButton.frame.width / 2
        triggerButton.layer.masksToBounds = true

Project 29 - Beauty Contest

BeautyContest.gif

我學到了

  • 這個項目是基于Yalantis的Koloda來制作的。 Koloda是一個非常好用的UIImage選擇器
  • Swift中的懶加載的使用方法:
    • 兩種方式:
lazy var firstWay = "first"

以及

lazy var secondWay: String = {return "Second"}()

注意:第二種方式要注意定義好字段類型,以便于編譯時的類型檢查;以及不要忘記最后的小括號

  • 為什么要用Lazy:因為這里面需要先知道KolodaView的尺寸,才能定Overlay的尺寸。因此這里有一個依賴關系,因此用懶加載最合適。
  • Swift中的unowned和weak的區別:
    • unowned更像OC里的unsafe_unretained; weak還是那個weak。
    • 如果確定使用時一定不會被釋放,可以用unowned;否則最好用weak

Project 28 - SnapChat Like App

Snap Chat Like App.gif

我學到了

  • UIScrollView的基本使用和細節小點,例如禁止彈跳的bounces屬性,整頁切換的isPagingEnabled屬性,起始位置contentOffset屬性等

  • 加載子Viewcontroller的addChildViewController方法

  • "xxx class has no initializers"問題:

      You have to use implicitly unwrapped optionals so that Swift can cope with 
      circular dependencies (parent <-> child of the UI components in this case) 
      during the initialization phase.
    
      @IBOutlet var imgBook: UIImageView!
      @IBOutlet var titleBook: UILabel!
      @IBOutlet var pageBook: UILabel!
    
  • 權限問題,具體錯誤描述為:
    "This app has crashed because it attempted to access privacy-sensitive data
    without a usage description. The app's Info.plist must contain an
    NSPhotoLibraryUsageDescription key with a string value explaining to the
    user how the app uses this data."
    解決方法:iOS10之后的權限問題,在info.plist里添加相應的權限以及描述即可。
    本例中權限為:
    <key>NSCameraUsageDescription</key>
    <string>PhotoMe needs the camera to take photos. </string>
    <key>NSMicrophoneUsageDescription</key>
    <string>PhotoMe needs the microphone to record audio with Live Photos.</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>PhotoMe will save photos in the Photo Library.</string>

  • AVCaptureSession 的使用方法:

    • AVCaptureSession是AVFoundation的核心類,用于捕捉視頻和音頻,協調視頻和音頻的輸入和輸出流.
    • 創建AVCaptureSession實例,并設置其sessionPreset值,也就是設置畫面的質量。
    • 給Session添加Input。一般是Video或者Audio數據,也可以兩者都添加,即AVCaptureSession的輸入源AVCaptureDeviceInput。具體步驟是先獲取對應的device實例(此時決定是用Video還是Audio),再由實例獲取其Input Source。最后將input source add到session中。
    • 給Session添加Output,即AVCaptureSession的輸出源。一般輸出源分成:音視頻源,圖片源,文件源等。這里以靜態圖片的輸出源為例,指的是AVCapturePhotoOutput。最后將其也add到session中。
    • 設置預覽圖層,即AVCaptureVideoPreviewLayer。在input,output等重要信息都添加到session以后,可以用session創建AVCaptureVideoPreviewLayer,這是攝像頭的視頻預覽層。這里千萬別忘了將Layer添加到View中。
    • 啟動Session,即captureSesssion.startRunning()
  • Photo的捕獲方法

    • AVCaptureSession設置成功,并啟動
    • 創建AVCapturePhotoSettings對象,并配置相應的屬性,例如是否打開flash,是否開啟防抖模式等等
    • 執行輸出源的capture方法,并制定具體的AVCapturePhotoSettings對象以及delegate對象
    • 在capture的delegate方法:
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?)```
中執行獲取圖像的具體邏輯。本例中是先將buffer轉換為data,再轉換為UIImage,最終write到相冊文件夾中。
- Reference: [iOS AVCaptureSession學習筆記](http://www.lxweimin.com/p/b5618066dc2c)



#Project 27: Carousel Effect (跑馬燈效果)

![Carousel Effect.gif](http://upload-images.jianshu.io/upload_images/2340489-f1c54a0e91776c53.gif?imageMogr2/auto-orient/strip)

#### 我學到了
- UICollectionView的使用
  - 與UItableView的不同在于,每一個對應的Cell(不論是content的cell還是header,footer的),都需要預先執行register方法
  - Cell間距太窄的問題,可以通過minimumLineSpacingForSection的DataSource代理方法來解決掉
  - 如果選擇的layout為UICollectionViewFlowLayout,可以通過修改scrollDirection屬性來修改滾動方向
- 自定義Layout要在對應的子類里實現如下方法
        prepare()
        shouldInvalidateLayout(forBoundsChange newBounds: CGRect)
        targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) 
        layoutAttributesForElements(in rect: CGRect)
其中:
  - prepare可以定義初始化操作
  - shouldInvalidateLayout,判斷是否需要更新每個Cell的bounds。如果我們的layout是那種每個cell需要動態變化的layout,則設置為true;否則為了性能考慮,請設置為false。默認為flase。
  - targetContentOffset,如果我們需要圖片在滾動的過程中在特定位置可以停下來(類似iphone上專輯圖片的選擇),請在此函數中國年給出停下來的具體規則
  - layoutAttributesForElements 返回所有元素此時的所有布局。我們會在這里定義在滾動過程中所有其他元素的attribute布局相關屬性。例如本例中,離屏幕中間越近,圖片被縮放的越大;離屏幕越小,圖片被縮放的越小。
  - Reference:
    - [UICollectionView綜合視圖](http://www.lxweimin.com/p/c0f4d0833ff8)
    - [用UICollectionView實現相冊功能]
(http://www.lxweimin.com/p/4f691ec731e5)

- Visual Effect View的使用
  - 盡量在需要模糊化的圖層之后添加進去,會自動虛化所覆蓋的圖層
        let blurEffect: UIBlurEffect = UIBlurEffect(style: .light)
        let blurView: UIVisualEffectView = UIVisualEffectView(effect: blurEffect)
        blurView.frame = self.view.bounds
        bgView.addSubview(blurView)

- UISegmentedControl 的使用(略)

- 其它:#selector()中的func如果帶有參數,請將具體參數也一起寫進去,例如: ``` #selector(action_segmentValueChanged(sender:)```這個規則和OC不太一樣,要注意。


#Project 26 - Twitter-like Splash

![TwitterLikeSplash.gif](http://upload-images.jianshu.io/upload_images/2340489-94851cd3a75aff9a.gif?imageMogr2/auto-orient/strip)

#### 我學到了
- 這個效果嘗試了用簡單的UIView的animation方法會比較吃力,因此轉而使用CAAnimation來做。
- 我們需要的效果,設置keyPath為"bounds"。吐槽一下,蘋果為什么不做一個枚舉。。。完整的keyPath列表如下所示:
![KeyPath 對照表](http://upload-images.jianshu.io/upload_images/2340489-8fd2ad3a1592d039.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640)
- 由于logo的動畫定制化要求比較高,所以關于這個變化的動畫,選擇CAAnimation里的CAKeyFrameAnimation來做。主要關注keyTimes,values屬性,也就是keyFrame的屬性。timingFunctions屬性是keyframe的count - 1, 也就是frame1到frame2,frame2到frame3的動畫過渡函數。這個不多說了,之前的Project有提到過。
- 在logo變大的過程中,logo中間的alpha值也應該有白色變為透明,因此應該先添加一個maskView,藏在最上層,logo層之下,作為白色的底。動畫trigger的時間和duration與logo的動畫保持步調一致,并且記得在動畫complete的時候被移除掉。這里使用了CABasicAnimation的animationDidStop代理來完成。
- logo的透明度變化既可以使用簡單的UIView的animation方法來做,也可以采用layer級別的CABasicAnimation來完成。因為對前者比較熟悉了,所以我在這里使用后者,注意keyPath是````opacity````。代碼比較簡單,這里不贅述。
- 整體效果還是很炫的:)
- CAKeyFrameAnimation參考[此篇文檔](http://blog.csdn.net/u013316626/article/details/54379540)

# Project 25 Custom Transition

![CustomTransition.gif](http://upload-images.jianshu.io/upload_images/2340489-8d998c7d397d2102.gif?imageMogr2/auto-orient/strip)

#### 我學到了
- NavigationController的動畫是可以自定義的,去實現UINavigationControllerDelegate里的方法就好
- 如果切換動畫只需要關注之前的VC和之后的VC,不需要關注中間過程,直接實現以下方法即可:

navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?

- 上述方法的返回值````UIViewControllerAnimatedTransitioning````需要自定義動畫,需要實現````UIViewControllerAnimatedTransitioning````代理,實現具體的兩個方法:
  - 轉場動畫時間,直接返回一個時間即可

transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval

 - 轉場動畫過程,如下所示,這個比較復雜:

animateTransition(using transitionContext: UIViewControllerContextTransitioning)

    - 第一步,獲得轉場動畫的fromVC,toVC,container:

let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! XXXController
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! YYYController
let container = transitionContext.containerView

    - 之后是動畫前的準備工作,例如image賦值,例如坐標的計算:

let snapshotView = fromVC.selectedCell.imageView.snapshotView(afterScreenUpdates: false)
snapshotView?.frame = container.convert(fromVC.selectedCell.imageView.frame, from: fromVC.selectedCell)
....

    - 最后當然是Animation動畫的執行邏輯了,可以通過UIView的animate方法去實現。具體參數和方法可以參考之前的Project來進靈活組合。
    - 進入的動畫最后一定不能忘記加上````            transitionContext.completeTransition(true)
````,說明了讓navigationController來接管控制權利(在completion的block中)
    - 退出的動畫記得帶上````            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
````說明動畫執行完成
- 如果需要關注動畫的執行過程,則在上述的基礎之上還應該實現下述方法:

navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?

其中,````UIViewControllerInteractiveTransitioning````是動畫過渡對象
- 獲取iOS中手從左往右沿屏幕滑動的事件,是通過````UIScreenEdgePanGestureRecognizer````方法并設置其edges為left實現的:

let edgePanGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(edgePanGestrueAction(_:)))
edgePanGesture.edges = UIRectEdge.left

- 這篇教程針對的是Push與Pop的自定義動畫的制作
- 參考[文檔1](http://kittenyang.com/magicmove/),[文檔2](http://www.lxweimin.com/p/38cd35968864),并在他們的基礎之上做了改動
- 這個例子我很喜歡,圖片是羅斯科。


# Project 24 - Vertical Menu Transition

![Vertical Menu Transition.gif](http://upload-images.jianshu.io/upload_images/2340489-d8ac5f10378c6199.gif?imageMogr2/auto-orient/strip)

#### 我學到了
- 本文和Google Now App項目思路一致,都是針對Present/Dismiss的操作進行自定義Transition
- 由于動畫需要局部截圖,因此建議將Present和Dismiss的Transition寫到一起,通過一個變量來進行不同動畫的切換和控制。變量可以通過````animationController(forDismissed dismissed: UIViewController)````與````animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController)````來進行設置。
- target-action方式,一般會將target設置為self。如果設置為對應的delegate,則action字段應該填寫````#selector(CustomTransitionDelegate.functionName)````
- 在Present/Dismiss的自定義轉場動畫中,記得在complete回調中加入動畫結束語句塊:
transitionContext.completeTransition(true)
fromViewController?.endAppearanceTransition()
toViewController?.endAppearanceTransition()


#Project 23 - Side Navigation App

![SideNavigation.gif](http://upload-images.jianshu.io/upload_images/2340489-649e18fc36d15c9a.gif?imageMogr2/auto-orient/strip)

####我學到了
- Swift-OC混編方法
  - 新建一個頭文件,例如名為Bridge.h
  - 單擊Project文件,選擇Build Setting,找到Objective-C Bridge Header,輸入Bridge.h的路徑
  - 之后所有需要在swift文件中引用的OC文件的頭文件放到Bridge.h中進行import
  
![Swift-OC](http://upload-images.jianshu.io/upload_images/2340489-dcfebb7a5446ea03.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/500)

- 側滑效果借鑒了[SWRevealViewController](https://github.com/John-Lluch/SWRevealViewController),使用步驟如下(原項目只提到了OC中的調用方法)
  - 項目中至少有以下幾類viewController:第一頁展示的VC,比如FrontViewController;tabeView所在的MenuViewController
  - 在AppDelegate中根據規則創建自定義Window,具體步驟為:
    - 建立UIWindow
    - 新建兩個UINavigationController,分別以FrontViewController和MenuViewController為rootViewController
    - 實例化SWRevealViewController,并設置rearViewController的值和frontViewController的值。其中,rearViewController是tableView所在的UINavigationController,frontViewController是FrontViewController所在的UINavigationController
    - 將實例化的SWRevealViewController設置為Window的rootViewController

window = UIWindow(frame: UIScreen.main.bounds)
let rearNavigationController = UINavigationController(rootViewController: MenuViewController())
let frontNavigationController = UINavigationController(rootViewController: FrontViewController())
let revealController = SWRevealViewController(rearViewController: rearNavigationController, frontViewController: frontNavigationController)
revealController?.delegate = self
window?.rootViewController = revealController
window?.makeKeyAndVisible()

  - 需要在每一個ViewController中加入左滑激活Menu的邏輯,一句話:

self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())

  - 效果需要,最好隱藏掉status bar以及navigationBar:

self.navigationController?.isNavigationBarHidden = true

override var prefersStatusBarHidden: Bool { return true }


# Project 22 - Basic Animations

![Basic Animations.gif](http://upload-images.jianshu.io/upload_images/2340489-bf50d015a23d2062.gif?imageMogr2/auto-orient/strip)

#### 我學到了
- 本次涉及到最基本的UIAnimation,很多復雜的Animation其實是各種簡單的Animation的疊加,所以不能輕視
- Position的Animation既可以通過直接修改frame的origin屬性,也可以直接通過UIView的transform來進行修改
- Opacity直接改Alpha值就可以了
- Scale是修改了UIView的transform,傳入要縮放的相對比例并創建對應的CGAffineTransform對象。例如:

heartView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)

- Color是直接修改backgroundColor就可以了
- Rotation是通過修改UIView的transform,傳入要旋轉的值并創建對應的CGAffineTransform對象。其中,值為0 - 2*Pi之間,表示0到360°之間。注意,正值為逆時針轉動。例如:

self.rollingBallView.transform = CGAffineTransform(6.28)


# Project 21 CoreData App

![CoreDataAppDemo.gif](http://upload-images.jianshu.io/upload_images/2340489-7113254108dd8b22.gif?imageMogr2/auto-orient/strip)

#### 我學到了
- 一定要勾選 UseCoreData,這樣在Appdelegate里會自動生成部分代碼
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/2340489-1f3d1001ef0571ec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 不一定非要通過Editor生成SubClass
- 本例中Entity如圖所示:

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/2340489-79ba92016a6aa5c7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800)

- 在需要調用CoreData的類中,import CoreData
- 本例比較簡單,只進行了getResult和Add的操作,思路分別為:
  - getResult:
  let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: EntityName)
    do {
        let searchResults = try getContext().fetch(fetchRequest)
        dataSource = searchResults as! [TodoList]
    } catch  {
       // todo error handler
    }
注意,或取出來的searchResult可以直接實例化為TodoList(TodoList是我的Entity名字),這樣后續就可以直接使用TodoList的content方法了。
  - saveContent:
    let context = getContext()
    // 定義一個entity,這個entity一定要在xcdatamodeld中做好定義
    let entity = NSEntityDescription.entity(forEntityName: EntityName, in: context)
    let todoList = NSManagedObject(entity: entity!, insertInto: context)
    todoList.setValue(content, forKey: "content"
    do {
        try context.save()
    }catch{}
對應getConent方法的代碼兩行:

let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext

如此操作后使用的時候直接通過獲取TodoList對象,然后調用其content方法即可完成。
````cell.textLabel?.text = (dataSource[indexPath.row]).content````
- UIAlertController添加輸入款的方法:

alertController.addTextField { (textField) in
textField.placeholder = "Please input the todo Item"}

- 該方法在XCode8.3 + Swift3.2測試通過,CoreData在iOS10的變化很大,之前的版本可能和上述操作方法有出入
- [參考文章](http://www.cnblogs.com/Free-Thinker/p/5944551.html)

#Project 20 - Apple Watch OS App - Guess Game


![WatchApp_Guess.gif](http://upload-images.jianshu.io/upload_images/2340489-7843cf75c364d69d.gif?imageMogr2/auto-orient/strip)


#### 我學到了
- Watch程序,需要在create project的先選擇Watch OS的Section,之后選擇如下:
![watchOS.png](http://upload-images.jianshu.io/upload_images/2340489-c35efafcea74c758.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/600)

- watch中的UI只可以通過Storyboard來進布局,布局文件在WatchKit App中的Interface.storyboard中

- 例子中涉及到了watch和主app的交互,這里使用的是````WCSession````方法,使用步驟如下:
  - 確定app所在設備是否支持WCSession
  - 生成一個WCSession對象,并設置其delegate
  - 激活此WCSession對象
至此部分,代碼為:
  let wcsession = WCSession.default()
    if WCSession.isSupported() {
        wcsession.delegate = self
        wcsession.activate()
    }
  - 發送通信(watch與主app之間)通過WCSession對象的updateApplicationContext方法來進行,例如
````try wcsession.updateApplicationContext(["numberToBeGuessed": number])````
  - 接收方通過代理方法來接收并解析發送的消息
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) 
- 示例參考了[WatchKit Introduction: Building a Simple Guess Game](http://www.appcoda.com/watchkit-introduction-tutorial/)

# Project 19 - TodayWidget


![TodayWidget.gif](http://upload-images.jianshu.io/upload_images/2340489-121ed2f83ae5409d.gif?imageMogr2/auto-orient/strip)


#### 我學到了
- 創建Today Widget: File > New > Target…,然后選擇 iOS 中的 Application Extension 的 Today Extension
- 為了方便Widget與App數據共享,需要切換成App Group模式。步驟為打開主target,選擇capability,找到App Group,打開:

![AppGroup](http://upload-images.jianshu.io/upload_images/2340489-f638e5eaf80c5bd7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640)
- 在主Target下為這個app group添加一個名稱,然后去Extension的target下去采用相同操作,并勾選這個group
- 我們可以采用UserDefault作為主app與widget之間的共享存儲。但是此處不能使用standardUserDefaults,只能通過suiteName的方式來進行共享,且名字是之前在app group中添加的名稱,代碼如下:

let userDefault = UserDefaults(suiteName: "group.nimoAndHisFriend.watchDemo")

- 在Widget的ViewController里寫入相應的讀取邏輯代碼:

let userDefaults = UserDefaults(suiteName: "group.nimoAndHisFriend.watchDemo")
var leftTimeWhenQuit = userDefaults?.double(forKey: "lefttime")

為了想讓widget里的數據也進行同步更新,可以在extension的代碼里也加入一個timer來進行同步操作。這樣widge和主程序的widge即可同步
- 如果想了解更多關于Widget的使用,請[參考文檔](https://onevcat.com/2014/08/notification-today-widget/)

# Project 18 - Spotlight Search

![SpotlightSearch.gif](http://upload-images.jianshu.io/upload_images/2340489-e7e702f5b8e98752.gif?imageMogr2/auto-orient/strip)

####我學到了
- Spotlight Search的使用:
  - 引入CoreSpotlight庫````import CoreSpotlight````
  - 這里用CSSearchableItem來進行需要被索引的對象的添加。先創建一個CSSearchableItemAttributeSet,也就是Item的屬性類,對具體屬性進行添加,包括title,contentDescription,以及thumbData的設置。
  - 需要單獨說明的是,CSSearchableItemAttributeSet對象的thumbData可以通過獲取UIImage對象后對其UIImageJPEGRepresentation方法來獲取或者UIImagePNGRepresentation方法來獲取(取決于image對應的文件是什么類型)
  - 創建CSSearchableItem對象,并進一步通過indexSearchableItems方法將創建的CSSearchableItem添加到索引中:

let tmpItems = [searchItem]
CSSearchableIndex.default().indexSearchableItems(tmpItems) { (error) in
}

  - 如果調試過程中,發現模擬器上重新了之前的spotlight緩存無法清除的情況,請更換新的模擬器,或者重置模擬器。或者干脆切換成真機進行調試,真機這種情況少一些:)


# Project 17 - 3D Touch Quick Action

![3DTouchQuickAction.gif](http://upload-images.jianshu.io/upload_images/2340489-66b03a3d2f4f541f.gif?imageMogr2/auto-orient/strip)

- 3D Touch的具體功能分成兩種:第一種是在SpringBoard里長按圖標進行直接功能跳轉,第二種是在APP內部對特定的視圖元素長按進行Peek & Pop
- 在做任何3D Touch相關功能的引入之前,務必確保用戶機型支持3D Touch。
````self.window?.traitCollection.forceTouchCapability == .available````
- 針對第一種功能,建立````UIApplicationShortcutItem````類型的Item,然后設置application的shortcutItems屬性即可。要注意,在設置icon時,只可以設置系統內置的集中icon,不支持自定義圖標
- 針對第二種功能,需要在想加入支持3D Touch的VC中注冊并添加相應事件
 - 添加````UIViewControllerPreviewingDelegate````
 - 在此VC種注冊3D Touch支持。````self.registerForPreviewing(with: self, sourceView: self.view)````
 - 實現兩種delegate:
````    func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? 
    func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) 
  • 如果想實現例子中的額外Action button,需要override對應的previewActionItems屬性,并返回你需要的UIPreviewAction的Array

Project 16 - LoginAnimation

LoginAnimation.gif

我學到了

  • 開始以為很簡單,普通UIView的Animation方法即可完成。后來發現彈跳效果并不是我使用的常規方法可以完成的
  • 彈跳動畫需要使用usingSpringWithDamping來完成,其中的屬性要注意:
    • usingSpringWithDamping:值越小動畫越夸張,借用網上圖來說明其區別:

      usingSpringWithDamping
    • initialSpringVelocity:值越大則起始速度越大,再借用網上圖片來說明其區別:


      initialSpringVelocity
    • options的各個動畫曲線有何區別:可以看圖來進行區分:

options
  • UIView.animation的usingSpringWithDamping與不帶usingSpringWithDamping的參數動畫有什么區別呢?可以看下圖:
Animation Comparation
  • 帶Spring屬性的動畫太有意思了!:)
  • 此部分參考文檔1,文檔2

Project 15 - Tumblr Menu

Tumblr Menu.gif

我學到了

  • 這個例子本質上是對動畫+BlurEffect
  • 三排的動畫有一個先后順序,這個可以通過animation的delay參數進行調節
  • button的上圖下文效果需要設置,這里自定義了一個CustomButton,對樣式進行了封裝。參考了這篇文章

Project 14 - Video Splash

VideoSplash.gif

我學到了

  • 創建一個AVPlayerViewController,并將其view放到背景中
  • 之后結合AVPlayerViewController進行視頻播放,并自動循環
  • 視頻播放部分借鑒了此篇文章中的第十個用例,據說也是參考了一個叫VideoSplashViewController的庫

Project 13: Animation In TableViewCell

AnimationInTableViewCell.gif

我學到了

  • 開始的思路是在willDisplay的delegate里進行動畫操作,效果良好,但是發現在滾動cell時發生cell錯亂的現象,原因是在滾動時cell重繪導致重新調用willDisplay進而坐標錯誤。粗看了下,解決起來有點兒麻煩,于是換思路。以此這種“進場動畫”不應該在渲染過程中的delegate中執行。
  • 將動畫放到ViewWillAppear里來做。可以通過tableView的visibleCells獲取將要顯示的所有cell的Array,逐一遍歷來進行動畫操作。
  • 改變Cell的動畫,采用上一章所說的usingSpringWithDamping的動畫,usingSpringWithDamping設置為0.8,initialSpringVelocity設置為0.(不然動畫會彈跳過大,造成順次露出白色間隙,很不美觀)
  • 改變Cell的具體方式,既可以直接操作cell.frame.origin.y,也可以通過cell.transform = CGAffineTransform(translationX: 0, y: tableHeight),效果是一樣的。不過如果要用到縮放或者旋轉的動畫,恐怕只能使用后者了。
  • 動畫確實是很有意思的:)

Project 12 - Emoji Slot Machine

Emoji Slot Machine.gif

我學到了

  • 乍一看沒思路,本來打算用三個collectionView來做,但是發現有點兒復雜
  • 后來轉變思路,用UIPickerView來做,component設置為3即可
  • 隨機數用arc4random()來算出來,之后使用UIPickerView的selectRow方法進行設置值即可達到老虎機的效果
  • 為了仿真,不能讓pickerView轉到第一個或者最后一個,不然就會碰到邊界了,因此在算隨機Row時,使用Int(arc4random())%(emojiArray.count - 2) + 1的方法來實現
  • 三個同時一致的情況實在太少了,因此為了方便模擬,我加了個雙擊操作,雙擊強制出666。。。
  • 這個case還挺有意思的,哈哈

Project 11 - Gradient in TableView

GradientInTableView.gif

我學到了

  • 這個比較簡單,注意將CAGradientLayer應用在UITableViewCell上即可
  • 建議將CAGradientLayer作為cell的backgroundView,而不是直接在cell.layer上進行添加
  • 美觀起見,隱藏掉Cell的Select效果以及separatorStyle:
    table.separatorStyle = .none
    cell.selectionStyle = .none

Project 10 - Stretchy Header

Stretchy Header.gif

我學到了

  • 通過監聽ScrollView(及其子類)的scrollViewDidScroll代理可以知道scrollView被拉動的位移(offset)
  • 通過位移以及限定的縮放值可以得出圖片需要放大的倍率
  • 通過設置ImageView的transform來完成修改即可,核心代碼為
bannerImgView.transform = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)

Project 9 - Swipeable Cell

Swipeable Cell.gif

我學到了

簡單起見,我用Project 13的代碼基礎上進行修改,換了個清爽的綠色:)

  • 實現editActionsForRowAt這個delegate方法,返回值是Array<UITableViewRowAction>,新建幾個你需要的功能返回即可
  • 每一個Action直接通過UITableViewRowAction的init方法新建即可。在新建方法里有block,直接將點擊邏輯寫進去就行了。
  • 這種交互適用于Accessory比較簡單的情況,例如對交互按鈕大小和內容無要求的情況;如果有特殊要求,需要自定義UITableViewCell,手動控制Cell與捕捉UIPanGesture來進行實現。注意,這種方式要排除上下滑動Cell的情況,不要錯誤觸發。

Project 8 - Color Gradient

ColorGradient.gif

我學到了

  • 顏色漸變效果采用的是類CAGradientLayer
  • 色彩空間的概念可以借助于Color數組來實現,注意,成員變量是CGColor類型,然后通過設置CAGradientLayer的colors屬性來實現
  • 上下滑動時改變顏色是通過加PanGestureRecognizer來實現。具體效果參考了應用Solar
    Solar

Project 7 - Simple Photo Browser

SimplePhotoBrowser.gif

我學到了

  • 縮放圖片的方式:將imageView添加到ScrollView上
  • 設置好scrollView的max/minZoomScale
  • 設置好delegate對象,至少實現viewForZooming的代理方法

Project 6 - Video Player

Video Player.gif

我學到了

  • AVPlayer:視頻播放器實體
  • AVPlayerViewController:簡單封裝了的視頻播放器,有簡單的控制功能
  • AVPlayerLayer:視頻的Layer層,所有功能需要寫控件進行控制,適合對播放器進行深度開發
  • 后臺播放的plist設置方式
  • do...catch...語法的使用
  • background modes的設置。
  • 如何做到app在后臺長期運行:參考簡書的文章
  • 如何顯示鎖屏信息,以及如何響應鎖屏設置(實現remoteControlReceived的代理方法)

Project 5 - Pull To Refresh

PullToRefresh.gif

我學到了

  • 下拉刷新組件: UIRefreshControl
    設置好提示文字attributedTitle,添加好target事件(UIControlEvents.valueChanged事件)后,添加到tableView中,即可

Project 4 - Limited Input Text Field

Limit Input Text Field.gif

我學到了

  • 通過新建UIBarButtonItem來創建navigationBarItem的左右Item
  • 通過TextView的textViewDidChange事件捕捉當前輸入內容,從而進行限制輸入字數
  • 通過監聽NSNotification.Name.UIKeyboardWillChangeFrame事件來監視Keyboard的彈出和收起。在對應回調中,通過note.userInfo?[UIKeyboardFrameEndUserInfoKey]來拿到鍵盤的endFrame,從而拿到鍵盤的高度,對計數器進行frame操作
  • 同理,通過note.userInfo?[UIKeyboardAnimationDurationUserInfoKey]拿到鍵盤的動畫duration,進而可以通過UIView的animation動畫做到同步變化計數器的frame

Project 3 - Find My Position

Find My Position.gif

我學到了

  • 定位點配置:
    在plist中添加配置:
    <key>NSLocationAlwaysUsageDescription</key>
    <true/>
  • 用CLLocationManager來進行定位
  • 在逆地址解析的方法reverseGeocodeLocation調用時如果遇到了block中一直出現Domain=GEOErrorDomain Code=-8 "(null)"之類的錯誤,將 CLGeocoder改成全局變量即可
  • 之后這種簡單功能可以直接通過蘋果內置方法來完成,不需要再通過引入高德SDK(省去了高德SDK的大小)

Project 2: Watch Demo

Watch's Demo.gif

我學到了

  • Update cocoaPods to 1.2.0
  • Learn how to use SnapKit (Quite similar with Masonry)
  • Learn how to use Timer in Swift
  • 我學到了: guard語句,詳見 guard詳解

Project 1: Change Custom Font

Custom Font.gif

我學到了

  • 如何修改字體屬性,熟悉字體屬性

  • 字體名稱可以去storyboard中查詢,或者通過如下代碼來進行查詢:

    func printAllSupportedFontNames() {
    let familyNames = UIFont.familyNames
    for familyName in familyNames {
        print("++++++ \(familyName)")
        let fontNames = UIFont.fontNames(forFamilyName: familyName)
        for fontName in fontNames {
            print("----- \(fontName)")
        }}}
    

寫在最后:

能堅持看到這里的,我給你們手動雙擊666!

image.png

實話實說,文章有點標題黨,實際開發時間是40天左右,因為開發時間在下班后到睡覺前,所以有時因為要出去聚餐,有時犯懶,還有時晚上要你懂得,所以完成這三十個項目的時間比計劃的時間要長。。。

image.png

寫完這些項目,感覺上一方面是提高了使用Swift語言的熟練度,另一方面更是復習了一遍iOS開發的知識點,因為寫到后來我已經基本感覺不出來跟用OC開發有什么思路上的差異。這也回答了別人問過我的問題,“如果我現在學iOS開發,是應該學OC還是Swift”:

我覺得從iOS SDK的熟悉角度來說,沒有本質區別,如果熟悉OC下對應語法去使用Swift寫沒有太大區別。所以與其花時間糾結不如趕緊找兩個項目上手進行練習。

image.png

下一步,我打算再重新梳理下Swift語法,對這些項目進行小規模的重構,從結構上去看看能否挖掘到Swift的特性,從另一個角度(目前是功能角度)來學習Swift。所以也許還會有下一篇。

image.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容