【iOS】纯Swift代码构建一个功能完善的APP

纯Swift代码构建一个功能完善的APP

源代码地址:https://github.com/yoferzhang/FoodPin

效果演示

iOS11之后,导航栏可以设置这样变大的效果。

在 ViewController 的 viewDidLoad() 方法中添加下面这行代码可以实现:

1
2
// iOS11之后这个属性可以让导航栏往下滑动的时候title变大
navigationController?.navigationBar.prefersLargeTitles = true

向右滑动菜单:

向左滑动菜单:

tableView,actionSheet

详情页面

导航栏透明,并修改大字体状态的title颜色, viewDidLoad()

1
2
3
4
5
6
7
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()

// 设置导航栏title的大字体状态的颜色
if let customFont = UIFont(name: "PingFangSC-Medium", size: 40.0) {
navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(red: 231.0/255.0, green: 76.0/255.0, blue: 60.0/255.0, alpha: 1.0), NSAttributedString.Key.font: customFont]
}

详情页面的导航栏变透明,返回按钮变色

1
2
3
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.navigationBar.tintColor = UIColor.white

调整tableView的顶部位置

1
detailTableView.contentInsetAdjustmentBehavior = .never

全局修改导航栏的返回按钮 application(_:didFinishLaunchingWithOptions:) 中添加

1
2
3
let backButtonImage = UIImage(named: "back")
UINavigationBar.appearance().backIndicatorImage = backButtonImage
UINavigationBar.appearance().backIndicatorTransitionMaskImage = backButtonImage

修改详情页状态栏,

1
2
3
4
/// 状态栏颜色
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}

可以没有生效,因为会用导航栏controller的颜色,为了让可以针对性修改页面,加一个Extension文件,UINavigationController+Ext.swift

1
2
3
4
5
6
7
import UIKit

extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return topViewController
}
}

添加地图信息

自定义 annotationView,实现 MKMapViewDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "MyMarker"

if annotation.isKind(of: MKUserLocation.self) {
return nil
}

// Reuse the annotation if possible
var annotationView: MKMarkerAnnotationView? = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView

if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
}

annotationView?.glyphText = "😋"
annotationView?.markerTintColor = UIColor.orange

return annotationView
}

1
2
3
mapView.showsTraffic = true
mapView.showsScale = true
mapView.showsCompass = true

测试一些动画

代理回调,将选择的表情回调给详情页,展示在 headerView 的右下角

静态列表,textField使用

图片选择器

改用 CoreData 存储数据,并用 NSFetchedResultsController 监听;新建局部刷新首页 tableview

删除后,局部刷新首页 tableview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (action, soureView, comletionHandler) in
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
let context = appDelegate.persistentContainer.viewContext

if let currentVC = self.currentViewController() as? YQRestaurantTableViewController {
let restaurantToDelete = currentVC.fetchResultController.object(at: indexPath)
context.delete(restaurantToDelete)

appDelegate.saveContext()
}

}

comletionHandler(true)
}

更新rating 表情,同样是数据库级别的更新,加 appDelegate.saveContext() 就可以

1
2
3
4
5
6
7
8
9
//MARK: - YQRestaurantReviewViewControllerDelegate
func onClickRateButtonInReviewVC(rate: RateModel) {
restaurant.rating = rate.image
refreshRatingImageView(rateImage: rate.image)

if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
appDelegate.saveContext()
}
}

添加搜索栏,支持name搜索,搜索状态时禁止左右滑动的编辑态

tabBarController

About页面;使用元组;分别用 WKWebViewSFSafariViewController 打开web页面

使用iCloud,在 CloudKit Dashboard 中创建数据;原本是想上传图片,但 Dashboard 一上传那图片就 停止响应了,苹果这个做的有点坑==

然后用 Convenience API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func fetchRecordsFromCloud() {
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) { (results, error) in
if let error = error {
print(error)
return
}

if let results = results {
print("Completed the download of Restaurant data")
self.restaurants = results
DispatchQueue.main.async(execute: {
self.discoverTableView.reloadData()
})
}
}
}

代码中拉取到的数据结构:

改用 Operational API。因为 Convenience API 不能只请求携带某些字段,它会把所有数据都完整拉下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func fetchRecordsFromCloud() {
let cloudContainer = CKContainer.default()
let publicDatabase = cloudContainer.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Restaurant", predicate: predicate)

let queryOperation = CKQueryOperation(query: query)
queryOperation.desiredKeys = ["name", "iamge"]
queryOperation.queuePriority = .veryHigh
queryOperation.resultsLimit = 50
queryOperation.recordFetchedBlock = { (record) -> Void in
self.restaurants.append(record)
}

queryOperation.queryCompletionBlock = { [unowned self] (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")
DispatchQueue.main.async(execute: {
self.discoverTableView.reloadData()
})
}

publicDatabase.add(queryOperation)
}

使用 UIActivityIndicatorViewUIRefreshControl

将数据存储到iCloud

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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")
record.setValue(restaurant.summary, forKey: "description")

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? scaledImage?.jpegData(compressionQuality: 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)
})
}