《UIKit Apprentice》

Bull’s Eye game

1、如何在项目中添加依赖

新建iOS工程后,在工程目录下运行pod init,生成podfile文件后用vs code打开,之后添加需要依赖的库,添加完成后执行pod install即可:

1
2
3
4
5
6
7
8
9
10
11
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'Eyeball' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  pod 'SnapKit'
  pod 'LookinServer'
  # Pods for Eyeball

end

2、如何在Xcode中使用Git

  • 创建一个新的Xcode工程,勾选Create Git repository on my Mac✅,此时Xcode就自动在项目中完成了Git仓库的初始化,如果创建工程时没有勾选,也可以手动使用git init指令来实现。

  • 添加远程仓库,xxx即为在Github中创建的仓库,这一步的目的是将本地仓库和远端的仓库连接起来

    git remote add origin git@github.com:QAutumn/xxx.git
    
  • 然后点击菜单栏中SouceControl里的Push,就可以愉快的提交代码了~

3、UIButton中的Configuration的用法

在初始化UIButton时,可以通过UIButton(configuration: UIButton.Configuration)方法进行初始化,这样做的好处是可以定义一个全局的UIButton.Configuration,然后使用上述构造方法批量创建相同格式的UIButton。

如果在创建UIButton时没有使用上述构造方法,那么该button的默认Configuration是空的,此时如果想修改button中Configuration的某个属性,需要先手动创建一个Configuration,构造方法为

1
2
3
4
5
6
7
static func plain() -> UIButton.Configuration
//Creates a configuration for a button with a transparent background.			

var config = UIButton.Configuration.plain()
config.imagePadding = 10
config.imagePlacement = .top
btn.configuration = config

之后修改该config的相关属性,最后再将其赋值回btn的configuration属性即可。这里要说明的一点是,UIButton.Configuration本质上是一个结构体,所以可以直接这样修改:

1
btn.configuration?.imagePadding = 0

但是如果使另外的变量等于当前的configuration,修改结束后要记得将该变量重新赋值回去。

4、UISlider控件的使用

首先介绍一下常用的一些属性和方法,详情🔎可以参考官方文档

img

Property Discription
value Slider的当前值
minimumValue Slider的最小值,必须设定
maximumValue Slider的最大值,必须设定
isContinuous 滑块的值更改是否会生成连续的更新事件,若否,则只有松开手指后触发
minimumValueImage 左边的小太阳
maximumValueImage 右边的小太阳

UISlider使用addTarget方法进行事件绑定,根据isContinuous属性的不同,决定方法调用的次数。

5、通过封装方法设置UIImage的大小

1
2
3
4
func pointIcon(_ iconName: String, _ pointSize: CGFloat = 22) -> UIImage?{
    let config = UIImage.SymbolConfiguration(pointSize: pointSize) //这个方法没有仔细看过
    return UIImage(systemName: iconName, withConfiguration: config)
}	

6、项目结构

  • 使用lazy var 的方式定义控件,控件的实现放到make xxx()方法中
  • 将子视图的添加和位置、大小的调整封装到setupUI()方法中
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

import UIKit
import SnapKit
import Foundation

class ViewController: UIViewController {
    lazy var hitBtn = makeHitButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    func setupUI() {
        view.addSubview(hitBtn)
        hitBtn.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.width.equalTo(200)
            make.height.equalTo(70)
            make.bottom.equalToSuperview().offset(-200)
        }
    }
    
    func makeHitButton() -> UIButton {
        let btn = UIButton()
        btn.setTitle("Hit me!", for: .normal)
        btn.setTitleColor(.systemPink, for: .normal)
        btn.backgroundColor = .systemGray
        btn.layer.cornerRadius = 10
        btn.setImage(pointIcon("personalhotspot.circle"), for: .normal)
        btn.tintColor = .systemPink
        
        var config = UIButton.Configuration.plain()
        config.imagePadding = 10
        config.imagePlacement = .top
        btn.configuration = config
        btn.addTarget(self, action: #selector(hitBtnClicked), for: .touchUpInside)
        return btn
    }
    
    @objc func hitBtnClicked() {
        let num = Int(sliderBar.value)
        if num != randomNumber {
            makeAlert("You almost had it! The gap is \(abs(num - randomNumber))!")
            numberOfAttemps -= 1
        } else {
            randomNumber = Int.random(in: 1...100)
            numberOfAttemps = 10
            totalScore += max(0, numberOfAttemps)
            makeAlert("SUCCESSFUL!!!!!!!!!!!!!")
        }
    }
}

7、UIAlert用法

1
2
3
4
let alert = UIAlertController(title: alertTitle, message: "Your total score is \(totalScore)", preferredStyle: .alert)
let action = UIAlertAction(title: actionTitle, style: .cancel, handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)

8、设置背景ImageView

一开始用了一个比较复杂的方法,首先定义一个ImageView,设置好image属性后,在view中addsubview,然后再通过view.sendSubviewToBack(backgroundImageView)方法将这个view移到最后,具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lazy var backgroundImageView = makeBackgroundImageView()

func makeBackgroundImageView() -> UIImageView {
    let imageview = UIImageView()
    imageview.image = UIImage(named: "background")
    return imageview
}

view.addSubview(backgroundImageView)
backgroundImageView.snp.makeConstraints { make in
    make.center.equalToSuperview()
    make.size.equalToSuperview()
}

view.sendSubviewToBack(backgroundImageView)

这种方法略微有点繁琐,也可以使用下面这种方法:

1
2
3
4
let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
backgroundImage.image = UIImage(named: "background")
backgroundImage.contentMode = .scaleAspectFill
self.view.insertSubview(backgroundImage, at: 0)

9、NSString用法

1
2
3
4
5
6
7
8
var attributes = [NSAttributedString.Key : Any]() 
attributes[NSAttributedString.Key.font] = UIFont.systemFont(ofSize: 30)
let string = NSAttributedString(string: "Put the Bull's Eye as close as you can to:\(randomNumber)", attributes: attributes)
let label = UILabel()
label.attributedText = string
label.textAlignment = .center
label.numberOfLines = 0
return label

10、函数注释风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 /// Calculates the average of three values /// - Parameters:

/// - a: The first value.

/// - b: The second value.

/// - c: The third value.

/// - Returns: The average of the three values.

func calculateAverage(of a: Double, and b: Double, and c: Double) -> Double {
    let total = a + b + c
    let average = total / 3
    return average 
} 
calculateAverage(of: 1, and: 3, and: 5)

Checklists

1、整体结构设计

img

2、Cell <-> indexPath

1
2
3
4
//1.cell-->indexPath
row = tableView.indexPath(for: cell)!.row
//2.indexPath-->cell(下述)
tableView.cellForRow(at: indexPath) as! TodoCell

3、UINavigationController、UINavigationBar和UINavigationItem的关系

Navigation Controller是一种容器视图控制器,通过属性viewControllers:[UIViewController]来管理一个或多个child view controllers。

img

Navigation controller管理着Navigation bar 和可选的 navigation toolbar的创建、配置和显示。

一旦顶层的View Controller发生改变,Navigation Controller会对Navigation Bar做出相应的调整,特别是,Navigation Controller会更新Navigation bar 的左边、中间和右边的三个按钮BarButtonItems: [UIBarButtonItem]

每个Navigation bar的tintColor属性都是由其自身设置的(独立的),Navigation bar不会从其他View Controller中继承该属性。

下图展示了Navigation Bar的一些常用属性:

A navigation bar diagram that depicts the fonts, color, and layout of a navigation bar, including the tint color, title text atributes, bar tint color, and the title vertical position.

A screenshot of a navigation bar with the location of a background image and a shadow image labeled.

UINavigationltemNavigation bar中展示的对象,主要包括 leftBarButtonItem,title,rightBarButtonItems(也就是两个按钮+中间的标题),如果左侧或右侧的按钮不止一个,那么可以使用setLeftBarButtonItems和setRightBarButtonItems方法来设置多个按钮。

当为UINavigationltem添加对象的时候,必须使用UIBarButtonItem对象,如果需要使用自定义的View,那么必须把这些View包裹到一个UIBarButtonItem对象中才能够正常使用。

总结:

  • Navigation ControllerView Controller的容器。
  • Navigation bar: [UINavigationBar]Navigation Controller的一个属性,该属性随着View Controller的push和pop自动产生和更新。Navigation bar主要的作用是可以自定义样式,比如tintColor,setBackgroundImage等等,也可以通过isHidden属性设置是否隐藏Navigation Bar
  • UINavigationltemView Controller中的属性,也是Navigation bar中展示的对象,负责Navigation bar中按钮的配置。

以下是如何在项目中修改rootViewController以及添加导航识图的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    
    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
//        var nav = UINavigationController()
//        nav.viewControllers = [TodoVC()]
    var nav = UINavigationController()
    nav.viewControllers = [CheckListsVC()]
    window?.rootViewController = nav
    window?.makeKeyAndVisible()
}

4、为UIBarButtonItem添加点击事件

分别设置UIBarButtonItem的 targetaction 属性即可。

1
2
3
4
5
6
7
8
let addChecklistBtn = navigationItem.rightBarButtonItems?[0]
    addChecklistBtn?.target = self
    addChecklistBtn?.action = #selector(addChecklistBtnClicked)

@objc func addChecklistBtnClicked () {
    checklistItems.append(ChecklistItem(name: "❤️", checked: true))
    checkListsTableView.reloadData()
}

5、关于Navigation Bar 和 View 重叠的问题:

目前的解决方法是把View的top加了一个高度为navigationController?.navigationBar.frame.maxY的偏移,不知道这种做法是否合适,也比较好奇为什么会出现这种重叠的问题,明明上一个界面还是好好的,跳转到这个界面后,重新写了一个Navigation Bar就出现问题了……

1
2
3
4
5
view.addSubview(itemTextField)
    itemTextField.snp.makeConstraints { make in
    make.centerX.equalToSuperview()
    make.top.equalToSuperview().offset(navigationController?.navigationBar.frame.maxY ?? 0)
}

6、UITextField用法

其实现在发现,所有的UI控件的套路基本都差不多,首先会有一些属性来定义常规功能,对UITextField来说,常见的属性有placeholdertextclearButtonModeleftViewleftViewMode等,详见🔎官方文档

  • 如果需要将UITextField设置为第一响应者,也就是键盘拉起状态,可以调用becomeFirstResponder()方法。UITextField也会在用户点击时自动调用该方法。对应的,可以通过调用resignFirstResponder()方法来请求系统取消键盘操作。通常,您会根据特定的交互操作取消键盘操作。

    例如,当用户点击键盘的Return键时,可能会取消键盘,此外当用户点击不支持键盘输入的新控件时,系统会取消键盘。

  • UITextField通过UITextFieldDelegate内置了很多delegate方法,这些方法可以允许我们对UITextField进行更多自定义的操作,例如:

7、Delegate

delegate是swift的三大设计模式之一,另外两个分别是target-actionModel-View-Controller

delegation的核心思想是:责任转移。简单来说就是,本来B该做的事情,现在交由A去做,并且设置A为B的代理。

创建delegation的五个步骤是:

  1. 在B中创建一个delegate protocol

    1
    2
    3
    protocol AddItemDelegate: AnyObject {
        func addItemComplete(_ cotroller: UIViewController,item: String)
    }
    
  2. 在B中添加一个名为delegate的变量,类型为刚才定义的protocol,并将其设置成weak

    1
    weak var delegate: AddItemDelegate? //这里要特别注意理解⚠️,delegate当前是一个遵循了AddItemDelegate协议的类的对象!!
    
  3. 在合适的时机(如用户按下按钮时)让B向其代理A发出消息delegate?.methodName(self, . . .)

    1
    2
    3
    @objc func doneButtonClicked() {
        delegate?.addItemComplete(self, item: text) 
    }
    
  4. 使A遵循delegate protocol中规定的方法。

    1
    2
    3
    4
    5
    6
    extension CheckListsVC: AddItemDelegate {
        func addItemComplete(_ controller: UIViewController, item: String) {
            checkListItems.append(CheckListItem(name: item))
            checkListsTableView.reloadData()
        }
    }
    
  5. 将B的代理设置为A。

    1
    addItemVC.delegate = self
    

8、Cell的默认配置

最近发现TableView中的Cell不需要每次都重新创建一个xxxCell,然后自定义里面的各种View,其实对于一些简单的Cell,完全可以使用Configuration来进行初始化。

首先可以通过cell.defaultConfiguration来获取到当前Cell的默认配置:

1
2
3
4
5
6
7
8
9
10
11
12
//注意,这个defaultContentConfiguration() 返回一个UIListContentConfiguration类型的对象
var config = cell.defaultContentConfiguration()
//修改config中的属性
config.text = ""
config.secondaryText = ""
config.image = ""
//还可以通过相应的Properties属性对标题、小标题、左侧图片进行更进一步的修改
config.textProperties.color = .red
config.secondaryTextProperties.color = .green
config.imageProperties.tintColor = .blue
//最后把配置好的Config对象重新赋值给Cell
cell.contentConfiguration = config

9、UITableViewDelegate

相比于UITableViewDatasouce中的cellForRownumberOfRowsInSection方法, UITableViewDelegate中包含有大量的方法,很多delegate方法,如果不知道的话,可能会导致自己写出相当不优雅的代码,比方说,cell的默认配置defaultConfiguration()中含有一个accessoryType属性,该属性可以定义cell最右侧的Button图标,可是如何为这个按钮绑定一个事件呢?

查阅文档🔎发现cell拥有accessoryView这个属性,所以尝试了一下给这个View添加一个手势事件,但是发现不行,目前推测可能是因为收拾冲突🤔,偶然间发现UITableViewDelegate有一个accessoryButtonTappedForRowWith方法专门用来处理accessory被点击时的情况:

1
2
3
4
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
    addItemVC.itemToEdit = checkListItems[indexPath.row]
    navigationController?.pushViewController(addItemVC, animated: true)
}

教训:以后要多看文档,养成delegate思维,避免做重复的无用功。

Mylocations

1、获取当前位置的gps坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import CoreLocation

class CurrentLocationViewController: UIViewController, CLLocationManagerDelegate {
  let locationManager = CLLocationManager()
  locationManager.delegate = self 
  locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters // 其实这就是一个Double类型的值,用来指定精度的范围
  locationManager.startUpdatingLocation()
  
  // MARK: - CLLocationManagerDelegate 
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error ) {
    print("didFailWithError \(error.localizedDescription)") 
  }

  func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { 
    let newLocation = locations.last! 
    print("didUpdateLocations \(newLocation)") 
  }
}

但是在此之前,需要向系统申请读取当前位置GPS的权限:

1
2
3
4
5
let authStatus = locationManager.authorizationStatus 
if authStatus == .notDetermined { //determined代表用户还没有决定是否授予权限,此时应该主动申请
  locationManager.requestWhenInUseAuthorization() //whenInUse代表只在使用APP过程中申请,区别于Always,即使在APP不活跃的时候也能同样申请
  return 
}

同时需要在Info.plist中添加一条:

1
Privacy - Location When In Use Usage Description

此时APP已经可以正常获取GPS位置了,但是还需要一些额外的代码来处理错误,比如用户没有给予权限:

1
2
3
4
5
6
7
8
9
10
11
12
if authStatus == .denied || authStatus == .restricted { 
  showLocationServicesDeniedAlert() 
  return 
}

// MARK: - Helper Methods 
func showLocationServicesDeniedAlert() { 
  let alert = UIAlertController( title: "Location Services Disabled", message: "Please enable location services for this app in Settings.", preferredStyle: .alert) //注意这里的preferredStyle是 .alert,如果是.actionSheet的话就是在底部弹出一个可选的列表	
  let okAction = UIAlertAction( title: "OK", style: .default, handler: nil) 
  alert.addAction(okAction)
	present(alert, animated: true, completion: nil)
}

2、将gps坐标转换成地理位置

这里加深了一下对比包的理解,placemark和error都是方法返回来的参数,而不是自己定义的。

好比说,我规定placemarks 和error的类型,你执行完reverseGeocodeLocation这个方法后,给我两个这个类型的参数

然后我再拿着这两个参数去做别的事情。

这也正是闭包的初衷:一……就……。

一返回结果,就用这个结果去做别的事。

1
2
3
4
5
6
7
8
9
10
geocoder.reverseGeocodeLocation(location) {placemarks, error in
  if let error = error {
    print("*** Reverse Geocoding error: \(error.localizedDescription)")
    return
  }

  if let places = placemarks {
    print("*** Found places: \(places)")
  }
}

3、在单个ViewController中隐藏NavigationBar

在使用navigationController进行push & pop的过程中,可能会需要在某些界面中隐藏navigationBar,对于需要隐藏navigationBar的ViewController来说,需要在viewWillAppear界面中将navigationController的isNavigationBarHidden设置为true:

1
2
3
4
override func viewWillAppear(_ animated: Bool) { 
  super.viewWillAppear(animated) 
  navigationController?.isNavigationBarHidden = true 
}

注意,如果只是单纯这样写,会导致所有页面的NavigationBar都被隐藏,这很容易理解,因为只有一个navigationController嘛,所以我们还需要在该viewController被弹出时重新将isNavigationBarHidden的值设置成false:

1
2
3
4
override func viewWillDisappear(_ animated: Bool) { 
  super.viewWillDisappear(animated) 
  navigationController?.isNavigationBarHidden = false 
}

####