My exercises while reading the appcoda book.
see commit history for details.
- change cell style from basic to custom.
- change table view row height from default(44) to 80
- change cell row height to 80 (只要勾上custom会自动变成80)
- drag an image view to the cell (14,10) 60*60
- Add 3 labels: Name(Headline), Location(Light, 14, Dark Gray), Type(Light, 13)
- Stackview the 3 labels, spacing: 0 -> 1
- Stackview the image and above stackview, spacing: 0 -> 10, Alignment: Top
- 设置最外层的stack view 和cell边距的上左下右分别为2, 6, 1.5, 0这时stackview会填充整个cell, 但是图片被横向拉伸了
- 在outline中ctrl水平拖动image view到自身, 设置width和height为60
- 在outline的tableviewCell上点右键可以看到这个cell中定义的所有outlet
- UIKit中所有View都自带CALayer, 这个layer对象可以控制view的背景色,边框, 透明度, 圆角
- 将image view在outline中拖动到cell之上
- aspect fill + clip to bounds
- cell中加两个Label: Field(Medium) + Value
- stackview这两个label
- 见251页,给stackview设置constraints(spacing以及垂直居中)
- 这时会产生一个layout warning: 不能给两个label设置相同的hugging priority, 原因是我们只给stack view设置了constraints,而让stack view自动管理它所包含label的constraints, 结果是Field被拉伸了, Value大小保持正常. 这是因为Field和Value有相同的hugging priority:251, 只要把Field的priority设置的更高比如261, 那么Field就会保持自己的本来大小(intrinsic size), 而Value则被拉伸.
cell.thumbnailImageView.layer.cornerRadius =30.0 cell.thumbnailImageView.clipsToBounds =true或者选中image view在identity inspector中新增一个runtime属性layer.cornerRadius值为Number:30 并在attributes inspector中勾选clip to bounds
// set table view bg color tableView.backgroundColor =UIColor(white:240.0/255, alpha:0.2) // remove empty rows tableView.tableFooterView =UIView(frame:CGRect.zero) //set separator color tableView.separatorColor =UIColor(white:240.0/255, alpha:0.8)- 在didFinishLaunchingWithOptions设置nav bar的背景色
// nav bar bg color UINavigationBar.appearance().barTintColor =UIColor(red:216.0/255, green:74.0/255, blue:32.0/255, alpha:1.0) // nav bar button style(可以点击的) UINavigationBar.appearance().tintColor =UIColor.white // nav title style iflet barFont =UIFont(name:"Avenir-Light", size:24.0){UINavigationBar.appearance().titleTextAttributes =[NSForegroundColorAttributeName:UIColor.white, NSFontAttributeName: barFont]}Navbar的背景色为UINavigationBar.appearance().barTintColor, 但是它还有一个backgroundColor属性,呃.
- 在segue.source这边viewDidLoad中重新定义后退按钮(不带文字)
navigationItem.backBarButtonItem =UIBarButtonItem(title:"", style:.plain, target:nil, action:nil)- 在detail view的viewDidLoad中设置nav bar title
title = restaurant.name修改status bar黑色文字为白色, 两种方式:
ViewController逐个修改, 覆盖preferredStatusBarStyle即可(.lightContent),我设置了但是不起作用, 参考这里的solution, 在 viewDidLoad加上
navigationController?.navigationBar.barStyle = .blackTranslucent全局修改
- Info.plist设置
View controller-based status bar appearance=NO - AppDelegate中
UIApplication.shared.statusBarStyle = .lightContent
- 将Value label的Lines从1改成0, 这样Label可以显示多行文字
- tableView.estimatedHeight改成它的预计行高值(36/44), 以优化性能, 默认值是0
- tableView.rowHeight = UITableViewAutomaticDimension, 从iOS10开始, 这已经是默认值
- 这时console会有个layout warning, 解决办法是给这个cell中包含的那个stack view设置top和bottom约束(之前已经给它设定了leading/trailing和center vertically的约束,但是对于自适应大小的cell来说还不够)
- title=blank, image=check, (type=system, tint=white设置按钮的颜色)
- 点pin按钮,设置top=8,right=8,width=28,height=28
- drag a new view controller
- drag image view onto it, resize to full screen
- add missing constraints, 但是Xcode8.1上这个选项是灰的, 最后用了reset to suggested constraints
- container view: drag a view object onto the image view(x=53, y=40, 269*420)
- Drag a button(top=-13, right=-12, 28*28), title=blank, image=cross
- 在前一屏中加入
@IBAction func close(segue: UIStoryboardSegue){}这句代码告诉Xcode这个viewController可以被unwind - Ctrl drag this close button to the exit button on this review scene, and select
closeWithSegue:
在viewDidLoad中加入
letblurEffect=UIBlurEffect(style:.dark)letblurEffectView=UIVisualEffectView(effect: blurEffect) blurEffectView.frame = view.bounds backgroundImageView.addSubview(blurEffectView)就是给ImageView加上一个大小相同的subview, 上面代码中第三行view变量是所有UIViewController都有的, 表示这个ViewController管理的顶层view对象.
怎么将view的大小变成0? 大小值用CGAffineTransform表示
- 大小为0:CGAffineTransform(scaleX: 0, y: 0)
- 原始大小及位置:CGAffineTransform.indentiy
- 在viewDidLoad中将view的transform属性设置为0
- 在viewDIdAppear中将view的transform属性设置为原始值.
简单动画
UIView.animate(withDuration:0.3, animations:{self.containerView.transform =CGAffineTransform.identity })Spring动画(UIView.animate多加些参数)
UIView.animate(withDuration:0.3, delay:0.0, usingSpringWithDamping:0.3, initialSpringVelocity:0.2, options:.curveEaseOut, animations:{self.containerView.transform =CGAffineTransform.identity }, completion:nil)letscaleTransform=CGAffineTransform(scaleX:0, y:0)lettranslateTransform=CGAffineTransform(translationX:0, y:-1000)letcombineTransform= scaleTransform.concatenating(translateTransform) containerView.transform = combineTransformCGAffineTransform(translationX:y:)中的x, y都是相对于目标原始位置的偏移量, 并不是相对屏幕左上角.
在detailViewController中加入@IBAction func ratingButtonTapped(segue: UIStoryboardSegue){} 分别拖动review界面上的三个评价按钮到exit button, 全部选择ratingButtonTappedWithSegue:, 这样在outline中会多出三个unwind segue, 设定它们的identifier为great/good/dislike
@IBActionfunc ratingButtonTapped(segue:UIStoryboardSegue){iflet rating = segue.identifier { restaurant.isVisited =trueswitch rating {case"great": restaurant.rating ="love it." // ... default:break}} tableView.reloadData()}- Switch ON map capability
- Drag a map view onto the table view footer(height=135)
- We want a static map, so untick all Allows(zooming, scrolling...)
- That's all
- Drag a new view controller
- Drag a new map view, resize it to be full-screen, add missing constraints
- Ctrl drag from detail view controller to this newly created controller, segue.identifier=showMap(为什么不从static map拖到map view controller, 这是因为table header和footer都没法点击, 只能通过代码来打开新的view controller
在detail view的viewDidLoad中
lettapGestureRecognizer=UITapGestureRecognizer(target:self, action: #selector(showMap)) mapView.addGestureRecognizer(tapGestureRecognizer)performSegue以编程的方式触发transition.
func showMap(){performSegue(withIdentifier:"showMap", sender:self)}注意是UITapGestureRecognizer而非UIGestureRecognizer.
地址转coordinate: 你输入一个文本地址, 地图服务器通常会返回一堆相似的地址, 这堆文本地址叫placemarks
letgeoCoder=CLGeocoder() geoCoder.geocodeAddressString(restaurant.location, completionHandler:{ placemarks, error inletcoordinate=placemarks?[0].location?.coordinate })在static map上标记位置.
// 搜索地址 letgeoCoder=CLGeocoder() geoCoder.geocodeAddressString("湖北省鄂州高中", completionHandler:{ placemarks, error inif error !=nil{print(error!); return} // 地址转坐标 iflet coordinate =placemarks?[0].location?.coordinate { // 在那个位置显示一个pin letannotation=MKPointAnnotation() annotation.coordinate = coordinate self.mapView.addAnnotation(annotation) // 以那个pin为中心显示多大的区域, 半径250米 letregion=MKCoordinateRegionMakeWithDistance(coordinate,250,250)self.mapView.setRegion(region, animated:true)}})显示多个pin, 并选择一个弹出气泡提示
iflet coordinate =placemarks?[0].location?.coordinate {letannotation=MKPointAnnotation() annotation.coordinate = coordinate annotation.title ="湖北省鄂州高中" annotation.subtitle ="滨湖南路特一号" //self.mapView.addAnnotation(annotation) self.mapView.showAnnotations([annotation], animated:true)self.mapView.selectAnnotation(annotation, animated:true)}和上面的代码区别很小(并且这种情况下map会选择最优region)
- 5 cells, rowHeight=250,72,72,72,72
- image view (64*64) center
- text field(placeholder, no border, width = 339)
- select labels + text fields, add missing constraints
- YES/NO buttons, color=white, bgColor=red/gray
- Embed in navigation controller
- NEW Form为什么要套在nav controller中?(是为了在左上角加上Cancel按钮)
- Ctrl drag + 号到nav controller, 类型为Present Modally, identifier=addRestaurant
- HomeController中写上
@IBAction func unwindToHomeScreen(segue: UIStoryboardSegue){} - New Form左上弄个Cancel按钮, 并拖到Exit button上新增unwind segue
因为image view是放在第一个cell中. 只要实现didSelectRowAt, 并且在其中present系统内置的UIImagePickerController
if indexPath.row ==0{ifUIImagePickerController.isSourceTypeAvailable(.photoLibrary){letimagePicker=UIImagePickerController() imagePicker.allowsEditing =false imagePicker.sourceType =.photoLibrary present(imagePicker, animated:true, completion:nil)}}从iOS10开始, 需要在Info.plist中显示指定打开图库的理由, 以便得到用户允许.Privacy - Photo Library Usage Description=就是要看!
直接把.photoLibrary改成.camera可以拍照取图
ImagePicker的delegate必须同时满足两个接口:UIImagePickerControllerDelegate, UINavigationControllerDelegate 在didSelectRow中present前imagePicker.delegate = self, 然后实现下面这个回调函数即可 UIImagePickerControllerDelegate.didFinishPickingMediaWithInfo
func imagePickerController(_ picker:UIImagePickerController, didFinishPickingMediaWithInfo info:[String:Any]){iflet selectedImage =info[UIImagePickerControllerOriginalImage]as?UIImage{ photoImageView.image = selectedImage photoImageView.contentMode =.scaleAspectFill photoImageView.clipsToBounds =true}dismiss(animated:true, completion:nil)}在didSelectRowAt中present, 在这个回调中dismiss(这个dismiss是关闭从当前view controller中打开的modal对话框,并不是关闭自身)
A layout constraint defines a relationship between two user interface objects用公式表示: photoImageView.leading = superview.leading * 1 + 0 用代码实现这个公式.
letleading=NSLayoutConstraint(item: photoImageView, attribute:.leading, relatedBy:.equal, toItem: photoImageView.superview, attribute:.leading, multiplier:1, constant:0) leading.isActive =true@IBActionfunc saveRestaurant(){ if checkForm(){ //dismiss(animated: true, completion: nil) performSegue(withIdentifier:"unwindToHomeScreen", sender:self)}else{因为我想沿用Cancel按钮使用的unwind segue,在用第2种方法的时候碰到了找不到unwind segue identifier的问题, 参见SO解决: 虽然unwind segue的Action segue是根据你所定义的func名字生成的, 但是identifier还是需要自己指定.
新建一个Data Model: FoodPinDemo.xcdatamodeld, Entity改名为Restaurant, Class改名为RestaurantMO(这个Managed Object类编译项目会自动生成, project中看不到)
RestaurantMO所有属性都变成了optional, 并且image(binary)的类型为NSData.
主要变化
- UIImage(named: restaurant.image) -> UIImage(data: restaurant.image as! Data)
- resturant.location -> restaurant.location!
- restaurant.rating -> restaurant.rating ?? ""
之前的Restaurant.swift废掉, 可以删除.
findAll, 在viewWillAppear中加入:
letappDelegate=UIApplication.shared.delegate as!AppDelegateletctx= appDelegate.persistentContainer.viewContext letrequest:NSFetchRequest<RestaurantMO>=RestaurantMO.fetchRequest()letrestaurants=try! ctx.fetch(request)简单封装的工具类CD(save传的参数没有用到, 只是为了好看!):
classCD{classvarappDelegate:AppDelegate{returnUIApplication.shared.delegate as!AppDelegate}classvarctx:NSManagedObjectContext{return appDelegate.persistentContainer.viewContext }classfunc delete<T:NSManagedObject>(_ o:T){ ctx.delete(o)save(o)}classfunc save(_ _:NSManagedObject){ appDelegate.saveContext()}classfunc image2Data(image:UIImage?)->NSData?{iflet image = image {iflet imageData =UIImagePNGRepresentation(image){returnNSData(data: imageData)}}returnnil}这个类间歇性的报ambiguous use of x 的错误, 但是程序能正常运行, 怀疑是Xcode8.1的bug.
增删改查:
// create (AddRestaurantController) letrestaurant=RestaurantMO(context:CD.ctx) restaurant.name = name restaurant.type = type restaurant.image =CD.image2Data(image: photoImageView.image)CD.save(restaurant) // delete (editActionsForRowAt) letrestaurant= restaurants.remove(at: indexPath.row) tableView.deleteRows(at:[indexPath], with:.fade)CD.delete(restaurant) // update (ratingButtonTapped) restaurant.isVisited =trueCD.save(restaurant) // find all (viewWillAppear) letrequest:NSFetchRequest<RestaurantMO>=RestaurantMO.fetchRequest() restaurants =try!CD.ctx.fetch(request) tableView.reloadData()待研究: http://stackoverflow.com/questions/37810967/how-to-apply-the-type-to-a-nsfetchrequest-instance
以为是拖的, 没想到是在viewDidLoad中加两行代码
searchController =UISearchController(searchResultsController:nil) tableView.tableHeaderView = searchController.searchBar- 按套路是用delegate实现,然而并没有使用UISearchControllerDelegate,用的是UISearchResultsUpdating, 在viewDidLoad中加入
searchController.searchResultsUpdater =self searchController.dimsBackgroundDuringPresentation =false第二句是让search结果弹出时, 背景模糊(因为我们没有用单独的view来显示查询结果, 没有背景层, 在此设为false)
- 处理search的回调函数: UISearchResultsUpdating.updateSearchResults(for:)
func updateSearchResults(for searchController:UISearchController){iflet text = searchController.searchBar.text { searchResults = restaurants.filter{ restaurant ->Booliniflet name = restaurant.name {letisMatch= name.localizedCaseInsensitiveContains(text)return isMatch }returnfalse} tableView.reloadData()}}在先前用到restaurants的地方依情况选用searchResults, 包含numberOfRowsInSection, cellForRowAtIndexPath, prepare(for:), 使用
searchController.isActive判断当前是否处在search模式下.在Search时禁用Delete/Share功能
overridefunc tableView(_:canEditRowAt:)->Bool{if searchController.isActive {returnfalse}returntrue}viewDidLoad设置前景色(Cancel按钮), 背景色.
letsearchBar= searchController.searchBar searchBar.placeholder ="Search restaurants..." searchBar.tintColor =UIColor.white searchBar.barTintColor =UIColor(red:218.0/255, green:100.0/255, blue:70.0/255, alpha:1.0) tableView.tableHeaderView = searchBar发现search bar上面会出现很丑的边框, 参考了作者的代码,发现作者和书中写的不一样, 最终选的灰色searchBar.barTintColor = UIColor(white: 236.0/255, alpha: 1.0), 去掉边框的解决办法参考这里(未试)
UIPageViewController(我喜欢叫它Page Container)是多个View的容器(类似Navigation Controller), 用来管理它所包含的多个子view, 但是由于子view的相似性, 我们只用拖一个View Controller做为模版(我叫它Single Page).
拖一个page view controller
Transition style从
Page Curl(翻书效果)改成ScrollStoryboard ID: PageContainer
在list view的viewDidAppear中显示page container
func viewDidAppear(){iflet pageContainer = storyboard?.instantiateViewController(withIdentifier:"PageContainer")as?PageContainer{present(pageContainer, animated:true, completion:nil)}}在page container的viewDidLoad中加载第一个page
使用UIPageViewController.setViewControllers指定显示哪个page, 实现DataSource接口中的两个方法viewControllerBefore, viewControllerAfter设定向前和向后翻页时显示哪个page.
classPageContainer:UIPageViewController,UIPageViewControllerDataSource{varpageHeadings=["Personalize","Locate","Discover"]varpageImages=["foodpin-intro-1","foodpin-intro-2","foodpin-intro-3"]varpageContent=["content1","2","3"]overridefunc viewDidLoad(){ dataSource =selfiflet startingPage =page(at:0){setViewControllers([startingPage], direction:.forward, animated:true, completion:nil)}}func pageViewController(_ pageViewController:UIPageViewController, viewControllerBefore viewController:UIViewController)->UIViewController?{varindex=(viewController as!SinglePage).index index -=1returnpage(at: index)}func pageViewController(_ pageViewController:UIPageViewController, viewControllerAfter viewController:UIViewController)->UIViewController?{varindex=(viewController as!SinglePage).index index +=1returnpage(at: index)}func page(at index:Int)->SinglePage?{if index <0 || index >= pageHeadings.count {returnnil}iflet page = storyboard?.instantiateViewController(withIdentifier:"SinglePage")as?SinglePage{ page.imageFile =pageImages[index] page.heading =pageHeadings[index] page.content =pageContent[index] page.index = index return page }returnnil}}Single Page是一个简单的View Controller,在Page Container的page(at)方法中动态的创建设置各个属性值并做为viewControllerBefore的返回值.
- Drag a view controller, view bgcolor=#c0392b
- Label("Personalize")=top, hCenter
- Image View(300*232)=Aspect Ratio
- Label("Pin your ...",align=center, lines=0)=w282 h64
- Storyboard ID = SinglePage
- Set custom class, bind IBOutlets
classSinglePage:UIViewController{@IBOutlet weak varheadingLabel:UILabel!@IBOutlet weak varcontentLabel:UILabel!@IBOutlet weak varcontentImageView:UIImageView!varindex=0varheading=""varimageFile=""varcontent=""overridefunc viewDidLoad(){ super.viewDidLoad() headingLabel.text = heading contentLabel.text = content contentImageView.image =UIImage(named: imageFile)}}在Page Container中实现UIPageViewControllerDataSource中的两个接口即可.
func presentationCount(for pageViewController:UIPageViewController)->Int{print("presentationCount: \(pageHeadings.count)")return pageHeadings.count }func presentationIndex(for pageViewController:UIPageViewController)->Int{print("presentationIndex")iflet page = storyboard?.instantiateViewController(withIdentifier:"SinglePage")as?SinglePage{print("page index is: \(page.index)")return page.index }print("no page found, return 0")return0}还不太理解两个方法的调用时机, 实测presentationIndex永远都是返回0, 感觉很诡异, 这两个方法造中出来indicator样式太丑,无视.
- 拖一个page control到single page底部
- 设置pages=3(默认值), add missing constraints, 设置outlet.
- 在single Page的viewDidLoad中
pageControl.currentPage = index - That's all
拖个button到最右下角(bottom=7, right=0), 设置outlet
在single page的viewDidLoad中动态修改按钮的文本
switch index {case0...1: forwardButton.setTitle("NEXT", for:.normal)case2: forwardButton.setTitle("DONE", for:.normal)default:break}- 给NEXT按钮绑定点击事件
@IBActionfunc buttonTapped(_ sender:UIButton){switch index {case0...1:letpageContainer= parent as!PageContainer pageContainer.forward(index: index)case2:dismiss(animated:true, completion:nil)default:break}}- PageContainer增加翻页的方法(forward)
func forward(index:Int){iflet nextPage =page(at: index +1){setViewControllers([nextPage], direction:.forward, animated:true, completion:nil)}}在DONE按钮的点击事件中,存个值到UserDefaults中: UserDefaults.standard.set(true, forKey: "hasViewedWalkthrough")
在列表页viewDidAppear中判断是否存过,存过直接返回: if UserDefaults.standard.bool(forKey: "hasViewedWalkthrough"){return}
把initial navigation controller直接embed in tab bar controller, 然后先前的initial nav controller的底部就会多出一个tab bar item, 点击它, 并设置System item: Favorites, 就这么简单!
在navigation controller内部非第1页的界面我们可以隐藏tab bar, 比如在detail view上要隐藏tab bar, 两种方式: 1)在IB中找到detail view勾选
Hide Bottom Bar on Push; 2) 在列表页的prepare(for:)中加上segue.destination.hidesBottomBarWhenPushed = true
吐槽一下Xcode8.1: 在embed in tab bar以后, 好几个view报missing constraints的错误, 但是constraint一点问题都没有并且运行也正常.
- 拖一个navigation controller到storyboard(会自动带出一个table view controller)修改它的navigation item title为Discover
- 从Tab bar controller拖Ctrl Drag到新的nav controller选择
Relationship Segue: view controllers - 修改tab bar item的类型为Recents
- 如法炮制创建一个About tab.
类似nav bar, 在didFinishLaunchingWithOptions中:
//前景色 UITabBar.appearance().tintColor =UIColor(red:235.0/255, green:75.0/255, blue:27.0/255, alpha:1.0) //背景色 UITabBar.appearance().barTintColor =UIColor(red:236.0/255, green:240.0/255, blue:241.0/255, alpha:1.0) //tab选中状态时的背景(默认无) //UITabBar.appearance().selectionIndicatorImage = #imageLiteral(resourceName: "tabitem-selected")改变tab item的文字和图片: Bar item设置title和image即可(此时System Item会自动变回Custom)
正式的名称叫Storyboard References(Xcode7的新特性), 比较简单, 直接圈住从tab bar controller出发的指向discover nav controller的segue 顺流而下 一直到 discover table view controller, 然后选择Editor > Refactor to storyboard... 取名为discover.storyboard, 如法炮制提取about.storyboard.
打开about.storyboard
- 拖image view到table view header(cell的上面), height=190, Content Mode=aspect fit, image=about-logo
- Cell的style设置为Basic
- 创建对应的AboutTableViewController(2 sections), 覆盖numberOfSections, numberOfRows, titleForHeaderInSection, cellForRowAt
- 隐藏table footer:
tableView.tableFooterView=UIView(frame: CGRect.zero)
classAboutTableViewController:UITableViewController{varsectionTitles=["",""]varsectionContent=[["",""],["","",""]]varlinks=["","",""]overridefunc viewDidLoad(){ super.viewDidLoad() tableView.tableFooterView =UIView(frame:CGRect.zero)}didSelectRowAt中处理第1个Row的点击事件(用safari打开某个link)
func tableView(didSelectRowAt){switch(indexPath.section, indexPath.row){case(0,0):iflet url =URL(string:"http://www.apple.com/itunes/charts/paid-apps"){UIApplication.shared.open(url)}default:break} tableView.deselectRow(at: indexPath, animated:false)}基本用法
letwebView=WKWebVew() webView.load(URLRequest(url:URL(string:"http://blabla")) webView.load(URLRequest(url:URL(fileURLWithPath:"about.html")))- 拖一个新的view controller(空的, 之前以为要添加一个全屏的web view, 事实上什么也不用加)
- 从About screen view controller Ctrl drag到这个新的View Controller(segue: Show, id: showWebView)
- 设置对应的UIViewController类.
import WebKit classWebViewController:UIViewController{varwebView:WKWebView!overridefunc viewDidLoad(){ super.viewDidLoad()iflet url =URL(string:"http://www.appcoda.com/contact"){letrequest=URLRequest(url: url) webView.load(request)}}overridefunc loadView(){ webView =WKWebView() view = webView }}其中loadView会在viewDidLoad之前被调用, 在这个方法中使用WKWebView替换掉View Controller自带的顶层view对象.
最后在About的didSelectRowAt, 在第2行点击的时候打开这个ViewController: performSegue(withIdentifier: "showWebView", sender: self)
从iOS9开始, Apple要求在后台只能打开HTTPS网站, 引入了 App Transport Security(简称ATS), 在这里我们想打开http网站, 在Info.plist中禁用ATS: ATS > Allow Arbitrary Loads = YES
内嵌Safari浏览器, 不用画任何界面, 直接present, 用法
import SafariServices letsvc=SFSafariViewController(url: url/*, entersReaderIfAvailable: true*/)present(svc)在About view controller的didSelectRowAt加上case(1,_)
func tableView(didSelectRowAt){switch(indexPath.section, indexPath.row){ // open url case(0,0):iflet url =URL(string:"http://www.apple.com/itunes/charts/paid-apps"){UIApplication.shared.open(url)} // WKWebView case(0,1):performSegue(withIdentifier:"showWebView", sender:self) // Embed safari browser case(1,_):iflet url =URL(string:links[indexPath.row]){letsvc=SFSafariViewController(url: url)present(svc, animated:true, completion:nil)}default:break} tableView.deselectRow(at: indexPath, animated:false)}CloudKit需要开发者账号才能玩, $99 啊啊, 大出血.
在CloudKit中一个App对应一个Container, container中包含public, private, shared三种类型的DB, (shared是iOS10新增的类型), 所有安装了FoodPinDemo的用户都能访问public db(如果是写需要登录一次iCould), private只有用户自己能访问, shared只有group内的用户能访问(相当于QQ群), Db下分为Default Zone和Custom Zone, Zone下面是Record(一条条记录)
基本使用:
Mobile端: 在Capabilities中将CloudKit打开, services从Key-value storage改成CloudKit, Containers选择默认的Use default container (然后Xcode会自动到iCould上创建相应的container, 若有失败,可能是Bundle ID重复, 尝试换个Bundle ID)
服务端: 用Safari浏览器登录到apple dev center打开CloudKit Dashboard 在对应的Container中新建叫Restaurant的Record Type, 定义字段String(name,type,location,phone), Asset(image), 最后在Public Data - Default Zone中插入若干测试数据, 就能玩了..(CK中所有图片, 文件的类型都叫Asset), 然后可以使用傻瓜版的叫convenience API或高级版的叫operational API来抓或存数据. Convenience API没什么卵用, 即不能指定select也不能指定where.
var restaurants:[CKRecord] = []并且在viewDidLoad中封装如下fetchRecordsFromCloud()
// Fetch data using Convenience API letcloudContainer=CKContainer.default()letpublicDatabase= cloudContainer.publicCloudDatabase letpredicate=NSPredicate(value:true)letquery=CKQuery(recordType:"Restaurant", predicate: predicate) publicDatabase.perform(query, inZoneWith:nil, completionHandler:{(results, error)->Voidinif error !=nil{return}iflet results = results {print("Completed the download of Restaurant data")self.restaurants = results self.tableView.reloadData()}})修改cellForRow
overridefunc tableView(cellForRowAt){letcell= tableView.dequeueReusableCell(withIdentifier:"Cell", for: indexPath) // Configure the cell... letrestaurant=restaurants[indexPath.row] cell.textLabel?.text = restaurant.object(forKey:"name")as?Stringiflet image = restaurant.object(forKey:"image"){letimageAsset= image as!CKAssetiflet imageData =try?Data.init(contentsOf: imageAsset.fileURL){ cell.imageView?.image =UIImage(data: imageData)}}return cellCKRecord是key-value pair,. image是CKAsset类型. fileURL是CloudKit下载资源时的临时存储位置.publicDatabase.perform在抓数据是会新开一个后台线程, 在下载完成后reloadData()应该放在UI Thread中来完成, 因为OS会给bg thread很低的处理优先级, 即使数据下完了, tableView.reloadData()也不会立即执行, 解决办法见swift3 concurrency:
OperationQueue.main.addOperation{self.tableView.reloadData()}fetchRecordsFromCloud
letcloudContainer=CKContainer.default()letpublicDatabase= cloudContainer.publicCloudDatabase // predicate指定空的where语句 letpredicate=NSPredicate(value:true)letquery=CKQuery(recordType:"Restaurant", predicate: predicate) // sortDescriptors按创建时间倒序排 query.sortDescriptors =[NSSortDescriptor(key:"creationDate", ascending:false)] // Create the query operation with the query letqueryOperation=CKQueryOperation(query: query) queryOperation.desiredKeys =["name","image"] queryOperation.queuePriority =.veryHigh queryOperation.resultsLimit =50 queryOperation.recordFetchedBlock ={(record)->Voidinself.restaurants.append(record)} queryOperation.queryCompletionBlock ={(cursor, error)->Voidiniflet error = error {print("Failed to get data from iCloud - \(error.localizedDescription)")return}print("Successfully retrieve the data from iCloud")OperationQueue.main.addOperation{self.tableView.reloadData()}} // Execute the query publicDatabase.add(queryOperation)desiredKeys指定select, resultsLimit指定limit, recordFetchedBlock是单条记录下载完成后的回调, queryCompletionBlock是所有记录下载完成后的回调.CKQueryCursor标记当前已经下载记录的位置(下次抓取时的起始位置), 可在分批下载数据时使用.
分为real performance和perceived performance.
tinypng.com
drag an Activity Indicator View object to the scene dock of the table view controller, 然后指定@IBOutlet
这样和直接拖到view controller上没什么区别, 把它扔到边边上然后在viewDidLoad中指定它的实际位置就行.
@IBOutletvarspinner:UIActivityIndicatorView! // viewDidLoad spinner.hidesWhenStopped =true spinner.center = view.center tableView.addSubview(spinner) spinner.startAnimating()- 在下载结束的回调函数中隐藏
OperationQueue.main.addOperation{self.spinner.stopAnimating()self.tableView.reloadData()}- 查的时候只查name, 将
queryOperation.desiredKeys = ["name", "image"]改成queryOperation.desiredKeys = ["name"], 并给cell一个默认的图片, 这样可以实现秒加载 - 在显示数据(cellForRowAt)的时候再去逐个下载图片
// Configure the cell... letrestaurant=restaurants[indexPath.row] cell.textLabel?.text = restaurant.object(forKey:"name")as?String // Set the default image cell.imageView?.image =UIImage(named:"photoalbum") // Fetch Image from Cloud in background letpublicDatabase=CKContainer.default().publicCloudDatabase letfetchRecordsImageOperation=CKFetchRecordsOperation(recordIDs:[restaurant.recordID]) fetchRecordsImageOperation.desiredKeys =["image"] fetchRecordsImageOperation.queuePriority =.veryHigh fetchRecordsImageOperation.perRecordCompletionBlock ={(record, recordID, error)->Void in iflet error = error {print("Failed to get restaurant image: \(error.localizedDescription)")return} if let restaurantRecord =record{OperationQueue.main.addOperation(){ if let image = restaurantRecord.object(forKey:"image"){letimageAsset= image as!CKAssetiflet imageData =try?Data.init(contentsOf: imageAsset.fileURL){ cell.imageView?.image =UIImage(data: imageData)... publicDatabase.add(fetchRecordsImageOperation)return cell }CKRecord中会自动包含recordID,用它来指定去下载哪条记录的image字段
在滑动表格的时候, cellForRowAt会不断被调用, 先前下载的图片每次都要重新下载, 使用NSCache来解决.
varimageCache=NSCache<CKRecordID,NSURL>()因为图片下载后会被CloudKit缓存到fileURL指定的位置, 我们只要在NSCache中存这个文件的位置就行.
if let image = restaurantRecord.object(forKey:"image"){letimageAsset= image as!CKAsset if let imageData =try?Data.init(contentsOf:imageAsset.fileURL){ cell.imageView?.image =UIImage(data: imageData) // Add the image URL to cache self.imageCache.setObject(imageAsset.fileURL asNSURL,forKey: restaurant.recordID)超级简单, 所有的tableViewController自带refreshControl属性,只要给它指定一个值即可.(viewDidLoad)
// Pull To Refresh Control refreshControl =UIRefreshControl() refreshControl?.backgroundColor =UIColor.white refreshControl?.tintColor =UIColor.gray refreshControl?.addTarget(self, action: #selector(fetchRecordsFromCloud), for:UIControlEvents.valueChanged)当下拉到一定程度的时候, ptr组件会触发UIControlEvent.valueChanged事件, 我们在上面的代码中监听这个事件, 指定回调函数就行(#selector是Xcode7.3/Swift2.2新增特性)
需要在刷新结束后隐藏ptr组件.
OperationQueue.main.addOperation{self.spinner.stopAnimating()self.tableView.reloadData()iflet refreshControl =self.refreshControl {if refreshControl.isRefreshing { refreshControl.endRefreshing()}}}在开始刷新数据前先清空旧数据.
func fetchRecordsFromCloud(){ // fix ptr bug restaurants.removeAll() tableView.reloadData()在添加界面除了用CoreData向本地写数据外, 同时往iCloud上的public db上写一份(共享给他人) saveRecordToCloud(restaurant), 放在dismiss(animated:completion:)之前
import CloudKit func saveRecordToCloud(restaurant:RestaurantMO!)->Void{ // Prepare the record to save letrecord=CKRecord(recordType:"Restaurant") record.setValue(restaurant.name, forKey:"name") record.setValue(restaurant.type, forKey:"type") record.setValue(restaurant.location, forKey:"location") record.setValue(restaurant.phone, forKey:"phone")letimageData= restaurant.image as!Data // Resize the image letoriginalImage=UIImage(data: imageData)! letscalingFactor=(originalImage.size.width >1024)?1024/ originalImage.size.width :1.0letscaledImage=UIImage(data: imageData, scale: scalingFactor)! // Write the image to local file for temporary use letimageFilePath=NSTemporaryDirectory()+ restaurant.name! letimageFileURL=URL(fileURLWithPath: imageFilePath)try?UIImageJPEGRepresentation(scaledImage,0.8)?.write(to: imageFileURL) // Create image asset for upload letimageAsset=CKAsset(fileURL: imageFileURL) record.setValue(imageAsset, forKey:"image") // Get the Public iCloud Database letpublicDatabase=CKContainer.default().publicCloudDatabase // Save the record to iCloud publicDatabase.save(record, completionHandler:{(record, error)->Voidin // Remove temp file try?FileManager.default.removeItem(at: imageFileURL)})对图片的处理稍显复杂: 先用UIImage对宽度超过1024的图片进行resize, 再用UIImageJPEGRepresentation将图片压缩并写入到临时文件夹NSTemporaryDirectory(), 然后再根据图片的地址构建CKAsset, 在save的回调函数中删除临时文件
- 取选中行的行号: tableView.indexPathForSelectedRow
- editActionsForRowAt
- UITableViewRowAction
- UIActivityViewController
- prepare(for:sender:)
- segue.destination/segue.identifier
- UINavigationBar.appearance().barTintColor
- UIApplication.shared.statusBarStyle
- Dynamic Type - use a text style instead of a fixed font type.
- CLGeocoder.geocodeAddressString
- mapView.addAnnotation(MKPointAnnotation)
- UIImagePickerController
- NSLayoutConstraint(..).isActive
- UIApplication.shared.delegate
- NSData(data: UIImagePNGRepresentation(image))
- UISearchResultsUpdating
- String.localizedCaseInsensitiveContains
- viewDidAppear: present(storyboard?.instantiateViewController(withIdentifier: "PageContainer") as? PageContainer)
- PageContainer.setViewControllers([startingPage])
- UIPageViewControllerDataSource.viewControllerBefore
- storyboard?.instantiateViewController(withIdentifier: "SinglePage") as? SinglePage
- PageControl.currentPage = index
- SinglePage中
let pageContainer = parent as! PageContainer - UserDefaults.standard
- UITabBar.appearance().tintColor
- Refactor to storyboard...
- UIApplication.shared.open(URL(string:))
- WKWebView().load(URLRequest(url: URL(fileURLWithPath:"about.html")))
- present(SFSafariViewController(url:))
- Swipe to hide
- MapKit: show image on callout bubble
- NSFetchedResultsController
- P393 Search bar延伸阅读
- How to switch between storyboard and swift file
View > Show Tab Bar, create a new tab, for one tab, you can open storyboard, for the other, you can open the swift file, then you can use
shift+cmd+]to switch between interface builder and source code file.
- Interface builder: Zoom to fit
- Can directly drag image from Finder to Simulator
