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
if let 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中加入
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = 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)
let scaleTransform = CGAffineTransform(scaleX: 0, y: 0)
let translateTransform = CGAffineTransform(translationX: 0, y: -1000)
let combineTransform = scaleTransform.concatenating(translateTransform)
containerView.transform = combineTransform
CGAffineTransform(translationX:y:)中的x, y都是相对于目标原始位置的偏移量, 并不是相对屏幕左上角.
在detailViewController中加入@IBAction func ratingButtonTapped(segue: UIStoryboardSegue) {}
分别拖动review界面上的三个评价按钮到exit button, 全部选择ratingButtonTappedWithSegue:, 这样在outline中会多出三个unwind segue, 设定它们的identifier为great/good/dislike
@IBAction func ratingButtonTapped(segue: UIStoryboardSegue) {
if let rating = segue.identifier {
restaurant.isVisited = true
switch 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中
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showMap))
mapView.addGestureRecognizer(tapGestureRecognizer)
performSegue以编程的方式触发transition.
func showMap(){
performSegue(withIdentifier: "showMap", sender: self)
}
注意是UITapGestureRecognizer而非UIGestureRecognizer.
地址转coordinate: 你输入一个文本地址, 地图服务器通常会返回一堆相似的地址, 这堆文本地址叫placemarks
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(restaurant.location, completionHandler: {
placemarks, error in
let coordinate = placemarks?[0].location?.coordinate
})
在static map上标记位置.
// 搜索地址
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString("湖北省鄂州高中", completionHandler: {
placemarks, error in
if error != nil { print(error!); return }
// 地址转坐标
if let coordinate = placemarks?[0].location?.coordinate {
// 在那个位置显示一个pin
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
self.mapView.addAnnotation(annotation)
// 以那个pin为中心显示多大的区域, 半径250米
let region = MKCoordinateRegionMakeWithDistance(coordinate, 250, 250)
self.mapView.setRegion(region, animated: true)
}
})
显示多个pin, 并选择一个弹出气泡提示
if let coordinate = placemarks?[0].location?.coordinate {
let annotation = 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 {
if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
let imagePicker = 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]) {
if let 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
用代码实现这个公式.
let leading = NSLayoutConstraint(item: photoImageView, attribute: .leading, relatedBy: .equal, toItem: photoImageView.superview, attribute: .leading, multiplier: 1, constant: 0)
leading.isActive = true
@IBAction func 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中加入:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let ctx = appDelegate.persistentContainer.viewContext
let request: NSFetchRequest<RestaurantMO> = RestaurantMO.fetchRequest()
let restaurants = try! ctx.fetch(request)
简单封装的工具类CD(save传的参数没有用到, 只是为了好看!):
class CD {
class var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
class var ctx: NSManagedObjectContext {
return appDelegate.persistentContainer.viewContext
}
class func delete<T: NSManagedObject>(_ o: T) {
ctx.delete(o)
save(o)
}
class func save(_ _: NSManagedObject) {
appDelegate.saveContext()
}
class func image2Data(image: UIImage?) -> NSData? {
if let image = image {
if let imageData = UIImagePNGRepresentation(image) {
return NSData(data: imageData)
}
}
return nil
}
这个类间歇性的报ambiguous use of x 的错误, 但是程序能正常运行, 怀疑是Xcode8.1的bug.
增删改查:
// create (AddRestaurantController)
let restaurant = RestaurantMO(context: CD.ctx)
restaurant.name = name
restaurant.type = type
restaurant.image = CD.image2Data(image: photoImageView.image)
CD.save(restaurant)
// delete (editActionsForRowAt)
let restaurant = restaurants.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
CD.delete(restaurant)
// update (ratingButtonTapped)
restaurant.isVisited = true
CD.save(restaurant)
// find all (viewWillAppear)
let request: 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) {
if let text = searchController.searchBar.text {
searchResults = restaurants.filter { restaurant -> Bool in
if let name = restaurant.name {
let isMatch = name.localizedCaseInsensitiveContains(text)
return isMatch
}
return false
}
tableView.reloadData()
}
}
-
在先前用到restaurants的地方依情况选用searchResults, 包含numberOfRowsInSection, cellForRowAtIndexPath, prepare(for:), 使用
searchController.isActive
判断当前是否处在search模式下. -
在Search时禁用Delete/Share功能
override func tableView(_:canEditRowAt:) -> Bool {
if searchController.isActive {
return false
}
return true
}
viewDidLoad设置前景色(Cancel按钮), 背景色.
let searchBar = 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(翻书效果)
改成Scroll
-
Storyboard ID: PageContainer
-
在list view的viewDidAppear中显示page container
func viewDidAppear() {
if let pageContainer = storyboard?.instantiateViewController(withIdentifier: "PageContainer") as? PageContainer {
present(pageContainer, animated: true, completion: nil)
}
}
-
在page container的viewDidLoad中加载第一个page
-
使用UIPageViewController.setViewControllers指定显示哪个page, 实现DataSource接口中的两个方法viewControllerBefore, viewControllerAfter设定向前和向后翻页时显示哪个page.
class PageContainer: UIPageViewController, UIPageViewControllerDataSource {
var pageHeadings = ["Personalize", "Locate", "Discover"]
var pageImages = ["foodpin-intro-1", "foodpin-intro-2", "foodpin-intro-3"]
var pageContent = ["content1","2","3"]
override func viewDidLoad() {
dataSource = self
if let startingPage = page(at: 0) {
setViewControllers([startingPage], direction: .forward, animated: true, completion: nil)
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = (viewController as! SinglePage).index
index -= 1
return page(at: index)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter
viewController: UIViewController) -> UIViewController? {
var index = (viewController as! SinglePage).index
index += 1
return page(at: index)
}
func page(at index: Int) -> SinglePage? {
if index < 0 || index >= pageHeadings.count {
return nil
}
if let page = storyboard?.instantiateViewController(withIdentifier: "SinglePage") as? SinglePage{
page.imageFile = pageImages[index]
page.heading = pageHeadings[index]
page.content = pageContent[index]
page.index = index
return page
}
return nil
}
}
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
class SinglePage: UIViewController {
@IBOutlet weak var headingLabel: UILabel!
@IBOutlet weak var contentLabel: UILabel!
@IBOutlet weak var contentImageView: UIImageView!
var index = 0
var heading = ""
var imageFile = ""
var content = ""
override func 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")
if let page = storyboard?.instantiateViewController(withIdentifier: "SinglePage") as? SinglePage {
print("page index is: \(page.index)")
return page.index
}
print("no page found, return 0")
return 0
}
还不太理解两个方法的调用时机, 实测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 {
case 0...1:
forwardButton.setTitle("NEXT", for: .normal)
case 2:
forwardButton.setTitle("DONE", for: .normal)
default:
break
}
- 给NEXT按钮绑定点击事件
@IBAction func buttonTapped(_ sender: UIButton) {
switch index {
case 0...1:
let pageContainer = parent as! PageContainer
pageContainer.forward(index: index)
case 2:
dismiss(animated: true, completion: nil)
default:
break
}
}
- PageContainer增加翻页的方法(forward)
func forward(index: Int) {
if let 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)
class AboutTableViewController: UITableViewController {
var sectionTitles = ["",""]
var sectionContent = [["",""],["","",""]]
var links = ["","",""]
override func 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):
if let url = URL(string: "http://www.apple.com/itunes/charts/paid-apps") {
UIApplication.shared.open(url)
}
default:
break
}
tableView.deselectRow(at: indexPath, animated: false)
}
基本用法
let webView = 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
class WebViewController: UIViewController {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
if let url = URL(string: "http://www.appcoda.com/contact") {
let request = URLRequest(url: url)
webView.load(request)
}
}
override func 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
let svc = 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):
if let 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,_):
if let url = URL(string: links[indexPath.row]) {
let svc = 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
let cloudContainer = CKContainer.default()
let publicDatabase = cloudContainer.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Restaurant", predicate: predicate)
publicDatabase.perform(query, inZoneWith: nil, completionHandler: {
(results, error) -> Void in
if error != nil {return}
if let results = results {
print("Completed the download of Restaurant data")
self.restaurants = results
self.tableView.reloadData()
}
})
修改cellForRow
override func tableView(cellForRowAt) {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for:
indexPath)
// Configure the cell...
let restaurant = restaurants[indexPath.row]
cell.textLabel?.text = restaurant.object(forKey: "name") as? String
if let image = restaurant.object(forKey: "image") {
let imageAsset = image as! CKAsset
if let imageData = try? Data.init(contentsOf: imageAsset.fileURL) {
cell.imageView?.image = UIImage(data: imageData)
}
}
return cell
CKRecord是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
let cloudContainer = CKContainer.default()
let publicDatabase = cloudContainer.publicCloudDatabase
// predicate指定空的where语句
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Restaurant", predicate: predicate)
// sortDescriptors按创建时间倒序排
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending:
false)]
// Create the query operation with the query
let queryOperation = CKQueryOperation(query: query)
queryOperation.desiredKeys = ["name", "image"]
queryOperation.queuePriority = .veryHigh
queryOperation.resultsLimit = 50
queryOperation.recordFetchedBlock = { (record) -> Void in
self.restaurants.append(record)
}
queryOperation.queryCompletionBlock = { (cursor, error) -> Void in
if let 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中指定它的实际位置就行.
@IBOutlet var spinner: 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...
let restaurant = 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
let publicDatabase = CKContainer.default().publicCloudDatabase
let fetchRecordsImageOperation = CKFetchRecordsOperation(recordIDs:
[restaurant.recordID])
fetchRecordsImageOperation.desiredKeys = ["image"]
fetchRecordsImageOperation.queuePriority = .veryHigh
fetchRecordsImageOperation.perRecordCompletionBlock = { (record, recordID,
error) -> Void in
if let 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") {
let imageAsset = image as! CKAsset
if let imageData = try? Data.init(contentsOf:
imageAsset.fileURL) {
cell.imageView?.image = UIImage(data: imageData)
...
publicDatabase.add(fetchRecordsImageOperation)
return cell
}
CKRecord中会自动包含recordID,用它来指定去下载哪条记录的image字段
在滑动表格的时候, cellForRowAt会不断被调用, 先前下载的图片每次都要重新下载, 使用NSCache来解决.
var imageCache = NSCache<CKRecordID, NSURL>()
因为图片下载后会被CloudKit缓存到fileURL指定的位置, 我们只要在NSCache中存这个文件的位置就行.
if let image = restaurantRecord.object(forKey: "image") {
let imageAsset = 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 as NSURL,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()
if let 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
let record = 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")
let imageData = restaurant.image as! Data
// Resize the image
let originalImage = UIImage(data: imageData)!
let scalingFactor = (originalImage.size.width > 1024) ? 1024 /
originalImage.size.width : 1.0
let scaledImage = UIImage(data: imageData, scale: scalingFactor)!
// Write the image to local file for temporary use
let imageFilePath = NSTemporaryDirectory() + restaurant.name!
let imageFileURL = URL(fileURLWithPath: imageFilePath)
try? UIImageJPEGRepresentation(scaledImage, 0.8)?.write(to: imageFileURL)
// Create image asset for upload
let imageAsset = CKAsset(fileURL: imageFileURL)
record.setValue(imageAsset, forKey: "image")
// Get the Public iCloud Database
let publicDatabase = CKContainer.default().publicCloudDatabase
// Save the record to iCloud
publicDatabase.save(record, completionHandler: { (record, error) -> Void in
// 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