iOS

WKWebView文字图片自适应

我们在使用 WKWebView 时,图片和文字有时不会进行自适应屏幕,这样我们可以通过嵌入 JavaScript 代码的方式,达到文字图片自适应的效果。

文字自适应屏幕

文字自适应屏幕,可以在创建 WKWebView 的时候,添加 JS 代码实现
// 自适应屏幕宽度js
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
WKWebView *contentWeb = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:wkWebConfig];

图片自适应屏幕

图片自适应屏幕可以在加载 HTML 代码前,拼接相应代码实现
NSString *htmlString = [NSString stringWithFormat:@"<html> \n"
    "<head><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no' charset=\"UTF-8\"> \n"
    "<style type=\"text/css\"> \n"
    "body {font-size:15px;}\n"
    "</style> \n"
    "</head> \n"
    "<body>"
    "<script type='text/javascript'>"
    "window.onload = function(){\n"
    "var $img = document.getElementsByTagName('img');\n"
    "for(var p in  $img){\n"
    "$img[p].style.width = '100%%';\n"
    "$img[p].style.height ='auto'\n"
    "}\n"
    "}"
    "</script>%@"
    "</body>"
    "</html>", html];

Link:

iOS 后台进入前台和进入后台通知

1. 注册通知

//后台进前台通知 UIApplicationDidBecomeActiveNotification
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];

//进入后台UIApplicationDidEnterBackgroundNotification
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];

2. 实现通知方法

//进入后台方法
- (void)didEnterBackground {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

//每次后台进前台都会执行这个方法
- (void)didBecomeActive {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

3. 移除通知

- (void)dealloc {
    [[NSNotificationCenter defaultCenter]removeObserver:self]; //移除通知
}

更新Xcode描述文件包含的设备UUID

更新Xcode描述文件

  • 找到 Xcode 描述文件并删除,
rm ~/Library/MobileDevice/Provisioning\ Profiles/
// XCode 16
rm ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles
  • 清理工程
  • 打包

查看ipa文件包含的设备UUID

  • 修改 ipa 包后缀为 zip 并解压
  • 查看包内容,找到 embedded.mobileprovision 文件下的 ProvisionedDevices,列表显示就是所有的设备UUID
查看描述文件内容命令:(需要先进入描述文件所在目录)
security cms -D -i embedded.mobileprovision

SQLite使用总结

使用的库 FMDB

创建数据库

//数据库的路径
NSString *path = [NSString stringWithFormat:@"%@/Documents/DB.sqlite",NSHomeDirectory()];
//创建数据库
self.database = [[FMDatabase alloc] initWithPath:path];

if (self.database.open == NO) {
    NSLog(@"失败");
    
    return ;
}
NSLog(@"成功");

创建表

NSString *createSQL = [NSString stringWithFormat:@"create table if not exists %@ (id varchar(32), type int(11), create_time int(11))", kTableName];

BOOL status = [self.database executeUpdate:createSQL];
NSLog(@"status: %@", @(status));

新增数据

NSString *sql = [NSString stringWithFormat:@"insert into %@(id,type,create_time) values(?,?,?)", kTableName];
BOOL status = [_database executeUpdate:sql,@(myId),@(type), @(time)];

查询数据

NSString *sql = [NSString stringWithFormat:@"select * from %@ where id = %@", kTableName, @(myId)];
FMResultSet *set = [self.database executeQuery:sql];
while ([set next]) {
    model.myId = [set stringForColumn:@"id"];
    model.type = [set intForColumn:@"type"];
    model.createTime = [set intForColumn:@"create_time"];
    
    [resultList addObject:model];
}

更新数据

NSString *sql = [NSString stringWithFormat:@"update %@ set create_time = %@ where id = %@", kTableName, @(time), @(myId)];
BOOL status = [_database executeUpdate:sql];

删除数据

NSInteger expireTime = [NSDate timeStamp] - 60 * 60 * 24 * 30;
NSString *sql = [NSString stringWithFormat:@"delete from %@ where create_time > %@", kTableName, @(expireTime)];
BOOL status = [self.database executeUpdate:sql];

通过命令行管理数据

打开 Terminal
sqlite3 filename; 

// 查看帮助
.help
// 查看所有数据库
.databases
// 查看所有表
.tables
// 显示/隐藏 查询结果表头
.headers on|off
// 显示数据操作历史记录
.dump
// 将查询结果导出为Excel
.excel
// 以什么格式显示,推荐 column 格式显示
.mode list|line|column
// 查看配置
.show
// 退出
.quit

// 更多命令查看帮助文档...

修改数据失败问题处理

错误: attempt to write a readonly database

原因:sqlite3所在的文件夹没有读写权限,或者权限不足

解决办法:

  • 普通用户的话提升文件夹的权限
chmod 777 db.sqlite3
cd ..
chmod 777  *
  • 将项目移动到有全部权限的文件夹下启动

Link: Mac终端查看sqlite3数据库、表数据等(含sqlite可视化工具下载)

OC调用私有方法

OC调用私有方法共有以下四种方式:

以Person类为例:
#import "Person.h"
@implementation Person
- (void)privateMethod
{
    NSLog(@"privateMethod");
}
@end

一、分类

#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Test)
//只有声明,没有实现
- (void)privateMethod;
@end
NS_ASSUME_NONNULL_END

调用方式如下:

Person *person = [[Person alloc] init];
[person privateMethod];

二、performSelector

调用方式如下:

Person *person = [[Person alloc] init];
[person performSelector:@selector(privateMethod) withObject:nil];

三、objc_msgSend

调用方式如下:

Person *person = [[Person alloc] init];
objc_msgSend(person,@selector(privateMethod));

四、IMP

调用方式如下:

Person *person = [[Person alloc] init];
IMP imp = [person methodForSelector:@selector(privateMethod)];
    void (* tempFunc)(id target, SEL) = (void *)imp;
    tempFunc(person, @selector(privateMethod));

Link: OC调用私有方法

通过命令行安装iPhone模拟器

查看所有模拟器

xcrun instruments -s

通过以上命令,可以看到系统有以下模拟器:

iPhone 8 (11.2) [39E670F7-F9B0-4A1F-92E7-202EED62E66A] (Simulator)
iPhone 8 (11.2) + Apple Watch Series 3 - 38mm (4.2) [02C9E3B3-9846-4AD0-9890-B2B840811BC9] (Simulator)
iPhone 8 Plus (11.2) [18B8751C-097C-427C-9DB8-BF59FB3C1D7E] (Simulator)
iPhone 8 Plus (11.2) + Apple Watch Series 3 - 42mm (4.2) [E381F8A1-54E9-4428-BE4A-7011902C5D69] (Simulator)
iPhone SE (11.2) [D8ACFB1F-6678-4014-8993-72050939481D] (Simulator)
iPhone X (11.2) [1346D0CB-C0A7-40F9-BB8B-C095B736C696] (Simulator)

打开指定模拟器

xcrun instruments -w "iPhone 8 (11.2)"

安装指定APP

xcrun simctl install booted <app路径>

运行指定的app (com.example.app)

xcrun simctl launch booted <app identifier>

卸载指定的应用

xcrun simctl uninstall booted <app identifier>

Link: 从命令行启动Xcode模拟器

Swift 打印二进制

示例代码:

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // 按位取反
let stringOfInvertedBits = String(invertedBits, radix: 2) // 转化为字符串
print(stringOfInvertedBits) // 输出结果为 11110000
let stringOfInvertedBits = String(invertedBits, radix: 2)
相当于
let stringOfInvertedBits = String(invertedBits, radix: 2, uppercase: false)

函数定义:

extension String {
    /// Create an instance representing `v` in base 10.
    public init<T : _SignedIntegerType>(_ v: T)
    /// Create an instance representing `v` in base 10.
    public init<T : UnsignedIntegerType>(_ v: T)
    /// Create an instance representing `v` in the given `radix` (base).
    ///
    /// Numerals greater than 9 are represented as roman letters,
    /// starting with `a` if `uppercase` is `false` or `A` otherwise.
    public init<T : _SignedIntegerType>(_ v: T, radix: Int, uppercase: Bool = default)
    /// Create an instance representing `v` in the given `radix` (base).
    ///
    /// Numerals greater than 9 are represented as roman letters,
    /// starting with `a` if `uppercase` is `false` or `A` otherwise.
    public init<T : UnsignedIntegerType>(_ v: T, radix: Int, uppercase: Bool = default)
}

Link:

isKindOf 和 isMemberOf 的区别

在iOS 开发中,在遇到需要判断一个对象是否属于某个类的时候,我们经常会用到 isKindOfisMemberOf 这两个方法。但往往我们稍不注意,就会将他们两个用混了。虽然有时候的结果是对的,但对于程序的健壮性来说却不是一个好的事情了。所以我们在平常的程序开发中,一定要做到代码所写的和心里所想的完全一致。这样才能保证我们的程序的质量。今天我们就来好好梳理下这两个判断方法的区别。

  • iskindOf 是用来判断一个对象是否为某个类以及该类的父类的对象
  • isMemberOf 只能用来判断一个对象是否当前类的对象

所以如果我们需要判断一个对象是否为某两个类的对象,只能使用 isMemberOf 来进行判断,这样才能保证不管以后这两个类怎么变,都可以得到正确的结果。

isKindOf 应该在什么时候使用呢?

排除上面那一种用法,那就是判断 一个对象是否属于某个类或属于某个类的父类 这一种情况了。

以上是我在实际编程中对这两个方法的一点理解。

SDWebImage 加载图片失败后,不重新加载的解决

今天测试发现一个 bug,说在弱网的情况下,列表图片会加载失败,等网络好了后,重新下拉刷新,加载失败的列表图片,也无法进行加载。
根据测试反馈信息,我觉得应该是 SDWebImage 在图片加载失败后,给图片链接做了标记,不再重新加载了。于是查看 SDWebImage 源码,发现了 SDWebImageRetryFailed 这个枚举值,查看解释,恍然大悟。都是使用时没有仔细查看源码的锅,以后多注意,使用一个第三方的库,就得好好了解下他的源码:

/**
 * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
 * This flag disable this blacklisting.
 */
SDWebImageRetryFailed = 1 << 0,

iOS 数据精度及大数的处理

在 iOS 开发中,我们很容易遇到使用 CGFloat 来标示浮点数,但这样的表示会造成精度失真。这时我们可以使用NSDecimalNumber来处理这个问题。

NSDecimalNumberNSNumber的子类,可以处理大数运算及数据的精度问题。

大数相乘可能导致的问题

我们先上一段代码:

NSString *priceStr = @"";
NSDecimalNumber *number = [NSDecimalNumber decimalNumberWithString:priceStr];
NSDecimalNumber *countNum = [NSDecimalNumber decimalNumberWithString:stringWithNSInteger(NSIntegerMax)];
number = [number decimalNumberByMultiplyingBy:countNum];

在这段代码中,number的值为:NaN,即:not a number ,非数值;
而countNum 是一个最大的整数,
最后,将NaN和最大的整数相乘,导致了overflow的crash。

解决方案:

//定义数值处理的行为
NSDecimalNumberHandler *roundUp = [NSDecimalNumberHandler
                                      decimalNumberHandlerWithRoundingMode:NSRoundBankers
                                      scale:2
                                      raiseOnExactness:NO
                                      raiseOnOverflow:NO
                                      raiseOnUnderflow:NO
                                      raiseOnDivideByZero:NO];
    
NSString *priceStr = @"";
NSDecimalNumber *number = [NSDecimalNumber decimalNumberWithString:priceStr];
NSDecimalNumber *countNum = [NSDecimalNumber decimalNumberWithString:stringWithNSInteger(NSIntegerMax)];

//使用数据处理行为的约定来进行运算,防止crash
number = [number decimalNumberByMultiplyingBy:countNum withBehavior:roundUp];

上面这个例子不会crash了,但是最终number的值为NaN,需要后续的业务逻辑进行判断处理;

NSDecimalNumberHandler 用到的参数,其中:

NSRoundBankers

枚举,截断的方式;完整的定义如下:

// Rounding policies :
// Original
//    value 1.2  1.21  1.25  1.35  1.27
// Plain    1.2  1.2   1.3   1.4   1.3
// Down     1.2  1.2   1.2   1.3   1.2
// Up       1.2  1.3   1.3   1.4   1.3
// Bankers  1.2  1.2   1.2   1.4   1.3

typedef NS_ENUM(NSUInteger, NSRoundingMode) {
    NSRoundPlain,   // Round up on a tie
    NSRoundDown,    // Always down == truncate
    NSRoundUp,      // Always up
    NSRoundBankers  // on a tie round so last digit is even
};

scale

小数点后面的位数(精度)

raiseOnExactness

The exception raised if there is an exactness error.

raiseOnOverflow

是否抛出溢出错误,如果为YES,则APP会捕获溢出错误,这会导致APPcrash;

The exception raised on overflow.

raiseOnUnderflow

The exception raised on underflow.

raiseOnDivideByZero

The exception raised on divide by zero.

补充知识:

  • 判断一个数值是否为NaN可以使用系统方法:isnan(x);注:x为数值类型,不是NSDecimalNumber,更不是NSNumber;
  • 如果要判断一个NSDecimalNumber 是否为 NAN ,则使用下面的方法:
if([number isEqualToNumber:NSDecimalNumber.notANumber]){
    NSLog(@"number is nan");
}else{
    NSLog(@"number:%@",number);
}

参考链接:iOS 两个数相乘导致 NSDecimalNumber overflow exception 错误的分析及解决