【iOS】常用技术点小记2

xcrun: error: unable to find utility "xcodebuild", not a developer tool or in PATH

【iOS】常用技术点小记2_第1张图片
command+,,选择Command Line Tools

Workaround for Cocoapods issue #7606

post_install do |installer|
    installer.pods_project.build_configurations.each do |config|
        config.build_settings.delete('CODE_SIGNING_ALLOWED')
        config.build_settings.delete('CODE_SIGNING_REQUIRED')
    end
end

关于NSArray+NSDictionary数据筛选逻辑

// 有如下一个array
NSArray *tempList = @[
                      @{
                          @"id1": 1,
                          @"name1": @"xxx",
                          @"list1": @[
                                  @{
                                      @"id2": 2,
                                      @"name2": @"yyy",
                                      @"list2": @[
                                              @{
                                                  @"id3": 3
                                                  }
                                              ]
                                      }
                                  ]
                          },
                      @{
                          @"id1": 1,
                          @"name1": @"xxx",
                          @"list1": @[
                                  @{
                                      @"id2": 2,
                                      @"name2": @"yyy",
                                      @"list2": @[
                                              @{
                                                  @"id3": 3
                                                  },
                                              @{
                                                  @"id3": 3
                                                  }
                                              ]
                                      }
                                  ]
                          }
                      ];
// 然后要求移除子list为空的元素
// 真正要执行remove操作的对象
    NSMutableArray *list = [tempList mutableCopy];
    for (id tempObj1 in tempList) {
        // 真正更新的obj1
        NSMutableDictionary *obj1 = [tempObj1 mutableCopy];
        NSArray *tempList1 = tempObj1[@"list1"];
        // 真正要执行remove操作的对象
        NSMutableArray *list1 = [tempList1 mutableCopy];
        for (id obj2 in tempList1) {
            //...类似上面步骤
        }
        // 数据更新,如果为空,则移除
        if (list1.count == 0) {
            // 由于list和tempList是copy方式,所以内含的object也是copy
            [list removeObject:tempObj1];
        }
        else{
            // 更新list
            obj1[@"list1"] = list;
            list[[list indexOfObject:obj1]];
        }
    }

iPhone全面屏适配问题

// 底部布局边距
    CGFloat layoutBottom = -20;
// iPhoneX是iOS11以后出现的(不包括越狱情况)
    if (@available(iOS 11.0, *)) {
        CGFloat bottom = UIApplication.sharedApplication.delegate.window.safeAreaInsets.bottom;
        // iPhoneX等
        if (bottom > 0) {
            layoutBottom = -bottom;
        }
    }
    [self.submitBtn mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.mas_equalTo(layoutBottom);
    }];

关于pod库如果只支持真机绕开pod lib lint的配置

  1. 命令行gem which cocoapods
  2. 命令行open /Library/Ruby/Gems/2.3.0/gems/cocoapods-1.5.3/lib/cocoapods/validator.rb
  3. when :ios相关改为command += %w(--help)
    【iOS】常用技术点小记2_第2张图片
    最终效果

关于多级页面跳转

注意popToRootpushNewVC的先后顺序,否则会出现内存泄漏问题

[[self.goMyOrderBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        @strongify(self);
        // 选中我的页面
        UINavigationController *nav = weakTabBarVC.viewControllers.lastObject;
        [weakTabBarVC setSelectedViewController:nav];
        
        // 跳转到订单页面
        UIViewController *vc = [ZSMyOrderModuler getMyOrderViewController];
        vc.hidesBottomBarWhenPushed = YES;
        [nav pushViewController:vc animated:NO];
        
        // 返回首页
        [self.navigationController popToRootViewControllerAnimated:NO];
    }];

关于tabbar选中某个页面

// 尽量用这种方式而不是setSelectedIndex
UINavigationController *myCourseNav = [ZSApp.rootTabBarController.viewControllers objectAtIndex:1];
[ZSApp.rootTabBarController setSelectedViewController:myCourseNav];

-ObjC

这个flag告诉链接器把库中定义的Objective-C类和Category都加载进来。这样编译之后的app会变大(因为加载了其他的objc代码进来)。但是如果静态库中有类和category的话只有加入这个flag才行。

-all_load

这个flag是专门处理-ObjC的一个bug的。用了-ObjC以后,如果类库中只有category没有类的时候这些category还是加载不进来。变通方法就是加入-all_load或者-force-load。-all_load会强制链接器把目标文件都加载进来,即使没有objc代码。-force_load在xcode3.2后可用。但是-force_load后面必须跟一个只想静态库的路径。

全局隐藏导航栏返回按钮文字

[UIBarButtonItem.appearance setBackButtonTitlePositionAdjustment:UIOffsetMake(NSIntegerMin, 0) forBarMetrics:UIBarMetricsDefault];

关于导航栏的正确隐藏

- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    
    [self.navigationController setNavigationBarHidden:YES animated:animated];
}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    
    [self.navigationController setNavigationBarHidden:NO animated:animated];
}

关于Xcode10断点不执行/执行混乱问题

【iOS】常用技术点小记2_第3张图片
File > Workspace Settings >设置

关于tableView: heightForHeader/FooterInSection:注意

  • 即使你返回了numberOfSections为0,如果你设置height为0,则height会变成默认的38,造成tableView的contentSize为2x38!=0,如果你监听这个contentSize则会出现问题。
  • 所以你的return 0.00001才行。
  • 然后监听contentSize.height需大于1才行。

关于RACObserve(self.array, count)监听不起作用问题

  • 使用mutableArrayValueForKey才能行
[[self mutableArrayValueForKey:@"array"] addObject:@"1"]

关于OC中对象地址和对象指针地址

打印对象地址: NSLog(@"%p", obj);
打印对象指针地址: NSLog(@"%x", obj);
判断两个对象地址相等: obj1 == obj2即可
判断字符串相等: [str1 isEqualToString: str2]
注意[obj1 isEqual: obj2]只是比较hash值,不是内存地址

关于Cell上的按钮rac_signalForControlEvents:UIControlEventTouchUpInside方法多次调用问题

// 这种方法最靠谱
[cell.btn addTarget:self action:@selector(btn_clicked:) forControlEvents:UIControlEventTouchUpInside];

// 还有一种说是这样,但亲测并不起作用
[[[cell.btn rac_signalForControlEvents:UIControlEventTouchUpInside] takeUntil:cell.rac_prepareForReuseSignal] subscribeNext:^(__kindof UIControl * _Nullable x) {

}];

iOS9及以下UIBarButtonItem设置问题

// 如果为customView时,则需指定customView的frame,否则显示不出来

- (UIBarButtonItem *)titleBtnItem{
    if (!_titleBtnItem) {
        UILabel *label = [UILabel new];
        label.font = ZSAppFont.system17.bold;
        label.text = @"学习内容";
        [label sizeToFit]; // 注意此处
        _titleBtnItem = [[UIBarButtonItem alloc] initWithCustomView:label];
    }
    return _titleBtnItem;
}

关于CocoaPods中bitcode设置问题:

  • 如果让当前pod库bitcode为NO,则在.podspec文件中设置如下:
s.pod_target_xcconfig = { 'ENABLE_BITCODE' => 'NO' }
  • 如果使工程中的所有pod库bitcode都为NO,则在Podfile文件中设置如下:
post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['ENABLE_BITCODE'] = 'NO'
        end
    end
end

UIProgressView的两种样式

UIProgressViewStyleBar //没有圆角
UIProgressViewStyleDefault //有圆角

// 如果改变圆角大小,可通过以下方式
for (UIImageView *iv in _progressView.subviews) {
  iv.layer.masksToBounds = YES;
  iv.layer.cornerRadius = 10;
 }

关于super view不可用而某个sub view可用的逻辑

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if ([self pointInside:point withEvent:event]) {
        CGPoint newPoint = [self convertPoint:point toView:self.rightView];
        BOOL rightViewEnabled  = CGRectContainsPoint(self.rightView.bounds, newPoint) && self.rightView.userInteractionEnabled;
        
        return rightViewEnabled
        ? self.rightView
        : (self.enabled && self.userInteractionEnabled && self.alpha > 0
           ? self
           : hitTestView);
    }
    return hitTestView;
}

Category交换系统dealloc方法

#import "UIView+Dealloc.h"
#import 

@implementation UIView (Dealloc)

+ (void)load{
    Method systemDealloc = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
    Method selfDealloc = class_getInstanceMethod(self, @selector(selfDealloc));
    method_exchangeImplementations(systemDealloc, selfDealloc);
}

- (void)selfDealloc{
    NSLog(@"%@ [deallocated]", self);
    
    [self selfDealloc];
}

@end

关于RAC下如何触发UIButton的点击事件

  • 定义按钮事件处理逻辑
@weakify(self);
    self.btn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
        return [RACSignal createSignal:^RACDisposable * _Nullable(id  _Nonnull subscriber) {
            @strongify(self);
            
// 事件处理...

            [subscriber sendCompleted];
            return nil;
        }];
    }];
  • 点击事件监听

注意这个地方要防止循环引用

    [[self.btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *x) {
// x不能再重复使用,否则会循环应用
// 应该用[weakSelf.btn.rac_command execute:x]
// ❌
        [x.rac_command execute:x];
// ✅
        [weakSelf.btn.rac_command execute:x];
    }];
  • 触发事件
[self.btn.rac_command execute: nil];

libc++abi.dylib`__cxa_throw:

【iOS】常用技术点小记2_第4张图片
编辑断点

Masonry警告调试

  • 添加断点


    【iOS】常用技术点小记2_第5张图片
    添加断点
  • 捕获约束警告


    【iOS】常用技术点小记2_第6张图片
    捕获约束警告
  • 代码中设置mas_key
// 按make_layout的顺序
MASAttachKeys(self.attachBtn, self.lineView, self.teacherLabel, self.downloadBtn);
// 或者
self.attachBtn.mas_key = @"xxx";

获取UIPageViewController当前页面的index

// delegate方法
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{
// 单页展示
    UIViewController *currentVC = pageViewController.viewControllers.firstObject;
    self.selectedPageIndex = [self.pageViewControllers indexOfObject:currentVC];
}

关于UITableViewheightForFooter/Header代理方法不执行问题

这种情况下,一般是因为只设置了heightForFooter/Header方法,而没有设置viewForFooter/Header

iOS打开设置页面

  • iOS 10之前
`prefs:root=WIFI`
// 或者 
`UIApplicationOpenSettingsURLString`
  • iOS 10之后
`App-Prefs:root=WIFI`
  • 各个设置对应shemes

无线局域网 App-Prefs:root=WIFI
蓝牙 App-Prefs:root=Bluetooth
蜂窝移动网络 App-Prefs:root=MOBILE_DATA_SETTINGS_ID
个人热点 App-Prefs:root=INTERNET_TETHERING
运营商 App-Prefs:root=Carrier
通知 App-Prefs:root=NOTIFICATIONS_ID
通用 App-Prefs:root=General
通用-关于本机 App-Prefs:root=General&path=About
通用-键盘 App-Prefs:root=General&path=Keyboard
通用-辅助功能 App-Prefs:root=General&path=ACCESSIBILITY
通用-语言与地区 App-Prefs:root=General&path=INTERNATIONAL
通用-还原 App-Prefs:root=Reset
墙纸 App-Prefs:root=Wallpaper
Siri App-Prefs:root=SIRI
隐私 App-Prefs:root=Privacy
Safari App-Prefs:root=SAFARI
音乐 App-Prefs:root=MUSIC
音乐-均衡器 App-Prefs:root=MUSIC&path=com.apple.Music:EQ
照片与相机 App-Prefs:root=Photos
FaceTime App-Prefs:root=FACETIME

iPhoneX判断

#define kIsIphoneX CGSizeEqualToSize(CGSizeMake(1125, 2436), UIScreen.mainScreen.currentMode.size)

UIAlertController循环引用问题

一般来说,如果在UIAlertAction的handlerBlock中如果调用了alert对象,就会产生循环引用,解决方法如下:

UIAlertController *alert = [UIAlertController alertxxx];
__weak typeof(UIAlertController *)weakAlert = alert;
// 然后使用weakAlert即可

另一种方法:

__wak __block UIAlertController *alert = nil; // __block可以在handlerBlock中保留该局部变量
// 做一些配置
alert = [UIAlertController showXXX]; // 这里是自定义的展示方法,展示完毕返回一个alert对象

Pods库头文件不提示问题

在工程配置中User Header Search Paths添加$(PODS_ROOT)选择recursive

正确获取UIApplication的当前显示viewController

向UIApplication添加Category方法:

+ (UIViewController *)currentViewController{
    UIViewController *vc = self.keyWindow.rootViewController;
    while (1) {
        if ([vc isKindOfClass:UITabBarController.class]) {
            vc = ((UITabBarController *)vc).selectedViewController;
        }
        else if ([vc isKindOfClass:UINavigationController.class]) {
            vc = ((UINavigationController *)vc).visibleViewController;
        }
        else if (vc.presentedViewController) {
            vc = vc.presentedViewController;
        }
        else{
            break;
        }
    }
    return vc;
}

This app could not be installed at this time.解决方法

打开~/Library/Logs/CoreSimulator/CoreSimulator.log日志,如果提示did not have a CFBundleIdentifier in its Info.plist,则删除~/Library/Developer/Xcode/DerivedData/文件夹重新编译运行。

UILongPressGesture使用注意

UILongPressGestureRecognizer *gesture = [UILongPressGestureRecognizer new];
        [gesture.rac_gestureSignal subscribeNext:^(__kindof UIGestureRecognizer * _Nullable x) {
            @strongify(self);
// Began、Changed、Ended
            NSLog(@"%@",x);
            if (x.state == UIGestureRecognizerStateBegan) {
                [self.viewModel.saveQRCodeImageSubject sendNext:[UIImage imageNamed:@"qrcode_app"]];
            }
        }];

UITableViewStyleGrouped样式下header多余空白问题

_tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];
如果设置了tableView.delegate,则还要设置footerView为[UIView new],且高度为CGFLOAT_MIN;

Pod lib lint/repo push错误include of non-modular header inside framework module

解决办法:pod lib lint xxx --use-libraries即可
或者
import ""头文件方式改为import <>,然后重新编译

iOS国际化

【iOS】常用技术点小记2_第7张图片
1. 新建.strings文件
【iOS】常用技术点小记2_第8张图片
2. 点击`Localize`
【iOS】常用技术点小记2_第9张图片
3. 勾选`Chinese`和`English`
【iOS】常用技术点小记2_第10张图片
4. 选择其他语言
5. 格式为`key = value;`

安装CocoaPods报错

执行sudo gem install cocoapods报错You don't have write permissions for the /usr/bin directory.
解决办法:sudo gem install -n /usr/local/bin cocoapods

CocoaPods

pod install --verbose --no-repo-update
pod update --verbose --no-repo-update

关于UIButton设置频繁setTitle时闪烁问题

其实这时候buttonType应该为custom类型,然后:

btn.titleLabel.text = @"xxx";
[btn setTitle:@"xxx" forState:UIControlStateNormal];

XCode8以后安装插件

https://blog.csdn.net/lincsdnnet/article/details/77412878

UIViewController生命周期

+[load]
-[initWithCoder:]
-[viewDidLoad:]
-[viewWillAppear:]
-[updateViewConstraints] //一般在这里进行自动布局的代码-Masonry/SnapKit
-[viewWillLayoutSubviews] //add/removeSubview操作都会引起多次条用layoutSubviews
-[viewDidLayoutSubviews]
-[viewWillLayoutSubviews]
-[viewDidLayoutSubviews]
-[viewDidAppear:]
-[viewWillDisappear:]
-[viewDidDisappear:]
-[dealloc]

OC Block

@property(nonatomic, copy)void (^属性名)(参数类型);
// 或者定义为类型
typedef 返回值类型(^Block名)(参数类型);

Swift中的available

@available(iOS x, *) //方法、属性
func xxx(){
}

if #available(iOS x, *) { //代码块
}

实现一个有placeholder的textView

import UIKit

class PlaceTextView: UITextView {

// MARK: - IBOutlets
    fileprivate lazy var placeholderLabel: UILabel = {
        let lb: UILabel = .init(frame: .init(x: 10, y: 10, width: self.bounds.width - 20, height: 20))
        lb.font = self.font
        lb.textColor = .lightGray
        lb.numberOfLines = 1
        lb.text = self.placeholder
        return lb
    }()
    
    fileprivate lazy var clearButton: UIButton = {
        let btn: UIButton = UIButton(type: .system)
        btn.setTitle("×", for: .normal)
        btn.titleLabel?.font = UIFont.systemFont(ofSize: 18)
        btn.tintColor = .white
        btn.backgroundColor = .gray
        btn.isHidden = true
        btn.layer.cornerRadius = self.clearButtonSize / 2
        btn.addTarget(self, action: #selector(self.clearInput), for: .touchUpInside)
        return btn
    }()
    
// MARK: - Properties
    fileprivate let clearButtonSize: CGFloat = 20
    
    var placeholder: String? = "请输入..."{
        didSet{
            placeholderLabel.text = placeholder
        }
    }
    
    override var textContainerInset: UIEdgeInsets{
        didSet{
            super.textContainerInset = UIEdgeInsets(top: textContainerInset.top, left: textContainerInset.left, bottom: textContainerInset.bottom, right: textContainerInset.right + clearButtonSize + 10)
        }
    }
    
// MARK: - Initial Method
    private func setupUI() {
        self.textContainerInset = super.textContainerInset
        
        addSubview(clearButton)
        addSubview(placeholderLabel)

        NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), name: NSNotification.Name.UITextViewTextDidChange, object: nil)
    }
    
 
// MARK: - Lifecycle Method
    override func awakeFromNib() {
        super.awakeFromNib()        
        setupUI()
    }
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        setupUI()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        layoutClearButton()
        layoutPlaceholderLabel()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
// MARK: - Action & IBOutletAction
    @objc func clearInput(){
        text = nil
        textDidChange()
    }
    
    @objc fileprivate func textDidChange(){
        let isEmpty = text == nil || text.isEmpty
        placeholderLabel.text = isEmpty ? placeholder : nil
        clearButton.isHidden = isEmpty
    }
    
// MARK: - Override Method3
    
// MARK: - Private method
    fileprivate func layoutClearButton() {
        let x = bounds.width - clearButtonSize - 10
        let y = bounds.height / 2 - clearButtonSize / 2 + contentOffset.y
        clearButton.frame = CGRect(origin: .init(x: x, y: y), size: CGSize(width: clearButtonSize, height: clearButtonSize))
    }
    
    fileprivate func layoutPlaceholderLabel() {
        let width = clearButton.frame.origin.x - 20
        placeholderLabel.frame = CGRect(origin: .init(x: textContainerInset.left + 4, y: textContainerInset.top), size: CGSize(width: width, height: 14))
    }
    
// MARK: - Public Method
}

统计代码行数

  • .swift、.c、.m、.h、.xib等文件
find . -name "*.m" -or -name "*.h" -or -name "*.xib" -or -name "*.c" -or -name "*.swift" |xargs wc -l

带icon的label文本

let attach = NSTextAttachment()
attach.image = UIImage(named: "icon")
attach.bounds = CGRect(origin: CGPoint(x: 0, y: -label.font.pointSize / 2), size: attach.image!.size)
let attr = NSAttributedString(attachment: attach)
let text = NSMutableAttributedString(string: "  xxx")
text.insert(attr, at: 0)
label.attributedText = text

判断字符是表情字符

extension String{
    var isEmoji: Bool{
        return rangeOfCharacter(from: .symbols) != nil
    }
}

修改UITextField的clear button

let clearButton = textFiled.value(forKeyPath:"_clearButton")
(clearButton as? UIButton)?.setImage(UIImage(named: "close"), for: .normal)

判断当前输入字符为表情

UITextView/UITextField如果在输入内容改变时.textInputMode?.primaryLanguage == nil则当前正在输入的是表情字符。

FileManager.default.fileExists

var isDirectory = ObjCBool(true)
FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)

关于Swift 字典-模型转换时报错:

[_SwiftTypePreservingNSNumber length]: unrecognized selector sent to instance xxx
  • 原因:在将dictionary转换为model过程中,出现了model的属性数据类型和字典值的类型不匹配情况;
  • 解决:将model中的属性数据类型改为和dictionary一致;
  • 示例:
class Student: NSObject{
var id = 0 //注意是Int类型
var name = ""
}
// dictionary自动填充为model
let student = Student()
student.setValuesForKeys(["id": "0", "name": "xiaoming"]) 
//注意此处字典中的id数据类型为String,与model的Int不符合,则会出现如上的报错信息;

Swift 函数

lroundf() //四舍五入 
ceil() //向上取整 
floor() //向下取整

Swift didSet, willSet记

var selectedClass: Class!{
    willSet{
        print(newValue)
    }
    didSet{
        guard selectedClass != oldValue else {
            return
        }
        requestStudentData()
    }
}

Swift reduce高阶函数用法

let totalProgress = learnings.reduce(0){ $0 + $1.displayProgress }

UITextField输入字符数限制

extension InformationEditVC: UITextFieldDelegate{
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let cell = textField.superview?.superview as? InformationEditCell {
            return string == "" || cell.data.maximumLength == 0 || 
                textField.text!.length < cell.data.maximumLength
        }
        return false
    }
}

关于UITableViewController在Storyboard上的Cell复用问题

  • 注意这种情况下 不要dequeueReusableCell(withIdentifier: , for:)指定indexPath方式;
  • 因为系统创建cell的方式是从xib中init的,然后加入复用队列;
  • 如果是自己手动register的cell,则要指定indexPath;

UISearchBar自定制

// 去掉黑边
searchBar.backgroundImage = UIImage()
if let tf = searchBar.value(forKey: "searchField") as? UITextField{
  tf.subviews[0].corner(radius: 14) // 输入框圆角
}
默认上下内边距为8pt

关于Swift的array.insert方法注意事项:

  • 在Swift中,Array、Dictionary、Set等均为基本数据类型,所以是值拷贝;
  • insert方法的执行过程类似于i += 1的操作,是在内存中进行的,完成之后将值重新付给array,所以会执行array的didSet方法;

UITableViewCell的xib在其他view上的使用方法

@IBOutlet weak var infoView: UIView!{
        didSet{
            if let cell = resourceCell {
                infoView.addSubview(cell) // 需cell和cell.contentView都添加
                infoView.addSubview(cell.contentView)
                cell.frame = infoView.bounds
                cell.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            }
        }
    }

UIButton设置选中/正常图片的另一种方式

fileprivate lazy var favoriteButton: UIButton = {
    let btn = UIButton(type: .system)
    btn.setImage(UIImage.named("icon_favorite_24"), for: .normal)
    btn.sizeToFit()
    btn.addTarget(self, action: #selector(actionToggleFavorite(_:)), for: .touchUpInside)
    return btn
}()

@objc fileprivate func actionToggleFavorite(_ sender: UIBarButtonItem) {
    data.favorited = !data.favorited
    favoriteButton.tintColor = data.favorited  ? .appOrange  : .white// 改变tintColor即可
}

forEach{}注意

  • .forEach { $0.xxx = xxx }注意这种情况下,并不能修改$0的属性值;
  • .forEach { (item) in item.xxx = xxx }才能修改属性值;

UISlider注意事项

  • 注意thumbTintColorsetThumbImage不可同时设置(只对一种起作用);
  • minimumTrackTintColormaximumTrackTintColor和图片的设置原理同上;

关于Storyboard上UITableViewController的设置问题:

  • 改变heightForHeaderInSectionheightForRowAt
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return section == 0 ? .zero : 10
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return indexPath.section == 0 ? (abs(tableView.contentInset.top) + 110) : 54
    }
  • 设置BasicCell的icon,并设置tintColor
【iOS】常用技术点小记2_第11张图片
1. 设置icon的`Render As` 为`Template Image`
2. 设置cell的imageView的tintColor
【iOS】常用技术点小记2_第12张图片
3. 效果

Swift中关于UIViewController的init方法

  • 该方法在从xib初始化或无参数的init()时均会调用
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

计算某个对象的所有后代节点-递归算法

private func descendantNodesOf(ancestor: TreeNode) -> [TreeNode]{
    var nodes = [TreeNode]()
    nodes.append(contentsOf: ancestor.subNodes)
    for node in ancestor.subNodes {
        nodes.append(contentsOf: descendantNodesOf(ancestor: node))
    }
    return nodes
}

App实时帧率计算

- (void)initCADisplayLink {
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
    
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)tick:(CADisplayLink *)link {
    if (self.lastTime == 0) {           //对LastTime进行初始化
        self.lastTime = link.timestamp;
        return;
    }
    
    self.count += 1;   //记录tick在1秒内执行的次数
    NSTimeInterval delta = link.timestamp - self.lastTime;  //计算本次刷新和上次更新FPS的时间间隔
    
    //大于等于1秒时,来计算FPS
    if (delta >= 1) {
        self.lastTime = link.timestamp;
        float fps = self.count / delta;         // 次数 除以 时间 = FPS (次/秒)
        self.count = 0;
        [self updateDisplayLabelText: fps];
    }
}

UIScrollView在当前有导航栏的控制器里offset偏移问题

self.automaticallyAdjustsScrollViewInsets = false
self.edgesForExtendedLayout = .all

隐藏状态栏

setStatusBarHidden(true, with: .none)
// 或者
app.isStatusBarHidden = true

iOS使用自定义字体

Info.plist
let font = UIFont(name: "xxx")

Swift4中系统通知名称

// Notification.Name.XXX
NotificationCenter.default.addObserver(self, selector: #selector(playerDidReachEnd(_:)), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)

设置view的layer的类型

class ScrollingView: UIView {
  
  override class var layerClass : AnyClass {
    return CAScrollLayer.self
  }
  
}

Swift 格式化数字

String(format: "%.1f", slider.value)

Swift - switch-case 用法

  • 与where使用
let row = Row(rawValue: indexPath.row)!
    
    switch row {
    case .contentsGravity where !contentsGravityPickerVisible:
      showContentsGravityPicker()
    default:
      hideContentsGravityPicker()
    }
  • 组合用法
  @IBAction func scrollingSwitchChanged(_ sender: UISwitch) {
    let xOn = horizontalScrollingSwitch.isOn
    let yOn = verticalScrollingSwitch.isOn
    switch (xOn, yOn) {
    case (true, true):
      scrollingViewLayer.scrollMode = kCAScrollBoth
    case (true, false):
      scrollingViewLayer.scrollMode = kCAScrollHorizontally
    case (false, true):
      scrollingViewLayer.scrollMode = kCAScrollVertically
    default:
      scrollingViewLayer.scrollMode = kCAScrollNone
    }
  }

Swift代码标记

// MARK: 
// TODO:
// FIXME:

上传图片自定义宽度

【iOS】常用技术点小记2_第13张图片
修改数值

自定义NSLog宏

#ifdef DEBUG
#define NSLog(FORMAT, ...) fprintf(stderr,"[%s][#%d] - %s\n",
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], 
__LINE__, 
[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);
#else
#define NSLog(FORMAT, ...) nil
#endif

__VA_ARGS__ 可变参数的宏
__FILE__  文件全路径
__FUNCTION__  类/实例方法
__LINE__  所在行

Xib/Storyboard兼容打开

【iOS】常用技术点小记2_第14张图片
Xcode设置Xib/Storyboard打开

导航栏动态透明

// 先设置为全透明
-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    
    UINavigationBar *navBar = self.navigationController.navigationBar;
    [navBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
    navBar.shadowImage = [UIImage new];
    navBar.translucent = true;
}

// 动态透明
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
    CGFloat alpha = scrollView.contentOffset.y / 100;
    self.navigationController.navigationBar.backgroundColor = [UIColor.redColor colorWithAlphaComponent:alpha];
}

一种创建对象的代码块

UIView *view = ({
    UIView *v = [UIView new];
// do something...
    
    v;  // copy `v` to `view`
});

CoreText获取CTLine的bounds方式

  • 方法1:
CGFloat a;
CGFloat d;
CGFloat l;
CGFloat width = CTLineGetTypographicBounds(moreLine, &a, &d, &l);
CGFloat height = a + d + l;
CGRect bounds = CGRectMake(0,0,width,height);
  • 方法2:
    只有当options设置不同时,两种方法计算得到的值才可能不一样
CGRect bounds = CTLineGetBoundsWithOptions(moreLine, kCTLineBoundsUseOpticalBounds) 

Git移除变基

rm -rf .git/rebase-apply

UICollectionView空白时无法使用下拉刷新问题

collectionView.alwaysBounceVertical = YES;

禁用导航栏滑动返回

self.navigationController.interactivePopGestureRecognizer.enabled = NO;

Swift利用Mirror获取class或struct的属性和值

func getAllProperties(){
  let mirror = Mirror.init(reflecting: self)
  for (key, value) in mirror.children {
    print("\(key!): \(value)")
  }
}

关于UIApplication的keyWindow

  • 如果设置了project的Main Interface为某个storyboard,则application: didFinishLaunchingWithOptions:时,会某人创建一个window,并且设置如下:
window.rootViewController = storyboard.initialViewController;
  • 等完成启动并return YES,之后才会将[window makeKeyAndVisible],而这时如果你想在该启动方法中进行一些UI方面的操作是不行的,因为它还没有被makeKeyAndVisible

  • 正确的做法是手动设置可见[window makeKeyAndVisible],然后做一些操作:

    [_window makeKeyAndVisible];

    // 广告图
    UIImageView *adIV = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    adIV.backgroundColor = [UIColor redColor];
    adIV.center = _window.center;
    [_window addSubview:adIV];
  • 另外,如果你没有设置项目的Main Interface,那么你需要手动实例化window对象,并设置rootViewController和makeKeyAndVisible:
    _window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    [_window makeKeyAndVisible];
    _window.rootViewController = [UIViewController new];

LaunchScreen.storyboard替换图片资源后无法显示问题

    1. 解决办法:Simulator > Reset Content and Settings...
    1. 然后重新运行项目

UICollectionView上cell的布局属性

UICollectionViewLayoutAttributes *attrs = 
[collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
【iOS】常用技术点小记2_第15张图片
UICollectionViewLayoutAttributes

UITextField事件

UIControlEventEditingDidBegin
UIControlEventEditingChanged
UIControlEventEditingDidEnd
UIControlEventEditingDidEndOnExit   

OC Block

@property(nonatomic,copy)void (^valueChanged)(HYRegisterInputView *input);

UITextView超链接

UITextView *textView = [[UITextView alloc] init];
textView.scrollEnabled = NO;
textView.editable = NO;
textView.textContainer.lineFragmentPadding = 0;
textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
textView.delegate = self;

//代理方法
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)url inRange:(NSRange)characterRange
{
     return YES;
}

CocoaPods

a pod project builds all the individual pods as their own framework, and then combines them into one single framework: Pods-XXX.
https://cocoapods.org
https://opensource.org

Storyboard使用framework注意

When using storyboards, references to custom classes need to have both the class name and module set in the Identity Inspector. At the time of this storyboard’s creation, ThreeRingView was in the app’s module, but now it’s in the framework.

【iOS】常用技术点小记2_第16张图片
update storyboard settings

Swift Code-Access-Control

  • Public: for code called by the app or other frameworks, e.g., a custom view.

  • Internal: for code used between functions and classes within the framework, e.g., custom layers in that view.By default, Swift makes everything internal or visible only within its own module.

  • Fileprivate: for code used within a single file, e.g., a helper function that computes layout heights.

  • Private: for code used within an enclosing declaration, such as a single class block. Private code will not be visible to other blocks, such as extensions of that class, even in the same file, e.g., private variables, setters, or helper sub-functions.

StatusBar网络请求指示器

UIApplication.shared.isNetworkActivityIndicatorVisible = true

SourceTree暂存

【iOS】常用技术点小记2_第17张图片
暂存
【iOS】常用技术点小记2_第18张图片
右键
【iOS】常用技术点小记2_第19张图片
恢复暂存

布局优先级priority

【iOS】常用技术点小记2_第20张图片
bottom
【iOS】常用技术点小记2_第21张图片
bottom 1000
【iOS】常用技术点小记2_第22张图片
bottom 250

App Dynamic Type

  • 设置>辅助功能>LargeText>支持动态字体的应用的阅读字体body
storyboard设置
// 代码设置
label.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)

// 字体改变通知
NotificationCenter.default.addObserver(forName: .UIContentSizeCategoryDidChange, object: .none, queue: OperationQueue.main) { [weak self] _ in
    self?.tableView.reloadData()
  }

NotificationCenter须知

Starting with iOS 9, it is no longer necessary to remove notification center observers. If your app’s deployment target is iOS 8, however, you will still need to do that!

  • iOS9以后,不再需要手动移除通知中心中add的observer

UITableView刷新

// Row insertion/deletion/reloading.

open func beginUpdates() // allow multiple insert/delete of rows and sections to be animated simultaneously. Nestable

open func endUpdates() // only call insert/delete/reload calls or change the editing state inside an update block. otherwise things like row count, etc. may be invalid.

UITextView自动高度

Disabling scrolling is of similar importance to setting a label to 0 lines

textView.isScrollEnabled = false; //类似于lable.numberOfLines = 0;

归档

  • swift3 - 只能对class类型归档
class Album: NSObject, NSCoding{
  var title = ""
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(title, forKey: "title")
    }
    
    required init?(coder aDecoder: NSCoder) {
        title = aDecoder.decodeObject(forKey: "title") as? String ?? ""
    }
}
  • swift4 - 对class、enum、struct类型归档
struct Album: Codable {
  let title : String
}

// encode
func saveAlbums() {
  let url = documents.appendingPathComponent(Filenames.Albums)
  let encoder = JSONEncoder()
  guard let encodedData = try? encoder.encode(albums) else {
    return
  }
  try? encodedData.write(to: url)
}

// decode
func getAlbums(){
let savedURL = documents.appendingPathComponent(Filenames.Albums)
var data = try? Data(contentsOf: savedURL)
if data == nil, let bundleURL = Bundle.main.url(forResource: Filenames.Albums, withExtension: nil) {
  data = try? Data(contentsOf: bundleURL)
}

if let albumData = data,
  let decodedAlbums = try? JSONDecoder().decode([Album].self, from: albumData) {
  albums = decodedAlbums
  saveAlbums()
}
}

KVO

  • swift4
private var valueObservation: NSKeyValueObservation!

valueObservation = coverImageView.observe(\.image, options: [.new]) { [unowned self] observed, change in
  if change.newValue is UIImage {
// do something...
  }
}
  • swift3
coverImageView.addObserver(self, forKeyPath: "image", options: .new, context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "image", 
            change?[.newKey] is UIImage  {
// do something...
        }
    }

UITable趣谈

Here’s a pseudo-explanation of what goes on when you create a new UITableView:
Table: Here I am! All I want to do is SHOW CELLS. Hey, how many sections do I have?
Data source: One!
Table: OK, nice and easy! And how many cells in that first section?
Data source: Four!
Table: Thanks! Now, bear with me, this might get a bit repetitive. Can I have the cell at section 0, row 0?
Data source: Here you go!
Table: And now section 0, row 1?
…and so on.

Swift元组类型Tuple

typealias AlbumData = (title: String, value: String)

let data = AlbumData(title: "xxx", value: "xxx")
// 也可以写成
let data = AlbumData("xxx",  "xxx")

导航栏左侧按钮设置

  • 返回按钮
// 默认是nil,在父controller中设置
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:nil action:nil];
  • 左边按钮
UIBarButtonItemStylePlain //细
UIBarButtonItemStyleDone //粗

// 保留返回按钮
self.navigationItem.leftItemsSupplementBackButton = true;
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Left" style:UIBarButtonItemStyleDone target:self action:@selector(action:)];

CSR申请

【iOS】常用技术点小记2_第23张图片
必填,不然创建的证书中没有key

URL特殊字符编码

"".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)

Git关于.xcuserstate不提交的方法

1. cd 项目工程目录下

2. git rm --cached 项目名.xcodeproj/project.xcworkspace/xcuserdata/用户名.xcuserdatad/UserInterfaceState.xcuserstate

// 重新提交改变到本地
3. git commit -m "removed `.xcuserstate` file;"

UILayoutGuide

UIView

  • layoutMarginsGuide
  • readableContentGuide

UIViewController

  • topLayoutGuide: UILayoutSupport (topAnchor、bottomAnchor、heightAnchor)
  • bottomLayoutGuide

NSLayoutAnchor

  • UIView

NSLayoutXAxisAnchor

  • leadingAnchor
  • trailingAnchor
  • leftAnchor
  • rightAnchor
  • centerXAnchor

NSLayoutYAxisAnchor

  • topAnchor
  • bottomAnchor
  • centerYAnchor
  • firstBaselineAnchor
  • lastBaselineAnchor

NSLayoutDimension

  • widthAnchor
  • heightAnchor

UIView的appearance配置

[UIActivityIndicatorView appearanceWhenContainedIn:[MBProgressHUD class], nil].color = UIColor.whiteColor;

strongSelf与weakSelf

  • 异步线程请求
var net: Network? = Network()
net!.request()
net = nil //线程回调的时候已经被销毁了,会出错,或无提示
class Network: NSObject {
    func request() {
        print("requesting...")
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(3)) {
            [weak self] in
            
            guard let strongSelf = self else {
          fatalError("`self is nil`")
            }
            
 //如果有多步后续操作的话,就不能少了上述`guard let`判断而直接用self?.updateModel
            strongSelf.updateModel()
            strongSelf.updateUI()
        }
    }
    
    func updateModel(){
        print("updated model")
    }
    
    func updateUI(){
        print("updated model")
    }
}

UITableView横向滚动

class ViewController: UITableViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        self.tableView.rowHeight = self.view.bounds.width
        self.tableView.isPagingEnabled = true
        self.tableView.separatorStyle = .none
        
        // 旋转
        self.view.transform = CGAffineTransform(rotationAngle: .pi/2)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        self.tableView.scrollToRow(at: IndexPath.init(row: 9, section: 0), at: .bottom, animated: false)
        
        super.viewDidAppear(animated)
    }

}

extension ViewController{
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        
        cell.contentView.transform = CGAffineTransform(rotationAngle: -.pi/2)
        
        return cell
    }
    
}

WKWebView进度条

// 监听进度条
wkWebView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        
        if keyPath == "estimatedProgress", 
            let progress = change?[.newKey] as? Float {
            
// 显示进度
            progressView.progress = progress == 1 ? 0 : progress
        }
}

iOS文字删除线

NSStrikethroughStyleAttributeName: @(NSUnderlineStyleSingle|NSUnderlinePatternSolid),
NSStrikethroughColorAttributeName: [UIColor lightGrayColor]

OC可视化

IB_DESIGNABLE
@interface CircleView : UIView

@property (nonatomic, assign) IBInspectable CGFloat radius;

@end

NSLayoutConstraint基本用法

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.translatesAutoresizingMaskIntoConstraints = false;
    
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn1 setTitle:@"Button1" forState:UIControlStateNormal];
    btn1.backgroundColor = [UIColor redColor];
    btn1.translatesAutoresizingMaskIntoConstraints = false;
    
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn2 setTitle:@"Button2" forState:UIControlStateNormal];
    btn2.backgroundColor = [UIColor greenColor];
    btn2.translatesAutoresizingMaskIntoConstraints = false;
    
    [self.view addSubview:btn1];
    [self.view addSubview:btn2];
    
    NSDictionary *views = NSDictionaryOfVariableBindings(btn1, btn2);
    
    // 等宽
    NSArray *cons1 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[btn1(btn2)]" options:0 metrics:nil views:views];
    NSArray *cons2 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[btn1(44)]" options:0 metrics:nil views:views];
    // 等高
    NSArray *cons3 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[btn2(btn1)]" options:0 metrics:nil views:views];
    NSArray *cons4 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[btn1][btn2]|" options:0 metrics:nil views:views];
    
    NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:btn2 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:btn1 attribute:NSLayoutAttributeTop multiplier:1 constant:0];
    
    [self.view addConstraints:cons1];
    [self.view addConstraints:cons2];
    [self.view addConstraints:cons3];
    [self.view addConstraints:cons4];
    [self.view addConstraint:top];
}

多个UIButton点击选中一个,并改变样式

UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    btn.layer.borderWidth = 1;
    btn.layer.cornerRadius = 5;
    [btn setTitle:title forState:UIControlStateNormal];
    [btn setTitle:title forState:UIControlStateSelected];
    [btn setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor orangeColor] forState:UIControlStateSelected];
    [btn addTarget:self action:@selector(actionForButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
    btn.tintColor = [UIColor clearColor];

// 点击事件
for (UIView *sub in self.subviews) 
    {
        if ([sub isKindOfClass:[UIButton class]]) 
        {
            UIButton *btn = (UIButton*)sub;
            btn.selected = [btn isEqual: sender];
            btn.layer.borderColor = btn.selected ? [UIColor orangeColor].CGColor : [UIColor lightGrayColor].CGColor;
        }
    }

UIScrollView约束

首先满足:frame[left, top, bottom, right]

【iOS】常用技术点小记2_第24张图片
约束postion
【iOS】常用技术点小记2_第25张图片
约束size

其次要通过子view约束[left + right + size]水平滚动[top + bottom + size]垂直滚动,来确定它的contentSize

【iOS】常用技术点小记2_第26张图片
约束子view位置
【iOS】常用技术点小记2_第27张图片
约束子view大小
【iOS】常用技术点小记2_第28张图片
宽度改为1/3
【iOS】常用技术点小记2_第29张图片
view2和view1等宽等高
【iOS】常用技术点小记2_第30张图片
view3和view2等宽等高
【iOS】常用技术点小记2_第31张图片
最后要注意view3的trailing和scrollView的trailing约束

(NSSet *)touches取元素

UITouch *touch = touches.anyObject;

sending const NSString * to parameter of type NSString * discards qualifiers

static const NSString* str = @"xxx";
改成 static NSString* const str = @"xxx";
// 前者相当于指针本身不可修改,后者表示指针指向的内容不可修改,两者的作用都是使str只只读。

UITableViewStylePlain使得sectionHeader“悬浮”问题解决

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat height = _tableView.sectionHeaderHeight;
    CGFloat offY = scrollView.contentOffset.y;
    
    if (offY >= 0 && offY <= height) {
        scrollView.contentInset = UIEdgeInsetsMake(-offY, 0, 0, 0);
    } 
    else if (offY >= height) {
        scrollView.contentInset = UIEdgeInsetsMake(-height, 0, 0, 0);
    }
}

UIButton.system选中设置

UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn sizeToFit]; // 如果未设置btn.frame时应该这么用
    btn.tintColor = [UIColor clearColor];
    [btn setTitle:@"编辑" forState:UIControlStateNormal];
    [btn setTitle:@"完成" forState:UIControlStateSelected];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];

关于删除cell

[_dataSource removeObjectAtIndex:index];
[_tableView beginUpdates];
[_tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:index]] withRowAnimation:UITableViewRowAnimationLeft];
// 如果是最后一行,则要删除对应的section
[_tableView deleteSections:[NSIndexSet indexSetWithIndex:index] withRowAnimation:UITableViewRowAnimationAutomatic];
[_tableView endUpdates];

mas_makeConstraints 与 mas_updateConstraints

注意在tableView.rowHeight = UITableViewAutomaticDimension时,尽量不要选用update方式,因为要动态计算高度,需一开始指定约束。

你可能感兴趣的:(【iOS】常用技术点小记2)