iOS小功能

URL

  1. URL的组成部分只能使用:

    • 26个英语字母(包括大写和小写)
    • 10个阿拉伯数字
    • 连词号(-)
    • 句点(.)
    • 下划线(_)

    此外还有18个保留字,只能在指定的位置出现,如果要在其他位置出现,就必须要对齐进行编码:

    • !:%21
    • #:%23 锚点
    • $:%24
    • &:%26 分隔多个查询参数
    • ':%27
    • (:%28
    • ):%29
    • *:%2A
    • +:%2B
    • ,:%2C
    • /:%2F
    • ::%3A 分隔协议和主机
    • ;:%3B
    • =:%3D
    • ?:%3F 分隔路径和查询参数
    • @:%40
    • [:%5B
    • ]:%5D
  2. URL编码是ASCII编码,而不是Unicode。

    • ASCII的局限在于只能显示26个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语
  3. URL格式

    1
    https://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#anchor
    1. 协议(scheme)https://

      浏览器请求服务器资源的方法

    2. 主机\域名(host)www.example.com

      资源所在的网站名或服务器的名字

    3. 端口(port)80 (默认为80)

      同一个域名下面可能同时包含多个网站,它们之间通过端口区分

    4. 路径(path)/path/index.html

      资源在网站的位置

    5. 查询参数(parameter)?key1=value1&key2=value

      提供给服务器的额外信息。参数的位置是在路径后面,两者之间使用?分隔。

      查询参数可以有一组或多组。

      ​ 每组参数以key=value的形式表示。

      ​ 多组参数之间使用&连接。

    6. 锚点(anchor)#anchor

      网页内部的定位点。锚点名称通过网页元素的id属性命名

Calendar

1
2
3
Calendar(identifier: .gregorian)	// 公历,最常使用
Calendar(identifier: .chinese) // 农历,辛丑年正月初三
Calendar(identifier: .republicOfChina) // 民国历法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//2000-01-1 00:00:00
let minDate = Date(timeIntervalSince1970: TimeInterval(946656000))
//2021-05-17 19:29:32
let maxDate = Date(timeIntervalSince1970: TimeInterval(1621250972))

/// 5
let month = calendar.component(.month, from: maxDate)

/// year: 21 month: 4 isLeapMonth: false
let diffComponent = calendar.dateComponents([.year, .month], from: minDate, to: maxDate)

/// calendar: gregorian (fixed) year: 2011 month: 6 isLeapMonth: false
let customDateCompents = DateComponents(calendar: calendar, year: 2011, month: 6)

/// "Jun 1, 2011 at 12:00 AM"
let customDate = customDateCompents.date
  • calendar.range( of: .day, in: .month, for: baseDate)?.count
    • baseDate所在月份的总天数
  • calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate))
    • baseDate所在月的第一天
  • calendar.component(.weekday, from: firstDayOfMonth)
    • firstDayOfMonth所在一周的第几天

UIStackView的inset

1
2
stackView.isLayoutMarginsRelativeArrangement = true
stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)

自定义UIStackView的spacing

1
2
嵌套stackView //iOS11之前
setCustomSpacing(_:after:) //iOS11之后

一个string是否包含另外一个string

1
2
3
4
contains(_:)
localizedCaseInsensitiveContains(_:) //大小敏感
str.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil //是否包含数字
CharacterSet(charactersIn: "1234").isSubset(of: digitsCharacters) //是否包含指定数字

两个string是否相等

1
2
3
==
compare(_:options:)
caseInsensitiveCompare(_:)

pod版本限制

pod 'FLEX', '~> 2.2.1':版本会维持在2.2.X的最大版本(2.2.99)

1
2
3
4
5
6
7
8
9
10
logical operators:
'> 0.1' Any version higher than 0.1
'>= 0.1' Version 0.1 and any higher version
'< 0.1' Any version lower than 0.1
'<= 0.1' Version 0.1 and any lower version

optimistic operator ~>:
'~> 0.1.2' Version 0.1.2 and the versions up to 0.2, not including 0.2 and higher
'~> 0.1' Version 0.1 and the versions up to 1.0, not including 1.0 and higher
'~> 0' Version 0 and the versions up to 1.0, not including 1.0 and higher

绘制圆角矩形

1
2
3
4
5
let placeholderImage = UIGraphicsImageRenderer(bounds: CGRect(origin: .zero, size: CGSize(widthHeight: 50))).image { context in
UIColor.leo_searchBackground.setFill()
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(widthHeight: 50)), cornerRadius: 20)
path.fill()
}

在viewDidDisappear的时候可以判断是否是被pop

若navigationController==nil,说明被pop掉

绘制image时,指定的frame和生成的image会有误差

systemLayoutSizeFitting

在调用systemLayoutSizeFitting计算view的高度之前需要先指定view中label的preferredMaxLayoutWidth

UIStackView

AutoLayout

  • stackView会自动管理布局

属性

Distribution:延轴方向分布

  • fill

    • 若stackView大小确定:子view会被拉伸或压缩来填充整个stackView
    • 若stackView大小不确定:需要每个子view大小确定,stackView会调整自身大小以适应子view
  • fillEqually

    • 所有子view一样大
  • fillProportionally

    • 根据子view的intrinsic content size的比例调整大小,以适应stackView
  • equalSpacing

    • 每个子view的间距相等
    • 当子view会超出stackView时,会根据子view的 compression resistance priority优先级压缩
  • equalCentering

    • 每个子view的中心距离相等
    • 当子view会超出stackView时,会根据子view的 compression resistance priority优先级压缩

Alignment:垂直轴方向分布

isLayoutMarginsRelativeArrangement

  • 设置边距

    1
    2
    stackView.isLayoutMarginsRelativeArrangement = true
    stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
  • 优点:

    • 简单

    • 可以设置动画

      1
      2
      3
      4
      stackView.directionalLayoutMargins.leading = 0
      UIView.animate(withDuration: 0.3) {
      stackView.layoutIfNeeded()
      }

setCustomSpacing(_:after:)

  • 自定义间距

    1
    2
    3
    4
    vStackView.addArrangedSubview(movieLabel)
    vStackView.addArrangedSubview(quoteLabel)
    vStackView.setCustomSpacing(10, after: quoteLabel) // <1>
    vStackView.addArrangedSubview(authorLabel)
  • iOS11之后才能用,在11之前只能用嵌套stackView

  • 只有在将quoteLabel加入stackView之后再设置间距才会生效

  • 若将quoteLabel设为隐藏,它后面的间距也会隐藏

设备及其在竖/横屏下的宽高是regular还是compact

Device Portrait orientation Landscape orientation
12.9” iPad Pro Regular width, regular height Regular width, regular height
11” iPad Pro Regular width, regular height Regular width, regular height
10.5” iPad Pro Regular width, regular height Regular width, regular height
9.7” iPad Regular width, regular height Regular width, regular height
7.9” iPad mini 4 Regular width, regular height Regular width, regular height
iPhone XS Max Compact width, regular height Regular width, compact height
iPhone XS Compact width, regular height Compact width, compact height
iPhone XR Compact width, regular height Regular width, compact height
iPhone X Compact width, regular height Compact width, compact height
iPhone 8 Plus Compact width, regular height Regular width, compact height
iPhone 8 Compact width, regular height Compact width, compact height
iPhone 7 Plus Compact width, regular height Regular width, compact height
iPhone 7 Compact width, regular height Compact width, compact height
iPhone 6s Plus Compact width, regular height Regular width, compact height
iPhone 6s Compact width, regular height Compact width, compact height
iPhone SE Compact width, regular height Compact width, compact height

UIScrollView

contentSize

scrollView内容的大小

contentOffset

contentSize的origin 和 frame的origin的偏移

当scrollview向下拉时,offsetY为负数;当上拉时,offsetY不断增大,当越过原点后会变成正数。

contentInset

contentview相对于scrollview的边距

contentInset和adjustedContentInset

iOS11提出了safeArea的概念,帮助scrollView放在屏幕的可视部分

  • 在iOS 11中决定tableView的内容与边缘距离的是adjustedContentInset属性,而不是contentInset。

  • contentsize是scrollView的滚动范围

  • contentinset是为scrollView增加额外的滚动区域

  • contentoffset是scrollView的滚动位置

  • safeAreaInsets是view到safeArea的距离

  • adjustedContentInset表示contentView.frame.origin偏移了scrollview.frame.origin多少。是系统计算得来的,计算方式由contentInsetAdjustmentBehavior决定。有以下几种计算方式:

    • ScrollableAxes

      • 在可滚动方向上:adjustedContentInset = safeAreaInset + contentInset,
      • 在不可滚动方向上:adjustedContentInset = contentInset;
    • Automatic:

      • scrollview在一个automaticallyAdjustsScrollViewInsets = YES的controller上,并且这个Controller包含在一个navigation controller中:

        在top & bottom上的 adjustedContentInset = safeAreaInset + contentInset,不管是否滚动。

      • 其他情况下:

        ScrollableAxes相同

    • Never

      • adjustedContentInset = contentInset
    • Always

      • adjustedContentInset = safeAreaInset + contentInset

ContentInsetAdjustmentBehavior

如上

automaticallyAdjustsScrollIndicatorInsets

是否自动调整滚动条的insets

translatesAutoresizingMaskIntoConstraints

默认值是true

若值是true时,系统会自动创建和view的 autoresizing mask完全相同的约束,并且这些约束包含了view的size、position。因此此时在添加额外的约束时,会有布局冲突。

所以在使用Auto Layout来进行动态布局时,需要将该属性设为false。

note:在使用snapKit布局时,snapkit会自动将该值设为false。但是若没有对一个view使用snp布局,该值仍然保持为true。

setContentCompressionResistancePriority &setContentHuggingPriority

  • setContentCompressionResistancePriority:抗压缩性,适用于布局空间比view的size要小
  • setContentHuggingPriority: 抗拉伸性,适用于布局空间比view的size要大

监听键盘事件

1
2
3
4
5
6
7
8
9
public class let keyboardWillShowNotification: NSNotification.Name
public class let keyboardDidShowNotification: NSNotification.Name
public class let keyboardWillHideNotification: NSNotification.Name
public class let keyboardDidHideNotification: NSNotification.Name

// Like the standard keyboard notifications above, these additional notifications include
// a nil object and begin/end frames of the keyboard in screen coordinates in the userInfo dictionary.
public class let keyboardWillChangeFrameNotification: NSNotification.Name
public class let keyboardDidChangeFrameNotification: NSNotification.Name

获取键盘信息

1
2
3
4
5
public class let keyboardFrameBeginUserInfoKey: String 	//键盘动画起始时的frame
public class let keyboardFrameEndUserInfoKey: String //键盘动画结束时的frame
public class let keyboardAnimationDurationUserInfoKey: String = "0.25" //动画持续时间
public class let keyboardAnimationCurveUserInfoKey: String = 7 //动画曲线类型
public class let keyboardIsLocalUserInfoKey: String = 1 //键盘是否显示,bool类型,1 show,2 hide

示例

1
2
3
4
5
6
7
8
9
10
11
12
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe(onNext: { [weak self] notification in
guard
let self = self,
let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval
else { return }
UIView.animate(withDuration: duration) { [weak self] in
self?.bottomOffsetConstraint?.update(offset: -keyboardRect.height)
}
})
.disposed(by: bag)

UIModalPresentationStyle和UIModalTransitionStyle

1
2
3
4
5
6
7
8
9
10
11
12
13
enum UIModalPresentationStyle : Int {
case automatic
case none
case fullScreen //由下到上,全屏覆盖
case pageSheet //在portrait时是FullScreen,在landscape时和FormSheet模式一样。
case formSheet // 会将窗口缩小,使之居于屏幕中间。在portrait和landscape下都一样,但要注意landscape下如果软键盘出现,窗口位置会调整。
case currentContext //这种模式下,presented VC的弹出方式和presenting VC的父VC的方式相同。
case custom //自定义视图展示风格,由一个自定义演示控制器和一个或多个自定义动画对象组成。符合UIViewControllerTransitioningDelegate协议。使用视图控制器的transitioningDelegate设定您的自定义转换。
case overFullScreen //如果视图没有被填满,底层视图可以透过
case overCurrentContext //视图全部被透过: is displayed over another view controller’s content.
case popover // iPad中常用的设置弹出模式
case blurOverFullScreen // blurs the underlying content before displaying new content in a full-screen presentation.
};
1
2
3
4
5
6
enum UIModalTransitionStyle : Int {
case coverVertical // 底部滑入。
case flipHorizontal // 水平翻转。
case crossDissolve // 交叉溶解。
case partialCurl // 翻页。
}

用UIViewControllerTransitioningDelegate显示蒙层

1
2
3
4
5
6
7
extension VC: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = DimBackgroundPresentationController(presentedViewController: presented, presenting: presenting)
presentationController.backgroundMaskView.backgroundColor = .leo_backgroundMask
return presentationController
}
}

内存泄漏

  • 自己监听自己

    self.rx.observe(CGRect.self, "bounds").map({ $0 ?? .zero }).distinctUntilChanged()

    解决方法:

    contentView.rx.observe(CGRect.self, "bounds").map({ $0 ?? .zero }).distinctUntilChanged()

  • viewModel中的监听被view的bag管理而不是reuseBag

    viewModel.relayObject.oberve()....disposed(by bag)

    解决方法:

    viewModel.relayObject.oberve()....disposed(by reuseBag)

监听View大小

bounds

scrollView.rx.observe(CGRect.self, "bounds").map({ $0 ?? .zero }).distinctUntilChanged()

scrolView的contentSize

scrollView.rx.observe(CGSize.self, "contentSize").map({ $0 ?? .zero }).distinctUntilChanged()

音频

AVQueuePlayer

可以自动管理多个网络音频

AVQueuePlayer在播放完成之后会把palyItem从队列中删除,因此在播放时要判断playItem是否为空,

若是空需要调整playItem的播放进度,然后将item重新加入queue中

1
2
3
4
5
6
7
8
9
10
if player.items().isEmpty {
self.audioItem.enumerated().forEach({ (index, item) in
guard player.canInsert(item, after: nil) else {
assertionFailure("音频(\(index):\(item.description))无法加入队列")
return
}
item.seek(to: CMTime(value: 0, timescale: 1), toleranceBefore: CMTime(value: 1, timescale: 2), toleranceAfter: CMTime(value: 1, timescale: 2))
player.insert(item, after: nil)
})
}

监听加载状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AVPlayerItem状态改变
player?.currentItem?.rx.observe(AVPlayerItem.Status.self, "status")
.distinctUntilChanged()
.subscribe(onNext: { [weak self] status in
guard let self = self, let status = status else { return }
switch status {
case .readyToPlay, .unknown:
break
case .failed:
showToast("音频加载失败")
@unknown default:
break
}
})
.disposed(by: bag)

监听播放完成(播放完成,重新将进度设为第一帧)

1
2
3
4
5
6
// 播放完成
NotificationCenter.default.rx.notification(.AVPlayerItemDidPlayToEndTime, object: player?.items().last)
.subscribe(onNext: { [weak self] _ in
.....
})
.disposed(by: bag)

监听播放失败

1
2
3
4
5
6
// 播放失败
NotificationCenter.default.rx.notification(.AVPlayerItemFailedToPlayToEndTime, object: player?.currentItem)
.subscribe(onNext: { [weak self] _ in
.....
})
.disposed(by: bag)

在OC和swift中的Int64类型转换

swift中的int传给OC后,OC会将其转为NSNumber类型,在OC中若想使用int类型,就需要调用NSNumber的方法将其转为Int类型

代码注意

  • 分母的变量要用.clamped(to: 1.0...)保证其值大于1
  • static的全局变量会自动转成lazy
  • 将计算属性var a { ... }转为var a = { ... }()可使得闭包内代码只运行一次

会导致source kit崩溃的原因

  • rx.tapGuesture用在非UIButton上

移除当前显示的controller

1
strongSelf.dismiss(animated: true, completion: nil)
1
2
3
current.navigationController?.viewControllers = current.navigationController?.viewControllers.filter {
$0 != current //会同时将当前vc从navigationControllers中移除
} ?? []

dismiss

func dismiss(animated: Bool, completion: (() -> Void)?)

Dismisses the view controller that was presented modally by the view controller.

A -> B -> C

C.dismiss : 移除C

B.dismiss : 移除C

A.dismiss : 移除B,C

PresentedViewController 与 PresentingViewController

假设Controller A通过present跳到Controller B,B又通过present跳到Controller C,那么:

B.presentedViewController 就是 C

B.presentingViewController 就是 A

iOS权限查询

1
2
3
4
5
6
func permissions() {
let cameraPermission = AVCaptureDevice.authorizationStatus(for: .video)//相机
let recordPermission = AVCaptureDevice.authorizationStatus(for: .audio)//麦克风
let photoPermission = PHPhotoLibrary.authorizationStatus()//相册
let locationPermission = CLLocationManager.authorizationStatus()//定位
}

通知权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UNUserNotificationCenter.current().getNotificationSettings { [weak self] (settings) in
guard let strongSelf = self else { return }
DispatchQueue.main.async {
switch settings.authorizationStatus {
case .authorized, .provisional:
strongSelf.handleCheckNotificationTrigger(true)
case .notDetermined:
NotificationManager.sharedInstance().setupNotification()
strongSelf.handleCheckNotificationTrigger(true)
case .denied:
strongSelf.showOpenNotificationPermissionDialog()
case .ephemeral:
assertionFailure()
strongSelf.handleCheckNotificationTrigger(true)
@unknown default:
strongSelf.showOpenNotificationPermissionDialog()
}
}
}

保证有相机权限

1
2
3
4
5
_ = AVCaptureDevice.rx.cameraAuthorizedOrShowAlertIfNeeded
.subscribe(onCompleted: { [weak self] in
guard let self = self else { return }

})

跳到设置界面

1
2
3
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

API获取的整数转为枚举

1
2
3
4
5
6
7
8
9
10
11
12
enum Grade: Int {
case none = 0
case good = 1
case bad = 2
}
struct Data: Codable {
let rawGrade
var grade: Grade { return Grade(rawType: rawGrade) ?? .none }
private enum CodingKeys: String, CodingKey {
case rawGrade = "grade"
}
}

根据不同的评级使用不同的图片

1
2
3
4
5
6
7
8
9
10
11
12
extension Grade {
var imageName: String {
switch self {
case .none:
return ""
case .good:
return "goodImage"
case .bad:
return "badImage"
}
}
}

图片旋转

仿射变换:是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。(来自百度百科)

在iOS中表现为平移、旋转、缩放等。

1
iamgeView.transfrom = CGAffineTransform(rotationAngle: CGFloat.pi * (-15.0 / 180.0))

根据不同的情景使用不同的布局

一、布局的offset不同

1
2
3
4
5
6
7
8
import SnapKit
private var constraint: Constraint?
// 在创建布局时正常创建
titleLabel.snp.makeConstraints { (make) in
constraint = make.trailing.lessThanOrEqualTo(scoreImageView.snp.leading).offset(-8).constraint
}
// 在获取数据时更新offset
constraint?.update(offset: 100)

二、布局的参考对象不同

1
2
3
4
5
6
7
8
9
10
11
12
import SnapKit
private var constraint1: Constraint?
private var constraint2: Constraint?
// 在创建布局时将两个约束都创建,然后让其中一个失效
titleLabel.snp.makeConstraints { (make) in
constraint1 = make.trailing.lessThanOrEqualTo(scoreImageView.snp.leading).offset(-8).constraint
constraint2 = make.trailing.lessThanOrEqualTo(contentView).offset(-8).constraint
constraint2.deactive()
}
// 在获取数据时激活对应的约束
constraint1.deactive()
constraint2.active()

rx只监听一次

1
.take(1)

定时器

1
2
3
[self.timer setFireDate:[NSDate distantPast]]	//开始计时器
[self.timer setFireDate:[NSDate distantFuture]] //停止计时器
[self.timer setFireDate:[NSDate date]] //继续计时器
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
final class TimerHelper {
@ObservableProperty
var accumulatedTimeInMillisecond: Int64 = 0
private var timestamp = Date()
private var timerBag = DisposeBag()

func start() {
self.accumulatedTimeInMillisecond = 0
self.setupTimer()
}

func pause() {
self.timerBag = DisposeBag()
}

func resume() {
self.setupTimer()
}

private func setupTimer() {
self.timerBag = DisposeBag()
self.timestamp = Date()
Observable<Int>.interval(RxTimeInterval.milliseconds(10), scheduler: MainScheduler.instance)
.dropValue()
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
let newDate = Date()
self.accumulatedTimeInMillisecond += Int64(newDate.timeIntervalSince(self.timestamp) * 1000)
self.timestamp = newDate
})
.disposed(by: self.timerBag)
}
}

从gerrit上download已经revert的提交

复制Cherry Pick URL,在本地分支运行,

若有冲突,解决冲突后,commit(message直接复制被revert的信息,包含changeID)

名词

  • keypoint:知识点
  • question:题目

时间

1
2
let timeString: String = DateUtils.dateString(from: .init(timeIntervalSince1970: someTime), withDateFormat: "yyyy年MM月dd日HH时mm分")
TimeUtils.serverDate().timeIntervalSince1970 //当前时间戳

把一个view包装为左侧有2pixl的新view

1
view.leo.wrapped(left: 2.0)

获取API错误返回的状态码

1
2
3
4
5
6
7
8
9
UpdateUserInfoAPI.rx.response()
.subscribe(onNext: {

}, onError: { (error) in
if let requestError = error as? LEOBaseRequest.RequestError, case let .requestError(request) = requestError {
let statusCode = request.responseStatusCode
}
})
.disposed(by: bag)

frog

1
2
ProfileFrogConstants.eventCameraPermission.leo.frog(parameters: ["permission": "true"])
VGOFrogUtils.logEvent(MainFrogConstants.debugCrashReporter, parameters: ["using": "sentry"])

判断是否是表情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private extension String {
var containsEmoji: Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x1F600...0x1F64F, // Emoticons
0x1F300...0x1F5FF, // Misc Symbols and Pictographs
0x1F680...0x1F6FF, // Transport and Map
0x2600...0x26FF, // Misc symbols
0x2700...0x27BF, // Dingbats
0xFE00...0xFE0F, // Variation Selectors
0x1F900...0x1F9FF: // Supplemental Symbols and Pictographs
return true
default:
continue
}
}
return false
}
}

判断字符是否是汉字

1
2
3
if  character >= "\u{4e00}" && character <= "\u{9fa5}" {
count += 2 // 一个汉字表示2个字符
}

日期扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension Date {

static func formateDate(timestamp: Int64, format: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
return dateFormatter.string(from: date)
}

static func weekDay(timestamp: Int64) -> String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
let weekdays = ["", "周日","周一","周二","周三","周四","周五","周六"]
let calendar = Calendar(identifier: .republicOfChina)
let theComponents = calendar.dateComponents([.weekday], from: date)
guard let weekday = theComponents.weekday else {
assertionFailure()
return weekdays[0]
}
return weekdays[weekday]
}
}

富文本

1
2
3
4
5
6
7
8
9
10
11
12
13
let price = NSMutableAttributedString(
string: "¥",
attributes: [
NSAttributedString.Key.foregroundColor: UIColor(hex: 0xFF7400),
NSAttributedString.Key.font: UIFont.mediumPingFangSC(size: 14)
])
price.append(NSAttributedString(
string: "\(lessonItem.product.price)",
attributes: [
NSAttributedString.Key.foregroundColor: UIColor(hex: 0xFF7400),
NSAttributedString.Key.font: UIFont.mediumPingFangSC(size: 24)
])
)

计算文字高度

boundingRect

1
2
3
4
5
6
let size = labelText.boundingRect(
with: CGSize(width: availabelTextWidth, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)],//若labelText是attributeString,改参数可省略
context: nil
).size
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
extension String {
func ga_widthForComment(font: UIFont, height: CGFloat = 15) -> CGFloat {
let rect = NSString(string: self).boundingRect(
with: CGSize(width: CGFloat(MAXFLOAT), height: height),
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font],
context: nil
)
return ceil(rect.width)
}

func ga_heightForComment(font: UIFont, width: CGFloat) -> CGFloat {
let rect = NSString(string: self).boundingRect(
with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font],
context: nil
)
return ceil(rect.height)
}

func ga_heightForComment(font: UIFont, width: CGFloat, maxHeight: CGFloat) -> CGFloat {
let rect = NSString(string: self).boundingRect(
with: CGSize(width: width, height: CGFloat(MAXFLOAT)),
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font],
context: nil
)
return ceil(rect.height)>maxHeight ? maxHeight : ceil(rect.height)
}
}

systemLayoutSizeFitting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func layoutSize(in context: layoutContext) -> layoutSize {
view.bindData()
view.label.preferredMaxLayoutWidth = ...
let size = view.systemLayoutSizeFitting(
CGSize(
width: context.collectionView.bounds.width,
height: UIView.layoutFittingExpandedSize.height
),
withHorizontalFittingPriority: .defaultHigh,
verticalFittingPriority: .fittingSizeLevel
)

return .init(
width: .absolute(context.collectionView.bounds.width),
height: .absolute(ceil(size.height) + 8)
)
}

对齐设置

方法一:使用snp

1
2
3
4
buttonC.snp.makeConstraints { make in
make.leading.equalTo(self.view)
make.top.equalTo(self.view.safeAreaLayoutGuide).offset(10)
}

方法二:直接设置

1
2
buttonC.translatesAutoresizingMaskIntoConstraints = false
buttonC.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true

设置根视图

在SceneDelegate.swift文件中的scene()函数中添加

1
2
3
4
let nav = UINavigationController(rootViewController: ViewController())
self.window?.rootViewController = nav
self.window?.backgroundColor = .white
self.window?.makeKeyAndVisible()

Podfile使用

  1. 安装cocoapod
  2. 在项目目录下创建Podfile文件
  3. 在文件中添加响应的依赖
  4. 允许pod install

分割线

1
2
3
4
5
6
7
8
private let line = UIView()    //存储属性
addSubview(line) //init中
line.backgroundColor = .lightGray //config中
line.snp.makeConstraints { (make) in
make.leading.trailing.equalToSuperview().inset(viewModel.leftInsets)
make.bottom.equalToSuperview()
make.height.equalTo(1.0)
}

或者使用SeparatorCollectionViewLayout(),在init()中传递给父类init()方法

监听某个存储属性

  1. 给属性增加@ObservableProperty

  2. 在界面中使用instance.$property.subscribe来监听

    1
    2
    3
    4
    5
    6
    7
    MyLogger.shared.$messages
    .observeOn(MainScheduler.instance) //把线程切换到主线程
    .subscribe(onNext: { [weak self] in
    guard let strongSelf = self else { return }
    //通过$0获取监听的属性
    })
    .disposed(by: bag)

也可以给属性包装为BehaviorRelay,用.accept添加值并通知监听者,用rx的.subscribe来处理响应

viewDidLoad的功能切分

  • setupSubviews()
  • setupLayout()
  • setupActions()

点击背景隐藏键盘

1
2
collectionView.keyboardDismissMode = .onDrag
collectionView.alwaysBounceVertical = true

方法二:

  1. 创建一个手势 private let tapBackground = UITapGestureRecognizer()

  2. 将手势添加到view中:view.addGestureRecognizer(tapBackground)

  3. 设置手势点击完成编辑:

    1
    2
    3
    4
    5
    6
    tapBackground.rx.event
    .subscribe(onNext: { [weak self] _ in
    guard let strongSelf = self else { return }
    strongSelf.view.endEditing(true)
    })
    .disposed(by: bag)

搜索框

用textField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private let textField = UITextField()
headerView.contentView.addSubview(textField)
//布局设置
textField.backgroundColor = 。。。
//设置框内文字边距
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
textField.leftViewMode = .always

textField.rx.value.distinctUntilChanged()
.subscribe(
onNext: { [weak self] (text) in
guard let strongSelf = self else { return }
if let fieldText = text, !fieldText.isEmpty {
} else {
}
}
)
.disposed(by: bag)
0%