PS:所有代碼已經更新到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
triggerButton.layer.cornerRadius = triggerButton.frame.width / 2 triggerButton.layer.masksToBounds = true
lazy var firstWay = "first"
以及
lazy var secondWay: String = {return "Second"}()
注意:第二種方式要注意定義好欄位類型,以便於編譯時的類型檢查;以及不要忘記最後的小括弧 - 為什麼要用Lazy:因為這裡面需要先知道KolodaView的尺寸,才能定Overlay的尺寸。因此這裡有一個依賴關係,因此用懶載入最合適。 - Swift中的unowned和weak的區別: - unowned更像OC裏的unsafe_unretained; weak還是那個weak。 - 如果確定使用時一定不會被釋放,可以用unowned;否則最好用weak
AVCaptureSession
sessionPreset
device
captureSesssion.startRunning()
AVCapturePhotoSettings
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?)
中執行獲取圖像的具體邏輯。本例中是先將buffer轉換為data,再轉換為UIImage,最終write到相冊文件夾中。 - Reference: iOS AVCaptureSession學習筆記
#selector(action_segmentValueChanged(sender:)
opacity
navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
UIViewControllerAnimatedTransitioning
transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
animateTransition(using transitionContext: UIViewControllerContextTransitioning)
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! XXXController let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! YYYController let container = transitionContext.containerView
let snapshotView = fromVC.selectedCell.imageView.snapshotView(afterScreenUpdates: false) snapshotView?.frame = container.convert(fromVC.selectedCell.imageView.frame, from: fromVC.selectedCell) ....
transitionContext.completeTransition(true)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
其中,UIViewControllerInteractiveTransitioning是動畫過渡對象 - 獲取iOS中手從左往右沿屏幕滑動的事件,是通過UIScreenEdgePanGestureRecognizer方法並設置其edges為left實現的:
UIViewControllerInteractiveTransitioning
UIScreenEdgePanGestureRecognizer
let edgePanGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(edgePanGestrueAction(_:))) edgePanGesture.edges = UIRectEdge.left
animationController(forDismissed dismissed: UIViewController)
animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController)
#selector(CustomTransitionDelegate.functionName)
transitionContext.completeTransition(true) fromViewController?.endAppearanceTransition() toViewController?.endAppearanceTransition()
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()
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
self.navigationController?.isNavigationBarHidden = true override var prefersStatusBarHidden: Bool { return true }
heartView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
self.rollingBallView.transform = CGAffineTransform(6.28)
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添加輸入款的方法:
cell.textLabel?.text = (dataSource[indexPath.row]).content
alertController.addTextField { (textField) in textField.placeholder = "Please input the todo Item"}
WCSession
let wcsession = WCSession.default() if WCSession.isSupported() { wcsession.delegate = self wcsession.activate() }
try wcsession.updateApplicationContext(["numberToBeGuessed": number])
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any])
- 在主Target下為這個app group添加一個名稱,然後去Extension的target下去採用相同操作,並勾選這個group - 我們可以採用UserDefault作為主app與widget之間的共享存儲。但是此處不能使用standardUserDefaults,只能通過suiteName的方式來進行共享,且名字是之前在app group中添加的名稱,代碼如下:
let userDefault = UserDefaults(suiteName: "group.nimoAndHisFriend.watchDemo")
let userDefaults = UserDefaults(suiteName: "group.nimoAndHisFriend.watchDemo") var leftTimeWhenQuit = userDefaults?.double(forKey: "lefttime")
為了想讓widget裏的數據也進行同步更新,可以在extension的代碼裏也加入一個timer來進行同步操作。這樣widge和主程序的widge即可同步 - 如果想了解更多關於Widget的使用,請參考文檔
import CoreSpotlight
let tmpItems = [searchItem] CSSearchableIndex.default().indexSearchableItems(tmpItems) { (error) in }
self.window?.traitCollection.forceTouchCapability == .available
UIApplicationShortcutItem
UIViewControllerPreviewingDelegate
self.registerForPreviewing(with: self, sourceView: self.view)
- 如果想實現例子中的額外Action button,需要override對應的previewActionItems屬性,並返回你需要的UIPreviewAction的Array
# Project 16 - LoginAnimation
![LoginAnimation.gif](https://user-gold-cdn.xitu.io/2019/2/11/168dce45c78f4774?w=292&h=529&f=gif&s=29569)
#### 我學到了 - 開始以為很簡單,普通UIView的Animation方法即可完成。後來發現彈跳效果並不是我使用的常規方法可以完成的 - 彈跳動畫需要使用````usingSpringWithDamping````來完成,其中的屬性要注意: - usingSpringWithDamping:值越小動畫越誇張,借用網上圖來說明其區別:
![usingSpringWithDamping](https://user-gold-cdn.xitu.io/2019/2/11/168dce45c7b0ce29?w=314&h=321&f=gif&s=37516) - initialSpringVelocity:值越大則起始速度越大,再借用網上圖片來說明其區別: ![initialSpringVelocity](https://user-gold-cdn.xitu.io/2019/2/11/168dce45e881257a?w=314&h=321&f=gif&s=29459) - options的各個動畫曲線有何區別:可以看圖來進行區分:
![options](https://user-gold-cdn.xitu.io/2019/2/11/168dce45e89637a1?w=640&h=451&f=png&s=97045)
- UIView.animation的usingSpringWithDamping與不帶usingSpringWithDamping的參數動畫有什麼區別呢?可以看下圖:
![Animation Comparation](https://user-gold-cdn.xitu.io/2019/2/11/168dce45f0a6bae0?w=640&h=349&f=png&s=35052) - 帶Spring屬性的動畫太有意思了!:) - 此部分參考[文檔1](http://blog.csdn.net/youshaoduo/article/details/53203211),[文檔2](http://easings.net/zh-cn)
# Project 15 - Tumblr Menu
![Tumblr Menu.gif](https://user-gold-cdn.xitu.io/2019/2/11/168dce45f13777f7?w=292&h=529&f=gif&s=58508)
#### 我學到了 - 這個例子本質上是對動畫+BlurEffect - 三排的動畫有一個先後順序,這個可以通過animation的delay參數進行調節 - button的上圖下文效果需要設置,這裡自定義了一個CustomButton,對樣式進行了封裝。參考了[這篇文章](http://blog.csdn.net/dfqin/article/details/37813591)
# Project 14 - Video Splash
![VideoSplash.gif](https://user-gold-cdn.xitu.io/2019/2/11/168dce45f2896286?w=292&h=529&f=gif&s=1567914)
#### 我學到了 - 創建一個AVPlayerViewController,並將其view放到背景中 - 之後結合AVPlayerViewController進行視頻播放,並自動循環 - 視頻播放部分借鑒了[此篇文章中](http://www.jianshu.com/p/52032bc4cbe4)的第十個用例,據說也是參考了一個叫VideoSplashViewController的庫
# Project 13: Animation In TableViewCell
![AnimationInTableViewCell.gif](https://user-gold-cdn.xitu.io/2019/2/11/168dce4608d26c5c?w=292&h=529&f=gif&s=575965)
#### 我學到了 - 開始的思路是在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](https://user-gold-cdn.xitu.io/2019/2/11/168dce460cdd4b83?w=292&h=529&f=gif&s=69234)
#### 我學到了 - 乍一看沒思路,本來打算用三個collectionView來做,但是發現有點兒複雜 - 後來轉變思路,用UIPickerView來做,component設置為3即可 - 隨機數用arc4random()來算出來,之後使用UIPickerView的selectRow方法進行設置值即可達到老虎機的效果 - 為了模擬,不能讓pickerView轉到第一個或者最後一個,不然就會碰到邊界了,因此在算隨機Row時,使用````Int(arc4random())%(emojiArray.count - 2) + 1````的方法來實現 - 三個同時一致的情況實在太少了,因此為了方便模擬,我加了個雙擊操作,雙擊強製出666。。。 - 這個case還挺有意思的,哈哈
# Project 11 - Gradient in TableView
![GradientInTableView.gif](https://user-gold-cdn.xitu.io/2019/2/11/168dce470e11f558?w=292&h=529&f=gif&s=248841)
#### 我學到了 - 這個比較簡單,注意將CAGradientLayer應用在UITableViewCell上即可 - 建議將CAGradientLayer作為cell的backgroundView,而不是直接在cell.layer上進行添加 - 美觀起見,隱藏掉Cell的Select效果以及separatorStyle: table.separatorStyle = .none cell.selectionStyle = .none
# Project 10 - Stretchy Header
![Stretchy Header.gif](https://user-gold-cdn.xitu.io/2019/2/11/168dce460e600aec?w=295&h=529&f=gif&s=138267)
#### 我學到了 - 通過監聽ScrollView(及其子類)的scrollViewDidScroll代理可以知道scrollView被拉動的位移(offset) - 通過位移以及限定的縮放值可以得出圖片需要放大的倍率 - 通過設置ImageView的transform來完成修改即可,核心代碼為
bannerImgView.transform = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor) ````
簡單起見,我用Project 13的代碼基礎上進行修改,換了個清爽的綠色:) - 實現editActionsForRowAt這個delegate方法,返回值是Array,新建幾個你需要的功能返回即可 - 每一個Action直接通過UITableViewRowAction的init方法新建即可。在新建方法裏有block,直接將點擊邏輯寫進去就行了。 - 這種交互適用於Accessory比較簡單的情況,例如對交互按鈕大小和內容無要求的情況;如果有特殊要求,需要自定義UITableViewCell,手動控制Cell與捕捉UIPanGesture來進行實現。注意,這種方式要排除上下滑動Cell的情況,不要錯誤觸發。
attributedTitle
UIControlEvents.valueChanged
Domain=GEOErrorDomain Code=-8 "(null)"
CLGeocoder
能堅持看到這裡的,我給你們手動雙擊666!
實話實說,文章有點標題黨,實際開發時間是40天左右,因為開發時間在下班後到睡覺前,所以有時因為要出去聚餐,有時犯懶,還有時晚上要你懂得,所以完成這三十個項目的時間比計劃的時間要長。。。
寫完這些項目,感覺上一方面是提高了使用Swift語言的熟練度,另一方面更是複習了一遍iOS開發的知識點,因為寫到後來我已經基本感覺不出來跟用OC開發有什麼思路上的差異。這也回答了別人問過我的問題,「如果我現在學iOS開發,是應該學OC還是Swift」:
我覺得從iOS SDK的熟悉角度來說,沒有本質區別,如果熟悉OC下對應語法去使用Swift寫沒有太大區別。所以與其花時間糾結不如趕緊找兩個項目上手進行練習。
下一步,我打算再重新梳理下Swift語法,對這些項目進行小規模的重構,從結構上去看看能否挖掘到Swift的特性,從另一個角度(目前是功能角度)來學習Swift。所以也許還會有下一篇。