注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

如何判断设备是否越狱?

iOS
前言 iPhone 越狱已经不是什么新鲜事,但是越狱之后意味着已经拿到了系统的所有权限,继续在越狱的设备上运行你的程序也就意味着不再安全,因此目前很多主流的 App 都是禁止运行在此类设备上的。 但是怎么判断一个设备是否为越狱的机器呢?今天就来讲讲我所知道的一...
继续阅读 »

前言


iPhone 越狱已经不是什么新鲜事,但是越狱之后意味着已经拿到了系统的所有权限,继续在越狱的设备上运行你的程序也就意味着不再安全,因此目前很多主流的 App 都是禁止运行在此类设备上的。


但是怎么判断一个设备是否为越狱的机器呢?今天就来讲讲我所知道的一些方法。


方法一


检查手机上是否安装了 Cydia,玩越狱的同学肯定都清楚,这个 app 堪称是越狱系统的 App Store,上边可以安装各种正规 App Store 安装不到的软件。Cydia 上除了独立的应用程序之外,更多的包是 iOS 本身和应用程序的扩展、修改和主题。


因此可以说只要是越狱的设备,都会安装这个应用,那我们只需要检测这个应用存不存在就行了。


这里主要用到的方法是用 canOpenURL 是否能打开 cydia:// 这个 URL Scheme。

func isJailBreak() -> Bool {
    return UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
}



这里记得把 cydia 加入到 info.plist 中的 LSApplicationQueriesSchemes 字段里才能正常检测应用是否安装



这种方式简单粗暴,不过不建议用,因为准确度可能不高,一方面 cydia 可能把这个 URL Scheme 改掉防止你检测。另一方面正常手机也可能会有一个 app 的 URL Scheme 叫这个名字,造成误判。


方法二


检测是否存在 MobileSubstrate 动态库,这个库是 cydia 的基石,越狱环境下安装绝大部分插件,必须要有 MobileSubstrate,因此我们只需要判断是否存在这个动态库即可。


我在网上找了一个 c 语言的实现:

bool
isJailBreak(void)
{
    const char *const imageName = "MobileSubstrate";
    if (imageName != NULL) {
        const uint32_t imageCount = _dyld_image_count();
        for (uint32_t iImg = 0; iImg < imageCount; iImg++) {
            const char *name = _dyld_get_image_name(iImg);
            if (strstr(name, imageName) != NULL) {
                return true;
            }
        }
    }
    return false;
}


方法三


还是检测文件,如果越狱的话,设备会创建许多文件,可以使用 FileManager 来检测这些文件是否存在:

func isJailBreak() -> Bool {
#if targetEnvironment(simulator)
    return false
#else
    let files = [
        "/private/var/lib/apt",
        "/Applications/Cydia.app",
        "/Applications/RockApp.app",
        "/Applications/Icy.app",
        "/Applications/WinterBoard.app",
        "/Applications/SBSetttings.app",
        "/Applications/blackra1n.app",
        "/Applications/IntelliScreen.app",
        "/Applications/Snoop-itConfig.app",
        "/bin/sh",
        "/usr/libexec/sftp-server",
        "/usr/libexec/ssh-keysign /Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt /System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
        "/System/Library/LaunchDaemons/com.ikey.bbot.plist",
        "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
        "/Library/MobileSubstrate/DynamicLibraries/Veency.plist"
    ]
    return files.contains(where: {
        return FileManager.default.fileExists(atPath: $0)
    })
#endif
}


这里有个条件编译,在模拟器下是不需要检查的。


方法四


越狱之后所有 App 都被授予 root 权限,并且可以修改沙箱之外的文件。利用这个特点,如果我们的 App 可以写入其沙箱之外的文件,则证明该设备已越狱:

func isJailBreak() -> Bool {
    let string = "iOS 新知"
    do {
        try string.write(to: URL(filePath: "/private/myfile.txt"), atomically: true, encoding: .utf8)
        return true
    } catch {
        return false
    }
}


方法五


越狱之后也就意味着 App 可以随意调用系统 API 了,因此我们可以尝试调用系统 API,来查看是否能得到正确结果,以此来判断是否越狱:

func isJailBreak() -> Bool {
    let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
    let forkPtr = dlsym(RTLD_DEFAULT, "fork")
    typealias ForkType = @convention(c) () -> Int32
    let fork = unsafeBitCast(forkPtr, to: ForkType.self)

    return fork() != -1
}


建议以上五种方法结合使用,提高检测的准确率


检测到越狱设备,禁止使用


如果检测到当前运行环境为越狱设备,可以强制退出 App,以确保安全。强制退出 app 的方法就很多了,可以使用 exit(-1),也可以人为做个数组越界之类的:

// 检测到越狱设备
if isJailBreak() {
    // 退出 app
    exit(-1)
    // 或者数组越界 crash
    // [0][1]
}

作者:iOS新知
链接:https://juejin.cn/post/7273435543676813375
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS气泡提示工具BubblePopup的使用

iOS
BubblePopup 气泡弹框,气泡提示框,可用于新手引导,功能提示。 在平时的开发中,通常新手引导页或功能提示页会出现气泡弹窗来做提示。如果遇到了这类功能通常需要花费一定的精力来写这么一个工具的,这里写了一个气泡弹窗工具,希望能帮你提升一些开发效率。 使用...
继续阅读 »

BubblePopup


气泡弹框,气泡提示框,可用于新手引导,功能提示。


在平时的开发中,通常新手引导页或功能提示页会出现气泡弹窗来做提示。如果遇到了这类功能通常需要花费一定的精力来写这么一个工具的,这里写了一个气泡弹窗工具,希望能帮你提升一些开发效率。


使用方法

  • 从gitHub上下载代码到本地,代码地址:github.com/zhfei/Bubbl…
  • 调用BubblePopupManager文件内的单例方法,在指定的页面上添加气泡提示。 普通文本气泡弹窗使用方式如下:
BubblePopupManager.shared.addPopup(toView: self.view, tips: "冒泡弹窗", popupType: .dotLine, positionType: .bottom, popupPoint: nil, linkPoint: CGPoint(x: sender.frame.midX, y: sender.frame.minY), maxWidth: 200.0)


自定义View气泡弹窗使用方式如下:

BubblePopupManager.shared.addPopup(toView: self.view, customContentView: MyContentView(), popupType: .triangle, positionType: .bottom, popupPoint: CGPoint(x: sender.frame.midX, y: sender.frame.minY), linkPoint: nil, maxWidth: 200.0)

注意:自定义内容View只能使用frame布局,不能使用约束。


设计模式


气泡弹窗View的结构设计采用的设计模式为组合模式

把气泡弹窗分为3个部分:气泡背景,气泡指示器,气泡提示内容。


在创建气泡弹窗时,根据子类的自定义实现,将这三部分分别创建并组装到一起。实现了功能的灵活插拔和自定义扩展。


气泡弹窗View类图



气泡弹窗生成算法采用的设计模式为模版方法模式

在气泡构建基类中设置好气泡的构建步骤,把必要的部分或者提供默认实现的部分在父类中提供默认的实现,对其他需要自定义实现的部分,只在父类中写了一个抽象方法,具体实现交给子类自己实现。


虚线气泡弹窗类图



三角形气泡弹窗类图



核心实现

  • BubblePopupManager: 使用气泡弹窗工具的入口,通过它创建并添加一个气泡弹窗到指定的View上。

  • BubblePopupBuilder: 气泡弹窗构建者基类,使用模版方法模式定义了气泡的构建流程,子类可以自定义各自的实现。

  • DotLineBubblePopupBuilder: 虚线气泡弹窗基类,它是基类BubblePopupBuilder的子类,内部包含了虚线气泡弹窗生成时所需要的工具方法和必要属性,方便创建top,bottom,left,right虚线气泡弹窗。

  • TriangleBubblePopupBuilder : 三角形气泡弹窗基类,它是BubblePopupBuilder的子类,内部包含了三角形气泡弹窗生成时所需要的工具方法和必要属性,方便创建top,bottom,left,right三角形气泡弹窗

  • BubblePopup: 气泡弹窗View,它内部使用组合模式将子部件组合起来,组成了一个气泡弹窗。

  • BubbleViewFactory: 气泡弹窗子视图创建工程,用于创建气泡弹窗所需要的子视图,并将各个子视图组装成一个最终的气泡弹窗。


BubblePopupBuilder

BubblePopupBuilder是所有气泡弹窗的公共基类,对于里面定义的属性和方法的功能分别为


  • 属性:属性里保存的是气泡弹窗公共的,必要的数据。
  • 方法:在基类提供的方法中主要用于定义气泡的构建流程。 核心方法如下:
   func setupUI() {
addBubbleContentView(to: bubblePopup)
addBubbleBGView(to: bubblePopup)
updateLayout(to: bubblePopup)
addBubbleFlagView(to: bubblePopup)
}

其中气泡内容展示视图和气泡背景视图有默认实现,子类可以直接使用默认样式。


而气泡标识View和气泡布局方法则需要子类自己实现,因为不同类型的气泡弹窗它们的气泡标识设布局方式是不一样的。


DotLineBubblePopupBuilder

虚线气泡基类DotLineBubblePopupBuilder,它继承自BubblePopupBuilder

  • 属性:增加了虚线弹窗必要的linkPoint属性,即:虚线与气泡弹窗的连接点。 增加了一个坐标系转换懒加载属性,用于将用户设置的屏幕坐标点转成气泡内部的视图坐标系中的点。

  • 重要方法说明:

getDrawDotLineLayerRectParams

用于虚线图层绘制:获取虚线绘制时所需要的绘制元素坐标,如:虚线的开始,结束坐标,连接点圆的直径等。

getDotLineLayerContainerViewFrame

更新虚线容器View的位置大小信息:获取不同情况下的虚线容器Frame。

layoutDotLineBubblePopupView

更新虚线气泡弹窗的frame。

updateBGBubbleViewFrame

更新气泡背景的frame。


这里提供的方法属于工具方法,子类可以通过传递自己的类型来得到对应的结果。这里按道理可以使用设计模式中策略模式来对算法进行封装,如:在基类定义一个抽象方法,将上面则4个工具方法分拆到各自的子类中,让子类在对应的自己的类中实现这个方法。


这里没有这样做原因是:这些方法在子类中的实现代码并不复杂,用一个方法根据条件集中返回是比较方便的,而分拆到不同类中反而很麻烦。所以选择在基类中以方法工具的形式统一放置了。


DotLineTopBubblePopupBuilder

top型虚线气泡弹窗DotLineTopBubblePopupBuilder,它继承自DotLineBubblePopupBuilder,属于一直具体的弹窗类型。


它里面只对下面两个方法进行了重写,根据自己的类型进行子类个性化实现。

override func updateLayout
override func addBubbleFlagView

具体实现如下:

class DotLineTopBubblePopupBuilder: DotLineBubblePopupBuilder {

override func updateLayout(to bubblePopup: BubblePopup) {
layoutDotLineBubblePopupView(bubblePopup: bubblePopup, positionType: .top)
}

override func addBubbleFlagView(to bubblePopup: BubblePopup) {
assert(!self.targetPoint.equalTo(.zero), "气泡提示点无效")

let flagFrame = getDotLineLayerContainerViewFrame(position: .top, targetPoint: self.targetPoint)
let params = getDrawDotLineLayerRectParams(position: .top)
let flagBubbleView = BubbleViewFactory.generateDotLineBubbleFlagView(flagFrame: flagFrame, position: .top, params: params)
bubblePopup.bubbleFlagView = flagBubbleView
bubblePopup.addSubview(flagBubbleView)
}

}

其他bottom, left, right类型相似。


TriangleBubblePopupBuilder

三角形气泡基类TriangleBubblePopupBuilder,它继承自BubblePopupBuilder

  • 属性:相对于基类增加了popupPoint属性,它是三角形顶点指向的坐标点 增加了一个坐标系转换懒加载属性,用于将用户设置的屏幕坐标点转成气泡内部的视图坐标系中的点。

  • 重要方法说明:

getDrawTriangleLayeyRectParams

为三角形图层绘制提供不同气泡类型所需要的绘制元素坐标,如:三角形的三个顶点。

getTriangleLayerContainerViewFrame

获取不同情况下三角形图层容器的Frame,用于更新三角形图层容器View的位置大小。

layoutTriangleBubblePopupView

更新三角形气泡弹窗的frame。

updateTriangleBGBubbleView

更新气泡背景的frame。


三角形弹窗基类TriangleBubblePopupBuilder的设计方式和虚线弹窗基类是一样的。
这里的方法属于工具方法,子类可以通过传递自己的类型来得到对应的结果,通过牺牲一点开发模式的规范化来换取开发效率的提升。


在三角形气泡基类的下面同样有4个子类top,bottom,left ,right进行各种的自定义实现。


TriangleTopBubblePopupBuilder

top型三角形气泡弹窗DotLineTopBubblePopupBuilder,它继承自DotLineBubblePopupBuilder,属于一直具体的弹窗类型。


它里面只对下面这两个方法做了重写,根据自己的类型进行子类个性化实现。

override func updateLayout
override func addBubbleFlagView

具体实现如下:

class TriangleTopBubblePopupBuilder: TriangleBubblePopupBuilder {
override func updateLayout(to bubblePopup: BubblePopup) {
layoutTriangleBubblePopupView(bubblePopup: bubblePopup, positionType: .top)
}
override func addBubbleFlagView(to bubblePopup: BubblePopup) {
assert(!self.targetPoint.equalTo(.zero), "气泡提示点无效")

let flagFrame = getTriangleLayerContainerViewFrame(position: .top, targetPoint: self.targetPoint)
let params = getDrawTriangleLayeyRectParams(position: .top)
let flagBubbleView = BubbleViewFactory.generateTriangleBubbleFlagView(flagFrame: flagFrame, position: .top, params: params)
bubblePopup.bubbleFlagView = flagBubbleView
bubblePopup.addSubview(flagBubbleView)
}
}

其他bottom, left, right类型相似。


弹窗效果展示


三角形气泡弹窗



虚线气泡弹窗



自定义气泡弹窗



作者:滴水微澜
链接:https://juejin.cn/post/7242290406637109306
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

百度工程师移动开发避坑指南——Swift语言篇

iOS
上一篇我们介绍了移动开发常见的内存泄漏问题,见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。 对于Swift开发者,Swift较于OC一个很大的不同就是引入了可选类型(Optional),刚接触Swift的开发者很容易...
继续阅读 »

上一篇我们介绍了移动开发常见的内存泄漏问题,见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。


对于Swift开发者,Swift较于OC一个很大的不同就是引入了可选类型(Optional),刚接触Swift的开发者很容易在相关代码上踩坑。


本期我们带来与Swift可选类型相关的几个避坑指南:可选类型要判空;避免使用隐式解包可选类型;合理使用Objective-C标识符;谨慎使用强制类型转换。希望能对Swift开发者有所帮助。


一、可选类型(Optional)要判空


在Objective-C中,可以使用nil来表示对象为空,但是使用一个为nil的对象通常是不安全的,如果使用不慎会出现崩溃或者其它异常问题。在Swift中,开发者可以使用可选类型表示变量有值或者没有值,可以更加清晰的表达类型是否可以安全的使用。如果一个变量可能为空,那么在声明时可以使用?来表示,使用前需要进行解包。例如:

var optionalString: String?

在使用可选类型对象时,需要进行解包操作,有两种解包方式:强制解包与可选绑定。


强制解包使用 ! 修饰一个可选对象 ,相当于告诉编译器『我知道这是一个可选类型,但在这里我可以保证他不为空,编译时请忽略此处的可空校验』,例如:

let unwrappedString: String = optionalString!  // 运行时报错:Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

这里使用 ! 进行了强制解包,如果optionalString为nil,将会产生运行时错误,发生崩溃。**因此,在使用 ! 进行强制解包时,必须保证变量不为nil,要对变量进行判空处理,**如下:

if optionalString != nil {
let unwrappedString = optionalString!
}

相较于强制解包的不安全性,一般而言推荐另一种解包方式,即可选绑定。例如:

if let optionalString = optionalString {
// 这里optionalString不为nil,是已经解包后的类型,可以直接使用
}

综上,在对可选类型进行解包时应尽量避免使用强制解包,采用可选绑定替代。如果一定要使用强制解包,那么必须在逻辑上完全保证类型不为空,并且做好注释工作,以增加后续代码的可维护性。


二、避免使用隐式解包可选类型(Implicitly Unwrapped Optionals)


由于可选类型每次使用之前都需要进行显式解包操作,有时变量在第一次赋值之后,就会一直有值,如果每次使用都显式解包,显得繁琐,Swift引入了隐式解包可选类型,隐式解包可选类型可以使用 ! 来表示,并且使用时不需要显式解包,可以直接使用,例如:

var implicitlyUnwrappedOptionalString: String! = "implicitlyUnwrappedOptionalString"
var implicitlyString: String = implicitlyUnwrappedOptionalString

上述例子的隐式解包,在编译和运行过程中都不会发生问题,但如果在两行代码中间插入一行 implicitlyUnwrappedOptionalString = nil将会产生运行时错误,发生崩溃。


在我们实际项目中,一个模块通常由多人维护,通常很难保证变量在第一次赋值之后一直不为nil或者只有在第一次正确赋值之后使用,从安全角度考虑,在使用隐式解包类型之前也要进行判空操作,但这样就和使用可选类型没有区别。对于可选类型(?),不经过解包直接使用编译器会报告错误,对于隐式解包类型,则可直接使用,编译器无法帮助我们做出是否为空的检查。因此,在实际项目中,不推荐使用隐式解包可选类型,如果一个变量是非空的,则选择非空类型,如果不能保证是非空的,则选择使用可选类型。


三、合理使用Objective-C标识符


与Swift不同的是,OC是一种动态类型语言,对于OC而言没有optional这个概念,无法在编译期间检查对象是否可空。苹果在 Xcode 6.3 中引入了一个 Objective-C 的新特性:Nullability Annotations,允许编码时使用nonnull、nullable、null_unspecified等标识符告诉编译器对象是否是可空或者非空的,各标识符含义如下:


nonnull,表示对象是非空的,有__nonnull和_Nonnull等价标识符。


nullable,表示对象可能是空的,有__nullable 和_Nullable等价标识符。


null_unspecified,不知道对象是否为空,有__null_unspecified等价标识符。


OC标识符标注的对象类型和Swift类型对应关系如下:




除了以上标识符外,现在通过Xcode创建的头文件默认被 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 包住,即在这之间声明的对象默认标识符是 nonnull 的。


在Swift与OC混编场景,编译器会根据OC标识符将OC的对象类型转换成Swift类型,如果没有显式的标识,默认是null_unspecified。例如:

@interface ExampleOCClass : NSObject
// 没有指定标识符,且没有被NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END包裹,标识符默认为null_unspecified
+ (ExampleOCClass *)getExampleObject;
@end

@implementation ExampleOCClass
+ (ExampleOCClass *)getExampleObject {
return nil; // OC代码直接返回nil
}
@end
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let _ = ExampleOCClass.getExampleObject().description // 报错:Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
}
}

在上面例子中,Swift代码调用OC接口获取一个对象,编译器隐式的将OC接口返回的对象转换为隐式解包类型来处理。由于隐式解包类型可以不显式解包直接使用,使用者往往会忽略OC返回的是隐式解包类型,不通过判空而直接使用。但当代码执行时,由于OC接口返回了一个nil,导致Swift代码解包失败,发生运行时错误。


在实际编码中,推荐显式指定OC对象为nonnull或者nullable,针对上述代码进行修改后如下:

@interface ExampleOCClass : NSObject
/// 获取可空的对象
+ (nullable ExampleOCClass *)getOptionalExampleObject;
/// 获取不可空的对象
+ (nonnull ExampleOCClass *)getNonOptionalExampleObject;
@end

@implementation ExampleOCClass
+ (ExampleOCClass *)getOptionalExampleObject {
return nil;
}
+ (ExampleOCClass *)getNonOptionalExampleObject {
return [[ExampleOCClass alloc] init];
}
@end
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 标注nullable后,编译器调用接口时,会强制加上 ?
let _ = ExampleOCClass.getOptionalExampleObject()?.description
// 标注nonnull后,编译器将会把接口返回当做不可空来处理
let _ = ExampleOCClass.getNonOptionalExampleObject().description
}
}

在OC对象加上nonnull或者nullable标识符后,相当于给OC代码增加了类似Swift的『静态类型语言的特性』,使得编译器可以对代码进行可空类型检测,有效的降低了混编时崩溃的风险。但这种『静态特性』并不对OC完全有效,例如以下代码,虽然声明返回类型是nonnull的,但是依然可以返回nil:

@implementation ExampleOCClass
+ (nonnull ExampleOCClass *)getNonOptionalExampleObject {
return nil; // 接口声明不可空,但实际上返回一个空对象,可以通过编译,如果Swift当作非空对象使用,则会发生崩溃
}
@end
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
ExampleOCClass.getNonOptionalExampleObject().description
}
}

基于以上例子,依然会产生运行时错误。从安全性的角度上来说,似乎Swift最好在使用所有OC的接口时都进行判空处理。但实际上这将导致Swift的代码充斥着大量冗余的判空代码,大大降低代码的可维护性,同时也违背了『暴露问题,而非隐藏问题』的编码原则,并不推荐这么做,合理的做法是在OC侧做好安全校验,OC对返回类型应做好检验,保证返回类型的正确性,以及返回值和标识符能够对应。


综合来看,OC侧标识符最好遵循如下使用原则:


1、不推荐使用NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END,因为默认修饰符是nonnull的,在实际开发中很容易忽略返回的对象是否为空。返回空则会导致Swift运行时错误。推荐所有涉及混编的OC接口都需要显式使用相应的标识符修饰。


2、OC接口要谨慎使用 nonnull 修饰 ,必须确保返回值不可能是空的情况下使用,任何不能确定不可空的接口都需要标注为nullable。


3、为避免Swift侧不必要的类型、判空等校验(违背Swift设计理念),在理想状态下需在OC侧进行类型的校验,保证返回对象和标注的标识符完全正确,这样Swift则可以完全信赖OC返回的对象类型。


4、在Swift调用OC代码时,要关注OC返回的类型,尤其是返回隐式解包类型时,要做好判空处理。


5、在OC代码支持Swift调用前,提前对OC代码做好返回类型和标识符的检查,确保返回Swift的对象是安全的。


四、谨慎使用强制类型转换


GEEK TALK


Swift 作为强类型语言,禁止一切默认类型转换,这要求编码者需要明确定义每一个变量的类型,在需要类型转换时必须显式的进行类型转换。Swift可以使用as和as?运算符进行类型转换。


as运算符用于强制类型转换,在类型兼容情况下,可以将一个类型转换为另一个类型,例如:

var d = 3.0 // 默认推断为 Double 类型
var f: Float = 1.0 // 显式指定为 Float 类型
d = f // 编译器将报错“Cannot assign value of type 'Float' to type 'Double'”
d = f as Double // 需要将Float类型转换为Double类型,才能赋值给f

除了以上列举的基本类型外,Swift还兼容基础类型与对应的OC类型的转换,比如NSArray/Array、NSString/String、NSDictionary/Dictionary。


如果类型转换失败,将会导致运行时错误。例如:

let string: Any = "string"
let array = string as Array // 运行时错误

这里string变量实际是一个String类型,尝试将String类型转换成Array类型,将导致运行时错误。


另一种类型转换的方式是使用as?运算符,如果转换成功,返回一个转换类型的可选类型,如果转换失败,返回nil。例如:

let string: Any = "string"
let array = string as? Array // 转换失败,不会产生运行时错误

这里由于无法将String类型转换为Array类型,因此转换失败,array变量的值为nil,但不会产生运行时错误。


综合来看,在进行类型转换时,需要注意以下几点:


1、类型转换只能在兼容的类型之间进行,例如Double和Float可以相互转换,但String和Array之间不能相互转换。


2、如果使用as进行强制类型转换,需要确保转换是安全的,否则将会导致运行时错误。如果不能确保转换类型之间是兼容的,则应该使用as?运算符,例如将网络数据解析成模型数据时,无法保证网络数据的类型,应该使用as?。


3、在使用as?运算符进行类型转换时,需要注意返回值可能为nil的情况。


----------  END  ----------


推荐阅读【技术加油站】系列:


百度工程师移动开发避坑指南——内存泄漏篇


百度程序员开发避坑指南(Go语言篇)


百度程序员开发避坑指南(3)


百度程序员开发避坑指南(移动端篇)


百度程序员开发避坑指南(前端篇)


作者:百度Geek说
链接:https://juejin.cn/post/7236554895141716026
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

将项目依赖从 CocoaPods 迁移到 SPM

iOS
昨天的文章讲了如何删除项目中的 CocoaPods 依赖,文章中也有提到未来的趋势一定是从 CocoaPods 到 Swift Package Manager(SPM),今天就来讲讲如何添加 SPM 依赖。 SPM 是苹果在2018年推出的供 Swift 开发...
继续阅读 »


昨天的文章讲了如何删除项目中的 CocoaPods 依赖,文章中也有提到未来的趋势一定是从 CocoaPods 到 Swift Package Manager(SPM),今天就来讲讲如何添加 SPM 依赖。


SPM 是苹果在2018年推出的供 Swift 开发者进行包管理的工具,从 Xcode 11 开始支持。


首先打开 Xcode,点击项目根目录,选择 PROJECT,然后选择第三个 Tab,Package Dependencies,最后点击下边的加号按钮。



之后会出现 Package 的选择面板:



然后在右上角的输入框中输入你要依赖的项目地址,如果不知道项目地址可以到依赖包的官方页面查看,比如我们要添加 Alamofire,就可以到其 Github 页面 github.com/Alamofire/A…,文档中有 Swift Package Manager 的安装方法:




拷贝这个地址复制到前边说的输入框内,Xcode 会自动帮我们找到这个库,在右侧可以选择你需要依赖的版本以及对应的 Target:




最后点击右下角的 Add Package 按钮,随后 Xcode 会下载这个仓库,并弹出面板让我们选择要添加到哪个 Target,最后再次点击 Add Package 即可



添加完成后,我们就可以在 Xcode 项目中看到这个依赖被成功添加进来了。



之后你就可以开始愉快的使用它们了:

import UIKit
import Alamofire

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
AF.request("https://apple.com").response { res in
debugPrint(res)
}
}
}


最后我还下载了一些 swift 开发中主流的一些库,安装都很快,用起来可以说非常方便了。



除了在 GitHub 上找 swift 包之外,Swift Package Index(SPI) 也是一个不错的选择,SPI 是一个开源的 swift 包集合地,这里包含了大量的 swift 开源库,并且在前不久,苹果官方赞助了 SPI,以确保它能正常的发展下去,在不久的将来,Swift 开源库可能不支持 CocoaPods,但一定会支持 Swift Package Manager。


参考资料


[1]


Swift Package Index: swiftpackageindex.com/


作者:iOS新知
链接:https://juejin.cn/post/7255973942543777850
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 判断系统版本

iOS
方案一double systemVersion = [UIDevice currentDevice].systemVersion.boolValue; if (systemVersion >= 7.0) { // >= iOS 7.0 }...
继续阅读 »

方案一

double systemVersion = [UIDevice currentDevice].systemVersion.boolValue;

if (systemVersion >= 7.0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

if (systemVersion >= 10.0) {
// >= iOS 10.0
} else {
// < iOS 10.0
}

如果只是大致判断是哪个系统版本,上面的方法是可行的,如果具体到某个版本,如 10.0.1,那就会有偏差。我们知道 systemVersion 依旧是10.0。


方案二

NSString *systemVersion = [UIDevice currentDevice].systemVersion;
NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1" options:NSNumericSearch];

if (comparisonResult == NSOrderedAscending) {
// < iOS 10.0.1
} else if (comparisonResult == NSOrderedSame) {
// = iOS 10.0.1
} else if (comparisonResult == NSOrderedDescending) {
// > iOS 10.0.1
}

// 或者

if (comparisonResult != NSOrderedAscending) {
// >= iOS 10.0.1
} else {
// < iOS 10.0.1
}

有篇博客提到这种方法不靠谱。比如系统版本是 10.1.1,而我们提供的版本是 8.2,会返回NSOrderedAscending,即认为 10.1.1 < 8.2 。


其实,用这样的比较方式 NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1"],的确会出现这种情况,因为默认是每个字符逐个比较,即 1(0.1.1) < 8(.2),结果可想而知。但我是用 NSNumericSearch 方式比较的,即数值的比较,不是字符比较,也不需要转化成NSValue(NSNumber) 再去比较。


方案三

if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

// 或者

if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

这些宏定义是 Apple 预先定义好的,如下:

#if TARGET_OS_IPHONE
...
#define NSFoundationVersionNumber_iOS_9_4 1280.25
#define NSFoundationVersionNumber_iOS_9_x_Max 1299
#endif


细心的童靴可能已经发现问题了。Apple 没有提供 iOS 10 以后的宏?,我们要判断iOS10.0以后的版本该怎么做呢?
有篇博客中提到,iOS10.0以后版本号提供了,并且逐次降低了,并提供了依据。

#if TARGET_OS_MAC
#define NSFoundationVersionNumber10_1_1 425.00
#define NSFoundationVersionNumber10_1_2 425.00
#define NSFoundationVersionNumber10_1_3 425.00
#define NSFoundationVersionNumber10_1_4 425.00
...
#endif


我想这位童鞋可能没仔细看, 这两组宏是分别针对iPhone和macOS的,不能混为一谈的。


所以也只能像下面的方式来大致判断iOS 10.0, 但之前的iOS版本是可以准确判断的。

if (NSFoundationVersionNumber > floor(NSFoundationVersionNumber_iOS_9_x_Max)) {
// > iOS 10.0
} else {
// <= iOS 10.0
}

方案四


在iOS8.0中,Apple也提供了NSProcessInfo 这个类来检测版本问题。

@property (readonly) NSOperatingSystemVersion operatingSystemVersion NS_AVAILABLE(10_10, 8_0);
- (BOOL) isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version NS_AVAILABLE(10_10, 8_0);

所以这样检测:

if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 8, .minorVersion = 3, .patchVersion = 0}]) {
// >= iOS 8.3
} else {
// < iOS 8.3
}

用来判断iOS 10.0以上的各个版本也是没有问题的,唯一的缺点就是不能准确版本是哪个版本,当然这种情况很少。如果是这种情况,可以通过字符串的比较判断。


方案五


通过判断某种特定的类有没有被定义,或者类能不能响应哪个特定版本才有的方法。
比如,UIAlertController 是在iOS 8.0才被引进来的一个类,我们这个依据来判断版本

if (NSClassFromString(@"UIAlertController")) {
// >= iOS 8.0
} else {
// < iOS 8.0
}

说到这里,就顺便提一下在编译期间如何进行版本控制,依然用UIAlertController 来说明。

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIAlertController : UIViewController

NS_CLASS_AVAILABLE_IOS(8_0) 这个宏说明,UIAlertController 是在iOS8.0才被引进来的API,那如果我们在iOS7.0上使用,应用程序就会挂掉,那么如何在iOS8.0及以后的版本使用UIAlertController ,而在iOS8.0以前的版本中仍然使用UIAlertView 呢?


这里我们会介绍一下在#import <AvailabilityInternal.h> 中的两个宏定义:


*__IPHONE_OS_VERSION_MIN_REQUIRED


*__IPHONE_OS_VERSION_MAX_ALLOWED


从字面意思就可以直到,__IPHONE_OS_VERSION_MIN_REQUIRED 表示iPhone支持最低的版本系统,__IPHONE_OS_VERSION_MAX_ALLOWED 表示iPhone允许最高的系统版本。


__IPHONE_OS_VERSION_MAX_ALLOWED 的取值来自iOS SDK的版本,比如我现在使用的是Xcode Version 8.2.1(8C1002),SDK版本是iOS 10.2,怎么看Xcode里SDK的iOS版本呢?



进入PROJECT,选择Build Setting,在Architectures中的Base SDK中可以查看当前的iOS SDK版本。



打印这个宏,可以看到它一直输出100200。


__IPHONE_OS_VERSION_MIN_REQUIRED 的取值来自项目TARGETS的Deployment Target,即APP愿意支持的最低版本。如果我们修改它为8.2,打印这个宏,会发现输出80200,默认为10.2。


通常,__IPHONE_OS_VERSION_MAX_ALLOWED 可以代表当前的SDK的版本,用来判断当前版本是否开始支持或具有某些功能。而__IPHONE_OS_VERSION_MIN_REQUIRED 则是当前SDK支持的最低版本,用来判断当前版本是否仍然支持或具有某些功能。


回到UIAlertController 使用的问题,我们就可以使用这些宏,添加版本检测判断,从而使我们的代码更健壮。

 - (void)showAlertView {
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Title" message:@"message" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
#else
if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_0) {
UIAlertController *alertViewController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *otherAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];

[alertViewController addAction:cancelAction];
[alertViewController addAction:otherAction];

[self presentViewController:alertViewController animated:YES completion:NULL];
}
#endif
}

方案六


iOS 11.0 以后,Apple加入了新的API,以后我们就可以像在Swift中的那样,很方便的判断系统版本了。

if (@available(iOS 11.0, *)) {
// iOS 11.0 及以后的版本
} else {
// iOS 11.0 之前
}

参考链接


作者:蒙哥卡恩就是我
链接:https://juejin.cn/post/7277111344003399734
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

紧急需求‼️实现iOS启动图动态置灰

iOS
前言 相信这几天各大互联网应用首页置灰已经接踵而至,事情缘由我就不太赘述。毫无疑问,我司从30号当晚就收到紧急需求,我们要求1号必须紧急发版,除了常规的首页支持配置的动态置灰外,我们还要求另外一个需求就是,启动图也需要支持动态配置灰功能,经过几个同事的努力,于...
继续阅读 »

前言


相信这几天各大互联网应用首页置灰已经接踵而至,事情缘由我就不太赘述。毫无疑问,我司从30号当晚就收到紧急需求,我们要求1号必须紧急发版,除了常规的首页支持配置的动态置灰外,我们还要求另外一个需求就是,启动图也需要支持动态配置灰功能,经过几个同事的努力,于1号当晚顺利的发版了,第二天一早便成功上线,在此记录一下实现iOS启动图动态置灰的方案心得。


方案过程


实话说,当我接到此需求时,我负责的是实现iOS启动图动态置灰,当时我不太确认是否能实现,我能想到的是马上搜百度、谷歌、掘金等看是否有现成的轮子,答案肯定是有的,分别是



此方案非常轻量级,只有BBADynamicLaunchImage一个类,功能也只有一个,即查找系统缓存的启动图路径,使用我们提供的UIImage替换掉。其他版本控制本非必要需求我们自己代码控制即可。最终我也是直接采用了这个方案,其他控制由我代码自己编写核心方法如下。PS:(虽然提供iOS13之前的启动图路径查找,但是经过我实测一台iOS12的设备是不生效的,只有iOS13意思机型生效)


/// 系统启动图缓存路径

+ (NSString *)launchImageCacheDirectory {

NSString *bundleID = [NSBundle mainBundle].infoDictionary[@"CFBundleIdentifier"];

NSFileManager *fm = [NSFileManager defaultManager];

// iOS13之前

NSString *cachesDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];

NSString *snapshotsPath = [[cachesDirectory stringByAppendingPathComponent:@"Snapshots"] stringByAppendingPathComponent:bundleID];

if ([fm fileExistsAtPath:snapshotsPath]) {

return snapshotsPath;

}

// iOS13

NSString *libraryDirectory = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];

snapshotsPath = [NSString stringWithFormat:@"%@/SplashBoard/Snapshots/%@ - {DEFAULT GROUP}", libraryDirectory, bundleID];

if ([fm fileExistsAtPath:snapshotsPath]) {

return snapshotsPath;

}

return nil;

}



稍微吐槽下这个库,此库也是我一开始使用的。它也是基于BBADynamicLaunchImage做了一些拓展。比如版本控制,但是它内置的版本控制有漏洞,它只支持CFBundleShortVersionString,也就是我们俗称的大版本,如果我build号改了版本号不变岂不是有问题?(这也是我打包后不生效调试了好久才发现的问题)而且要支持动态置灰,不发版恢复原图就更加有问题。最后也是弃用了,当然这个库支持暗黑模式下的启动图,但是我本身app就是不支持的这个功能就聊胜于无了,最终该用了上边的方案,动态控制由我自己处理。


启动图如何置灰


要实现启动图和原图一模一样只是变成灰白,这里就稍微要花一点点心思了。众所周知我们现在iOS启动图都是直接用LaunchScreen这个Storyborad生成的,那我们是否能加载这个LaunchScreen,然后截取UIView的图片,之后再通过bitmap转换成一张灰白图?答案是显而易见的,代码如下。


首先我们要给LaunchScreen定义一个id,因为默认没有人去加载它,它也没有id。



代码如下:


生成启动图原图或灰白图方法,注意此方法要在主线程跑。


+ (UIImage *)createLaunchScreenImage:(BOOL)isNeedGray {

UIStoryboard *sb = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];

UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"LaunchScreen"];

[vc loadViewIfNeeded];

vc.view.frame = UIScreen.mainScreen.bounds;

UIImage *image = [vc.view snapshotImage];

if (isNeedGray) {

image = [image createGrayImage];

}

return image;

}



UIView截图

func snapshotImage() -> UIImage? {

UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0);

self.layer.render(in: UIGraphicsGetCurrentContext()!)

let image = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()

return image

}


生成灰白图方法,由于启动图必须size匹配,所以scale那些要处理好。


-(UIImage*)createGrayImage {

int width = self.size.width * self.scale;

int height = self.size.height * self.scale;

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();

CGContextRef context =CGBitmapContextCreate(nil,

width,

height,

8,// bits per component

0,

colorSpace,

kCGBitmapByteOrderDefault);

CGColorSpaceRelease(colorSpace);

if(context ==NULL) {

return nil;

}

CGContextDrawImage(context,

CGRectMake(0,0, width, height), self.CGImage);

UIImage*grayImage = [UIImage imageWithCGImage:CGBitmapContextCreateImage(context) scale:self.scale orientation:self.imageOrientation];

CGContextRelease(context);

return grayImage;

}


动态替换


我们只需要请求后台配置,需要灰白就提供灰白图,当配置失效,需要还原时候,根据上面方法,直接渲染一个LaunchScreen原图即可,当然其中还要做好持久化控制,不要处理多次替换,替换生效后不再处理。


末尾


以上就是我实现此次iOS启动图动态置灰的全过程,由于过程的艰辛,加之我自己是一个Swifter。估计不久将来,我也会基于Swift写一个稍微友好点的库,在此立个Flag。


作者:jerryfans
链接:https://juejin.cn/post/7172983260966813709
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS Universal link

iOS
1. Universal link 介绍 1.1 Universal link 是什么 Universal Link 是苹果在 WWDC 上提出的 iOS9 的新特性之一。此特性类似于深层链接,并能够方便地通过打开一个 Https 链接来直接启动您的客户端应用...
继续阅读 »

1. Universal link 介绍


1.1 Universal link 是什么


Universal Link 是苹果在 WWDC 上提出的 iOS9 的新特性之一。此特性类似于深层链接,并能够方便地通过打开一个 Https 链接来直接启动您的客户端应用(手机有安装 App)。对比起以往所使用的 URL Scheme,这种新特性在实现 web-app 的无缝链接时能够提供极佳的用户体验。


当你的应用支持 Universal Link(通用链接),当用户点击一个链接是可以跳转到你的网站并获得无缝重定向到对应的 APP,且不需要通过 Safari 浏览器。如果你的应用不支持的话,则会在 Safari 中打开该链接。在苹果开发者中可以看到对它的介绍是:



Seamlessly link to content inside your app, or on your website in iOS 9 or later. With universal links, you can always give users the most integrated mobile experience, even when your app isn’t installed on their device.



1.2 Universal link 的应用场景


使用 Universal Link(通用链接)可以让用户在 Safari 浏览器或者其他 APP 的 webview 中拉起相应的 APP,也可以在 APP 中使用相应的功能,从而来把用户引流到 APP 中。


这具体是一种怎样的情景呢?举个例子,你的用户 safari 里面浏览一个你们公司的网页,而此时用户手机也同时安装有你们公司的 App;而 Universal Link 能够使得用户在打开某个详情页时直接打开你的 app 并到达 app 中相应的内容页面,从而实施用户想要的操作(例如查看某条新闻,查看某个商品的明细等等)。比如在 Safari 浏览器中进入淘宝网页点击打开 APP 则会使用 Universal Link(通用链接)来拉起淘宝 APP。


1.3 Universal link 跳转的好处

  • 唯一性: 不像自定义的 URL Scheme,因为它使用标准的 HTTPS 协议链接到你的 web 站点,所以一般不会被其它的 APP 所声明。另外,URL scheme 因为是自定义的协议,所以在没有安装 app 的情况下是无法直接打开的(在 Safari 中还会出现一个不可打开的弹窗),而 Universal Link(通用链接)本身是一个 HTTPS 链接,所以有更好的兼容性;

  • 安全: 当用户的手机上安装了你的 APP,那么系统会去你配置的网站上去下载你上传上去的说明文件(这个说明文件声明了当前该 HTTPS 链接可以打开那些 APP)。因为只有你自己才能上传文件到你网站的根目录,所以你的网站和你的 APP 之间的关联是安全的;

  • 可变: 当用户手机上没有安装你的 APP 的时候,Universal Link(通用链接)也能够工作。如果你愿意,在没有安装你的 app 的时候,用户点击链接,会在 safari 中展示你网站的内容;

  • 简单: 一个 HTTPS 的链接,可以同时作用于网站和 APP;

  • 私有: 其它 APP 可以在不需要知道你的 APP 是否安装了的情况下和你的 APP 相互通信。


2. Universal link 配置和运行


2.1 配置 App ID 支持 Associated Domains


登录developer.apple.com/ 苹果开发者中心,找到对应的 App ID,在 Application Services 列表里有 Associated Domains 一条,把它变为 Enabled 就可以了。



2.2 配置 iOS App 工程


Xcode 11.0 版本


工程配置中相应功能:targets->Signing&Capabilites->Capability->Associated Domains,在其中的 Domains 中填入你想支持的域名,也必须必须以 applinks:为前缀。


具体步骤如下图:





Xcode 11.0 以下版本


工程配置中相应功能:targets->Capabilites->Associated Domains,在其中的 Domains 中填入你想支持的域名,必须以 applinks:为前缀。


配置项目中的 Associated Domains:



2.2 配置和上传 apple-app-association


究竟哪些的 url 会被识别为 Universal Link,全看这个 apple-app-association 文件Apple Document UniversalLinks.html

  • 你的域名必须支持 Https

  • 域名 根目录 或者 .well-known 目录下放这个文件apple-app-association,不带任何后缀

  • 文件为 json 保存为文本即可

  • json 按着官网要求填写即可


apple-app-site-association模板:

{    "applinks": {        "apps": [],        "details": [            {                "appID": "9JA89QQLNQ.com.apple.wwdc",                "paths": [ "/wwdc/news/", "/videos/wwdc/2015/*"]            },            {                "appID": "ABCD1234.com.apple.wwdc",                "paths": [ "*" ]            }        ]    }}

复制代码


说明:



appID: 组成方式是 teamId.yourapp’s bundle identifier。如上面的 9JA89QQLNQ 就是 teamId。登陆开发者中心,在 Account -> Membership 里面可以找到 Team ID。




paths: 设定你的 app 支持的路径列表,只有这些指定的路径的链接,才能被 app 所处理。星号的写法代表了可识 别域名下所有链接。



上传指定文件:上传该文件到你的域名所对应的根目录或者.well-known 目录下,这是为了苹果能获取到你上传的文件。上传完后,自己先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载 apple-app-site-association 文件。


2.4 如何验证 Universal link 生效

  • 可以使用 iOS 自带的备忘录程序,输入链接,长按链接,如果弹出菜单中有”在‘xxx’中打开”,即表示配置生效。

  • 或者将要测试的网址在Safari中打开,在出现的网页上方下滑,可以看到有在”xxx”应用中打开, 出现菜单:



当点击某个链接,直接可以进我们的 app 了,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容。


AppDelegate里中实现代理方法,官方链接:Handling Universal Links


Objective-C:

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb])    {        NSURL *url = userActivity.webpageURL;        if (url是我们希望处理的)        {            //进行我们的处理        }        else        {            [[UIApplication sharedApplication] openURL:url];        }    }         return YES;}

复制代码


Swift:

func application(_ application: UIApplication,                 continue userActivity: NSUserActivity,                 restorationHandler: @escaping ([Any]?) -> Void) -> Bool{    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,        let incomingURL = userActivity.webpageURL,        let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),        let path = components.path,        let params = components.queryItems else {            return false    }        print("path = (path)")        if let albumName = params.first(where: { $0.name == "albumname" } )?.value,        let photoIndex = params.first(where: { $0.name == "index" })?.value {                print("album = (albumName)")        print("photoIndex = (photoIndex)")        return true            } else {        print("Either album name or photo index missing")        return false    }}

复制代码


3. Universal link 遇到的问题和解决方法


3.1 跨域


前端开发经常面临跨域问题,恩 Universal Link 也有跨域问题,但不一样的是,Universal Link,必须要求跨域,如果不跨域,就不行,就失效,就不工作。(iOS 9.2 之后的改动,苹果就这么规定这么设计的)


这也是上面拿知乎举例子的时候重点强调的一个问题,知乎为什么使用oia.zhihu.com做 Universal Link?

  • 假如当前网页的域名是 A

  • 当前网页发起跳转的域名是 B

  • 必须要求 B 和 A 是不同域名,才会触发 Universal Link

  • 如果 B 和 A 是相同域名,只会继续在当前 WebView 里面进行跳转,哪怕你的 Universal Link 一切正常,根本不会打开 App


是不是不太好理解,那直接拿知乎举例子


有心人可能看到,知乎的 Universal Link 配置的是 oia.zhihu.com 这个域名,并且对这个域名下比如/answers /questions /people 等 urlpath 进行了识别,也就是说,知乎的 universal link,只有当你访问 https://oia.zhihu.com/questions/xxxx,在移动端会触发 Universal Link,而知乎正经的 Urlhttps//www.zhihu.com/questions/xxx是不会触发 Universal Link 的,知乎为什么制作,为什么不把他的主域名配置 Universal Link,就是由于 Universal Link 的跨域的原因。


知乎的一般网页 URL 都是http://www.zhihu.com域名,你在微信朋友圈看到了知乎的问题分享,如果 copy url 你就能看到这样的链接


http://www.zhihu.com/question/22…



微信里其实是屏蔽 Schema 的,但是你依然能看到大大的一个按钮App内打开,这确实就是通过 Universal Link 来实现的,但如果知乎把 Universal Link 配在了http://www.zhihu.com域名,那么即便已经安装了 App,Universal Link 也是不会生效的。


一般的公司都会有自己的主域名,比如知乎的http://www.zhihu.com,在各处分享传播的时候,也都是直接分享基于主域名的 url,但为了解决苹果强制要求跨域才生效的问题,Universal Link 就不能配置在主域名下,于是知乎才会准备一个oia.zhihu.com域名,专为 Universal Link 使用,不会跟任何主动传播分享的域名撞车,从而在任何活动 WAP 页面里,都能顺利让 Universal Link 生效。


跨域的另外一个好处是可以突破微信跳转限制,支持微信无缝跳转到 App.


简单一句话



只有当前 webview 的 url 域名,与跳转目标 url 域名不一致时,Universal Link 才生效



3.2 更新


apple-app-association 的更新时机有以下两种:

  • 每次 App 安装后的第一次 Launch,会拉取 apple-app-association

  • Appstore 每次 App 的版本更新后的第一次 Launch,也会更新 apple-app-association


所以反复重新杀 APP 重开完全没用,删了 App 重装确实有用,但不可能让用户这么去做。也就是说,一旦不小心因为意外 apple-app-association,想要挽回又让那部分用户无感,App 再发一个版本就好了


3.3 Universal Link 用户行为


Universal Link 触发后打开 App,这时候 App 的状态栏右上角会有文字提示来自 XXApp,可以点状态栏的文字快速返回原来的 AP


如果用户点了返回微信,就会被苹果记住,认为用户并不需要跳出原 App 打开新 App,因此这个 App 的 Universal Link 会被关闭,再也无效。


想要开启也不是不行,让用户重新用 safari 打开,universal link 的页面,然后会出现很像苹果 smart bar 的东西,那个东西点了后就能打开


4. H5 端的 Universal Link 业务部署


H5 端的 Universal Link 跳转,从产品经理的角度看,需要满足以下 2 个需求:

  • 如果已安装 App,跳转对应界面

  • 如果没安装 App,跳转 App 下载界面


H5 端部署 Universal Link 示例:

router.use('/view', function (req, res, next) {    var path = req.path;    res.redirect('https://www.xxx.com/view' + path + '?xxx=xxx');});

复制代码


整个效果就是

  • 跳转https://www.xxx.com/view/*

  • 已安装 App

  • 打开 App 触发 handleUniversalLink

  • 走到/view/分支,拼接阅读页路由跳转

  • 未安装 AppWebView

  • 原地跳转https://``www.xxx.com``/view/*

  • 命中服务器的重定向逻辑

  • 重定向到https://``www.xxx.com``/view/*

  • 打开相应的 H5 页面



作者:Silence_xl
链接:https://juejin.cn/post/7233626231579689019
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

价格.0处理

iOS
在项目中有500.0或者500.00的情况需要处理 实习的同学写了一段这样的代码public extension String { var trimZero: String { replacingOccurrences(of: ".00...
继续阅读 »

在项目中有500.0或者500.00的情况需要处理


实习的同学写了一段这样的代码

public extension String {
var trimZero: String {
replacingOccurrences(of: ".00", with: "").replacingOccurrences(of: ".0", with:"")
}
}

咋一看似乎没啥问题,结果也符合预期




但是上面的case其实没有覆盖全,例如:500.01,那上面的处理方式就有bug了,会被处理成5001


正确的处理方式

public extension String {
var trimZero: String {
guard let value = Double(self) else { return self }
let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
return formatter.string(from: NSNumber(value: value)) ?? self
}
}

测试结果




参考



作者:xiAo_Ju
链接:https://juejin.cn/post/7273684196635525139
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS项目运行时XCode内存暴涨、速度慢、卡的解决过程

iOS
XCode老罢工 从今年开始,项目中一个组件的主工程在开发过程中,运行编译时间耗时长,XCode是不是都会转菊花,平均每次编译的时间大概在5min左右,非常影响开发效率,今日刚好提测完,抽空仔细看看为何如此卡顿。环境 在卡顿的时候打开活动监视器,发现XCode...
继续阅读 »

XCode老罢工


从今年开始,项目中一个组件的主工程在开发过程中,运行编译时间耗时长,XCode是不是都会转菊花,平均每次编译的时间大概在5min左右,非常影响开发效率,今日刚好提测完,抽空仔细看看为何如此卡顿。

环境



在卡顿的时候打开活动监视器,发现XCode占用内存非常高,平均在20GB左右,峰值达到60GB




在Command + k 删除DerivedData 里面的缓存之后,还是没有明显的加速结果。


寻找原因


查看编译日志




发现组件内的所有文件在编译的时候都会有几个相似的警告。


这些警告来自同一个文件,通过pch文件引用。


有警告的文件是该组件的网络请求文件,是很早以前建立的,文件里面没有自动生成NS_ASSUME_NONNULL_BEGIN文件内大概有几百个警告。在编译文件的时候,这些警告都会去做缓存、分析。导致运行起来非常卡顿。


解决


消除警告,重新编译,发现项目跑起来非常的舒畅!


如果是有其他第三方库或者组件的警告,可以在podFile中增加 :inhibit_warnings => true 来避免编译的时候检查警告。这种方式也会加快编译速度。

pod 'XXNetEngineModule', :inhibit_warnings => true

可以看到解决完XCode的内存大小基本就在1GB左右。编译速度也基本上能达到秒启(10s内)。




作者:王飞飞不会飞
链接:https://juejin.cn/post/7207737667990945850
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

WWDC23发布了什么 (速看版)

iOS
今天凌晨WWDC 2023正式召开,本文分析介绍了其中的精华部分 有关如何观看可以阅读👉 WWDC 2023 观看指南 Keynote 常规硬件发布 Mac Macbook Air 新款 M2 芯片的15 寸 Macbook Air 拥有8核CPU以及10核G...
继续阅读 »

今天凌晨WWDC 2023正式召开,本文分析介绍了其中的精华部分


有关如何观看可以阅读👉 WWDC 2023 观看指南


Keynote


常规硬件发布


Mac


Macbook Air


新款 M2 芯片的15 寸 Macbook Air


  • 拥有8核CPU以及10核GPU
  • 边框厚度5毫米
  • 屏幕亮度最高可达500尼特
  • 15.3英寸支持1080P高清摄像头
  • 支持Six-Speaker Sound system六声道音响以及Touch ID指纹识别
  • 硬盘方面最高可拓展至2TB
  • 内存最高可拓展至24GB
  • 提供18个小时电池续航
  • 售价10499元起,即日起开始预订,下周发售







Mac Studio


新款 Mac Studio 搭载M2 Max和M2 Ultra两款芯片

  • 拥有24核心CPU以及76核心GPU
  • 配备32核心网络神经引擎
  • 支持最高192GB内存拓展
  • 8TB硬盘拓展
  • 支持8K外接显示
  • 售价16499元起,下周起售







Mac Pro


Mac 产品线最强大的一员,Mac Pro 也迎来了 Apple Silicon,至此全系 Mac 产品线已完成从 Intel 芯片向 Apple Silicon 转变


  • 配置基本同 Mac Studio
  • 售价55999元起





常规软件发布


iOS 17


iOS 17主要进行了细节优化和小功能迭代更新

  • 全新自定义来电界面形象

  • Facetime新增语音留言

  • Messages支持搜索 & 地图信息

  • 新增 Check In功能

  • 新增全局 Live Sticker

  • 改进键盘输入法,增加词语联想输入与纠错功能

  • 可交互Widget

  • 新系统级App Journal 手记 App [今年稍晚推出]

  • NameDrop: AirDrop的升级功能,可在一台手机与另外设备接触时进行隔空投送,如超过隔空投送距离,还可通过蜂窝数据将剩余未传完内容继续投送

  • 待机体验功能:将iPhone横放在手机支架上能够显示时钟,天气以及小组件





iOS 开发者需要关心的是:


  • 可交互 Widget,已有Widget的App可以重新思考Widget的设计
  • 全局Live Sticker,兼容性测试 & 是否需要进行专门适配


iPadOS 17


除了共享上述提到的iOS更新外,iPadOS主要有以下方面的更新

  • 去年iOS 16的自定义壁纸功能加入 iPadOS

  • 健康 App 登陆 iPadOS,提供大屏健康信息查阅体验

  • 更好的系统级 PDF 支持







macOS 14



新一代 macOS 命名为 Sonoma,主要的特点如下

  • 加入 Metal 3和MetalFX Upscaling功能

  • 添加系统级别游戏模式,为主流手柄提供更好的蓝牙采样支持

  • 《死亡搁浅》登录macOS平台,制作人现场展示了“死亡搁浅导演剪辑版”

  • 支持添加 Widget 到 macOS 桌面

  • 支持添加 iPhone 上的Widget 到 macOS,会通过 iPhone 端进行更新然后传输到 macOS 渲染显示







watchOS 10

  • 全新设计的智能叠放组件

  • 运动方面:更加详细的运动数据记录,同时数据也会同步显示在配对的iPhone上

  • 户外方面:支持记录离开信号区的位置,发送卫星求助信息,自动生成海拔图

  • 心理健康:增加对抑郁症和焦虑症的自测功能,距离屏幕距离过近时还会进行提醒,降低近视风险




tvOS 17 & AirPods


tvOS 17:

  • 支持 FaceTime 和视频流转,可将iPhone与iPad收到的FaceTime来电投射到Apple TV上进行视频通话

  • 支持 FaceTime 时的人物居中模式

  • 允许第三方视频通话应用程序,利用iPhone和iPad作为直播源,在Apple TV进行FaceTime视频通话


AirPods:


  • 添加自适应模式,在通透模式和降噪模式中智能切换



One More Thing



新硬件 VisionPro + 对应新操作系统 visionOS



时隔十年,苹果终于发布自家的 VR/AR 头戴式设备,入局该领域



TLDR:发售价3499$



硬件


  • M2 芯片 + R1 芯片
  • 2300万像素 Micro-OLED 屏幕
  • 单眼分辨率超 4K 电视
  • 满电续航 2h
  • 12个摄像头 + 5个传感器 + 6个麦克风
  • 全新空间音频体验

交互


  • 搭载 visionOS 系统
  • 使用 眼睛、手势、声音完成操控
  • 与 iPhone Mac设备无缝联动使用
  • 支持 Optic ID虹膜识别

体验


  • 全新 App Store
  • 大部分 iOS & iPadOS 可以直接兼容使用
  • 首个 3D 相机







新的 VisionPro 和 visionOS 的信息后续会有专门介绍,这里就不再过多展开


Platforms State of the Union


上面的 Keynote 部分是全球消费者比较关注的,而后续的PSTU则是 iOS 开发者更为关心的更新


这里主要突出下 IDE 和 Language 的更新


Xcode 15

  • 发布了最新的 Static Linker,据称最快是 ld64 的 5 倍性能提升

  • 新的 library format: mergeable libraries ,这是一种动静结合的二进制,Debug 的时候动态链接,Release 的时候静态链接,兼顾性能和开发体验

  • 支持自动生成对图片和颜色资源的静态访问API



Swift 5.9

  • 添加了 Swift Macro 支持,简化了大量的模版代码编写

  • 新的 SwiftData 数据库框架



作者:叶絮雷
链接:https://juejin.cn/post/7244062903203004473
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

RxSwift核心流程简介

iOS
前言 RxSwift是一个基于响应式编程的Swift框架,它提供了一种简洁而强大的方式来处理异步和事件驱动的编程任务。在RxSwift中,核心流程包括观察者、可观察序列和订阅。 RxSwift核心流程三部曲 // 1.创建序列 _ = Observa...
继续阅读 »

前言


RxSwift是一个基于响应式编程的Swift框架,它提供了一种简洁而强大的方式来处理异步和事件驱动的编程任务。在RxSwift中,核心流程包括观察者可观察序列订阅


RxSwift核心流程三部曲

   // 1.创建序列
_ = Observable<String>.create { ob in
// 3.发送信号
ob.onNext("你好")
return Disposables.create()
// 2.订阅序列
}.subscribe(onNext: { text in
print("订阅到了\(text)")
})
}

  • 1.创建序列
  • 2.订阅序列
  • 3.发送信号

上面三部曲的执行结果:



 第一次玩RxSwift比较好奇为什么会打印订阅到了你好,明明是两个闭包里面的代码。
我们先简单分析下:

  • 序列创建create后面带了闭包A闭包A里面执行了发送信号的流程
  • 订阅subsribe后面带了闭包B
  • 根据结果我们知道一定是先执行了闭包A,再把闭包A你好传给了闭包B,然后输出结果

RxSwift核心逻辑分析


创建序列




点进create函数可以看到它是拓展了ObservableType这个协议,同时创建了一个AnonymousObservable内部类(看名字是匿名序列,具备一些通用的特性)分析AnonymousObservable的继承链可以得到下面的关系图:




AnonymousObservable



 AnonymousObservable是接受Element泛型的继承自Producer的类,他接受并保存一个闭包subscribeHandler的参数,这个其实就是上面我们说的闭包A,另外有一个run函数(后面会提到)


Producer



 Producer是接受Element泛型的继承自Observable的类,有一个subscribe的实现,run的抽象方法,这个subscribe非常重要


Observable



 Observable是接受Element泛型的实现ObservableType协议的类,有一个subscribe的抽象方法,asObservable的实现(返回self,统一万物皆序列)
同时Observable有统计引用计数的能力(Resources这个结构体在序列观察者销毁者等都用到,可以调试是否有内存泄露),其中的AtomicInt是一把NSLock的锁,保证数据的存取安全




ObservableType




ObservableType是拓展ObservableConvertibleType协议的协议,定义了subscribe协议方法,实现了asObservable()方法,所以这里我们得出结论,不一定要继承Observable的才是序列,只要是实现了ObservableTypesubscribe的协议方法的也可以算是序列,进一步佐证万物接序列


ObservableConvertibleType




ObservableConvertibleType是个协议,关联了Element类型,定义asObservable的协议方法


订阅序列


点击subscribe函数




它是ObservableType的拓展能力,创建了一个AnonymousObserver(匿名观察者)
,接受的Element仔细查看继承链代码会发现跟序列创建的泛型是同一个


分析AnonymousObserver的继承链我们可以得到下图:




AnonymousObserver



 AnonymousObserver是接受Element泛型的继承自ObserverBase的类
保存了一个eventHandler的闭包,这个我们定义是闭包C
同时也有统计引用计数的能力,有一个onCore的实现


ObserverBase




ObserverBase是接受Element泛型的实现DisposableObserverType两个协议的类,有一个on的实现,onCore的抽象方法


ObserverType




ObserverType关联了Element,定义了on的协议方法,拓展定义了onNextonCompletedonError的方法,这三个方法其实都是on一个Event


其中Event是个枚举,有三类事件:next事件error事件completed事件

  • next事件next事件携带了一个值,表示数据的更新或新的事件。
  • error事件error事件表示发生了一个错误,中断了事件的正常流程。
  • completed事件completed事件表示事件流的结束,不再有新的事件产生。 观察者通过订阅可观察序列来接收事件。

Disposable




Disposable这个协议比较简单,定义了dispose方法


订阅流程分析

  • 1.调用self.asObservable().subscribe(observer)

    • 这个selfAnonymousObservable的实例
    • 调用asObservable方法通过继承链最终调用Observable的实现,返回self,也就还是AnonymousObservable的实例
  • 2.调用AnonymousObservable的实例的subscribe方法,通过继承链调用Producersubscribe方法


    • 3.Producerrun方法在AnonymousObservable有实现

     这个sink的处理是相当不错的,很好的做到了业务下沉,同时很好的运用了中间件单一职责的设计模式,值得学习。

    sink是管道的意思,下水道,什么东西都会往里面丢,这里面有订阅者销毁者

      1. sink.run
      1. parent.subscribeHandler(AnyObserver(self))这里的parent就是AnonymousObservable的实例,调用subscribeHandler这个也就是我们定义的闭包A 这里解释了订阅的时候会来到我们的闭包A的原因。 这里需要注意到AnyObserver这个类,他里面保存的observer属性其实是AnonymousObservableSink.on函数

发送信号


有了上两步的基础我们分析发送信号的流程应该比较清晰了

    1. obserber.onNext 其实就是AnyObserver.onNext
    1. ObserverType.onNext其实就是ObserverType.on
    1. 其实就是AnyObserver.on

    • 4.这个observer就是上面第二步最后的AnonymousObservableSink.on函数

    • 5.父类Sink.forwardOn函数 这里的self.observer类型是 AnonymousObserver

    • 6.调用AnonymousObserver的父类ObserverBaseon方法

    • 7.调用AnonymousObserveronCore方法

    • 8.调用eventHandler,也就是我们定义的闭包C
    • 9.闭包C根据Event调用闭包B闭包B输出了控制台的结果,至此,整个链路执行完毕了。




把整个核心流程用思维导图描述出来:




总结

  • 万物皆序列,序列的概念统一了编码
  • 完整的继承链做到了业务分离单一职责
  • 中间价模式很好的做到了业务下沉

作者:大帅哥哦
链接:https://juejin.cn/post/7233980221107748921
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

SwiftData-苹果最先进的数据库

iOS
SwiftData 用于在声明式UI开发(SwiftUI)中进行数据持久化。您可以使用 Swift 代码查询和过滤数据了。 创建模型 使用带有@Model的普通 Swift 类型对数据进行建模,无需关心底层文件存储。 SwiftData 自动推断关系(rel...
继续阅读 »

SwiftData 用于在声明式UI开发(SwiftUI)中进行数据持久化。您可以使用 Swift 代码查询和过滤数据了。




创建模型


使用带有@Model的普通 Swift 类型对数据进行建模,无需关心底层文件存储。


SwiftData 自动推断关系(relationships),您可以使用清晰的声明比如@Attribute(.unique)来描述属性约束

@Model
class Recipe {
@Attribute(.unique) var name: String // 在相同类型的所有模型中属性的值是唯一的。
var summary: String?
var ingredients: [Ingredient]
}

自动持久性


SwiftData 使用Model(模型)构建自定义schema,并将其字段有效地映射底层存储


由 SwiftData 管理的对象在需要时从数据库中获取,并在适当的时候自动保存,您无需进行额外的工作


您还可以使用 ModelContext API 进行完全控制。


与 SwiftUI 集成


在 SwiftUI views中使用@Query来获取数据。SwiftData 和 SwiftUI 协同工作,在基础数据更改时提供视图的实时更新无需手动刷新

@Query var recipes: [Recipe] // 获取一组模型并使模型与底层数据保持同步的property wrapper(属性包装器)。

var body: some View {
List(recipes) { recipe in
NavigationLink(recipe.name, destination: RecipeView(recipe))
}
}

Swift-native predicates


无需使用复杂 SQL, 使用表达式(编译器自动类型检查)来查询和筛选数据,以便在开发过程中捕获拼写错误。


当表达式无法映射到基础存储引擎时,谓词会提供编译时错误

let simpleFood = #Predicate<Recipe> { recipe in
recipe.ingredients.count < 3
}

CloudKit同步


您的数据可以使用DocumentGroup储存在文件中并通过 iCloud Drive 同步到云端,,也可以使用 CloudKit 在设备之间同步数据。


与Core Data兼容


SwiftData 使用经过验证的 Core Data 存储架构,因此您可以在具有相同底层存储的同一App中使用两者。


Xcode 将 Core Data Models转换为类以与 SwiftData 一起使用。


作者:Bowen_J
链接:https://juejin.cn/post/7241562753163870269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS非公开App分发实践

iOS
一、前言 非公开分发是苹果新推出的一种分发方式,适用于为有限范围用户开发、不适合在App Store上公开分发的App,比如一些没有注册功能,由公司下发账号密码的企业内部应用。 苹果官方对非公开App分发的描述: developer.apple.com/cn/...
继续阅读 »

一、前言


非公开分发是苹果新推出的一种分发方式,适用于为有限范围用户开发、不适合在App Store上公开分发的App,比如一些没有注册功能,由公司下发账号密码的企业内部应用。


苹果官方对非公开App分发的描述:
developer.apple.com/cn/support/…


二、苹果分发方式对比


三、非公开分发



作为苹果新推出的分发方式,非公开分发有如下特点:

  1. 要为非公开分发的App申请非公开App链接
  2. 用个人或公司开发者账号在App Store发布,但是不能直接在App Store搜到,只能通过短链接被访问
  3. 由于要上架App Store,和普通app一样,要提交到苹果审核,审核通过之后可访问
  4. 已经在App Store中公开上架的app可以申请非公开App链接,转为非公开分发App
  5. 非公开分发App的销售范围是App Store支持的所有区域

四、分发非公开App


创建App并提交审核

1. 按照公开分发的方式创建App并填写信息

2. 初始创建App提交审核时,App分发方式选择公开,非公开App链接申请通过后App分发方式会自动转为非公开分发 image.png


3. 审核信息备注里说明App用于非公开分发


 

4. App提交审核


申请非公开App链接


非公开App链接的申请地址如下:
developer.apple.com/contact/req…


提交非公开分发请求时需要满足以下两点:

  1. App已经提交至苹果进行审核或者已经上架,不能为处于Beta版本的App提交非公开请求,否则会被拒
  2. 如果使用的是公司开发者账号,只有主账号有提交非公开请求的权限,使用子账号申请时页面打不开,错误信息如下:



非公开链接申请通过后开发者账号邮箱会收到一封通知邮件:




App的分发方式也会自动的变成非公开分发:




如果非公开App链接申请下来之前App审核因为3.2被拒,不用着急,等非公开链接申请通过之后再次提交即可。


非公开App链接申请页信息是英文,输入填写相关信息时用中、英文都可以,问题描述的越详细审核越容易过,我第一次提交后几个小时就过了。


最后


随着苹果公司对企业账号的收紧,2022年不少公司在续费时遇到了账号重新审查,万一审查不过,结果就是账号不能续费无法继续使用,之前通过企业账号分发的App必须考虑别的分发方式。


苹果官方给的建议是Apple 商务管理非公开 App 分发两种方案,相对于商务管理下载时需要管理兑换码,下载更方便的非公开App分发不失为一种新尝试。


作者:是Susie啊
链接:https://juejin.cn/post/7184765499836743738
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

ios 打包静态库

iOS
前言: 各位同学大家, 有段时间没有跟大家见面了。 相信很多做IOS手游sdk 的同学 都会用到静态库, 我们不用把我们都源代码都发给对接方 就可以把我们的逻辑跟研发都代码融合在一起 具体实现: 第一步 点击file  第二步创建一个pr...
继续阅读 »

前言:


各位同学大家, 有段时间没有跟大家见面了。 相信很多做IOS手游sdk 的同学 都会用到静态库, 我们不用把我们都源代码都发给对接方 就可以把我们的逻辑跟研发都代码融合在一起


具体实现:


第一步 点击file 


 第二步创建一个project 


 第三步我们选择 static Library 工程


最终我们这样的一个工程



 在xcode 最新版本里面 有的同学 发现没有 Prodoucts 这个目录 这个是因为xcode的bug








mainGroup = 0D7441EC2A0A715000C95252;
productRefGroup = 0D7441EC2A0A715000C95252;

保证这2行后面都配置一样的如果不一样 就复制 mainGroup 后面到productRefGroup 然后保存即可 然后刷新xcode 就就会出现 Prodoucts


暴露头文件 我们需要把我们对外开放都类的头文件 也就是.h文件 暴露出去 然后方便对接方 接入



 如图我们将我们ninefunsdk.h这个文件

 还有我们都 Roleinfo.h 和Seriveinfo. h 文件也需要暴露出去

 打包 cmd +b 



具体接入




效果图




最后总结:


IOS 打包静态库 我们就讲完, 比较简单 我们只需要对流程清除即可 有兴趣同学可以根据教程一步一步学习

最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢


作者:xq9527
链接:https://juejin.cn/post/7231720920984322105
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Xcodes 管理多个 Xcode 的版本,简直泰酷辣

iOS
为什么要使用多个 Xcode? 有些时候,我们可能需要多个版本的 Xcode,比如: 情景1: 每年的6月 WWDC 大会发布后,都伴随着 iOS 系统的更新,当你想体验下新的功能的时候,你想下载 Xcode 的 Beta 版本尝试适配新版本的变化,但是又不...
继续阅读 »

为什么要使用多个 Xcode?




有些时候,我们可能需要多个版本的 Xcode,比如:


情景1:
每年的6月 WWDC 大会发布后,都伴随着 iOS 系统的更新,当你想体验下新的功能的时候,你想下载 Xcode 的 Beta 版本尝试适配新版本的变化,但是又不想覆盖原有的 Release 版本。


情景2:
你们公司的项目复杂又庞大,你担心更新 Xcode 后,项目运行报错,不得不回退旧版本的 Xcode。


像上面两种情况,我们就希望多个版本的 Xcode 同时存在,既能体验新版本的功能变化,也能确保我们项目在原有版本正常运行。


Xcodes - 轻松管理多个 Xcode


给大家推荐一款轻松管理 Xcode 的一个工具包 Xcodes,它的下载地址在 GitHub 上,点我直达


Xcodes 优点

  • 简洁的桌面,可快速发现想要安装的版本。
  • 安装包很小,只有 23MB 左右。
  • 下载速度快,使用了 aria2 下载工具,比 URLSession 快 3-5 倍。
  • 如果网络错误,可自动恢复安装。
  • 可选择默认 Xcode。

由于我不知道 aria2 是什么,所以 chatGPT 了一下 😁,下面是 chatGPT 给出的答案



aria2是一款开源的多协议、多线程下载工具,可以用来在命令行界面下载文件。它支持HTTP、HTTPS、FTP、BitTorrent等多种协议,可以同时下载多个文件,并自动利用多个连接和线程来加快下载速度。aria2在Linux、Windows和macOS等多个操作系统上都可用,并且可以通过命令行进行控制和配置。



下载安装


XcodesAppREADME.md 也有说明可以使用 两种安装方式


安装方式 1: 借助Homebrew安装

brew install --cask xcodes

安装方式 2: 手动安装 (我是手动安装的)



README.md 里找如上图:滚动到 Manually install(手动安装)这里,点击here 蓝色高亮的地方,会进入 release 下载链接,然后滚动到页面底部,看见下图 Xcodes.zip 点击下载,安装到 /Applications下即可。




使用教程


安装完成后,打开 Xcodes 的页面,非常简洁,能看到目前可安装的最新的 Beta 版本,以及最开始的1.0版本,看见这个觉得很酷 👻




使用 Xcodes 需要登录 Apple ID,以及怎么使用,都用图片说明吧,稍微摸索一下都能看明白,使用起来非常简单。






感谢阅读,如果您感觉这篇文章对您有帮助的话,请给它点赞以鼓励我持续创作 ^‿^


作者:心向暖阳
链接:https://juejin.cn/post/7271924341734555703
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 开发中的AES加密

iOS
前言 在iOS的日常开发中,特别是设计网络请求时,会用到加密算法,例如在客户端需要发起一个HTTP请求给服务端,其中会传递一些参数,为了防止参数在网络传输过程中被窃取或者篡改,我们就需要使用一些加密算法来对请求参数加密和签名。今天就重点介绍一下AES和HMAC...
继续阅读 »

前言


在iOS的日常开发中,特别是设计网络请求时,会用到加密算法,例如在客户端需要发起一个HTTP请求给服务端,其中会传递一些参数,为了防止参数在网络传输过程中被窃取或者篡改,我们就需要使用一些加密算法来对请求参数加密和签名。今天就重点介绍一下AES和HMAC_SHA256两个算法,因为服务端大多数都是使用java语言来编写,AES算法在iOS的Objective-C中和java的实现有些差异,本文重点介绍AES在iOS开发中的应用和需要注意的事项。


AES 加密算法简介


AES是一种典型的对称加密/解密算法,使用加密函数和密钥来完成对明文的加密,然后使用相同的密钥和对应的函数来完成解密。AES的优点在于效率非常高,相比RSA要高得多。AES共有ECB、CBC、CFB和OFB四种加密模式。


在iOS中的实现


Objective-C中支持AES的ECB和CBC两种模式。
1、电码本模式(Electronic Codebook Book (ECB))
这种模式主要是将明文划分为几个明文段,分块加密,但是加密密钥是相同的。
2、密码分组链接模式(Cipher Block Chaining (CBC))
这种模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。


ECB是最简单的一种模式,只需要传入待加密的内容和加密的key即可。(一般不推荐ECB模式)
CBC的特点是,除了需要传入加密的内容和加密的key,还需要传入初始化向量iv。即使每次加密的内容和加密的key相同,只要调整iv就可以让最终生成的密文不同。
在客户端和服务端之间传输数据一般是使用约定好的key对指定参数做AES的CBC加密,初始化向量可以随机动态生成,最终将生成好的密文和随机向量iv拼接在一起传给服务端。如:iv+密文。
iv是指定的长度如16位,这样服务端拿到客户端传输过来的数据可以先取前16位作为iv,剩余的是需要解析的密文。这么做大大提升了数据的安全性和破解难度。即使相同的带加密参数,因为有随机向量的参入,最终生成的密文也不相同。


iOS中一般使用#import <CommonCrypto/CommonCryptor.h>库中的这个函数:

CCCryptorStatus CCCrypt(
CCOperation op, /* kCCEncrypt, etc. */
CCAlgorithm alg, /* kCCAlgorithmAES128, etc. */
CCOptions options, /* kCCOptionPKCS7Padding, etc. */
const void *key,
size_t keyLength,
const void *iv, /* optional initialization vector */
const void *dataIn, /* optional per op and alg */
size_t dataInLength,
void *dataOut, /* data RETURNED here */
size_t dataOutAvailable,
size_t *dataOutMoved)
API_AVAILABLE(macos(10.4), ios(2.0));
  • CCOperationkCCEncrypt 加密,kCCDecrypt 解密
enum {
kCCEncrypt = 0,
kCCDecrypt,
};
typedef uint32_t CCOperation;
  • CCAlgorithm:加密算法、默认为AES
enum {
kCCAlgorithmAES128 = 0, /* Deprecated, name phased out due to ambiguity with key size */
kCCAlgorithmAES = 0,
kCCAlgorithmDES,
kCCAlgorithm3DES,
kCCAlgorithmCAST,
kCCAlgorithmRC4,
kCCAlgorithmRC2,
kCCAlgorithmBlowfish
};
typedef uint32_t CCAlgorithm;

  • CCOptions:加密模式
    ECBkCCOptionPKCS7Padding | kCCOptionECBMode
    CBCkCCOptionPKCS7Padding
enum {
/* options for block ciphers */
kCCOptionPKCS7Padding = 0x0001,
kCCOptionECBMode = 0x0002
/* stream ciphers currently have no options */
};
typedef uint32_t CCOptions;

  • key:密钥
  • keyLength:密钥长度
  • iviv 初始化向量,ECB 不需要。iv定长所以不需要长度(8字节)。
  • dataIn:加密/解密的数据
  • dataInLength:加密/解密的数据长度
  • dataOut:缓冲区(地址),存放密文/明文
  • dataOutAvailable:缓冲区大小
  • dataOutMoved:加密/解密结果大小

封装如下:

/**
* 解密字符串
*
* @param string 加密并base64编码后的字符串
* @param keyString 解密密钥
* @param iv 初始化向量(8个字节)
*
* @return 返回解密后的字符串
*/
- (NSString *)decryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv {

// 设置秘钥
NSData *keyData = [keyString dataUsingEncoding:NSUTF8StringEncoding];
uint8_t cKey[self.keySize];
bzero(cKey, sizeof(cKey));
[keyData getBytes:cKey length:self.keySize];

// 设置iv
uint8_t cIv[self.blockSize];
bzero(cIv, self.blockSize);
int option = 0;
if (iv) {
[iv getBytes:cIv length:self.blockSize];
option = kCCOptionPKCS7Padding;//CBC 加密!
} else {
option = kCCOptionPKCS7Padding | kCCOptionECBMode;//ECB加密!
}

// 设置输出缓冲区
NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:0];
size_t bufferSize = [data length] + self.blockSize;
void *buffer = malloc(bufferSize);

// 开始解密
size_t decryptedSize = 0;

CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
self.algorithm,
option,
cKey,
self.keySize,
cIv,
[data bytes],
[data length],
buffer,
bufferSize,
&decryptedSize);

NSData *result = nil;
if (cryptStatus == kCCSuccess) {
result = [NSData dataWithBytesNoCopy:buffer length:decryptedSize];
} else {
free(buffer);
NSLog(@"[错误] 解密失败|状态编码: %d", cryptStatus);
}

return [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
}

上文提到使用CBC模式,可以创建一个随机的iv:

#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonCryptor.h>

NSData *generateRandomIV(size_t length) {
NSMutableData *randomIV = [NSMutableData dataWithLength:length];
int result = SecRandomCopyBytes(kSecRandomDefault, length, randomIV.mutableBytes);

if (result == errSecSuccess) {
return randomIV;
} else {
// 处理生成随机IV失败的情况
return nil;
}
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 设置AES加密参数
NSData *key = [@"YourAESKey123456" dataUsingEncoding:NSUTF8StringEncoding];
size_t ivLength = kCCBlockSizeAES128; // IV长度为16字节(AES-128)

// 生成随机IV
NSData *randomIV = generateRandomIV(ivLength);

if (randomIV) {
// 使用randomIV进行AES加密
// 这里你可以调用相应的加密方法,传入randomIV作为IV参数
// 例如,使用CommonCrypto库进行AES加密
// 具体实现将取决于你所使用的加密库和算法

// 示例:在这里调用AES加密函数,传入key和randomIV
// ...
} else {
NSLog(@"生成随机IV失败");
}
}
return 0;
}

作者:lefht0pz408mdl7yvptpm23ci
链接:https://juejin.cn/post/7273674732448792639
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Xcode15Beta填坑-修复YYLabel的Crash问题

iOS
前言 趁着版本空隙,升级到了Xcode15-Beta2本想提前体验下iOS17。本以为这次升级Xcode能直接运行应该没什么大问题,没曾想到一运行后程序直接Crash了,Crash是在YYLabel下的YYAsyncLayer类里面。众所周知,YYLabel是...
继续阅读 »

前言


趁着版本空隙,升级到了Xcode15-Beta2本想提前体验下iOS17。本以为这次升级Xcode能直接运行应该没什么大问题,没曾想到一运行后程序直接Crash了,Crash是在YYLabel下的YYAsyncLayer类里面。众所周知,YYLabel是由远古大神ibireme开发的YYKit下属的组件。已经多年没有适配了,但是依然老当益壮,只有部份由于Api变更导致的问题需要简单维护即可。以下就是此次问题定位与修复的全过程。


Crash定位


此次升级后编译我司项目,直接Crash,Crash日志如下。




Crash是在YYTextAsyncLayer类下面的第193行代码如下:


UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);


其实第一眼看代码崩溃提示就很明显了,这次Xcode15在UIGraphicsBeginImageContextWithOptions下面加了断言,如果传入的size width 或者 height其中一个为0,会直接return 返回断言。并且提示我们升级Api为UIGraphicsImageRenderer可以解决此问题。


本着探究的精神,我重新撤回用Xcode14.3.1编译,看为什么不会崩溃,结果其实也会报Invalid size警告但是不会崩溃,警告如下。




解决方案


我们使用UIGraphicsImageRenderer替代老旧的UIGraphicsBeginImageContextWithOptions(其实早已标记为过时),实测即使size为 zero,UIGraphicsImageRenderer在Xcode15下依然会渲染出一个zero size的Image,但是这毫无意义,所以我们简单判断一下,如果是非法的size我们直接retrun,代码如下:


从193行开始一直替换到self.contents = xxx。为止,即可解决此次问题。


if (self.bounds.size.width < 1 || self.bounds.size.height < 1) {

CGImageRef image = (__bridge_retained CGImageRef)(self.contents);

self.contents = nil;

if (image) {

CFRelease(image);

}

return;

}

UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:self.bounds.size];

UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {

if (self.opaque) {

if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {

CGContextSetFillColorWithColor(context.CGContext, [UIColor whiteColor].CGColor);

[context fillRect:self.bounds];

}

if (self.backgroundColor) {

CGContextSetFillColorWithColor(context.CGContext, self.backgroundColor);

[context fillRect:self.bounds];

}

}

task.display(context.CGContext, self.bounds.size, ^{return NO;});

}];

self.contents = (__bridge id)(image.CGImage);


结尾


以上就是Xcode15修复UIGraphicsBeginImageContextWithOptions由于加了断言导致的Crash问题。我也强烈建议各位有时间检查项目其他代码直接升级成UIGraphicsImageRenderer的方案。如果确实没时间,要加上如下判断,防止Crash。由于我是在Debug上必崩,如果是断言问题Release不一定会有事,但是还是建议大家修改一下。


if (self.size.width < 1 || self.size.height < 1) {

return nil;

}

作者:jerryfans
链接:https://juejin.cn/post/7250288616534261797
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

99% 的 iOS 开发都不知道的 KVO 崩溃

iOS
背景 crash 监控发现有大量的新增崩溃,堆栈如下0 libsystem_platform.dylib __os_unfair_lock_corruption_abort() 1 libsystem_platform.dylib __os_unfair_lo...
继续阅读 »

背景


crash 监控发现有大量的新增崩溃,堆栈如下

0	libsystem_platform.dylib	__os_unfair_lock_corruption_abort()
1 libsystem_platform.dylib __os_unfair_lock_lock_slow()
2 Foundation __NSSetBoolValueAndNotify()

分析堆栈


__os_unfair_lock_corruption_abort


log 翻译:lock 已损坏

_os_unfair_lock_corruption_abort(os_ulock_value_t current)
{
__LIBPLATFORM_CLIENT_CRASH__(current, "os_unfair_lock is corrupt");
}

__os_unfair_lock_lock_slow


在这个方法里面 __ulock_wait 返回 EOWNERDEAD 调用 corruption abort 方法。

int ret = __ulock_wait(UL_UNFAIR_LOCK | ULF_NO_ERRNO | options,
l, current, 0);
if (unlikely(ret < 0)) {
switch (-ret) {
case EINTR:
case EFAULT:
continue;
case EOWNERDEAD:
_os_unfair_lock_corruption_abort(current);
break;
default:
__LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wait failure");
}
}

EOWNERDEAD 的定义


#define EOWNERDEAD      105             /* Previous owner died */


到这里猜测是 lock 的 owner 已经野指针了,继续向下看。


__NSSetBoolValueAndNotify


google 下这个方法是在 KVO 里面修改属性的时候调用,伪代码:

int __NSSetBoolValueAndNotify(int arg0, int arg1, int arg2) {
r31 = r31 - 0x90;
var_30 = r24;
stack[-56] = r23;
var_20 = r22;
stack[-40] = r21;
var_10 = r20;
stack[-24] = r19;
saved_fp = r29;
stack[-8] = r30;
r20 = arg2;
r21 = arg1;
r19 = arg0;
r0 = object_getClass(arg0);
r0 = object_getIndexedIvars(r0); // 理清这个崩溃的关键方法,这里和汇编代码不一致,汇编代码的入参是 r0 + 0x20
r23 = r0;
os_unfair_recursive_lock_lock_with_options();
CFDictionaryGetValue(*(r23 + 0x18), r21);
r22 = _objc_msgSend$copyWithZone:();
os_unfair_recursive_lock_unlock();
if (*(int8_t *)(r23 + 0x28) != 0x0) {
_objc_msgSend$willChangeValueForKey:();
(class_getMethodImplementation(*r23, r21))(r19, r21, r20);
_objc_msgSend$didChangeValueForKey:();
}
else {
_objc_msgSend$_changeValueForKey:key:key:usingBlock:();
}
var_38 = **qword_9590e8;
r0 = objc_release_x22();
if (**qword_9590e8 != var_38) {
r0 = __stack_chk_fail();
}
return r0;
}

os_unfair_recursive_lock_lock_with_options


崩溃调用栈中间还有这一层的内联调用 os_unfair_recursive_lock_lock_with_options。这里的 lock owner 有个比较赋值的操作,如果 oul_value 等于 OS_LOCK_NO_OWNER 则赋值 self 然后 return。崩溃时这里继续向下执行了,那这里的 oul_value 的取值只能是 lock->oul_value。到这里猜测崩溃的原因是 lock->oul_value 野指针了。

void
os_unfair_recursive_lock_lock_with_options(os_unfair_recursive_lock_t lock,
os_unfair_lock_options_t options)
{
os_lock_owner_t cur, self = _os_lock_owner_get_self();
_os_unfair_lock_t l = (_os_unfair_lock_t)&lock->ourl_lock;

if (likely(os_atomic_cmpxchgv2o(l, oul_value,
OS_LOCK_NO_OWNER, self, &cur, acquire))) {
return;
}

if (OS_ULOCK_OWNER(cur) == self) {
lock->ourl_count++;
return;
}

return _os_unfair_lock_lock_slow(l, self, options);
}


OS_ALWAYS_INLINE OS_CONST
static inline os_lock_owner_t
_os_lock_owner_get_self(void)
{
os_lock_owner_t self;
self = (os_lock_owner_t)_os_tsd_get_direct(__TSD_MACH_THREAD_SELF);
return self;
}

object_getIndexedIvars


__NSSetBoolValueAndNotify 里面的获取 lock 的方法,这个函数非常关键。

/** 
* Returns a pointer to any extra bytes allocated with an instance given object.
*
* @param obj An Objective-C object.
*
* @return A pointer to any extra bytes allocated with \e obj. If \e obj was
* not allocated with any extra bytes, then dereferencing the returned pointer is undefined.
*
* @note This function returns a pointer to any extra bytes allocated with the instance
* (as specified by \c class_createInstance with extraBytes>0). This memory follows the
* object's ordinary ivars, but may not be adjacent to the last ivar.
* @note The returned pointer is guaranteed to be pointer-size aligned, even if the area following
* the object's last ivar is less aligned than that. Alignment greater than pointer-size is never
* guaranteed, even if the area following the object's last ivar is more aligned than that.
* @note In a garbage-collected environment, the memory is scanned conservatively.
/**
* Returns a pointer immediately after the instance variables declared in an
* object. This is a pointer to the storage specified with the extraBytes
* parameter given when allocating an object.
*/
void *object_getIndexedIvars(id obj)
{
uint8_t *base = (uint8_t *)obj;

if (_objc_isTaggedPointerOrNil(obj)) return nil;

if (!obj->isClass()) return base + obj->ISA()->alignedInstanceSize();

Class cls = (Class)obj;
if (!cls->isAnySwift()) return base + sizeof(objc_class);

swift_class_t *swcls = (swift_class_t *)cls;
return base - swcls->classAddressOffset + word_align(swcls->classSize);
}

上层调用 __NSSetBoolValueAndNotify 里面:


r0 = object_getClass(arg0),arg0 是实例对象,r0 是类对象,因为这里是个 KVO 的调用,那正常情况下r0 是 NSKVONotifying_xxx。


对于 KVO 类,object_getIndexedIvars 返回的地址是 (uint8_t *)obj + sizeof(objc_class)。根据函数的注释,这个地址指向创建类时附在类空间后 extraBytes 大小的一块内存。


debug 调试


object_getIndexedIvars


__NSSetBoolValueAndNotify 下的调用



object_getIndexedIvars 入参是 NSKVONotifying_KVObject,object_getClass 获取的是 KVO Class。


objc_allocateClassPair


动态创建 KVO 类的方法。

 thread #8, queue = 'com.apple.root.default-qos', stop reason = breakpoint 1.1
* frame #0: 0x000000018143a088 libobjc.A.dylib`objc_allocateClassPair
frame #1: 0x000000018259cd94 Foundation`_NSKVONotifyingCreateInfoWithOriginalClass + 152
frame #2: 0x00000001825b8fd0 Foundation`_NSKeyValueContainerClassGetNotifyingInfo + 56
frame #3: 0x000000018254b7dc Foundation`-[NSKeyValueUnnestedProperty _isaForAutonotifying] + 44
frame #4: 0x000000018254b504 Foundation`-[NSKeyValueUnnestedProperty isaForAutonotifying] + 88
frame #5: 0x000000018254b32c Foundation`-[NSObject(NSKeyValueObserverRegistration) _addObserver:forProperty:options:context:] + 404
frame #6: 0x000000018254b054 Foundation`-[NSObject(NSKeyValueObserverRegistration) addObserver:forKeyPath:options:context:] + 136
frame #7: 0x00000001040d1860 Test`__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000282a55170) at ViewController.m:28:13
frame #8: 0x00000001043d05a8 libdispatch.dylib`_dispatch_call_block_and_release + 32
frame #9: 0x00000001043d205c libdispatch.dylib`_dispatch_client_callout + 20
frame #10: 0x00000001043d4b94 libdispatch.dylib`_dispatch_queue_override_invoke + 1052
frame #11: 0x00000001043e6478 libdispatch.dylib`_dispatch_root_queue_drain + 408
frame #12: 0x00000001043e6e74 libdispatch.dylib`_dispatch_worker_thread2 + 196
frame #13: 0x00000001d515fdbc libsystem_pthread.dylib`_pthread_wqthread + 228

_NSKVONotifyingCreateInfoWithOriginalClass


objc_allocateClassPair 的上层调用。 allocate 之前的 context w2 是个固定值 0x30,即创建 KVO Class 入参 extraBytes 的大小是 0x30

    0x18259cd78 <+124>: mov    x1, x21
0x18259cd7c <+128>: mov x2, x22
0x18259cd80 <+132>: bl 0x188097080
0x18259cd84 <+136>: mov x0, x20
0x18259cd88 <+140>: mov x1, x19
0x18259cd8c <+144>: mov w2, #0x30
0x18259cd90 <+148>: bl 0x1880961f0 // objc_allocateClassPair
0x18259cd94 <+152>: cbz x0, 0x18259ce24 ; <+296>
0x18259cd98 <+156>: mov x21, x0
0x18259cd9c <+160>: bl 0x188096410 // objc_registerClassPair
0x18259cda0 <+164>: mov x0, x19
0x18259cda4 <+168>: bl 0x182b45f44 ; symbol stub for: free
0x18259cda8 <+172>: mov x0, x21
0x18259cdac <+176>: bl 0x1880967e0 // object_getIndexedIvars
0x18259cdb0 <+180>: mov x19, x0
0x18259cdb4 <+184>: stp x20, x21, [x0]

_NSKVONotifyingCreateInfoWithOriginalClass+184 处将 x20 和 x21 写入 [x0],此时 x0 指向的是大小为 extraBytes 的内存,打印 x20 和 x21 的值


    x20 = 0x00000001117caa10  (void *)0x00000001117caa38: KVObject(向上回溯这个值取自 _NSKVONotifyingCreateInfoWithOriginalClass 的入参 x0)


    x21 NSKVONotifying_KVObject


根据这里可以看出 object_getIndexedIvars 返回的地址,依次存储了 KVObject(origin Class) 和 NSKVONotifying_KVObject(KVO Class)。


查看 _NSKVONotifyingCreateInfoWithOriginalClass 的伪代码,对 [x0] 有 5 次写入的操作,并且最终这个方法返回的是 x0 的地址。

function __NSKVONotifyingCreateInfoWithOriginalClass {
r31 = r31 - 0x50;
stack[32] = r22;
stack[40] = r21;
stack[48] = r20;
stack[56] = r19;
stack[64] = r29;
stack[72] = r30;
r20 = r0;
if (*(int8_t *)0x993e78 != 0x0) {
os_unfair_lock_assert_owner(0x993e7c);
}
r0 = class_getName(r20);
r22 = strlen(r0) + 0x10;
r0 = malloc(r22);
r19 = r0;
strlcpy(r0, "NSKVONotifying_", r22);
strlcat(r19, r21, r22);
r0 = objc_allocateClassPair(r20, r19, 0x30);
if (r0 != 0x0) {
objc_registerClassPair(r0);
free(r19);
r0 = object_getIndexedIvars(r21);
r19 = r0;
*(int128_t *)r0 = r20; // 第一次写入 Class
*(int128_t *)(r0 + 0x8) = r21; // 第二次写入 Class
*(r19 + 0x10) = CFSetCreateMutable(0x0, 0x0, *qword_9592d8); // 第三次写入 CFSet
*(int128_t *)(r19 + 0x18) = CFDictionaryCreateMutable(0x0, 0x0, 0x0, *qword_959598); // 第四次写入 CFDictionary
*(int128_t *)(r19 + 0x20) = 0x0; // 第五次写入空值
if (*qword_9fc560 != -0x1) {
dispatch_once(0x9fc560, 0x8eaf98);
}
if (class_getMethodImplementation(*r19, @selector(willChangeValueForKey:)) != *qword_9fc568) {
r8 = 0x1;
}
else {
r0 = *r19;
r0 = class_getMethodImplementation(r0, @selector(didChangeValueForKey:));
r8 = *qword_9fc570;
if (r0 != r8) {
r8 = *qword_9fc570;
if (CPU_FLAGS & NE) {
r8 = 0x1;
}
}
}
*(int8_t *)(r19 + 0x28) = r8;
_NSKVONotifyingSetMethodImplementation(r19, @selector(_isKVOA), 0x44fab4, 0x0);
_NSKVONotifyingSetMethodImplementation(r19, @selector(dealloc), 0x44fabc, 0x0);
_NSKVONotifyingSetMethodImplementation(r19, @selector(class), 0x44fd2c, 0x0);
}
else {
if (*qword_9fc558 != -0x1) {
dispatch_once(0x9fc558, 0x8eaf78);
}
if (os_log_type_enabled(*0x9fc550, 0x10) != 0x0) {
_os_log_error_impl(0x0, *0x9fc550, 0x10, "KVO failed to allocate class pair for name %s, automatic key-value observing will not work for this class", &stack[0], 0xc);
}
free(r19);
r19 = 0x0;
}
if (**qword_9590e8 == **qword_9590e8) {
r0 = r19;
}
else {
r0 = __stack_chk_fail();
}
return r0;
}

_NSKVONotifyingCreateInfoWithOriginalClass 的上层调用,入参是 [x19, #0x8],返回的参数写入 [x19, #0x28]

    0x1825b8fc0 <+40>: ldr    x0, [x19, #0x28]
0x1825b8fc4 <+44>: b 0x1825b8fd4 ; <+60>
0x1825b8fc8 <+48>: ldr x0, [x19, #0x8]
-> 0x1825b8fcc <+52>: bl 0x18259ccfc ; _NSKVONotifyingCreateInfoWithOriginalClass
0x1825b8fd0 <+56>: str x0, [x19, #0x28]
0x1825b8fd4 <+60>: ldp x29, x30, [sp, #0x10]
0x1825b8fd8 <+64>: ldp x20, x19, [sp], #0x20

打印 x19 是一个 NSKeyValueContainerClass 类型的实例对象,这个对象类的 ivars layout

ivars 0x99f3c0 __OBJC_$_INSTANCE_VARIABLES_NSKeyValueContainerClass
entsize 32
count 5
offset 0x9e6048 _OBJC_IVAR_$_NSKeyValueContainerClass._originalClass 8
name 0x90bd27 _originalClass
type 0x929ae6 #
alignment 3
size 8
offset 0x9e6050 _OBJC_IVAR_$_NSKeyValueContainerClass._cachedObservationInfoImplementation 16
name 0x90bd36 _cachedObservationInfoImplementation
type 0x92bb88 ^?
alignment 3
size 8
offset 0x9e6058 _OBJC_IVAR_$_NSKeyValueContainerClass._cachedSetObservationInfoImplementation 24
name 0x90bd5b _cachedSetObservationInfoImplementation
type 0x92bb88 ^?
alignment 3
size 8
offset 0x9e6060 _OBJC_IVAR_$_NSKeyValueContainerClass._cachedSetObservationInfoTakesAnObject 32
name 0x90bd83 _cachedSetObservationInfoTakesAnObject
type 0x92a01a B
alignment 0
size 1
offset 0x9e6068 _OBJC_IVAR_$_NSKeyValueContainerClass._notifyingInfo 40
name 0x90bdaa _notifyingInfo
type 0x92bdd7 ^{?=##^{__CFSet}^{__CFDictionary}{os_unfair_recursive_lock_s={os_unfair_lock_s=I}I}B}
alignment 3
size 8

offset 0x8 name:_originalClass type:Class


offset 0x28 name:_notifyingInfo type:struct


_notifyingInfo 结构体

{
Class,
Class,
__CFSet,
__CFDictionary,
os_unfair_recursive_lock_s
}

type encoding:


developer.apple.com/library/arc…


从 context 可以看出_NSKVONotifyingCreateInfoWithOriginalClass 这个方法入参是 _OBJC_IVAR__NSKeyValueContainerClass._originalClass。返回值 x0 是 _OBJC_IVAR__NSKeyValueContainerClass._notifyingInfo。5 次对 [x0] 的写入是在初始化 _notifyingInfo。


崩溃时的 context:

    0x1825231f0 <+56>:  bl     0x1880967c0 // object_getClass
0x1825231f4 <+60>: bl 0x1880967e0 // object_getIndexedIvars
0x1825231f8 <+64>: mov x23, x0 // x0 == _notifyingInfo
0x1825231fc <+68>: add x24, x0, #0x20 // x24 == os_unfair_recursive_lock_s
0x182523200 <+72>: mov x0, x24
0x182523204 <+76>: mov w1, #0x0
0x182523208 <+80>: bl 0x188096910 // os_unfair_recursive_lock_lock_with_options crash 调用栈

调用 object_getClass 获取 Class,调用 object_getIndexedIvars 获取到 _notifyingInfo,_notifyingInfo + 偏移量 0x20 获取 os_unfair_recursive_lock_s,崩溃的原因是这把锁的 owner 损坏了,lock 也是一个结构体,ower 也是根据 offset 获取的。


结论


从崩溃的上下文来看,最可能出问题的是获取 _notifyingInfo,因为只有 KVO  Class 才能获取到 _notifyingInfo 这个结构体,如果在调用 __NSSetBoolValueAndNotify 的过程中,在其它线程监听被移除,此时 object_getClass 取到的不是 KVO Class 那后续再根据 offset 去取 lock,这个时候就有可能发生上述崩溃。


线下暴力复现验证了上述猜测。

- (void)start {
__block KVObject *obj = [KVObject new];
dispatch_async(dispatch_get_global_queue(0, 0x0), ^{
for (int i = 0; i < 100000; i++) {
[obj addObserver:self forKeyPath:@"value" options:0x7 context:nil];
[obj removeObserver:self forKeyPath:@"value"];
}
});

dispatch_async(dispatch_get_global_queue(0, 0x0), ^{
for (int i = 0; i < 100000; i++) {
obj.value = YES;
obj.value = NO;
}
});
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {}

解决这个问题的思路就是保证线程安全,我们在线上断点找到了 removeObserver 的代码,将 removeObserver 和触发监听的代码放在了同一个串行队列。当然如果 removeObserver 在 dealloc 里面,理论上也不会出现这类问题。


__NSSetxxxValueAndNotify 系列方法都有可能会触发这个崩溃,类似的问题可以按照相同的思路解决。

00000000004e05cd t __NSSetBoolValueAndNotify
00000000004e0707 t __NSSetCharValueAndNotify
00000000004e097b t __NSSetDoubleValueAndNotify
00000000004e0abc t __NSSetFloatValueAndNotify
00000000004e0bfd t __NSSetIntValueAndNotify
00000000004e10e7 t __NSSetLongLongValueAndNotify
00000000004e0e6f t __NSSetLongValueAndNotify
00000000004e0491 t __NSSetObjectValueAndNotify
00000000004e15d5 t __NSSetPointValueAndNotify
00000000004e1734 t __NSSetRangeValueAndNotify
00000000004e188a t __NSSetRectValueAndNotify
00000000004e135f t __NSSetShortValueAndNotify
00000000004e19e8 t __NSSetSizeValueAndNotify
00000000004e0841 t __NSSetUnsignedCharValueAndNotify
00000000004e0d36 t __NSSetUnsignedIntValueAndNotify
00000000004e1223 t __NSSetUnsignedLongLongValueAndNotify
00000000004e0fab t __NSSetUnsignedLongValueAndNotify
00000000004e149a t __NSSetUnsignedShortValueAndNotify
00000000004de834 t __NSSetValueAndNotifyForKeyInIvar

作者:yuec
链接:https://juejin.cn/post/7193677784097488933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

让 Xcode 15 拥有建置给 macOS 10.9 的能力

iOS
免责声明:理论上而言,用这招类推可以建置给早期版本的 iOS。但实际上管不管用我就没法保证了,因为我不是 iOS 程式师。 本文专门给那些需要在新版本系统当中用新版本 Xcode 将祖产专案建置给早期系统版本的资工业者们。 Xcode 15 需要打 liba...
继续阅读 »

免责声明:理论上而言,用这招类推可以建置给早期版本的 iOS。但实际上管不管用我就没法保证了,因为我不是 iOS 程式师。



本文专门给那些需要在新版本系统当中用新版本 Xcode 将祖产专案建置给早期系统版本的资工业者们。


Xcode 15 需要打 libarclite 才能给早于 macOS 10.13 的系统建置应用程式。


通用做法就是从 Xcode 14.2 或 Xcode 13 当中提取出 libarclite 套装,然后植入到 Xcode 15 当中。先开启 toolchains 资料夹:




再把 libarclite 的东西放进去(也就是 arc 这个资料夹):




然而,如果是 macOS 10.9 的话,事情还要复杂一个层次:


macOS 14 Sonoma 开始的 SDK 几乎把整个 Foundation 当中的很多基础类型都重写了。这就导致之前那些被 Swift 从 Objective-C 借走的基础类型全部都得重新打上「NS」开头的后缀才可以直接使用。但这还有一个问题:NSLocalizedString 的建构子不能使用了,因为这玩意在 macOS 14 当中也是被(用纯 Swift)彻底重构的基础类型之一。Apple 毫不留情地给这些基础类型都下了全局的「@available(macOS 10.10, *)」的宣告: 



这样一来,除了 libarclite 以外,还需要旧版 macOS SDK 才可以。虽然 macOS 13 Ventura 的 SDK 也可以凑合用,但(保险起见)笔者推荐 macOS 12 Monterey 的 SDK:Release macOS 12.3 SDK · alexey-lysiuk/macos-sdk (github.com)。该 SDK 的安置位置:




再修改一下 Xcode 专案当中对 macOS SDK 的指定(不用理会 not found):




这样应该就可以正常组建了。如果有提示说 Date 不符合最新版本要求的话,把 Date 改成 NSDate 即可。


$ EOF.


作者:ShikiSuen
链接:https://juejin.cn/post/7242927398294421541
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

苹果的产品经理设计的App Clip是有意为之,还是必然趋势,详解 App Clip技术之谜

iOS
苹果在 WWDC2020 上发布了 App Clip,有媒体叫做“苹果小程序”。虽然 Clip 在产品理念上和小程序有相似之处,但是在技术实现层面却是截然不同的东西。本文会针对 Clip 的技术层面做全面的介绍。 实现方式:native 代码、native 框...
继续阅读 »

苹果在 WWDC2020 上发布了 App Clip,有媒体叫做“苹果小程序”。虽然 Clip 在产品理念上和小程序有相似之处,但是在技术实现层面却是截然不同的东西。本文会针对 Clip 的技术层面做全面的介绍。


实现方式:native 代码、native 框架、native app 一样的分发


在实现上,Clip 和原生的 app 使用一样的方式。在 UI 框架上同时支持 UIKit 和 SwiftUI,有些开发者认为只能使用 SwiftUI 开发,这点是错误的。Clip 的定位和 watch app、app extension 类似,和 app 在同一个 project 里,是一个单独的 target。只是 Clip 并没有自己的专属 framework(其实有一个,但是主要包含的是一些特色 api),使用的框架和 app 一致,可以认为是一个精简版的原生 App。




Clip 不能单独发布,必须关联一个 app。因此发布的流程和 app 和一样的,在 apple connect 上创建一个版本,和 app 一起提交审核。和 app 在技术上的最大区别只是大小限制在 10MB 以内,因为 Clip 的基础就是希望用户可以最迅速的被用户使用,如果体积大了就失去了产品的根本。


产品定位:用完即走




苹果对 Clip 的使用场景非常明确:在一个特定的情境里,用户可以快速的使用 app 的核心服务。是小程序内味了!


坦率的说,很难说 Clip 的理念是苹果原创的,在产品的定位上和微信小程序如出一辙。尤其是微信小程序在国内已经完全普及了,微信小程序初始发布的时候也被苹果加了多条限制。其中一条就是小程序不能有虚拟商品支付功能。现在回头看苹果自己的 Clip 可以完美支持 apple pay,很难说苹果没有私心。


触手可及




Clip 使用一段 URL 标识自己,格式遵从 universal link。因为苹果对 Clip 的使用场景非常明确,所以在 Clip 的调起方式做了严格限制。Clip 的调用只能是用户主动要发起才能访问,所以不存在用户在某个 app 里不小心点了一个按钮,就跳转下载了 Clip。


Clip 的发起入口有以下几种:

  • NFC
  • 二维码
  • Safari 中关联了 Clip 的网页
  • 苹果消息应用
  • Siri 附近建议和苹果地图

NFC 和二维码的入口很容易理解,必须用户主动拿出手机靠近 NFC、打开相机扫描。苹果专属的 Clip 码生成工具在年底才会开放。




Safari 中发起和之前的 universal link 类似,在网站配置了关联的 Clip 信息后,会有一个 banner 提示打开应用。




因为 Clip 提交 app store 审核的信息里也会配置好相关的 url,因此如果在 message 里发了 clip 的链接,操作系统也会在应用里生成一个 Clip 的卡片,用户如果需要可以主动点击。




Siri 附近建议和苹果地图(在 connect 中可以配置 clip 的地理位置)。场景和前面的二维码类似,如果我在地图上看到一个商家,商家有提供服务的 Clip,我可以在地图或者 Siri 建议里直接打开 Clip。




再次总结一下 Clip 的入口限制:只能是用户主动发起才能访问。虽然 Clip 的入口是一段 universal link,在代码里的处理方式也和 universal link 一致,但是为了 Clip 不被滥用,Clip 的调起只能是操作系统调起。App 没有能力主动调起一个 Clip 程序。


无需安装、卸载


因为 Clip 的大小被限制在了 10MB 以下,在当下的网络状态下,可以实现快速的打开。为了给用户使用非常轻松的感觉,在 UI 上不会体现“安装”这样的字眼,而是直接“打开”。预期的场景下用户打开 Clip 和打开一个网页类似。因此在用户的视角里就不存在软件的安装、卸载。


Clip 的生命周期由操作系统全权接管。如果 Clip 用户一段时间后没有使用,操作系统就会自动清除掉 Clip,Clip 里存储的数据也会被一并清除。因此虽然 Clip 提供了存储的能力,但是程序不应该依赖存储的数据,只能把存储当做 cache 来使用,操作系统可能自动清除缓存的数据。


横向比较:PWA、Instant Apps、小程序


Instant Apps


18 年正式发布的 Android Instant apps 和 Clip 在技术上是最接近的。Instant apps 中文被翻成“免安装应用”,在体验上也是希望用户能够最低成本的使用上 app,让用户感受不到安装这个步骤。Instant apps 也可以通过 url 标识(deep link),如果在 chrome 里搜索到应用的网站,chrome 如果识别到域名下有关联应用,可以直接“打开”。消息中的链接也可以被识别。只是 Instant apps 发布的早,国外用户也没有使用二维码的习惯,所以入口上不支持二维码、NFC。


两者的根本区别还是在定位上,Instant apps 提出的场景是提供一个 app 的试用版。因此场景是你已经到了 app 的下载页面,这个时候如果一个 app 几百兆你可能就放弃下载了,但是有一个极简的试用版,就会提高你使用 app 的可能。这个场景在游戏 app 里尤其明显,一方面高质量的游戏 app 体积比较大。另一方面,如果是一个付费下载的应用,如果有一个免费的试用版,也可以增加用户的下载可能。在苹果生态里很多应用会提供一个受限的免费 lite 版本也是一样的需求。


但是 Instant apps 在国内没有产生任何影响。因为政策的原因,Google Play 不支持在国内市场使用。国内的安卓应用市场也是鱼龙混杂,对于 Instant apps 也估计也没有统一支持。另外国内的安卓生态也和欧美地区区别比较大,早期安卓市场上收费的应用很少,对于用户而言需要试用免费 app 的场景很少。另外大厂也可能会推出专门的急速版应用,安装后利用动态化技术下发代码,应用体积也可以控制在 10 MB 以内。


Clip 则是非常明确的面向线下提供服务的场景,在应用能力上可以接入 sign in with apple,apple pay。这样一个全新的用户,可以很快速的使用线下服务并且进行注册、支付。用户体验会好的多。安卓因为国内生态的原因,各个安卓厂商没有统一的新用户可以快速注册的接口,也没有统一的支付接口,很难提供相匹敌的体验。如果开发者针对各个厂商单独开发,那成本上就不是“小程序”了。


Progressive Web App(PWA)




Progressive Web App 是基于 web 的技术。在移动互联网兴起之后,大家的流量都转移到了移动设备上。然而在移动上的 web 体验并不好。于是 W3C 和谷歌就基于浏览器的能力,制定了一套协议,让 web app 可以拥有更多的 native 能力。


PWA 不是特指某一项技术,而是应用了多项技术的 Web App。其核心技术包括 App Manifest、Service Worker、Web Push。


PWA 相当于把小程序里的代码直接下载到了本地,有了独立的 app 入口。运行的时候基于浏览器的能力。但是对于用户感受和原生 app 一样。


我个人对 PWA 技术很有好感,它的初衷有着初代互联网般的美好。希望底层有一套协议后,用户体验还是没有边界的互联网。然而时代已经变了。PWA 在中国基本上是凉了。


PWA 从出生就带了硬伤,虽然谷歌希望有一套 web 标准可以运行在移动设备上,但是对于苹果的商业策略而言,这并不重要。因此 PWA 的一个协议,从制定出来,再到移动设备(iOS)上支持这个特性,几年就过去了。而且对于移动用户而言,可以拥有一个美好的 web app 并不是他们的痛点。


总结起来 PWA 看着美好,但似乎更多是对于 web 开发者心中的美好愿景。在落实中遇到了很多现实的问题,技术支持的不好,开发者就更没有动力在这个技术上做软件生态了。


微信小程序


前面提过在产品理念上小程序和 Clip 很相似,甚至说不定 Clip 是受了小程序的启发。在市场上,小程序是 Clip 的真正对手。


小程序基于微信的 app,Clip 基于操作系统,因此在能力上 Clip 有优势。小程序的入口需要先打开微信,而 Clip 可以通过 NFC 靠近直接激活应用。对于开发者而言,Clip 可以直接获得很多原生的能力(比如 push),如果用户喜欢可以关联下载自己的原生应用。在小程序中,微信出于商业原因开发者不能直接跳转到自有 app,小程序的能力也依赖于微信提供的接口。


对于从 Clip 关联主 app 苹果还挺重视的,提供了几个入口展示关联 app。


首先在 clip 的展示页就会显示:




每次使用 Clip 时也会有一个短暂的浮层展示:




开发者也可以自己通过 SKOverlay 来展示:




不过如果开发者没有自己的独立 app,那么也就只能选择小程序了。小程序发展到现在场景也比最早提出的线下服务更加多了,反而类似 Instant apps,更像一个轻量级的 app。


考虑到国内很多小程序的厂商都没有自己的独立 app,因此 clip 对于这部分群体也并没有什么吸引力。不过对于线下服务类,尤其有支付场景的,Clip 在用户体验上会比小程序好一些。


总结,Clip 的业务场景和小程序有一小部分是重叠的,小程序覆盖的场景还是更多一些。两者在大部分时候并不是互斥式的竞争关系,即便在一些场景下 Clip 有技术优势,商家也不会放弃小程序,因为还有安卓用户嘛。还是看商家在某些场景里,是否愿意为用户多提供一种更好的交互方式。


对比原生 app 的技术限制


虽然 Clip 可以直接使用 iOS framework,但是因为 Clip 的使用场景是新用户的初次、简短、当下(in-the-moment experience)的使用,相比原生 app 苹果还是进行了一些限制。


App 不能访问用户的隐私信息:

  • 运动和健身数据
  • Apple Music 和多媒体文件
  • 通讯录、信息、照片、文件等数据

不过为了能够提供给用户更加轻便的体验,通过专门为 Clip 设计了免申请的通知、定位权限。不过也有限制:免申请的通知只在 8 个小时内有效。位置只能获取一次。如果 app 需要重度使用这两类权限就还是和原来一样,可以弹窗申请。


某些高级应用能力也会受限,需要在完整的应用中才能使用:

  • 不能请求追踪授权
  • 不能进行后台请求任务
  • 没在激活状态蓝牙连接会断开

总的而言虽然有一些限制,但是这些限制的出发点是希望开发者关注 Clip 的正确使用场景。对于 Clip 所提倡的使用场景里,苹果提供的能力是完全够用的。


一些技术细节


可以建立一个共享 targets 的 Asset catalog 来共用图片资源。




在 Clip 中申请的授权,在下载完整应用后会被同步到应用中。


通过 App Group Container 来共享 clip 和 app 的数据。




image


Clip 的 url 可以配置参数:




在 App Store connect 中还可以针对指定的参数配置不一样的标题和图片。比如一家连锁咖啡店,可能不同的店你希望弹出的标题图片是不一样的,可以进行单独的配置。




总结


苹果给定义的 Clip 的关键词是:lightweight、native、fast、focused、in-the-moment experience。


Clip 在特定的线下场景里有着相当好的用户体验。对于已经拥有独立 app 的公司来说,开发一个 clip 应用的成本并不高。我个人还是期待这样一个好的技术可以被更多开发者接纳,可以提供给用户更好的体验。对于小程序,clip 的场景窄的多,两者并不是直接竞争关系。我更愿意看做是特定场景下,对于小程序原生能力不足的一种补充。


作者:会飞的金鱼
链接:https://juejin.cn/post/7219889814116024380
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin和Swift的前世一定是兄弟

iOS
Swift介绍 Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。&nbs...
继续阅读 »

Swift介绍


Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。 


 playground这个名字起的好,翻译成中文就是操场,玩的地方,也就是说,你可以尽情的测试你的Swift代码。


声明变量和常量


Kotlin的写法:

var a: Int = 10
val b: Float = 20f

Swift的写法:

var a: Int = 10
let b: Float = 20.0

你会发现它俩声明变量的方式一模一样,而常量也只是关键字不一样,数据类型我们暂不考虑。


导包


Kotlin的写法:

import android.app.Activity

Swift的写法:

import SwiftUI

这里kotlin和swift的方式一模一样。


整形


Kotlin的写法:

val a: Byte = -10
val b: Short = 20
val c: Int = -30
val d: Long = 40

Swift的写法:

let a: Int8 = -10
let b: Int16 = 20
let c: Int32 = -30
let d: Int = -30
let e: Int64 = 40
let f: UInt8 = 10
let g: UInt16 = 20
let h: UInt32 = 30
let i: UInt = 30
let j: UInt64 = 40

Kotlin没有无符号整型,Swift中Int32等价于Int,UInt32等价于UInt。无符号类型代表是正数,所以没有符号。


基本运算符


Kotlin的写法:

val a: Int = 10
val b: Float = 20f
val c = a + b

Swift的写法:

let a: Int = 10
let b: Float = 20
let c = Float(a) + b

Swift中没有隐式转换,Float类型不用写f。这里Kotlin没那么严格。


逻辑分支


Kotlin的写法:

val a = 65
if (a > 60) {
}

val b = 1
when (b) {
1 -> print("b等于1")
2 -> print("b等于2")
else -> print("默认值")
}

Swift的写法:

let a = 65
if a > 60 {
}

let b = 1
switch b {
case 1:
print("b等于1")
case 2:
print("b等于2")
default:
print("默认值")
}

Swift可以省略if的括号,Kotlin不可以。switch的写法倒是有点像Java了。


循环语句


Kotlin的写法:

for (i in 0..9) {
}

Swift的写法:

for var i in 0...9 {
}
// 或
for var i in 0..<10 {
}

Kotlin还是不能省略括号。


字符串


Kotlin的写法:

val lang = "Kotlin"
val str = "Hello $lang"

Swift的写法:

let lang = "Swift"
let str = "Hello \(lang)"

字符串的声明方式一模一样,拼接方式略有不同。


数组


Kotlin的写法:

val arr = arrayOf("Hello", "JYM")
val arr2 = emptyArray<String>()
val arr3: Array<String>

Swift的写法:

let arr = ["Hello", "JYM"]
let arr2 = [String]()
let arr3: [String]

数组的写法稍微有点不同。


Map和Dictionary


Kotlin的写法:

val map = hashMapOf<String, Any>()
map["name"] = "张三"
map["age"] = 100

Swift的写法:

let dict: Dictionary<String, Any> = ["name": "张三", "age": 100]

Swift的字典声明时必须初始化。Map和Dictionary的本质都是哈希。


函数


Kotlin的写法:

fun print(param: String) : Unit {
}

Swift的写法:

func print(param: String) -> Void {
}

func print(param: String) -> () {
}

除了关键字和返回值分隔符不一样,其他几乎一模一样。


高阶函数和闭包


Kotlin的写法:

fun showDialog(build: BaseDialog.() -> Unit) {
}

Swift的写法:

func showDialog(build: (dialog: BaseDialog) -> ()) {
}

Kotlin的高阶函数和Swift的闭包是类似的概念,用于函数的参数也是一个函数的情况。


创建对象


Kotlin的写法:

val btn = Button(context)

Swift的写法:

let btn = UIButton()

这里kotlin和swift的方式一模一样。


类继承


Kotlin的写法:

class MainPresenter : BasePresenter {
}

Swift的写法:

class ViewController : UIViewController {
}

这里kotlin和swift的方式一模一样。


Swift有而Kotlin没有的语法


guard...else的语法,通常用于登录校验,条件不满足,就执行else的语句,条件满足,才执行guard外面的语句。

guard 条件表达式 else {
}

另外还有一个重要的语法就是元组。元祖在Kotlin中没有,但是在一些其他编程语言中是有的,比如Lua、Solidity。元组主要用于函数的返回值,可以返回一个元组合,这样就相当于函数可以返回多个返回值了。
Swift的元组:

let group = ("哆啦", 18, "全宇宙最强吹牛首席前台")

Lua的多返回值:

function group() return "a","b" end

Solidity的元组:

contract MyContract {
mapping(uint => string) public students;

function MyContract(){
students[0] = "默认姓名";
students[1] = "默认年龄";
students[2] = "默认介绍";
}

function printInfo() constant returns(string,uint,string){
return("哆啦", 18, "全宇宙最强吹牛首席前台");
}
}

总结


编程语言很多地方都是相通的,学会了面向对象编程,你学习其他编程语言就会非常容易。学习一门其他编程语言的语法是很快的,但是要熟练掌握,还需要对该编程语言的API有大量的实践。还是那句话,编程语言只是工具,你的编程思维的高度才是决定你水平的重要指标。所以我给新入行互联网的同学的建议是,你可以先学习面向对象的编程思想,不用局限于一门语言,可以多了解下其他的编程语言,选择你更喜欢的方向。选择好后,再深耕一门技术。每个人的道不一样,可能你就更适合某一个方向。


作者:dora
链接:https://juejin.cn/post/7248962809023316028
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 开发中如何禁用第三方输入法

iOS
iOS 目前已允许使用第三方输入法,但在实际开发中,无论是出于安全的考虑,还是对某个输入控件限制输入法,都有禁用第三方输入法的需求。基于此,对禁用第三方输入法的方式做一个总结。 1. 全局禁用 Objective-C 语言版本:- (BOOL)applicat...
继续阅读 »

iOS 目前已允许使用第三方输入法,但在实际开发中,无论是出于安全的考虑,还是对某个输入控件限制输入法,都有禁用第三方输入法的需求。基于此,对禁用第三方输入法的方式做一个总结。


1. 全局禁用


Objective-C 语言版本:

- (BOOL)application:(UIApplication *)application
shouldAllowExtensionPointIdentifier:(UIApplicationExtensionPointIdentifier)extensionPointIdentifier
{
// 禁用三方输入法
// UIApplicationKeyboardExtensionPointIdentifier 等价于 @"com.apple.keyboard-service"
if ([extensionPointIdentifier isEqualToString:UIApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}

Swift 语言版本:

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 禁用三方输入法
if extensionPointIdentifier == .keyboard {
return false
}
return true
}

2. 针对某个视图禁用

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 遍历当前根控制器的所有子控制器,找到需要的子控制器
for vc in self.window?.rootViewController?.childViewControllers ?? []
      where vc.isKind(of: BaseNavigationController.self)
{
// 如果首页禁止使用第三方输入法
for vc1 in vc.childViewControllers where vc1.isKind(of: HomeViewController.self) {
      return false
    }
  }
return true
}

3. 针对某个 inputView 禁用


3.1 自定义键盘


如果需求只是针对数字的输入,优先使用自定义键盘,将 inputView 绑定自定义键盘,不会出现第三方输入法。


3.2 遍历视图内控件,找到需要设置的 inputView,专门设置

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 遍历当前根控制器的所有子控制器,找到需要的子控制器
for vc in self.window?.rootViewController?.childViewControllers ?? []
      where vc.isKind(of: BaseNavigationController.self)
{
// 如果想要禁用的 inputView 在首页上
for vc1 in vc.childViewControllers where vc1.isKind(of: HomeViewController.self) {
// 如果 inputView.tag == 6 的 inputView 禁止使用第三方输入法
      for view in vc1.view.subviews where view.tag == 6 {
      return false
      }
    }
  }
return true
}

作者:Artisan
链接:https://juejin.cn/post/7239715562834083897
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

OC项目用Swift开发方便吗?

iOS
前言 公司有个项目一直是用 OC 进行开发,现在想改成 Swift 来开发。那先说一下为什么有这样的想法,我们都知道 Swift 代码更简单,易维护,安全而且快,网络上也是很多描述,那我们主要的是担心一旦变成混编工程,会不会出现很多问题,还有如何解决这些问题。...
继续阅读 »

前言


公司有个项目一直是用 OC 进行开发,现在想改成 Swift 来开发。那先说一下为什么有这样的想法,我们都知道 Swift 代码更简单,易维护,安全而且快,网络上也是很多描述,那我们主要的是担心一旦变成混编工程,会不会出现很多问题,还有如何解决这些问题。性能问题方面Swift 和 OC 共用一套运行时环境,而且支持 Swift 桥接 到 OC上,所以呢,问题不大。如果有不同的想法,也欢迎留意指教。


桥接文件


我们只要在 OC 项目中,创建一个 swift 文件,系统就会弹出桥接文件,我们点击 "Create Bridging Header"即可。




OC 工程接入 Swift


OC 类 引用 Swift 类


如上面我们创建了一个 swift 文件,里面写一些方法提供给 OC 使用。

@objcMembers class SwiftText: NSObject {

func sayhello() -> String{

return "hello world"

}
}

class SwiftText2: NSObject {

@objc func sayhello() ->String{

returnOCAPI.sayOC()

}
}

这里我们有关键字2个,1个是@objcMembers,表示所有方法属性都可以提供给 OC 使用。另外一个是@objc,表示修饰的方法属性才可以提供给OC使用。


那我们 OC 类怎么用这个 swift 文件呢。
先在我们该类添加头文件

#import "项目Target-Swift.h"

然后我们点进去看下。




可以看到我们写的 swift 文件类,方法,属性,都被转化为 OC 了,有了这个我们直接使用即可。


OC类 使用 swift Pod库


说实话,这种用的比较少,但有时候我们真的觉得 swift Pod库 会更好用,那我们怎么去处理呢?


首先我们要搞懂一点,有些是支持使用的,如PromiseKit,有些是不支持使用的如Kingfisher


先说第一种支持使用的,我们直接导入#import <PromiseKit/PromiseKit.h>即可。


那要是第二种的话,我们还有一种办法,就是先用 swift 写一个该库管理类,然后里面引用我们该库的内容,我们通过 @objc 来提供给我们 OC 使用。


Swift类 引用 OC 类


如果我们编写的 Swift 类,想要用到 我们 OC 的方法,那我们如何处理呢?


我们直接在桥接文件"Target-Bridging-Header.h"里面,直接导入头文件#import "XXX.h"即可使用。


Swift类 使用 OC pod库


其实这个更简单,和 Swift 工程引入 OC pod库一样,在该类里面导入头文件即可。

import MJRefresh

遇到问题


问题1:引入swift pod库 问题


如果我们 OC 项目 是没有 使用use_frameworks!。那我们导入swift Pod库 就会报错。


那我们就在工程配置里面 Build Settings里面,搜索 Defines Module, 更改为 YES 即可。




问题2:OC 类继承问题


OC的类是不能继承至Swift的类,但Swift 类是可以继承 OC类的,其实方式也是"Target-Bridging-Header.h"导入头文件即可。


问题3:宏定义问题


我们自己重新一份
原来的是

#define kScreenWidth        [UIScreen mainScreen].bounds.size.width                      
#define kScreenHeight [UIScreen mainScreen].bounds.size.height

现在的是

let kScreenWidth = UIScreen.main.bounds.width
let kScreenHeight = UIScreen.main.bounds.height

有一些,我们可以定义问方法来替代宏。


问题4:OC经常调用swift库导入问题


我们知道xxx-Swift.h都是包含所有swift 提供给 OC 使用的类,所以我们可以把xxx-Swift.h放到 pch 文件里面,就可以在任意一个 OC 工程文件直接调用 swift 类。


OC 在线转为 swift


提供一个链接,可以支持 OC 转为 swift。
在线链接


最后


经过上面的总结,OC 项目 使用 swift 开发 的确是问题不大,使用过程中可能也会遇到编译问题,找不到文件问题,只要细心排查,也是很容易解决,那等后续项目用上正轨,还会把遇到的坑填补上来,如有不足,欢迎指点。


作者:可爱亲宝宝
链接:https://juejin.cn/post/7153564241659297806
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 使用 CoreNFC 读取第三代社保卡信息

iOS
NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。 iOS 和 w...
继续阅读 »

NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。



iOS 和 watchOS 设备内置 NFC 硬件已经很多年了。在现实生活中,Apple Pay 就是使用这项技术与商店的支付终端进行交互。然而直到 iOS 11 开发者才能够使用 NFC 硬件。后来 Apple 在 iOS 13 系统中提升了 CoreNFC 的功能,开发者可以借助这项新技术,对 iOS 设备进行编程,使其以新的方式与周围的互联世界进行交互。


说明:本文提供的代码示例所用的开发环境为 Xcode14 + Swift 5.7 + iOS 13。需要登录已付费的开发者账号才能开启 NFC Capability。


工程配置


设置 Capability


在项目导航器中选中项目,转到 Signing & Capabilities 标签页并选择 +Capability,在弹出的列表中选择 Near Field Communication Tag Reading。这会自动生成 entitlements 文件中的必要配置信息,同时为您的应用程序激活 NFC 功能。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
</dict>

设置 Info.plist


添加 NFC 相关的隐私设置,向 Info.plist 文件中添加 Privacy - NFC Scan Usage Description 隐私设置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFCReaderUsageDescription</key>
<string>应用需要您的同意,才能访问 NFC 进行社保卡信息的读写。</string>
</dict>

添加 AID 相关的设置项,向 Info.plist 文件中添加 ISO7816 application identifiers for NFC Tag Reader Session 配置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A000000632010105</string>
</array>
</dict>


说明:第三代社保卡使用统一的交通联合卡电子钱包规范,A000000632010105 为交通联合卡 AID 标识。参考网址:wiki.nfc.im/books



导入 CryptoSwift 第三方库


在项目导航器中选中项目,右键菜单选择 Add Packages...,在搜索框中输入 github.com/krzyzanowsk… 并点击 Add Package 按钮完成导入。





说明:CryptoSwift 提供了相关的十六进制字符串与 UInt8 相互转换的方法。



代码编程


扩展 NFCISO7816Tag


由于 Apple 是从 iOS 14 系统开始提供了 sendCommand API 的异步调用形式,为兼容 iOS 13 系统,并更好的使用 Swift 提供的 async/await 语法,现对其 NFCISO7816Tag 进行方法扩展。

import CoreNFC
import CryptoSwift

@available(iOS 13.0, *)
extension NFCISO7816Tag {

  @discardableResult
  func sendCommand(_ command: String) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
      // 通过 CryptoSwift 库提供的 API,将十六进制表示命令字符串转换成字节
      let apdu = NFCISO7816APDU(data: Data(hex: command))!
      // 将同步调用形式转换成异步调用形式
      sendCommand(apdu: apdu) { responseData, _, _, error in
        if let error {
          continuation.resume(throwing: error)
        } else {
          continuation.resume(returning: responseData)
        }
      }
    }
  }
}

封装 NFCTagReaderSession

import CoreNFC

@available(iOS 13.0, *)
class NFCISO7816TagSession: NSObject, NFCTagReaderSessionDelegate {

  private var session: NFCTagReaderSession? = nil
  private var sessionContinuation: CheckedContinuation<NFCISO7816Tag, Error>? = nil

  func begin() async throws -> NFCISO7816Tag {
// 实例化用于检测 NFCISO7816Tag 的会话
    session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
    session?.alertMessage = "请将社保卡靠近手机背面上方的 NFC 感应区域"
    session?.begin()
    return try await withCheckedThrowingContinuation { continuation in
      self.sessionContinuation = continuation
    }
  }

  func invalidate(with message: String) {
// 关闭读取会话,以防止重用
    session?.alertMessage = message
    session?.invalidate()
  }

  // MARK: - NFCTagReaderSessionDelegate

  func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}

  func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
// 检测到 NFCISO7816Tag
    if let tag = tags.first, case .iso7816(let iso7816Tag) = tag {
      session.alertMessage = "正在读取信息,请勿移动社保卡"
// 连接到 NFCISO7816Tag 并将同步调用形式转换成异步调用形式
      session.connect(to: tag) { error in
        if let error {
          self.sessionContinuation?.resume(throwing: error)
        } else {
          self.sessionContinuation?.resume(returning: iso7816Tag)
        }
      }
    }
  }

  func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
// 读取过程中发生错误
    self.session = nil
    sessionContinuation?.resume(throwing: error)
  }
}

编写 UI 界面


使用 SwiftUI 编写如下代码所示的页面,包含一个显示卡号的标签和一个读取按钮。

import SwiftUI

struct ContentView: View {
  @State private var cardNo = ""

  var body: some View {
    VStack(alignment: .leading) {
      Text("卡号:\(cardNo)")
        .font(.system(size: 17))
      Button(action: read) {
        Text("读取")
          .padding()
          .frame(maxWidth: .infinity)
          .foregroundColor(.white)
          .background(.blue)
          .cornerRadius(8)
      }
      Spacer()
    }
    .padding()
  }
}

实现读取逻辑

import SwiftUI
import CryptoSwift

struct ContentView: View {
// var body: some View {...}

private func read() {
    Task {
      let session = NFCISO7816TagSession()
      do {
// 检测 NFCISO7816Tag
        let tag = try await session.begin()
// 发送命令 00B0950A12 并截取前 10 个字节转换为 20 位卡号
        let cardNo = try await tag.sendCommand("00B0950A12")[0..<10].toHexString()
        self.cardNo = cardNo
// 关闭读取会话
        session.invalidate(with: "读取成功")
      } catch {
        print(error)
      }
    }
  }
}


说明:APDU 是卡与读卡器之间传送的信息单元,具体指令描述请参考 wiki.nfc.im/books



运行过程截图




作者:满天飞舞的蒲公英
链接:https://juejin.cn/post/7213565990055362616
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

数字签名为什么可以防篡改

iOS
数字签名是什么 数字签名是一种数字技术,用于验证和保护数据的完整性。 数字签名是通过一些加密算法将消息或文件与公钥(如果是非对称加密就有公钥不然就不用)绑定在一起,并生成唯一的签名。 数字签名的工作原理 数字签名的核心在于加密算法。最常用的是非对称加密算法,它...
继续阅读 »

数字签名是什么


数字签名是一种数字技术,用于验证和保护数据的完整性


数字签名是通过一些加密算法将消息或文件与公钥(如果是非对称加密就有公钥不然就不用)绑定在一起,并生成唯一的签名。


数字签名的工作原理


数字签名的核心在于加密算法。最常用的是非对称加密算法,它将原文通过特定HASH函数得到的摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文提炼出一个摘要信息,与解密得到的摘要进行对比。


数字签名也可以使用哈希函数对文件或消息的散列值进行加密,确保消息不会被篡改。(也有人认为摘要算法不能逆向也就是解密所以不是加密算法,在此不做讨论)


数字签名可以与数字证书结合使用,以证明密钥的归属和真实性,从而保护数字签名过程不被破坏。


数字签名的应用


JWT


JWT通常由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature),以点号分隔。第一部分是头部,第二部分是载荷,第三部分是签名。以下是一个包含了用户ID、用户名和时间戳的JWT实例,格式为 Header.Payload.Signature

// 为方便展示,在'.'处作了换行处理,可以更好地看清楚结构
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

HeaderPayload都是经过 Base64URL 编码的,所以每个人都能通过解码得到原来的信息,固不应该在里面存一些敏感信息。



Signature就是我们要讨论的数字签名了!Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)


HMAC算法 是一种基于密钥的报文完整性的验证方法。HMAC算法利用哈希运算,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。其安全性是建立在Hash加密算法基础上的。


由于Signature是根据HeaderPayload以及服务器的secret来生成的,由于secret只有服务器知道,所以只要HeaderPayloadSignature其中一个被篡改了,那么后续验证的时候就不能通过。同时只有知道secret才能产生与HeaderPayload配对的Signature,所以也能确认该 Token 是否是该服务器所颁发的。


验证过程是用服务器的密钥通过同样的算法计算出一个新的 Signature 然后和旧的 Signature 进行比较,只要被篡改那么 Signature就会跟着改变,所以通过比较, Signature 一样的话则证明没有被篡改,否则则认为被篡改了。


CA 证书


其实就是这个 CA 证书的数字签名为什么可以防篡改,困扰了我好久,所以才去稍微深入了解了一下然后写下了这篇博客。就是为了这点醋,我才包的这顿饺子~


之前学习 https 的时候,看了各大论坛的帖子发现有挺多帖子对于 CA 证书是怎么做放篡改的讲的不太对或者讲的不太清晰,所以这个问题困扰了我挺久。以下是随便找的一些帖子(对事不对人):


例1:



例2:



例3:



(例1和例2是随便在掘金上面搜到的相关文章,例3是newbing的回答)


先回顾一下数字证书验证的大概过程:


CA 签发证书的过程:

  • 首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;
  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;
  • 最后将 Certificate Signature 添加在文件证书上,形成数字证书;

客户端校验服务端的数字证书的过程:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

核心问题在于验证服务器发来的数字证书的数字签名时所用到的公钥是哪里来的。


假设有那么一个场景:


客户端A 和 服务器A 的通信过程中,私钥是Secret_RSA_A,公钥是Secret_PUB_A。服务器A 将自己的证书CA发给客户端A的过程中被 中间人B 给截获了,中间人B 用自己的公钥Secret_PUB_B 替换了 服务器A 发给 客户端A 的CA证书的公钥Secret_PUB_A,并且用和公钥Secret_PUB_B 配对的私钥Secret_RSA_B 对替换公钥后的CA证书的公钥、用途、颁发者、有效时间等信息生成的新HASH 进行加密,生成新的 Certificate Signature 并把原本证书上的 Certificate Signature 替换掉。但客户端A 对这并不知情。然后在后续客户端对该 CA证书验证的过程中,如果使用的是证书上的公钥,那么计算出来的 H1 和 H2 就会一样,也就是认为证书是可信的。(实际上加密使用的是CA私钥而不是服务器私钥所以中间人伪造不了一对新的公私钥,但是如果使用服务器发送过来的公钥去验证的话那么就有可能被伪造)


所以更加安全的做法应该是不使用传过来的证书上面的公钥(证书上的公钥是服务器持有者的公钥而不是CA公钥),而是使用预置在操作系统里面的公钥,因为证书加密是用CA私钥加密的而不是用服务器持有者的私钥进行加密的,传服务器持有者的公钥过来是为了和客户端协商然后生成后续对称加密通信需要用到的秘钥。这也是我之前看到的一些文章没有提到的(如上面的图1/2/3所示,没有针对原作者的意思),容易让人困惑。服务器发送过来的证书中的公钥是服务器的公钥而不是可以解密数字签名的公钥(数字签名的公钥也就是和CA证书配对的公钥)。 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用操作系统内置的 CA 的公钥解密 Certificate Signature 内容。这行验证过程中存在一个证书信任链的问题。客户端收到服务器发送过来的CA证书后,浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发,如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密。


综上,数字签名只能验证数据的完整性(JWT 只有服务端可以验证他的身份,因为它有解密需要的密钥,而客户端是验证不了的),而验证身份需要的是数字证书。


最后


以上是本人在学习数字签名原理的过程中的一些感悟,由于个人的局限性,所以可能存在纰漏的情况,欢迎大家批评指正。


作者:龙崎爱吃糖
链接:https://juejin.cn/post/7219304050667470903
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

放弃使用Merge,开心拥抱Rebase!

iOS
1. 引言 大家好,我是比特桃。Git 作为现在最流行的版本管理工具,想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的,并且相对也不容易出错,所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Git 中主...
继续阅读 »

1. 引言


大家好,我是比特桃。Git 作为现在最流行的版本管理工具,想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的,并且相对也不容易出错,所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Git 中主要命令之一,我们还是有必要了解一下,在适合的场景中进行使用。


2. Rebase 的作用


Rebase 中文翻译过来:变基,我觉得这个翻译挺生硬的,导致很多人没有彻底理解变基的含义。我个人把 Rebase 意为 认爸爸,比如可以 Rebase 到马爸爸分支上,成为他的合理继承人。


上图为一次 Rebase 的情况,可以看到最终效果仿佛 Feature 分支没有存在过,新提交的 Commit 像真的在主分支上提交一样。而如果我们用 Merge 就会产生一个合并节点:


可能只看到一次合并所产生的 Commit 节点并没有什么,但实际项目中大概率会变成这样:


简直是乱的一批,仿佛看到了多年前其他人写的一堆代码,啥啥啥,这都是啥!反过来看看采用 Rebase 开发的真实项目,没有对比就没有伤害:


这也是为什么尤雨溪也比较推荐使用 rebase的原因:


3. Rebase 怎么用


其实很多人不用 Rebase ,一方面是不了解实际项目协同中怎么用。另一方面是用了,但问题很多,所以就误认为不好用从而再也不用。这里分享一下,我最近在做项目时所采用 Rebase 方面的协同流程(为了好说明,适当的进行了简化):


3.1 Checkout


首先,我们想从 master 分支上开发新的功能或者修复 bug ,需要 checkout出来一个分支。比如在

A节点中 checkout dev 分支,为了让场景更复杂,在 git checkout dev 分支后。master 上继续有人提交了B、C,形成如下Git 结构:


这里强调一下,很多人用 Rebase 出问题,都是出在了想要 Rebase 的分支是公共分支。其实这里的 dev 应该是只有自己用的分支才合适,回想一下,Git 本身就是分布式版本管理。其实不用远程仓库也是可以非常好的进行版本控制的,我们要将本地分支和远程分支的概念区分的开一些,这俩没有直接联系。所以你本机随便做个 NB 分支一样可以的,Rebase后没人知道你自己起了个什么鬼名字。


3.2 远程管理


如果自己的dev分支并不一定在一台电脑上开展,为了可以自己在多个电脑上开发,我们可以关联了一个自己的远程仓库。这一步是可选的。


3.3 开始变基


现在我们在 dev 上开发了D、E,然后dev rebase master,形成了A、B、C、D、E:


这里虽然看似已经一条直线了,但实际 只有 dev 知道自己的爸爸成为了 master,但 master 并没有认这个儿子。所以我们还需要:master merge dev,这样就在master上形成了一条完美的直线:


最后,再 git push origin master 到远程分支,完成本次开发。


3.4 善后


Rebase 后 dev 由于变基了,相当于已经认贼作父了,现在还想再认回来?休想!所以只能强制解决,在非保护分支中强制push到自己的远程仓库:git push --force origin dev,最后再将dev变基到自己的远程分支:git rebase origin dev,方便自己远程仓库的维护。至此,完成了一次rebase形式的开发,并且可以继续进行下次开发。


4. Rebase 的优缺点


先说说优点:

  • 保持提交历史的线性:使用 merge 合并分支时,会创建一个新的合并提交,从而在提交历史中形成一条新的分支。而使用 rebase,可以将提交记录直接添加到目标分支的末尾,从而保持提交历史的线性。
  • 减少不必要的合并提交:使用 merge 合并分支时,会创建一个新的合并提交,它可能会包含很多无意义的合并信息。而使用 rebase,可以将提交记录逐个添加到目标分支的末尾,避免了创建不必要的合并提交。
  • 更好的代码审查和追溯:使用 rebase,可以让提交历史更加直观和易于理解,从而更容易进行代码审查和问题追溯。
  • 避免冲突的产生:在合并分支时,可能会因为两个分支之间存在冲突而导致合并失败。而使用 rebase,可以在变基之前先解决这些冲突,从而避免了合并时出现的冲突。

总之,虽然 rebase 不是适用于所有情况的万能解决方案,但在大多数情况下,使用 rebase 能够帮助我们创建更加干净和直观的提交历史,提高团队的协作效率。


说了这么说好像都在说 Rebase 的优点,那 Rebase就没有缺点嘛?当然不是,要不然大家早就都从 Merge 转 Rebase了。Rebase 的缺点:

  • 解决冲突繁琐,rebase冲突 是按每个commit来对比的,merge冲突 是按最终结果来对比的,如果用rebase最好是经常去合并一下代码,不然长周期的开发如果就在最后rebase真的是解冲突解到人傻掉。
  • 没有合并记录,Merge 有记录,出了问题好解决。
  • 操作步骤相对繁琐。

5. 结语


协同开发最核心的问题其实就是合并,如何合理的合并,优雅的合并,是每个团队需要考虑的问题。Merge 和 Rebase 作为 Git 中主要的命令,其实各有各的优点,两个一起用也是很常见的。根据自身团队及项目情况,选择合适的方式才是最好的。最后,祝大家合并代码一切顺利~


作者:比特桃
链接:https://juejin.cn/post/7273018561962377216
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS横滑组件实现

iOS
这是我早先实现的一个自定义横滑组件,本文回顾一下当时实现过程遇到的问题和细节,最后有源码地址 文中所有图片托管在Github上 所谓横滑组件其实就如图所示的效果: 列一下UI上的要求:每次滑动一页,有pageEnable的效果每次显示在屏幕中的item...
继续阅读 »

这是我早先实现的一个自定义横滑组件,本文回顾一下当时实现过程遇到的问题和细节,最后有源码地址




文中所有图片托管在Github上



所谓横滑组件其实就如图所示的效果:



列一下UI上的要求:

  • 每次滑动一页,有pageEnable的效果
  • 每次显示在屏幕中的item其实是三个而不是一个
  • 每个item的间距、视图与屏幕边缘的边距严格按照UI上样子

UICollectionView+pageEnable


使用UICollectionView并开启pageEnable是最容易想到的方案,我们来试一下能否满足需要


关键的几个参数如下所示

container.width = 375
collectionView.isPagingEnable = true
collectionView.width = 375
leftPadding = rightPadding = 16
cell.width = container.width - leftPadding - rightPadding
collectionView.contentInset = UIEdgeInset(0,16,0,0)

效果如下所示:



显然,没有达到预期:

  • 问题1,每次滑动停止后,cell的位置不对
    • 通过打印contentOffset得知,UIScrollView开启pagingEnable后的自动翻页,每次修改contentOffset的值等于UIScrollView.width
    • 而且我们无法自定义每次翻页移动的距离
  • 问题2,由于设置了collectionView.contentInset.left,所以第一cell可以移动到屏幕最左边而不能自动还原到初始位置

不甘心,继续调整


我画了一张图来表示要实现的效果:



  • 根据上图的效果,我们希望的效果是每次移动cell时移动的距离(两条红竖线之间的距离)是一个cell的宽度+cell之间的距离--cell.width+interval
  • 既然pageEnable特性每次移动的距离一定是scrollView.width,所以我们可以让scrollView.width = cell.width+interval
  • 这或许能解决上面显示异常问题

我们更新一下配置参数,如下:

leftPadding = rightPadding = 16
container.width = 375
collectionView.isPagingEnable = true
cell.width = container.width - leftPadding - rightPadding
interval = 8
collectionView.width = cell.width + interval
collectionView.contentInset = UIEdgeInset(0,0,0,interval) // 这一句可能会引起你的困惑,但经过测试必须设置成这样,否则效果有问题,本文不做详细解释,跟scrollView自身对于contentSize和contentOffset的调整有关

来看一下效果:



哇,好像不错!但还是有问题:

  • 我们希望同时显示三个cell,但该效果却只能显示1个cell
  • 这是因为collectionView的宽度刚好能显示下一个cell和一个interval,没有更多空间来显示其他cell了

这就很尴尬了,为了利用pageEnable的特性,我们不得不修改collectionView的宽度小一些,但这却导致无法足够的cell个数


所以,结论是:❌


UICollectionView + UIScrollView


在调研其他技术方案时,受一Paging a overflowing collection view启发,可以使用一个UICollectionView和一个UIScrollView一同实现类似效果


核心思想如下:

  • 单独用一个UIScrollView,利用pageEnable特性来实现符合要求的横滑、拖拽翻页效果
  • 单独用一个UICollectionView来利用它的cell显示、复用机制
  • UIScrollView是不显示的,只用它的拖拽手势能力。当拖拽UIScrollView时,将contentOffset的移动应用到UICollectionView中

具体实现过程中有些细节需要注意,比如:

  1. collectionView的contentInset需要设置
  2. 将scrollView的移动应用到collectionView中时如何计算准确
  3. 需要关闭collectionView的panGesture

再放一下效果



结论是:✅


源码地址:SlideView.swift


优缺点


优点很明显:

  • 既复用了UIScrollView的pageEnable手势和动画效果,也复用了UICollectionView的cell复用机制
  • 由于复用了UICollectionView,所以相比通过UIScrollView自定义实现,在一些用户交互体验上可能更好,比如在快速横滑时,自定义的实现可能就没办法快速的准备好每一个cell并无缝从上一页切换过来,可能会有点卡顿
  • 所有实现细节都是通过系统官方的public API,不存在任何trick行为,稳定性好

缺点:


在用户体验上没发现缺点。只是在封装为独立组件时需要注意更多细节,比如:

  • 该组件将CollectionView封装了起来,所以必须给外部使用者暴露dataSource和delegate等必要的回调和数据源方法

使用UIScrollView完全自定义实现


我还看过另一种方案:

  • 自己创建cell视图,添加到UIScrollView上
  • 完全由自己来控制cell的复用和显示逻辑
  • 滑动手势和效果方面,利用UIScrollViewDelegate方法来控制抬起手指后移动到到下一个或上一个cell的效果(该效果我曾经也实现过,可以参考设计与Swipe-Delete不冲突的UIPageViewController

这个思路看上去应该是可行的,我也看过类似的源码实现,是Github上的一个代码


但该源码的显示逻辑写的不好:

  • 每次切换cell时,会同时通过delegate要求更新所有的cell数据(显示在屏幕中的cell和在缓存池中未用到的cell)

作者:songgeb
链接:https://juejin.cn/post/7165738755490807844
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS17兼容问题,[NSURL URLWithString:]逻辑BUG,影响WKWebView

iOS
[NSURL URLWithString:urlString]默认实现逻辑变动 [NSURL URLWithString:urlString]以前的逻辑是urlString有中文字符就返回nil,现在是默认对非法字符(包含中文)进行%转义。 URLWithSt...
继续阅读 »

[NSURL URLWithString:urlString]默认实现逻辑变动


[NSURL URLWithString:urlString]以前的逻辑是urlString有中文字符就返回nil,现在是默认对非法字符(包含中文)进行%转义。


URLWithString:方法并没有给出说明,但是iOS17新增了URLWithString:encodingInvalidCharacters:方法,具体可以参照此方法。

/// Initializes and returns a newly created `NSURL` with a URL string and the option to add (or skip) IDNA- and percent-encoding of invalid characters.
/// If `encodingInvalidCharacters` is false, and the URL string is invalid according to RFC 3986, `nil` is returned.
/// If `encodingInvalidCharacters` is true, `NSURL` will try to encode the string to create a valid URL.
/// If the URL string is still invalid after encoding, `nil` is returned.
///
/// - Parameter URLString: The URL string.
/// - Parameter encodingInvalidCharacters: True if `NSURL` should try to encode an invalid URL string, false otherwise.
/// - Returns: An `NSURL` instance for a valid URL, or `nil` if the URL is invalid.
+ (nullable instancetype)URLWithString:(NSString *)URLString encodingInvalidCharacters:(BOOL)encodingInvalidCharacters API_AVAILABLE(macos(14.0), ios(17.0), watchos(10.0), tvos(17.0));

附带的BUG


这一个改动本来没有什么大问题,但问题是有BUG。


如果urlString中没有中文,那urlString里原有的%字符不会转义。

(lldb) po [NSURL URLWithString:@"http://a.com?redirectUri=http%3A%2F%2Fb.com"]
http://a.com?redirectUri=http%3A%2F%2Fb.com

如果urlString中有中文字符,那么中文字符和%字符都会被转义,最终会影响运行效果。


(我就是因为这个BUG,从而导致原本能正常进行302重定向的页面无法重定向。)

(lldb) po [NSURL URLWithString:@"http://a.com?title=标题&redirectUri=http%3A%2F%2Fb.com"]
http://a.com?title=%E6%A0%87%E9%A2%98&redirectUri=http%253A%252F%252Fb.com

修改方案


对原方法进行替换,保证[NSURL URLWithString:urlString]在iOS17系统上的运行逻辑和iOS17以下系统保持一致。这样对于现有代码逻辑的影响最小。

#import "NSURL+iOS17.h"

@implementation NSURL (iOS17)

+(void)load {
[self sv_swizzleClassMethod:@selector(URLWithString:) withClassMethod:@selector(wt_URLWithString:) error:NULL];
}

+ (instancetype)wt_URLWithString:(NSString *)URLString {
if (@available(iOS 17.0, *)) {
return [self URLWithString:URLString encodingInvalidCharacters:NO];
} else {
return [self wt_URLWithString:URLString];
}
}

@end

作者:奇风FantasyWind
链接:https://juejin.cn/post/7269390456530157608
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS小技能:去掉/新增导航栏黑边(iOS13适配)

iOS
引言 背景: 去掉导航栏下边的黑边在iOS15失效 原因:必须使用iOS13之后的APIUINavigationBarAppearance设置才能生效UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) NS_SW...
继续阅读 »

引言


背景: 去掉导航栏下边的黑边在iOS15失效
原因:必须使用iOS13之后的APIUINavigationBarAppearance设置才能生效

UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) NS_SWIFT_UI_ACTOR
@interface UINavigationBarAppearance : UIBarAppearance

I 导航栏的黑边设置


1.1 去掉导航栏下边的黑边(iOS15适配)



iOS15之前: [self.navigationBar setShadowImage:[[UIImage alloc] init]];

        [vc.navigationController.navigationBar setBackgroundImage:[ImageTools createImageWithColor: [UIColor whiteColor]] forBarMetrics:UIBarMetricsDefault];


iOS15之后


if(@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];

//去掉透明后导航栏下边的黑边
appearance.shadowImage =[[UIImage alloc] init];

appearance.shadowColor= UIColor.clearColor;



navigationBar.standardAppearance = appearance;

navigationBar.scrollEdgeAppearance = appearance;

}

1.2 设置导航栏下边的黑边(iOS13适配)




// 设置导航栏下边的黑边
+ (void)setupnavigationBar:(UIViewController*)vc{



if (@available(iOS 13.0, *)) {

UINavigationBar *navigationBar = vc.navigationController.navigationBar;

UINavigationBarAppearance *appearance =navigationBar.standardAppearance;


appearance.shadowImage =[UIImage createImageWithColor:k_tableView_Line];

appearance.shadowColor=k_tableView_Line;


navigationBar.standardAppearance = appearance;
navigationBar.scrollEdgeAppearance = appearance;

} else {
// Fallback on earlier versions

UINavigationBar *navigationBar = vc.navigationController.navigationBar;
[navigationBar setBackgroundImage:[[UIImage alloc] init] forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; //此处使底部线条颜色为红色
// [navigationBar setShadowImage:[UIImage createImageWithColor:[UIColor redColor]]];

[navigationBar setShadowImage:[UIImage createImageWithColor:k_tableView_Line]];

}



}




II 去掉TabBar的顶部黑线


  • setupshadowColor


- (void)setupshadowColor{

UIView * tmpView = self;
tmpView.layer.shadowColor = [UIColor blackColor].CGColor;//设置阴影的颜色
tmpView.layer.shadowOpacity = 0.08;//设置阴影的透明度
tmpView.layer.shadowOffset = CGSizeMake(kAdjustRatio(0), kAdjustRatio(-5));//设置阴影的偏移量,阴影的大小,x往右和y往下是正
tmpView.layer.shadowRadius = kAdjustRatio(5);//设置阴影的圆角,//阴影的扩散范围,相当于blur radius,也是shadow的渐变距离,从外围开始,往里渐变shadowRadius距离


//去掉TabBar的顶部黑线
[self setBackgroundImage:[UIImage createImageWithColor:[UIColor clearColor]]];
[self setShadowImage:[UIImage createImageWithColor:[UIColor clearColor]]];

}


see also


iOS小技能:自定义导航栏,设置全局导航条外观。(iOS15适配)
blog.csdn.net/z929118967/…


作者:公众号iOS逆向
链接:https://juejin.cn/post/7146388120076812324
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

苹果回应 iPhone14 电池老化快:属于正常现象,iPhone 15 系列顶配机型有望首次搭载潜望式镜头

iOS
国内要闻 曝小米自研系统为全端系统 日前,有数码博主爆料,小米自研操作系统属于全端自研系统,兼容AOSP(Android 开放源代码项目)。如此看来,小米自研操作系统还可能有车机、平板、手表等终端系统,而且小米走的是华为鸿蒙操作系统的路子,前期先兼容安卓更为稳...
继续阅读 »



国内要闻


曝小米自研系统为全端系统

日前,有数码博主爆料,小米自研操作系统属于全端自研系统,兼容AOSP(Android 开放源代码项目)。如此看来,小米自研操作系统还可能有车机、平板、手表等终端系统,而且小米走的是华为鸿蒙操作系统的路子,前期先兼容安卓更为稳妥,保住既有的用户量。(手机中国)


华为辟谣网传3.2万名科学家正式移籍:造谣者毫无根据、无中生有

近日,网络上多家平台发布了针对华为公司的系列言论,经证实,该系列言论均为谣言,华为表示,造谣者毫无根据、无中生有。此外,华为呼吁各位网友“勿信勿传,果断举报”。(第一财经)


清华大学联合字节跳动,开源听觉大语言模型 SALMONN

清华大学联合字节火山语音团队提出了一种全新的「听觉」大语言模型——SALMONN (Speech Audio Language Music Open Neural Network)。相较于仅仅支持语音输入或非语音音频输入的其他大模型,SALMONN 对语音、音频事件、音乐等各类音频输入都具有感知和理解能力,相当于给大语言模型「加了个耳朵」,从而涌现出多语言和跨模态推理等高级能力。


钉钉公布 AI 版本商业定价:调用一次大模型不到 5 分钱

8 月 22 日,钉钉召开 2023 生态大会,据总裁叶军介绍:截至今年 3 月末,钉钉软件付费企业数达 10 万家,其中小微企业占 58%,中型企业占 30%,大型企业占 12%;钉钉付费 DAU 超过 2300 万。这也是钉钉首次公布其商业化核心进展。此外,钉钉还公布了大模型落地应用场景的商业化方案:在钉钉专业版年费 9800 元基础上,增加 10000 元即可获得 20 万次大模型调用额度;在专属钉钉年费基础上,增加 20000 元即可获得 45 万次大模型调用额度,约等于平均每次调用只需 0.44~5 分钱。(IT 之家)


吉利回应与百度合作造车:开发一直为吉利主导,百度提供技术支持

不久前,吉利与百度合作造车计划突遭生变,“集度”变身“极越”。近日,在吉利汽车半年业绩发布会上,吉利控股集团CEO李东辉回应腾讯新闻《远光灯》,极越定位为吉利控股旗下高端智能汽车机器人品牌。在极越的开发过程中,一直都是吉利控股来主导的,百度提供了大数据、无人驾驶等领域的技术支持。


非法注册 300 万个微信号!央视曝光特大黑灰产系列案

8月7日、8日、9日中午,中央电视台《今日说法》栏目以《揭秘“黑灰产”》为题,分上、中、下三集对山东淄博周村公安分局破获的特大黑灰产案件侦破过程进行专题报道。犯罪分子批量注册并贩卖微信号,形成产业链。这些微信号多用于电信诈骗等违法犯罪活动。该犯罪团伙共非法注册微信号 300 余万个,非法获利达 1000 余万元。警方通过追查嫌疑人注册微信的手机号码来源,打掉一个号商团伙,揪出二十余名省级运营商“内鬼”。他们利用手中的权力,牟取巨额私利,为犯罪团伙提供非法注册的手机号码。(今日说法)


国际要闻


苹果回应 iPhone14 电池老化快:属于正常现象

苹果公司的 iPhone 14 系列手机上市不到一年,就出现了电池健康度下降过快的问题。一些用户反映,他们的手机电池在使用几个月后,就损耗了 10% 以上的容量。苹果公司表示,这种情况属于正常现象,只有当电池容量低于 80% 时,才能在保修期内享受免费更换服务。


据了解,如果使用非正品电池或者其他 iPhone 14 手机上拆下来的电池进行更换,那么手机将无法识别新电池,并且会禁用电池健康度功能,这意味着用户无法查看电池的剩余容量和性能状况。(IT之家)


Meta 推出 AI 模型 SeamlessM4T,可翻译和转录近百种语言

Meta 近日发布了人工智能模型 SeamlessM4T,可以翻译和转录近 100 种语言的文本和语音。SeamlessM4T 支持对近百种语言进行语音以及文本识别,同时支持近 100 种输入语言和 36 种输出语言的语音到语音翻译。Meta 表示,将会以研究许可证的形式公开发布 SeamlessM4T,以便研究人员和开发人员在此基础上开展工作。Meta 还将发布 SeamlessAlign 的元数据,这是迄今为止最大的开放式多模态翻译数据集,共挖掘了 27 万小时的语音和文本对齐。(品玩)


iPhone 15 系列顶配机型有望首次搭载潜望式镜头

来自摩根士丹利的一份分析师报告指出,iPhone 15 Pro Max(或改名为 iPhone 15 Ultra)将获得苹果有史以来第一款潜望式镜头,其变焦能力从前代的 3 倍将提升到 5-6 倍,表现令人相当期待。这份报告还提到,由于全新传感器的加入,使 iPhone 15 系列顶配机型的备货能力受到影响,或许会在 iPhone 15 系列开售后 3-4 周时间才会陆续发货。毫无疑问,这将会是 iPhone 15 系列中最值得关注的一款机型。(雷科技)


微软宣布将把动视暴雪云游戏权益出售给育碧,以安抚英国监管机构

8 月 22 日消息,据外媒报道,当地时间周一,微软宣布将把动视暴雪的云游戏权益出售给育碧,以重组其拟议的动视暴雪收购交易。报道称,微软此举旨在安抚英国监管机构英国竞争和市场管理局(CMA),因为该机构担心这笔交易会扼杀快速增长的云游戏市场的竞争。当地时间周一,微软与育碧签署了一项为期 15 年的协议,将《使命召唤》等动视暴雪的云游戏相关权益授权给育碧,这是微软为其收购动视暴雪获得反垄断批准的最新举措。(TechWeb)


Meta 推出拥有 12 种复杂技能机器人,上得厅堂下得厨房

耗时 2 年,Meta 联手卡耐基梅隆大学推出通用机器人智能体——RoboAgent,可以通过图像或者语言指令,来指挥机器人完成任务。它拥有 12 种不同的复杂技能,泡茶、烘焙不在话下,未来还能泛化 100 多种未知任务。(网易科技)


IBM 推企业级 AI 平台!剑指企业级 AI 应用三大挑战

日前,IBM 面向中国区正式推出企业级 AI 平台 watsonx,包含企业级 AI 与数据平台 watsonx.ai、湖仓一体的数据存储方案 watsonx.data 以及 AI 治理工具包 watsonx.governance。


程序员专区


微软 Excel 宣布集成 Python

微软已经将 Python 原生集成到 Excel 公测版中,首先向 Microsoft 365 Insiders 推出,从而使用户能够借助 Python 库、数据可视化和分析的能力更好地使用 Excel。目前该功能只能在桌面版 Excel 中使用,但微软表示 Python 计算也可以在微软云中运行。


Google 更新 Android 运行时应用提速最高三成

Android 运行时 (Android Runtime 或 ART)的最新更新将帮助应用在部分设备上的启动时间缩短最多 30%。ART 是 Android 操作系统的引擎,提供了所有 Android 应用和绝大多数服务所依赖的运行时和核心 API。改进 ART 将能让所有开发者受益,让应用执行更快,字节码编译更高效。Google 表示它正致力于让 ART 模块化独立于操作系统更新。ART 的可独立更新将能让用户更快获得性能优化和安全更新,让开发者更快获得 OpenJDK 改进和编译器优化。它的测试显示,ART 13 的运行时和编译器优化在部分设备上实现了最高 30% 的应用启动改进。


作者:小莫欢
链接:https://juejin.cn/post/7270343009789624372
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS16 中的 3 种新字体宽度样式

iOS
前言 在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。CompressedCondensedExpend UIFont.Width Apple 引入了新的结构体 UIFont.Width,这代表了一种新的宽度样式。 目前已有的四...
继续阅读 »

前言


在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。

  1. Compressed

  2. Condensed

  3. Expend



UIFont.Width


Apple 引入了新的结构体 UIFont.Width,这代表了一种新的宽度样式。


目前已有的四种样式。

  • standard:我们总是使用的默认宽度。

  • compressed:最窄的宽度样式。

  • condensed:介于压缩和标准之间的宽度样式。

  • expanded:最宽的宽度样式。



SF 字体和新的宽度样式


如何将 SF 字体和新的宽度样式一起使用


为了使用新的宽度样式,Apple 有一个新的 UIFont 的类方法来接收新的 UIFont.Width

class UIFont : NSObject {
class func systemFont(
ofSize fontSize: CGFloat,
weight: UIFont.Weight,
width: UIFont.Width
) -> UIFont
}

你可以像平常创建字体那样来使用新的方法。

let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed)
let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed)
let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard)
let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded)

SwiftUI



更新:在 Xcode 14.1 中,SwiftUI 提供了两个新的 API 设置这种新的宽度样式。
width(_:)fontWidth(_:)



目前(Xcode 16 beta 6),这种新的宽度样式和初始值设定只能在 UIKit 中使用,幸运的是,我们可以在 SwiftUI 中轻松的使用它。


有很多种方法可以将 UIKit 集成到 SwiftUI 。我将会展示在 SwiftUI 中使用新宽度样式的两种方法。

  1. 将 UIfont 转为 Font。
  2. 创建 Font 扩展。

将 UIfont 转为 Font


我们从 在 SwiftUI 中如何将 UIFont 转换为 Font 中了解到,Font 有初始化方法可以接收 UIFont 作为参数。


步骤如下

  1. 你需要创建一个带有新宽度样式的 UIFont。
  2. 使用该 UIFont 创建一个 Font 。
  3. 然后像普通 Font 一样使用它们。
struct NewFontExample: View {
// 1
let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed)
let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed)
let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard)
let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded)

var body: some View {
VStack {
// 2
Text("Compressed")
.font(Font(compressed))
Text("Condensed")
.font(Font(condensed))
Text("Standard")
.font(Font(standard))
Text("Expanded")
.font(Font(expanded))
}
}
}

  • 创建带有新宽度样式的 UIFont。
  • 用 UIFont 初始化 Font, 然后传递给 .font 修改。

创建一个 Font 扩展


这种方法实际上和将 UIfont 转为 Font 是同一种方法。我们只需要创建一个新的 Font 扩展在 SwiftUI 中使用起来更容易一些。

extension Font {
public static func system(
size: CGFloat,
weight: UIFont.Weight,
width: UIFont.Width) -> Font
{
// 1
return Font(
UIFont.systemFont(
ofSize: size,
weight: weight,
width: width)
)
}
}

创建一个静态函数传递 UIFont 需要的参数。然后,初始化 UIFont 和创建 Font


我们就可以像这样使用了。

Text("Compressed")
.font(.system(size: 46, weight: .bold, width: .compressed))
Text("Condensed")
.font(.system(size: 46, weight: .bold, width: .condensed))
Text("Standard")
.font(.system(size: 46, weight: .bold, width: .standard))
Text("Expanded")
.font(.system(size: 46, weight: .bold, width: .expanded))

如何使用新的宽度样式


你可以在你想使用的任何地方使用。不会有任何限制,所有的新宽度都有一样的尺寸,同样的高度,只会有宽度的变化。


这里是拥有同样文本,同样字体大小和同样字体样式的不同字体宽度样式展示。



新的宽度样式优点


你可以使用新的宽度样式在已经存在的字体样式上,比如 thin 或者 bold ,在你的 app 上创造出独一无二的体验。


Apple 将它使用在他们的照片app ,在 "回忆'' 功能中,通过组合不同的字体宽度和样式在标题或者子标题上。



这里有一些不同宽度和样式的字体组合,希望可以激发你的灵感。

Text("Pet Friends")
.font(Font(UIFont.systemFont(ofSize: 46, weight: .light, width: .expanded)))
Text("OVER THE YEARS")
.font(Font(UIFont.systemFont(ofSize: 30, weight: .thin, width: .compressed)))

Text("Pet Friends")
.font(Font(UIFont.systemFont(ofSize: 46, weight: .black, width: .condensed)))
Text("OVER THE YEARS")
.font(Font(UIFont.systemFont(ofSize: 20, weight: .light, width: .expanded)))


你也可以用新的宽度样式来控制文本的可读性。


下面的这个例子,说明不同宽度样式如何影响每行的字符数和段落长度



下载这种字体


你可以在 Apple 字体平台 来下载这种新的字体宽度样式。


下载安装后,你将会发现一种结合了现有宽度和新宽度样式的新样式。




基本上,除了在模拟器的模拟系统 UI 中,在任何地方都被禁止使用 SF 字体。请确保你在使用前阅读并理解许可证。


作者:Swift社区
链接:https://juejin.cn/post/7158003140258709518
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不用太深奥简单解决iOS上拉边界下拉白色空白问题

iOS
表现 手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。 产生原因 在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview ...
继续阅读 »

表现


手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。




产生原因


在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器,容器自然会被拖动,剩下的部分会成空白。




解决方案


1. 监听事件禁止滑动


移动端触摸事件有三个,分别定义为

  1. touchstart :手指放在一个DOM元素上。

  2. touchmove :手指拖曳一个DOM元素。

  3. touchend :手指从一个DOM元素上移开。


显然我们需要控制的是 touchmove 事件,由此我在 W3C 文档中找到了这样一段话


Note that the rate at which the user agent sends touchmove events is implementation-defined, and may depend on hardware capabilities and other implementation details.(注意,用户代理发送touchmove事件的速率是实现定义的,并且可能取决于硬件功能和其他实现细节。)


If the preventDefault method is called on the first touchmove event of an active touch point, it should prevent any default action caused by any touchmove event associated with the same active touch point, such as scrolling.(如果在活动触摸点的第一个touchmove事件上调用preventDefault方法,它应该防止由与同一个活动触摸点关联的任何touchmove事件(如滚动)引起的任何默认操作。)


touchmove 事件的速度是可以实现定义的,取决于硬件性能和其他实现细节


preventDefault 方法,阻止同一触点上所有默认行为,比如滚动。




由此我们找到解决方案,通过监听 touchmove,让需要滑动的地方滑动,不需要滑动的地方禁止滑动。


值得注意的是我们要过滤掉具有滚动容器的元素。


实现如下:

document.body.addEventListener('touchmove', function(e) {
if (e._isScroller) return;
// 阻止默认事件
e.preventDefault();
}, {
passive: false
});

2. 滚动妥协填充空白,装饰成其他功能


在很多时候,我们可以不去解决这个问题,换一直思路。


根据场景,我们可以将下拉作为一个功能性的操作


作者:会飞的金鱼
链接:https://juejin.cn/post/7160968758570762254
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

可能是全网第一个适配iOS灵动岛的Toast库-JFPopup

iOS
前言 我去年的一篇文章详细的介绍了我编写的一套Swift弹窗组件库一个优雅的Swift弹窗组件-JFPopup。里面适配了一套ToastView,恰逢今年苹果iPhone14 Pro以上系列新出了一套灵动岛的交互风格,所以就意外想到能否把ToastView也适...
继续阅读 »

前言


我去年的一篇文章详细的介绍了我编写的一套Swift弹窗组件库一个优雅的Swift弹窗组件-JFPopup。里面适配了一套ToastView,恰逢今年苹果iPhone14 Pro以上系列新出了一套灵动岛的交互风格,所以就意外想到能否把ToastView也适配进去灵动岛,所以此文就应运而生。我上篇文章已经很详细的介绍了JFPopup具体用法,这篇文章主要讲解适配灵动岛的心路历程。


具体效果:




用法


虽然我上篇文章已经介绍了一遍,这里我还是再写一下。另外灵动岛Toast默认适配iPhone14 Pro以上机型,无需另外操作,若不是灵动岛机型,则是默认居中,还支持top及bottom。更多详细参数请看一个优雅的Swift弹窗组件-JFPopup


Toast:


//默认仅文案

JFPopupView.popup.toast(hit: "默认toast,支持灵动岛")

//带logo ,内置success or fail

JFPopupView.popup.toast(hit: "支付成功", icon: .success)

JFPopupView.popup.toast(hit: "支付失败", icon: .fail)

//自定义logo

JFPopupView.popup.toast(hit: "自定义", icon: .imageName(name: "face"))


Loading:


DispatchQueue.main.async {

JFPopupView.popup.loading()

}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {

JFPopupView.popup.hideLoading()

JFPopupView.popup.toast(hit: "刷新成功")

}


适配灵动岛具体过程


由于苹果官方已经说了要在下半年推出的ActivityKit才会加入适配灵动岛的Api。所以目前并没有官方的api可以给我们适配。所以只能硬着头皮自己去思考适配方案了。



- 首先要知道灵动岛的区域大小


我们用最笨的方法,直接给模拟器截个图自己去算大小。至少能还原99%的效果了。如图得知,灵动岛的区域大概是宽120dt,高34dp,那半圆圆角自然为17dt。居顶部大约10dp,以及在屏幕居中。有了这些信息,我们自然就能模拟灵动岛的放大缩小转场效果了。



- ToastView新增灵动岛动画


我们在原先基础上新增灵动岛动画枚举


public enum JFToastPosition {

case center

case top

case bottom

case dynamicIsland //新增灵动岛位置动画

}


重新实现下present 及 dismiss协议的转场动画代码如下


展开:


let originSize = contianerView.jf.size

if config.toastPosition == .dynamicIsland {

contianerView.jf_size = CGSize(width: 120, height: 34)

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: 27)

}

let updateV = {

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: CGSize.jf.screenSize().height / 2)

if config.toastPosition == .top {

contianerView.jf_top = CGFloat.jf.navigationBarHeight() + 15

} else if config.toastPosition == .bottom {

contianerView.jf_bottom = CGSize.jf.screenHeight() - CGFloat.jf.safeAreaBottomHeight() - 15

} else if config.toastPosition == .dynamicIsland {

contianerView.jf_size = originSize

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: originSize.height / 2 + 10)

}

contianerView.layoutIfNeeded()

}

guard config.withoutAnimation == false else {

updateV()

transitonContext?.completeTransition(true)

completion?(true)

return

}

if config.toastPosition == .dynamicIsland {

UIView.animate(withDuration: 0.25) {

updateV()

} completion: { finished in

transitonContext?.completeTransition(true)

completion?(finished)

}

return

}


消失:


UIView.animate(withDuration: 0.25, animations: {

if config.toastPosition == .dynamicIsland {

contianerView?.layer.cornerRadius = 17

contianerView?.jf_size = CGSize(width: 120, height: 34)

contianerView?.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: 27)

}

contianerView?.subviews.forEach({ v in

if config.toastPosition == .dynamicIsland {

v.isHidden = true

} else {

v.alpha = 0

}

})

contianerView?.alpha = 0

}) { (finished) in

transitonContext?.completeTransition(true)

completion?(finished)

}


末尾


以上即是我JFPopup内置组件JFToastView适配灵动岛动画的全过程,假如下半年苹果更新了Api我也会第一时间重新适配。


作者:jerryfans
链接:https://juejin.cn/post/7145630021372084232
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift 中怎样更快地 reduce

iOS
在 Swift 中,对于集合类型,Swift 标准库提供了若干方便的方法,可以对数据进行处理,其中一个比较常见的就是 reduce。reduce 这个单词,通过查阅字典,可以发现其有“简化、归纳”的意思,也就是说,可以用 reduce 把一组数据归纳为一个数据...
继续阅读 »

在 Swift 中,对于集合类型,Swift 标准库提供了若干方便的方法,可以对数据进行处理,其中一个比较常见的就是 reduce。reduce 这个单词,通过查阅字典,可以发现其有“简化、归纳”的意思,也就是说,可以用 reduce 把一组数据归纳为一个数据,当然这个一个数据也可以是一个数组或任何类型。


比较常见的 reduce 使用案例,例如:


求和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
print(sum) // 输出 15

字符串拼接:

let words = ["hello", "world", "how", "are", "you"]
let sentence = words.reduce("", { $0 + " " + $1 })
print(sentence) // 输出 " hello world how are you"

两个 reduce API


观察 reduce 方法的声明,会发现有两个不同的 API,一个是 reduce 一个是 reduce(into:),他们的功能是一样的,但是却略有不同。


reduce 方法的函数签名如下:

func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result

该方法接收一个初始值和一个闭包作为参数,该闭包将当前的结果值和集合中的下一个元素作为输入,并返回一个新的结果值。reduce 方法依次迭代集合中的每个元素,并根据闭包的返回值更新结果值,最终返回最终结果值。


还是回到最简单的求和上来,下面的代码使用 reduce 方法计算一个数组中所有元素的总和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, { $0 + $1 })
print(sum) // 输出 15

reduce(into:) 方法的函数签名如下:

func reduce<Result>( into initialResult: __owned Result, _ updateAccumulatingResult: (inout Result, Element) throws -> Void ) rethrows -> Result

该方法接收一个初始值和一个闭包作为参数,该闭包将当前的结果值和集合中的下一个元素作为输入,并使用 inout 参数将更新后的结果值传递回去。reduce(into:) 方法依次迭代集合中的每个元素,并根据闭包的返回值更新结果值,最终返回最终结果值。


下面的代码使用 reduce(into:) 方法计算一个数组中所有元素的总和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(into: 0, { result, element in
result += element
})
print(sum) // 输出 15

可以看到,reduce(into:) 方法中闭包的参数使用了 inout 关键字,使得闭包内部可以直接修改结果值。这样可以避免不必要的内存分配和拷贝,因此在处理大量数据时,使用 reduce(into:) 方法可以提高性能。


观察源码


我们再通过观察源码证实这一结论


reduce 方法的源码实现如下:

public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult: (Result, Element) throws -> Result
) rethrows -> Result {
var accumulator = initialResult
for element in self {
accumulator = try nextPartialResult(accumulator, element)
}
return accumulator
}

可以发现这里有两处拷贝,一处是在 accumulator 传参给 nextPartialResult 时,一处是在把 nextPartialResult 的结果赋值给 accumulator 变量时,由于这里的 accumulator 的类型是一个值类型,每次赋值都会触发 Copy-on-Write 中的真正的拷贝。并且这两处拷贝都是在循环体中,如果循环的次数非常多,是会大大拖慢性能的。


再看 reduce(into:) 方法的源码:

func reduce<Result>( 
into initialResult: __owned Result,
_ updateAccumulatingResult: (inout Result, Element) throws -> Void
) rethrows -> Result {
var result = initialResult
for element in self {
try updateAccumulatingResult(&result, element)
}
return result
}

在方法的实现中,我们首先将 initialResult 复制到一个可变变量 result 中。然后,我们对序列中的每个元素调用 updateAccumulatingResult 闭包,使用 & 语法将 result 作为 inout 参数传递给该闭包。因此这里每次循环都是在原地修改 result 的值,并没有发生拷贝操作。


总结


因此,reduce 方法和 reduce(into:) 方法都可以用来将集合中的元素组合成单个值,但是对于会触发 Copy-on-Write 的类型来说, reduce(into:) 方法可以提供更好的性能。在实际使用中,应该根据具体情况选择合适的方法。


作者:SwiftFun
链接:https://juejin.cn/post/7219712310586982457
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如何用原生的方式来定义Swift JSON Model

iOS
在Swift开发中,处理JSON数据序列化是一项常见任务。由于Swift的类型安全特性,处理类似JSON这样的弱类型数据一直是一个挑战。然而,Swift 4引入了一个令人欣喜的特性,即Codable协议。Codable协议为我们提供了一种简洁的方式来序列化和反...
继续阅读 »

在Swift开发中,处理JSON数据序列化是一项常见任务。由于Swift的类型安全特性,处理类似JSON这样的弱类型数据一直是一个挑战。然而,Swift 4引入了一个令人欣喜的特性,即Codable协议。Codable协议为我们提供了一种简洁的方式来序列化和反序列化JSON数据。


尽管Codable协议在处理大多数情况下表现得很出色,但它并不能完全满足所有需求。例如,它不支持自动类型转换,也无法友好地处理默认值。


如果我们能解决这些问题,就能更完美地处理JSON数据了。我们可以自定义解码器和编码器,以提供更高级的功能。通过自定义解码器,我们可以实现类型的自动转换,将JSON数据转换为目标类型,而无需手动处理。此外,我们还可以通过自定义编码器,在编码过程中为属性设置默认值,以确保生成的JSON数据符合预期。


总之,通过充分利用Swift的特性和自定义解码器、编码器,我们可以更好地处理JSON数据,满足我们更复杂的需求。
传送门ObjMapper


Codable坑点1:不支持类型转换

// JSON:
{
"uid":"123456",
"name":"Harry",
"age":10
}

// Model:
struct Dog: Codable{
var uid: Int
var name: String?
var age: Int?
}

在json转换过程中,我们常常与遇到类型模型与json的类型不一致的情况,就像上面的uid字段,uid在json中是String,但是我们的模型是Int,由于swift是类型安全的,所以,转换就不会成功。


Codable坑点2:不支持默认值


话不多说,上代码

struct Activity: Codable {
enum Status: Int {
case start = 1//活动开始
case processing = 2//活动进行中
case end = 3//活动结束
}

var name: String
var status: Status//活动状态
}

这儿有一个活动,活动现目前有三种状态,到目前为止,一切都很美好。有一天,突然说需要给活动添加已下架的状态,what?

//JSON
{
"name": "元旦迎新活动",
"status": 4
}

用Activity解析上面的JSON就会报错,我们如何规避呢,像下面一样

var status: Status?

答案是no、no、no,因为可选值的解码所表达的是“如果不存在,则置为 nil”,而不是“如果解码失败,则置为 nil”。


解决方案


有没有更好的方式来处理上面这两个问题呢?具体代码见ObjMapper,这儿简单描述下如何使用。


1、Model与JSON相互转换

// JSON:
{
"uid":888888,
"name":"Tom",
"age":10
}

// Model:
struct Dog: Codable{
//如果字段不是可选类型,则使用Default,提供一个默认值,像下面一样
@Default<Int.Zero> var uid: Int
//如果是可选类型,则使用Backed
@Backed var name: String?
@Backed var age: Int?
}

//JSON to model
let dog = Dog.decodeJSON(from: json)

//model to json
let json = dog.jsonString

当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,ObjMapper 将会进行如下自动转换。自动转换不支持的值将会被设置为nil或者默认值。




2、Model的嵌套

let raw_json = """
{
"author":{
"id": 888888,
"name":"Alex",
"age":"10"
},
"title":"model与json互转",
"subTitle":"如何优雅的转换"
}
"""

// Model:
struct Author: Codable{
@Default<Int.Zero> var id: Int
@Default<String.Empty> var name: String
//使用Backed后,如果类型不匹配,则类型会自动转换
//比如,上面的json中,age是个字符串,我们定义的模型是Int,
//那么声明@Backed后,会自动转换成Int类型
@Backed var age: Int?
}

struct Article: Codable {
//如果json中的title为nil或者不存在,则会给title赋一个默认值
@Default<String.Empty> var title: String
var subTitle: String?
var author: Author
}

//JSON to model
let article = Article.decodeJSON(from: raw_json)

//model to json
let json = article.jsonString
print(article?.jsonString ?? "")

3、自定义类型的可选值


话不多说,上代码

struct Activity: Codable {
enum Status: Int {
case start = 1//活动开始
case processing = 2//活动进行中
case end = 3//活动结束
}

@Default<String.Empty> var name: String
var status: Status//活动状态
}

这儿有一个活动,活动现目前有三种状态,到目前为止,一切都很美好。有一天,突然说需要给活动添加已下架的状态,what?

//JSON
{
"name": "元旦迎新活动",
"status": 4
}

用Activity解析上面的JSON就会报错,我们如何规避呢,像下面一样

var status: Status?

答案是no、no、no,因为可选值的解码所表达的是“如果不存在,则置为 nil”,而不是“如果解码失败,则置为 nil”,那就用我们的Default吧,请看下面代码:

struct Activity: Codable {
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
case start = 1//活动开始
case processing = 2//活动进行中
case end = 3//活动结束
case unknown = 0//默认值,无意义

///Step 2:实现DefaultValue协议,指定一个默认值
static func defaultValue() -> Status {
return Status.unknown
}
}

@Default<String.Empty> var name: String
///Step 3:使用Default
@Default<Status> var status: Status//活动状态
}

//{"name": "元旦迎新活动", "status": 4 }
//Activity将会把status解析成unknown

4、为普通类型设置不一样的默认值


本库已经内置了很多默认值,比如Int.Zero, Bool.True, String.Empty...,如果我们想为字段设置不一样的默认值,见下面代码:

public extension Int {
enum One: DefaultValue {
static func defaultValue() -> Int {
return 1
}
}
}

struct Dog: Codable{
@Backed var name: String?
@Default<Int.Zero> var uid: Int
//如果json中没有age字段或者解析失败,则模型的age被设置成默认值1
@Default<Int.One> var age: Int
}

5、数组支持


对于数组,可以使用@Backed,@Default来解析

// JSON:
let raw_json = """
{
"code":0,
"message":"success",
"data": [{
"name": "元旦迎新活动",
"status": 4
}]
}
"""

struct Activaty: Codable{
@Default<String.Empty> var name: String
@Default<Int.Zero> var status: Int
}

// 如果数组是可选类型,可以使用@Backed
struct Response1: Codable {
@Default<Int.Zero> var code: Int
@Default<String.Empty> var message: String
@Backed var data: [Activaty]?
}

// 为数组,设置默认值,如果数组不存在或者解析错误,则使用默认值
struct Response2: Codable {
@Default<Int.Zero> var code: Int
@Default<String.Empty> var message: String
@Default<Array.Empty> var data: [Activaty]
}
//JSON to model
let rsp1 = Response1.decodeJSON(from: raw_json)
let rsp2 = Response2.decodeJSON(from: raw_json)

//model to json
let json1 = rsp1.jsonString
let json2 = rsp2.jsonString
// print(rsp1?.jsonString ?? "")
// print(rsp2?.jsonString ?? "")

6、设置通用类型


我们在开发过程中,第一个遇到的json可能是这样的:

// JSON:
{
"code":0,
"message":"success",
"data":[]//这个data可以是任何类型
}

由于data字段的类型不固定,有时候为了统一处理,我们定义模型可以像下面这样,有枚举类型JsonValue来表示。

struct Response: Codable { 
var code: Int
var message: String
var data: JsonValue?
}

如果要取data字段的值,我们可以这样用data?.intValue或者data?.arrayValue等等,具体使用见源码。


注意:这种对于data是一个简单的model(比如就是一个整形、字符串等等),可以起到事半功倍的效果;如果data是一个大型model,建议还是将data指定为具体类型。


7、如果是从1.0.x升级到2.0版本,修改了DefaultValue协议。如果之前的代码中使用了DefaultValue协议,则会报错,修改如下:

原来为:
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
case start = 1//活动开始

///Step 2:实现DefaultValue协议,指定一个默认值
static let defaultValue = Status.unknown
}

修改成:
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
case start = 1//活动开始

///Step 2:实现DefaultValue协议,返回一个默认值
static func defaultValue() -> Status {
return Status.unknown
}
}


参考文档

  1. 用 Codable 协议实现快速 JSON 解析
  2. Swift 4 踩坑之 Codable 协议
  3. 使用 Property Wrapper

不喜勿喷,有问题请留言😁😁😁,欢迎✨✨✨star✨✨✨和PR


作者:大儿童梦里花开
链接:https://juejin.cn/post/7260776783394422821
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS整理: 关于动态库和静态库

iOS
之前对于这两者的概念仅仅停留在八股文的认知水平(可能八股都答的一塌糊涂)亦或者就是道听途说,知道下怎么用就完事儿了,看了很多相关的资料,看了就忘,索性自己整理一下,理顺一下自己的思路,体系化的理解一下,防止自己变成脑残。。。 在此之前,我们对一些常识性的东西复...
继续阅读 »

之前对于这两者的概念仅仅停留在八股文的认知水平(可能八股都答的一塌糊涂)亦或者就是道听途说,知道下怎么用就完事儿了,看了很多相关的资料,看了就忘,索性自己整理一下,理顺一下自己的思路,体系化的理解一下,防止自己变成脑残。。。


在此之前,我们对一些常识性的东西复习一下


.a .framework

.a 是单纯的二进制文件,.framework是二进制文件+资源文件。


程序执行的流程


预处理--->编译--->汇编--->链接(汇编程序生成的目标文件并不能被立即执行,还需要通过链接器(Linker),将有关的目标文件彼此相连接,使得所有的目标文件成为一个能够被操作系统载入执行的统一整体。)

  • 静态链接直接在编译阶段就把静态库加入到可执行文件当中去。优点:不用担心目标用户缺少库文件。缺点:最终的可执行文件会较大;且多个应用程序之间无法共享库文件,会造成内存浪费。
  • 动态链接在链接阶段只加入一些描述信息,等到程序执行时再从系统中把相应的动态库加载到内存中去。优点:可执行文件小;多个应用程序之间可以共享库文件。缺点:需要保证目标用户有相应的库文件。

关于iOS应用的启动流程


1. 解析Info.plist


2. Mach-O(可执行文件)加载


dylib loading time


rebase/binding time


3. 程序执行


....

这里其实还是想简述一下加载流程,因为我在这儿一直也有个误区,应用在启动前静态库已经存在于可执行的二进制文件当中了,而动态库在启动后才进行加载等一系列操作。


为什么要阐述这些老生常谈的东西呢,因为我以前一直对动态库的加载和编译时机存在误解,我们拿一个具体的工程举例 




我们从产物的包内容找到一路找到可执行文件,可以看到可执行文件和framework是单独存在的
静态库在编译的时候就被打到二进制文件当中了


怎么区分动态库还是静态库


一般来说,动态库以 .dylib 或者 .framework 后缀结尾;静态库以 .a 和 .framework 结尾。
这里列出几种方法区分动态库还是静态库

  1. 查看Mach-O Type来区分
  2. 查看ipa的目录结构
  3. 通过file工具查看

动态库/静态库的加载过程 & 两者之间的区别


一般来说,build一个项目的过程是先compile然后再link,然后才有一个可执行文件。link的时候要做的一件事情就是把各种函数符号转换成函数调用地址,然后最终生成的可执行文件就能够直接调用到函数了。




1.静态库在build的时候就把库里面的代码链接进可执行文件。这里还要再补充一句,会将静态库中 被使用的部分 都添加到应用程序的可执行文件,这意味着应用程序的可执行文件大小会随着静态库数量的增加而增大。在运行时,静态库会随着应用程序的可执行文件一起加载到同一代码区。在 iOS 开发中,应用程序的可执行文件就是 ipa 解压后,包内容中与 app 同名的可执行文件




2.动态库的做法不一样,不会在build的时候就把代码link进可执行文件,这里我们只对动态链接库进行阐述
对于动态链接库而言,build可执行文件的时候需要指定它依赖哪些库,当可执行文件运行时,如果操作系统没有加载过这些库,那就会把这些库随着可执行文件的加载而加载进内存中,供可执行程序运行。如果多个可执行文件依赖同一个动态链接库,那么内存中只会有一份动态链接库的代码,然后把它共享给所有相关可执行文件(APP)的进程使用,所以它也叫共享库


那简言之:动态链接库在可执行文件得到运行的时候就加载 这句话很有营养


在ios程序的启动流程中,我们会先加载应用的可执行文件(这就包括了静态库文件)然后才是动态库的一系列加载流程(程序执行
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝,存在形式:.a和.framework
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。存在形式:.dylib和.framework


之前对这里一直存在误区,这里的加载是以可执行文件(APP)为单位的。还有就是我们这里谈的动态库都都是系统层面的动态库,区别也是针对于静态库和系统动态库而言的。另外就是静态库在一开始就存在于可执行文件中,而动态库在运行时动态的进行绑定。


use_frameworks!


podfile中经常会加上这句话,我们来看一下实际的作用和效果
当使用 use_frameworks的时候 cocoapods会生成对应的 frameworks 文件(动态库)
在Link Binary With Libraries:会生成Pods_工程名.framework,包含了其它用cocoapods导入的第三方框架的.framework文件




当不使用use_frameworks!(静态库)cocoapods会生成相应的 .a文件(静态链接库)
Link Binary With Libraries: libPods-工程名.a 包含了其他用cocoapods导入有第三库的 .a 文件




当然我还注意到一些其他文件的diff 比较令我好奇的就是这个modulemap 之前也没了解过,以后有机会研究一下




Xcode Embed




对于这个设置,之前也是不太清楚,

  • 对于 系统动态库,可以将 Embed 属性设置成 Do Not Embed,因为 iOS 系统提供了相关的库,我们无需将它们再嵌入到应用程序的 ipa 包中,如:Foundation.frameworkUIKit.framework
  • 对于 用户动态库,需要将 Embed 属性设置成 Embed,因为链接发生在运行时,链接器需要从应用程序的 ipa 包中加载完整的动态库。
  • 对于 静态库,需要将 Embed 属性设置成 Do Not Embed,因为链接发生在编译时,编译完成后相关代码都已经包含在了应用程序的可执行文件中了,无需在应用程序的 bundle 中再保存一份。

动态库和静态库的使用场景


静态库

  1. 静态库主要应用于模块化,分工合作
  2. 避免少量改动经常导致大量的重复编译连接
  3. 也可以重用,注意不是共享使用

动态库


1.使用动态库,可以将最终可执行文件体积缩小


2.对于 iOS 开发来说, 因为我们只能使用 Embedding Frameworks 来使用动态库, 这样的动态库并不是真正的动态库, 其会在编译时全部置入 app, 然后再 app 启动时全部加载, 这样的话会导致体积大, 加载速度慢.


动静态库的混用

  • 静态库可以依赖静态库
  • 动态库可以依赖动态库
  • 动态库不能依赖静态库! 动态库不能依赖静态库是因为静态库不需要在运行时再次加载, 如果多个动态库依赖同一个静态库, 会出现多个静态库的拷贝, 而这些拷贝本身只是对于内存空间的消耗

但其实两者之间都是可以通过各种操作进行依赖的
静态库也是可以依赖动态库的
动态库也是可以依赖静态库的


总结


以上就是我对动态库以及静态库一些盲区的的具体总结和详细分析,总的来说,对每个角色的定位,有了更清晰的认知


补充


用户创建伪动态库 和静态库有什么区别呢,如果有区别 具体是怎么应用的呢 有知道的朋友可以帮我解释下吗?不胜感激


参考链接


blog.csdn.net/GeekLee609/…


juejin.cn/post/704110…


zhuanlan.zhihu.com/p/346683326


http://www.jianshu.com/p/662832e16…


chuquan.me/2021/02/14/…


zhuanlan.zhihu.com/p/346683326


作者:脑残coder
链接:https://juejin.cn/post/7262558218168729660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

解决 App Store 默认语言设置的问题

iOS
问题背景 一个很奇怪的问题,在没有支持多语言的时候,明明在 App Store Connect 上选择了 Primary Language 为 Chinese,为什么在 App Store 页面上还是显示主要语言为英文? 问题解决 实际上在做 App 多语...
继续阅读 »

问题背景


一个很奇怪的问题,在没有支持多语言的时候,明明在 App Store Connect 上选择了 Primary Language 为 Chinese,为什么在 App Store 页面上还是显示主要语言为英文?






问题解决


实际上在做 App 多语言适配之前,除了 App Store Connect 上需要选择对应的 Primary Language 以外,代码配置上也仍然需要做一些配置,将中文设置为默认语言。


首先在本地化 Locallization 处增加新语言,位于 Project -- Info -- Localizations



注意下图是增加成功之后的结果,这一步只需要增加新语言就行了,不需要关注 Development Localization 是具体哪个语言





第二步是找到 project.pbxproj 文件(右键点击 .xcodeproj 项目文件,然后 show package contents,参考:stack overflow - Vladimir's Answer),并修改其中的 developmentRegion 字段。


如果上一步中成功增加了新的语言,那么在 knownRegions 处就能找到对应的。




问题验证


上面这么修改一番之后,其实已经成功了,那么接下来正常发版就可以生效了。不过在发版之前,最好可以提前检查一次:


在 App Store Connect -- TestFlight 中找到对应修改过后的包,然后找到 Build Metadata:




然后找到 Localizations,如果这里的语言更新成功,那么就代表没问题了!




作者:RickeyBoy
链接:https://juejin.cn/post/7237424021758443577
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift 周报 第三十期

iOS
前言 本期是 Swift 编辑组自主整理周报的第二十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。 欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。 求人不如求己,你多一样本领,就少一点啊乞求;Swift社区...
继续阅读 »

前言


本期是 Swift 编辑组自主整理周报的第二十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。


欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。


求人不如求己,你多一样本领,就少一点啊乞求;Swift社区让你多一样技能,少一些嘲讽!



周报精选


新闻和社区:码出新宇宙,WWDC23 就在眼前


提案:有 4 个提案通过,本期没有产生新的提案


Swift 论坛:PermutableCollection 协议


推荐博文:SwiftUI 中 LinearGradient的用法


话题讨论:


有博主在视频社交平台说,2023年已然迎来了经济危机,只是有些人不愿意相信而已,那么你认为国内2023年是否真的进入了经济危机?



上期话题结果



上期话题讨论结果表明,社交隔阂个人选择标准的提高是导致男女群体互不干涉的主要原因,而社会观念的变化也起到了一定的影响。这些因素共同作用导致了男群体和女群体相互独立地寻找伴侣的现象。


新闻和社区


App、App 内购买项目和订阅即将实行税率调整


App Store 的交易和支付机制旨在帮助你在覆盖全球的 175 个国家和地区的商店中,以 44 种货币为你的产品和服务便捷地进行定价与销售。Apple 会为开发者管理其中 70 多个国家和地区的税收,而且你还能够为 App 和 App 内购买项目分配税务类别。我们会根据税务法规的变化,定期更新你在某些地区的收益。


从 5 月 31 日起,你从 App 和 App 内购买项目 (包括自动续期订阅) 销售中获得的收益将进行调整,以反映以下税率调整。请注意,相关内容的价格将保持不变。


加纳:增值税率从 12.5% 上调至 15%。
立陶宛:对于符合条件的电子书和有声书,增值税率从 21% 下调至 9%。
摩尔多瓦:对于符合条件的电子书和期刊,增值税率从 20% 下调至 0%。
西班牙:收取 3% 的数字服务税。
由于巴西税务法规的变化,在巴西开展的所有 App Store 销售现由 Apple 代扣税款。我们会按月代扣代缴应向相应税务机关缴纳的税款。自 2023 年 6 月开始,你可以在 5 月份的收入中查看从你的收益中扣除的税款金额。巴西境内的开发者不会受到这一变化的影响。


以上调整生效后,App Store Connect 中“我的 App”的“价格与销售范围”部分会随即更新。一如既往,你可以随时更改你的 App 和 App 内购买项目的价格 (包括自动续期订阅)。现在,你可以从 900 个价格点中选择,为任何店面更改定价。


码出新宇宙



WWDC23 就在眼前。太平洋夏令时间 6 月 5 日上午 10 点,Apple 主题演讲将在 apple.com 和 Apple Developer App 线上提供,为本次大会拉开序幕。你还可以通过同播共享,邀请朋友一起观看。


现在,符合条件的开发者可以开始报名参加活动了。相关活动包括 Q&A、“会见演讲者”以及社区暖场活动等线上聊天室活动,旨在促进你与开发者社区和 Apple 专家的沟通和交流。


Apple 公证服务更新


正如去年在 WWDC (简体中文字幕) 上宣布的那样,如果你目前使用 altool 命令行工具或者 Xcode 13 或更早版本通过 Apple 公证服务对 Mac 软件进行公证,则需要改为使用 notarytool 命令行工具,或者升级到 Xcode 14 或更高版本。自 2023 年 11 月 1 日起,Apple 公证服务将不再接受从 altool 或者 Xcode 13 或更早版本上传的内容。已经过公证的现有软件可以继续正常工作。


Apple 公证服务是一个自动化系统,它会扫描 Mac 软件中有没有恶意内容,检查有没有代码签名问题,并快速返回结果。对软件进行公证可向用户保证,Apple 已检查且未发现软件中包含恶意软件。


为改进 Apple 平台的安全性和隐私保护,用于验证 App 和关联 App 内购买项目销售的 App Store 收据签名媒介证书将更新为使用 SHA-256 加密算法。此更新将分多个阶段完成,新的 App 和 App 更新可能会受影响,具体取决于它们验证收据的方式。


Apple 设计大奖入围名单公布



Apple 设计大奖旨在表彰在多元包容、乐趣横生、出色互动、社会影响、视觉图像,以及创新思维等类别中表现出色的 App 和游戏。马上一睹今年的入围作品,我们将在太平洋夏令时间 6 月 5 日下午 6:30 揭晓获奖者,敬请关注。


提案


通过的提案


SE-0399 value 包展开的元组 提案通过审查。该提案已在 二十九期周报 正在审查的提案模块做了详细介绍。


SE-0397 独立声明 Macros 提案通过审查。该提案已在 二十八期周报 正在审查的提案模块做了详细介绍。


SE-0392 自定义 Actor 执行器 提案通过审查。该提案已在 二十五期周报 正在审查的提案模块做了详细介绍。


SE-0390 **引入 @noncopyable ** 提案通过审查。该提案已在 二十四期周报 正在审查的提案模块做了详细介绍。


Swift论坛



  1. 讨论从 Realm 数据库迁移提示?


提问


目前正在寻求迁移到更轻量级的解决方案(realm 目前对我的用例来说太过分了)并且想迁移到 grdb,但不必将 realm 作为依赖项持续一年或更长时间......


回答


在没有 Realm 库的情况下,您是否能够读取 Realm 数据库文件的内容? 否则,您必须将 Realm 作为依赖项保留,直到您的用户迁移完毕。


您可以通过发布能够要求用户升级的应用程序版本来缩短时间跨度。 这将允许您使用 “Realm-only”、“Realm-to-GRDB” 和最终的 “GRDB-only” 版本进行过渡。



  1. 提议允许 protocol 嵌套在非通用上下文中


介绍


允许协议嵌套在非通用 struct/class/enum/actors 和函数中。


动机


将标称类型嵌套在其他标称类型中允许开发人员表达内部类型的自然范围——例如,String.UTF8View 是嵌套在 struct String 中的 struct UTF8View,它的名称清楚地传达了它作为 UTF-8 代码接口的用途 - 字符串值的单位。


但是,嵌套目前仅限于在其他 struct/class/enum/actors 中的 struct/class/enum/actors; 协议根本不能嵌套,因此必须始终是模块中的顶级类型。 这很不幸,我们应该放宽此限制,以便开发人员可以表达自然作用于某些外部类型的协议。


建议的解决方案


我们将允许在非泛型 struct/class/enum/actors 中以及在不属于泛型上下文的函数中嵌套协议。


例如,TableView.Delegate 自然是与表视图相关的委托协议。 开发人员应该这样声明它——嵌套在他们的 TableView 类中:

class TableView {
protocol Delegate: AnyObject {
func tableView(_: TableView, didSelectRowAtIndex: Int)
}
}

class DelegateConformer: TableView.Delegate {
func tableView(_: TableView, didSelectRowAtIndex: Int) {
// ...
}
}

目前,开发人员采用复合名称(例如 TableViewDelegate)来表达可以通过嵌套表达的相同自然范围。


作为一个额外的好处,在 TableView 的上下文中,可以使用更短的名称来引用嵌套协议委托(与所有其他嵌套类型一样):

class TableView {
weak var delegate: Delegate?

protocol Delegate { /* ... */ }
}

协议也可以嵌套在非泛型函数和闭包中。 不可否认,这在某种程度上是有限的实用性,因为对此类协议的所有一致性也必须在同一功能内。 但是,也没有理由人为地限制开发人员在函数中创建的模型的复杂性。 一些代码库(值得注意的是,Swift 编译器本身)使用带有嵌套类型的大型闭包,并且它们受益于使用协议的抽象。

func doSomething() {

protocol Abstraction {
associatedtype ResultType
func requirement() -> ResultType
}
struct SomeConformance: Abstraction {
func requirement() -> Int { ... }
}
struct AnotherConformance: Abstraction {
func requirement() -> String { ... }
}

func impl<T: Abstraction>(_ input: T) -> T.ResultType {
// ...
}

let _: Int = impl(SomeConformance())
let _: String = impl(AnotherConformance())
}


  1. 提议PermutableCollection 协议


简介


该提案旨在添加一个 PermutableCollection 协议,该协议将位于集合协议层次结构中的 Collection 和 MutableCollection 之间。


动机


在某些情况下,人们希望能够移动和排序元素,同时不允许(或限制)元素的突变。 鉴于大量不太重要的收集协议,这是一个值得注意的遗漏。 创建自定义集合类型时,PermutableCollection 协议在任何强制元素唯一性和/或身份的有序集合中都是首选。 用例将包括即将推出的 OrderedDictionary 和 OrderedSet。 对于不可变和可变集合,它还可以提供对 Swift 使用的底层(并且可能是高度优化的)排序算法的统一访问。


设计


协议设计简单,只需一个 swapAt 要求

/// A collection that supports sorting.
protocol PermutableCollection<Element> : Collection where Self.SubSequence : PermutableCollection {

mutable func swapAt(_ i: Index, _ j: Index)

}

通过 swapAt 函数,通过扩展添加额外的排序函数实现。

extension PermutableCollection {

mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) {
// move algorithm enacts changes via swapAt()
}

mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index {
// partition algorithm enacts changes via swapAt()
}

mutating func sort() where Self: RandomAccessCollection, Self.Element: Comparable {
// partition algorithm enacts changes via swapAt()
}

// ... more permutation operations that mimic those available for MutableCollection

}



  1. 讨论 Vapor 和 query 缓存?




  2. 讨论在 Swift 系统中,如何将文件内容读取为字符串?




提问


我有一个文件的 FileDescriptor:


let fd = try FileDescriptor.open(<#filepath#>, .readOnly) 我可以使用 fd.read(into:) 将文件内容加载到 UnsafeMutableRawBufferPointer,但这是将文件内容加载到字符串中的正确第一步吗? 如果是这样,


在将它传递给 fd.read(into:) 之前,

  1. 我需要使用 .allocate(byteCount:alignment:) 分配 UnsafeMutableRawBufferPointer。 正确的 byteCount 取决于文件的大小。那么如何使用 Swift System 获取文件的大小呢?
  2. 如何从 UnsafeMutableRawBufferPointer 获取字符串?

回答


可以参考这个Git库:github.com/tayloraswif…




  1. 讨论为什么我不能使用 @dynamicMemberLookup 转发 enum cases?




  2. 讨论如何在 swift-foundation 中正确地进行性能测试?




提问


我想对比一下swift-foundation 和 Xcode 自带的 JSONDecoder 解码的速度。


我在一个新项目中使用单元测试和 measureBlock 以及在 swift-foundation 中使用 JSONEncoderTests 对其进行了测试。


swift-foundation 中的 JSONDecoder 看起来太慢了,我认为这是因为 swift-foundation 还没有作为一个库被引入。


推荐博文


iOS crash 报告分析系列 - 看懂 crash 报告的内容


摘要: 本篇文章主要介绍了iOS崩溃报告的解读方法,从报告的 Header、Exception information、Diagnostic messages、Backtraces、Thread state 和 Binary images 六个部分详细讲解了各字段含义,并提供示例代码帮助读者更好地理解。同时也引导读者去深入学习符号化的相关知识来获得更多信息。通过阅读本文,开发者可轻松看懂代码中产生的崩溃报告,并进行问题定位和处理。


SwiftUI 中 LinearGradient的用法


摘要: 这篇博文探讨了在 SwiftUI 中使用 LinearGradient 为对象创建渐变颜色效果。它展示了如何定义颜色数组、使用标准和自定义起点和终点,以及设置坐标以改进铅笔对象上的颜色笔尖。本文还包括用于创建具有各种起点终点组合的不同线性渐变的示例代码。文章以示例结束,展示了如何使用这些技术来自定义一支蓝色铅笔或整套铅笔的外观。


Swift 中的动态成员查找


摘要: 本文介绍了 Swift 语言中的动态成员查找(Dynamic Member Lookup)特性。通过在类型上使用 @dynamicMemberLookup 属性,我们可以重载该类型的 subscript 方法来更方便地访问其数据。但是,这也意味着缺乏编译时安全性。为了解决这个问题,本文提到了使用 KeyPath 作为参数的 subscript 方法来实现编译时安全检查。最后,作者建议我们可以谨慎地使用 @dynamicMemberLookup 特性来改进 API 设计。


话题讨论


有博主在视频社交平台说,2023年已然迎来了经济危机,只是有些人不愿意相信而已,那么你认为国内2023年是否真的进入了经济危机?


1.是的。确实已经经济危机了,今年工作很难找,同事比以前更卷啦,各种裁员消息不断。


2.经济危机不可能。五一淄博接待游客超过了100万人次,人挤人的旅游景象依然常在。


3.经济危机应该是相对的。对于大多数上班族来说,2023年很难,奉劝大家且行且珍惜。


关于我们


Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。


特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。


作者:Swift社区
链接:https://juejin.cn/post/7242339570145886267
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS webview跳转链接带#问题

iOS
一、问题引出 在iOS中,如果WKWebview跳转的链接不带参数但是带了#网页锚点,而你这边项目因为要兼容所有跳转链接,对链接进行了百分比编码,将#编码为了23%, 那么将出现”无法显示网页“或空白网页的情况。 同时满足下面3个条件会出现这个问题:配置的广...
继续阅读 »

一、问题引出


在iOS中,如果WKWebview跳转的链接不带参数但是带了#网页锚点,而你这边项目因为要兼容所有跳转链接,对链接进行了百分比编码,将#编码为了23%, 那么将出现”无法显示网页“或空白网页的情况。



同时满足下面3个条件会出现这个问题:

  • 配置的广告跳转链接中带了#符号,即有网页锚点。
  • 链接中是没有参数部分的,即?param1=value1&之类的。
  • webview加载这个链接之前,对链接整体进行了百分比编码,“#”符号被编码为”23%“

在实际的场景中,产品或运维配置广告链接时,有时需要打开网页后跳转到某个元素节点的,也就是有链接中带#这种需求的。


为了兼容他们配置带#链接这种情况,我们iOS这边需要代码上做兼容。


二、问题根因


1. 链接中#的作用


一般用于较长网页中,跳转到网页中的某个节点。 



2. 对配置链接进行调试探索


拿一个链接进行举例:
"juejin.cn/post/717682…" ,


进行百分比编码后:

对于上述链接"#"不进行编码:
  • 直接能加载成功, 并且跳转到锚点‘heading-4’。
  • 如果锚点名称写错了,如‘heading-4’写成了‘heading-400’,那么也能加载成功,只不过不会跳到锚点。

那么为什么#被编码为23%之后,就不能请求成功呢?


3. 链接中#是否被编码,服务器收到请求时有何异同?


我们对链接进行百分比编码后,通过Charles抓包请求的结果: 


可以看到:

  • 如果#编码为23%,则服务器收到的请求路径也是带23%.
  • 如果是未编码#,则服务器收到的请求路径是不带#后面的内容的。

这也就是说,对于iOS端来说,客户端发送请求时未发送#及后面的内容,但是会发送23%及后面的内容。 具体的响应是服务器决定的。


其中#编码为23%的两种情况:

  • 23%后面还有/, 比如https:www.xxx.com/path1/path23%/
  • 23%后面没有/,比如https:www.xxx.com/path1/path23%https:www.xxx.com/path1/path23%section1

第一种情况下,有的网页能加载出来,有的网页会找不到网页,能否加载成功是根据服务器能否找到网页来定;第二种加载会失败,原因是23%也被服务器拿去查找资源路径。


我相信到这里,应该已经解释清楚了问题发生的原因。


三、兼容链接#的解决方案


我们客户端APP上显示的营销广告链接都是来源于后台配置的,有时配置的链接是有需要跳到锚点的需求的,那么我们该怎么兼容呢?

  • 需要对链接进行百分比编码.
  • 百分比编码时需要屏蔽掉#.

解决方案

let url = "https://juejin.cn/post/7176823567059779639#heading-4"
var notEncodeSet = CharacterSet.urlQueryAllowed

// 关键代码:
// 在对链接进行百分比编码时,不编码字符集中追加#
notEncodeSet.insert(charactersIn: "#")

if let urlPath = url.addingPercentEncoding(withAllowedCharacters: notEncodeSet) {
// 一般会有对path追加自定义公参或者设置自定义请求头之类的事情...
let URL = URL(string: urlPath)!
let request = MutableURLRequest(url: URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
// 具体的加载
webview.load(request as URLRequest)
}

使用Alamofire的字符编码不能解决问题


在找到上述原因后,我们可能会考虑使用Alamofire的字符集CharacterSet.afURLQueryAllowed使用来代替系统的CharacterSet.urlQueryAllowed去编码,但这样有用吗?


首先来看下CharacterSet.afURLQueryAllowed是怎么生成的:

public static let afURLQueryAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4

let subDelimitersToEncode = "!$&'()*+,;="

let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters)
}()

可以看到是由CharacterSet.afURLQueryAllowed中除去通用分隔符和子分隔符后生成,也就是说是系统字符集的一个子集,对于这个问题也是行不通的!!!


作者:sweet丶
链接:https://juejin.cn/post/7256014547067682853
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS小技能: 抽奖轮盘跑马灯边框的实现

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 前言 跑马灯的应用场景:iOS 抽奖轮盘边框动画 原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果 - (void)touchesBega...
继续阅读 »

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情


前言


跑马灯的应用场景:

  1. iOS 抽奖轮盘边框动画


原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

[self rotate:_rotaryTable];

}

/**

iOS翻牌效果

*/
- (void)rotate:(id)sender {

[UIView beginAnimations:@"View Filp" context:nil];
[UIView setAnimationDelay:0.25];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:sender
cache:NO];
[UIView commitAnimations];

}


2. 在待办界面或者工作台界面,往往需要应用到跑马灯的地方


原理:利用QMUIMarqueeLabel 进行cell封装简易的跑马灯 label 控件


文章:kunnan.blog.csdn.net/article/det…





如用户登陆未绑定手机号,进行提示。



简易的跑马灯 label 控件,在文字超过 label 可视区域时会自动开启跑马灯效果展示文字,文字滚动时是首尾连接的效果 



I iOS 抽奖轮盘边框动画


1.1 原理


用NSTimer无限替换UIImageView的Image为互为错位的bg_horse_race_lamp_1或者bg_horse_race_lamp_2,达到跑马灯的效果



应用场景: iOS 抽奖轮盘边框动画



审核注意事项:



  1. 在抽奖页面添加一句文案“本活动与苹果公司无关”
    2, 在提交审核时修改分级至17+



1.2 实现代码

//
// ViewController.m
// horse_race_lamp
//
// Created by mac on 2021/4/7.
#import <Masonry/Masonry.h>


#import "ViewController.h"
NSString *const bg_horse_race_lamp_1=@"bg_horse_race_lamp_1";
NSString *const bg_horse_race_lamp_2=@"bg_horse_race_lamp_2";

@interface ViewController ()
/**

用NSTimer无限替换bg_horse_race_lamp_1和bg_horse_race_lamp_2,达到跑马灯的效果

应用场景: iOS 抽奖轮盘边框动画
*/
@property (nonatomic,strong) UIImageView *rotaryTable;
@property (nonatomic,strong) NSTimer *itemBordeTImer;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.


//通过以下两张图片bg_lamp_1 bg_lamp_2,用NSTimer无限替换,达到跑马灯的效果
_rotaryTable = [UIImageView new];
_rotaryTable.tag = 100;

[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];

[self.view addSubview:_rotaryTable];

[_rotaryTable mas_makeConstraints:^(MASConstraintMaker *make) {

make.center.offset(0);

}];



_itemBordeTImer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(itemBordeTImerEvent) userInfo:nil repeats:YES];


[[NSRunLoop currentRunLoop] addTimer:_itemBordeTImer forMode:NSRunLoopCommonModes];







}
// 边框动画
- (void)itemBordeTImerEvent
{
if (_rotaryTable.tag == 100) {
_rotaryTable.tag = 101;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_2]];
}else if (_rotaryTable.tag == 101){
_rotaryTable.tag = 100;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];
}
}




@end


1.3 下载Demo


从CSDN下载Demo:https://download.csdn.net/download/u011018979/16543761



private :https://github.com/zhangkn/horse_race_lamp


作者:公众号iOS逆向
链接:https://juejin.cn/post/7128560927393185799
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

[译] 2021 年的 SwiftUI: 优势、劣势和缺陷

iOS
2021 年的 SwiftUI: 优势、劣势和缺陷 在生产环境使用 SwiftUI?仍然不可行。 过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数...
继续阅读 »

2021 年的 SwiftUI: 优势、劣势和缺陷



在生产环境使用 SwiftUI?仍然不可行。



由 Maxwell Nelson 在 Unsplash 发布


过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数问题的解决方法。


简而言之,SwiftUI 是一个很棒的框架,并且极具前景。我认为它就是未来。但是要达到和 UIKit 同等的可靠性和健壮性,可能还需要 3-5 年。但是这并不意味着现在不应该使用 SwiftUI。我的目的是帮助你理解它的利弊,这样你可以就 SwiftUI 是否适合下一个项目做出更明智的决定。


SwfitUI 的优势


1. 编写 SwiftUI 是一件乐事,而且你可以快速构建用户界面


使用 addSubviewsizeForItemAtIndexPath,小心翼翼地计算控件的大小与位置,应对烦人的约束问题,手动构建视图层次结构,这样的日子已经一去不复返了。SwiftUI 的声明式和响应式设计模式使得创建响应式布局和 React 一样简单,同时它还背靠 Apple 强大的 UIKit。用它构建、启动并运行视图快到不可思议。


2. SwiftUI 简化了跨平台开发


我最兴奋的事情就是只需要编写一次 SwiftUI 代码,就可以在 iOS (iPhone 和 iPad),WatchOS 和 macOS 上使用。同时开发和维护 Android 和 Windows 各自的代码库已经很困难了,所以在减少不同代码库的数量这方面,每一个小的改变都很有帮助。当然还是有一些缺点,我将会在 “劣势” 章节分享。


3. 你可以免费获取漂亮的转场效果,动画和组件


你可以把 SwiftUI 当作一个 UI 工具箱,这个工具箱提供了开发专业应用程序所需的所有构建块。另外,如果你熟悉 CSS 的 Transition 属性,你会发现 SwiftUI 也有一套类似的方法,可以轻松创建优雅的交互过程。声明式语法的魅力在于你只需要描述你需要什么样的效果,效果就实现了,这看上去像魔法一样,但是也有不好的一面,我之后将会介绍。


4. UI 是完全由状态驱动并且是响应式的


如果你熟悉 React 的话,SwiftUI 在这一点上完全类似。当你监听整个 UI 的”反应“,动画和所有一切的时候,你只需要修改 @State@Binding 以及 @Published 属性,而不是使用多达几十层的嵌套回调函数。使用 SwiftUI,你可以体会到 CombineObservableObject 以及 @StateObject 的强大。这方面是 SwiftUI 和 UIKit 最酷的区别之一,强大到不可思议。


5. 社区正在拥抱 SwiftUI


几乎每个人都在因为 SwiftUI 而兴奋。SwiftUI 有许多学习资源可供获取,从 WWDC 到书,再到博客 —— 资料就在那里,你只需要去搜索它。如果不想搜索的话,我这里也汇总了一份最佳社区资源列表。


拥有一个活跃且支持度高的社区可以加速学习,开发,并且大量的新库会使得 SwiftUI 用途更加广泛。


劣势


1. 不是所有组件都可以从 SwiftUI 中获取到


在 SwiftUI 中有许多缺失、不完整或者过于简单的组件,我将在下面详细介绍其中一部分。


使用 UIViewRepresentableUIViewControllerRepresentableUIHostingController 协议可以解决这一问题。前两个让你可以在 SwiftUI 视图层中嵌入 UIKit 视图和控制器。最后一个可以让你在 UIKit 中嵌入 SwiftUI 视图。在 Mac 开发中也存在类似的三种协议 (NSViewRepresentable 等)。


这些协议是弥补 SwiftUI 功能缺失的权宜之计,但并不是一直天衣无缝。而且,尽管 SwiftUI 的跨平台承诺很好,但是如果某些功能不可用的话,你仍然需要为 iOS 和 Mac 分别实现协议代码。


2. NavigationView 还没有真正实现


如果你想在隐藏导航栏的同时仍然支持滑动手势,这是不可能的。我最终参考一些找到的代码创建了一个 UINavigationController wrapper。尽管可以起作用,但这不是一个长远的解决方案。


如果你想要在 iPad 上拥有一个 SplitView,但目前你还不能以纵向模式同时展示主视图和详情视图。他们选择用一个简陋的按钮展示默认关闭的抽屉。显然,你可以通过添加 padding 来解决这个问题,它可以突出显示你在使用 SwiftUI 时必须做的事情。


当你想使用编程式导航的时候,NavigationLink 是一种流行的解决方案。这里有一个有趣的讨论


3. 文本输入十分受限


TextFieldTextEditor 现在都太简单了,最终你还是会退回到 UIKit。所以我不得不为 UITextFieldUITextView 构建自己的 UIViewRepresentable 协议(以实现文本行数的自动增加)。


4. 编译器困境


当视图开始变得笨重,并且你已经竭尽所能去提取分解,编译器仍然会冲着你咆哮:



The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.



这个问题已经多次拖慢进度。由于这个问题,我已经很擅长注释代码定位到引起问题的那一行,但是 2021 年了还在用这种方法调试代码感觉非常落后。


5. matchedGeometryEffect


我第一次发现这个的时候,感觉很神奇。它目的是通过匹配一隐一现的几何形状,帮助你更加流畅地转换两个不同标识的视图。我觉得这有助于从视图 A 优雅地转场到 B 视图。


我一直想让它起作用。但最终还是放弃了,因为它并不完美。此外,在包含大量列表项的 ListScrollView 中使用它会导致项目瘫痪。我只推荐在同一视图中使用这个做简单的转换过渡。当你在多个不同的视图中共享一个命名空间的时候(包括转场期间的视图剪裁在内),事情就会开始变得奇怪。


6. 对手势的支持有限


SwiftUI 提供了一系列新的手势(即 DragGestureLongPressGesture)。这些手势可以通过 gesture 修饰符(如 tapGesturelongPressGesture)添加到视图中。它们都能正常工作,除非你想要做更复杂的交互。


比如,DragGestureScrollView 交互就不是很好。即使有了 simultaneousGesture 修饰符,在 ScrollView 中放一个 DragGesture 还是会阻止滚动。在其他情况下,拖动手势可以在没有任何通知的情况下被取消,使得手势处于不完整状态。


为了解决这个问题,我构建了自己的 GestureView,它可以在 SwiftUI 中使用 UIKit 手势。我会在下一篇关于最佳 SwiftUI 库和解决方案的文章中分享这部分内容。


7. 分享扩展中的 SwiftUI


我可能是错的,但是分享扩展还是使用 UIKit 吧。我通过 UIHostingController 用 SwiftUI 构建了一个分享扩展,当分享扩展加载完毕后,有一个非常明显的延迟,用户体验较差。你可以尝试通过在视图中添加动画去掩盖它,但是仍然有 500 毫秒左右的延迟。


值得一提的点

  • 无法访问状态栏 (不能修改颜色或拦截点击)
  • 由于缺少 App,我们仍然需要 @UIApplicationDelegateAdaptor
  • 不能向后兼容
  • UIVisualEffectsView 会导致滚动延迟(来源于推特:@AlanPegoli

缺陷


1. ScrollView


这是迄今为止最大的缺点之一。任何一个构建过定制化 iOS 应用的人都知道我们有多依赖 ScrollView 去支持交互。

  • 主要的障碍:视图中的 LazyVStack 导致卡顿、抖动和一些意外的行为LazyVStack 对于需要滚动的混合内容(如新闻提要)的长列表至关重要。仅凭这一点,SwiftUI 就还没准备好投入生产环境: Apple 已经证实,这是 SwiftUI 自身的漏洞。尚未清楚他们什么时候会修复,但是一旦修复了,这将是一个巨大的胜利。
  • 滚动状态:原生不支持解析滚动的状态(滚动视图是否正在被拖拽?滚动?偏移多少?)。尽管有一些解决方案,但是还是很繁琐且不稳定。
  • 分页:原生不支持分页滚动视图。所以打消实现类似于可滑动的媒体库的念头吧(但是如果你想要关闭一些东西的时候,可以使用 SwiftUIPager)。在技术上你可以使用 TabView 加 PageTabViewStyle,但是我认为它更适合少部分的元素,而不是大的数据集。
  • 性能:使用 List 是性能最好的,并且避免了 LazyVStack 的卡顿问题,但由于工作方式的转换,它仍然不适合显示可变大小的内容。例如,在构建聊天视图时,其过渡很奇怪,会裁剪子视图,并且无法控制插入的动画样式。

结论


毫无疑问我觉得应该学习 SwiftUI ,自己去理解它,并享受乐趣。但是先别急着全盘采用。


SwiftUI 已经为简单的应用程序做好了准备,但是在写这篇文章的时候(iOS 15,beta 4 版本),我不认为它已经适合复杂应用程序的生产环境,主要是由于 ScrollView 的问题和对 UIViewRepresentable 的严重依赖。我很遗憾,尤其是像即时通信产品,新闻摘要,以及严重依赖复杂视图或者想要创建手势驱动的定制体验产品,目前还不适合使用 SwiftUI。


如果你想要精细的控制和无限的可能性,我建议在可预见的未来坚持使用 UIKit。你可以在一些视图(如设置页)里通过使用 UIHostingController 包装 SwiftUI 视图以获得 SwiftUI 的好处。


未来会发生什么?


当开始着手我们项目的下一次大迭代的时候。我知道这个新项目的交互范围不在 SwiftUI 目前支持的范围之内。即使当我知道 SwiftUI 在某些关键方面存在不足的时候,我的心都碎了,但是我还是不打算退回到 UIKit,因为我知道当 SwiftUI 运行起来时,构建它是一件多么快乐的事情。它的速度如此之快。


SwiftUI 会兼容 UIKit 么?如果这样的话,我们可能需要等待 SwiftUI 使用 3-5 年的时间来移植所有必要的 UIKit API。如果 SwiftUI 不准备兼容 UIkit,那你也能通过 SwiftUI 封装的方式使用 UIKit。


我好奇的是 Apple 会在 SwiftUI 上投入多少。他们是否有让所有的开发者采用 SwiftUI 的长期计划,或者说 SwiftUI 只是另一个界面构建器而已?我希望不是,也希望他们能全心投入 SwiftUI,因为它的前景是非常诱人的。


更多看法


作者:yeanyue
链接:https://juejin.cn/post/7140825514108780580
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS crash 报告分析系列 - 看懂 crash 报告的内容

iOS
在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。 不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。 每当 App 发生崩溃时,系统会自动生成一个后缀 i...
继续阅读 »

在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。


不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。


每当 App 发生崩溃时,系统会自动生成一个后缀 ips 的崩溃报告。我们可以通过崩溃报告来进行问题定位。但崩溃报告的内容繁多,新手看很容易一脸懵。所以本文先讲解一下报告中各字段的含义,后面再说报告符号化。


废话不多说,让我们开始吧!


前期准备


首先,报告解读我们需要先生成一个 crash 报告。


1、新建一个项目,在 ViewController 中写下面的代码:

NSString *value;
NSDictionary *dict = @{@"key": value}; // 字典的 value 不可为 nil,所以会崩溃

2、在真机上运行项目,然后去设置 - 隐私与安全性 - 分析与改进 - 分析数据,拿去生成的 crash 报告(报告的名字与项目名字一致,比如我的项目名为:CrashDemo,崩溃报告的名则为:CrashDemo-2023-05-30-093930.ips)。


注意:连着 Xcode 运行时不会产生崩溃报告,需要真机拔掉数据线再次运行 app 才会生成崩溃报告。


拿到报告,接下来就是解读了。


报告内容解读


官网的示例图:




Header


首先来看 Header:

Incident Identifier: 9928A955-FE71-464F-A2AF-A4593A42A26B
CrashReporter Key: 7f163d1c67c5ed3a6be5c879936a44f10b50f0a0
Hardware Model: iPhone14,5
Process: CrashDemo [45100]
Path: /private/var/containers/Bundle/Application/6C9D4CF7-4C16-4B50-A4A5-389BED62C699/CrashDemo.app/CrashDemo
Identifier: cn.com.fengzhihao.CrashDemo
Version: 1.0 (1)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: cn.com.fengzhihao.CrashDemo [3547]

Date/Time: 2023-05-30 09:39:29.6418 +0800
Launch Time: 2023-05-30 09:39:28.5579 +0800
OS Version: iPhone OS 16.3.1 (20D67)
Release Type: User
Baseband Version: 2.40.01
Report Version: 104

Header 主要描述了目标设备的软硬件环境。比如上图可以看出:是 iphone 14 的设备,系统版本是16.3,发生崩溃的事件是 2023-05-30 09:39:29 等等。


需要注意的是 Incident Identifier 相当于当前报告的 id,报告和 Incident Identifier 是一一对应的关系,绝对不会存在两份不同的报告 Incident Identifier 相同的情况。


Exception information

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000

这一部分主要是告诉我们 app 是因为什么错误而导致的崩溃,但不会包含完整的信息。


可以看到当前的 Type 为:EXC_CRASH (SIGABRT),这代表当前进程因收到了 SIGABRT 信号而导致崩溃,这是一个很常见的类型,字典 value 为nil或者属于越界等都会是此类型。更多的 Exception Type 解释请参见此处


Diagnostic messages

Application Specific Information:
abort() called

操作系统有时包括额外的诊断信息。此信息使用多种格式,具体取决于崩溃的原因,并且不会出现在每个崩溃报告中。


本次的崩溃原因是因为调用了 abort() 函数。


接下来,就是报告的重点了。


Backtraces


这部分记录了当前进程的线程的函数调用栈,我们可以通过调用栈来定位出问题的代码。


崩溃进程的每一条线程都会被捕获成回溯。回溯会展示当前线程被中断时的线程的函数调用栈。如果崩溃是由于语言异常造成的,会额外有一个Last Exception Backtrace,位于第一个线程之前。关于 Last Exception Backtrace 的详细介绍请看这里


比如我们示例中的崩溃就是由于语言异常造成的,所以崩溃报告中会有 Last Exception Backtrace。

Last Exception Backtrace:
0 CoreFoundation 0x191560e38 __exceptionPreprocess + 164
1 libobjc.A.dylib 0x18a6f78d8 objc_exception_throw + 60
2 CoreFoundation 0x191706078 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1917113ac -[__NSPlaceholderDictionary initWithCapacity:].cold.1 + 0
4 CoreFoundation 0x19157c2b8 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 320
5 CoreFoundation 0x19157c158 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 52
6 CrashDemo 0x104a69e0c -[ViewController touchesBegan:withEvent:] + 152
.... 中间内容省略
25 CrashDemo 0x104a6a0c4 main + 120
26 dyld 0x1afed0960 start + 2528

以下是上述每一列元素的含义:

  • 第一列:栈帧号。堆栈帧按调用顺序排列,其中帧 0 是在执行暂停时正在执行的函数。第 1 帧是调用第 0 帧函数的函数,依此类推
  • 第二列:包含正在执行函数的二进制包名
  • 第三列:正在执行的机器指令的地址
  • 第四列:在完全符号化的崩溃报告中,正在执行的函数的名称。出于隐私原因,函数名称有时限制为前 100 个字符
  • 第五列(+ 号后面的数字):函数入口点到函数中当前指令的字节偏移量

通过第 6 行我们可以推断出问题是由 NSDictionary 引起的。


但大部分时候我们得到的报告都是未符号化的,我们需要对报告进行符号化来获得更多的信息。关于符号化的相关内容可以看这里


Thread state

Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x0000000000000000 x1: 0x0000000000000000 x2: 0x0000000000000000 x3: 0x0000000000000000
...中间内容省略
far: 0x00000001e4d30560 esr: 0x56000080 Address size fault

崩溃报告的线程状态部分列出了应用程序终止时崩溃线程的 CPU 寄存器及其值。


Binary images

0x1cf074000 -        0x1cf0abfeb libsystem_kernel.dylib arm64e  <c76e6bed463530c68f19fb829bbe1ae1> /usr/lib/system/libsystem_kernel.dylib
...中间内容省略
0x18b8ca000 - 0x18c213fff Foundation arm64e <e5f615c7cc5e3656860041c767812a35> /System/Library/Frameworks/Foundation.framework/Foundation

以下是上述每一列元素的含义:

  • 第一列:二进制镜像在进程中的地址范围
  • 第二列:二进制镜像的名称
  • 第三列:操作系统加载到进程中的二进制映像中的 CPU 架构
  • 第四列:唯一标识二进制映像的构建 UUID。符号化崩溃报告时使用此值定位相应的 dSYM 文件
  • 第五列:二进制文件在磁盘上的路径

至此,报告上的所有 section 都已经解读完。希望大家看完这篇文章后,再分析崩溃日志的时候能更加得心应手。


作者:冯志浩
链接:https://juejin.cn/post/7238802590661476412
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

扒一扒uniapp是如何做ios app应用安装的

iOS
为何要扒 因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来...
继续阅读 »

为何要扒


因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。




开干


官方模板




先打开uniapp云打包一下项目看看


image-20230824112232275.png




复制地址到移动端浏览器打开看看


image-20230824112410817.png


这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。




开扒




F12打开choromdevtools,ctrl+s保存网页html。


image.png


保存成功,接下来看看html代码(样式代码删除了)


    <!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 </head>

<body>
<br><br>
   <center>
       <a class="button" href="itms-services://?action=download-manifest&amp;url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
   </center>
   <br><br>
   <center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>



解析




从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")


先看看itms-services是什么意思,下面是代码开发助手给的解释


image-20230824113418246.png


大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。




什么又是plist呢,这里再请我们的代码开发助手解释一下


image-20230824113748570.png


对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。




打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。


image-20230824114108792.png


访问后会出现


image-20230824115354028.png




别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求


image-20230824115609551.png


直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
      <dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>

直接抓重点,这里存你存放ipa包的地址


image-20230824120013828.png


这里改你应用的昵称


image-20230824120453368.png


这里改图标


image-20230824120509797.png


因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。




为我所用


分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:


image-20230824155040313.png


将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:


image-20230824155306228.png


可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至

作者:廿一c
来源:juejin.cn/post/7270799565963149324
此,本次扒拉过程结束,需求落幕!

收起阅读 »

iOS - 上手AR

iOS
前言 随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo 开始 要在iOS中创建一个的AR物体,你可以使用 ARKit 和 SceneKit 来实现 首先,确保你的项目已经导入了 ARKit 和 SceneKit...
继续阅读 »

前言


随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo


开始


要在iOS中创建一个的AR物体,你可以使用 ARKitSceneKit 来实现


首先,确保你的项目已经导入了 ARKit 和 SceneKit 框架。你可以在 Xcode 中的项目设置中添加 ARKit.framework 和 SceneKit.framework 到 "Frameworks, Libraries, and Embedded Content" 部分




然后,在你的程序文件中,导入 ARKit 和 SceneKit

import UIKit
import ARKit

接下来,创建一个 ARSCNView,并将其添加到你的视图层次结构中:

// 创建 ARSCNView 实例
sceneView = ARSCNView(frame: view.bounds)
view.addSubview(sceneView)
sceneView.delegate = self

// 创建 SCNScene 实例,并设置为 sceneView 的场景
let scene = SCNScene()
sceneView.scene = scene

然后,在视图控制器的生命周期方法中,配置 ARSession 并启动 AR 会话:

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// 配置 AR 会话并启动
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// 暂停 AR 会话
sceneView.session.pause()
}

现在,已经设置好 ARKit 和 AR 会话已经开始运行。接下来,我们将创建3D 模型:

func createBallNode() {
// 创建球体几何体
let ballGeometry = SCNSphere(radius: 0.1)
let ballMaterial = SCNMaterial()
ballMaterial.diffuse.contents = UIImage(named: "cxkj.webp") // 使用纹理图片
ballGeometry.materials = [ballMaterial]

// 在屏幕范围内生成随机位置坐标
let randomX = Float.random(in: -1.0...1.0) // 在屏幕宽度范围内生成随机 X 坐标
let randomY = Float.random(in: -1.0...1.0) // 在屏幕高度范围内生成随机 Y 坐标
let randomZ = Float.random(in: -3.0...0.0) // 在屏幕深度范围内生成随机 Z 坐标
ballNode = SCNNode(geometry: ballGeometry)
ballNode.position = SCNVector3(randomX, randomY, randomZ)

// 将球体节点添加到场景的根节点上
sceneView.scene.rootNode.addChildNode(ballNode)
}

最后我们通过点击事件将3D模型添加到场中

@objc func handleTap(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
createBallNode() // 创建球体节点
}
}

效果




作者:土豆崽xxx
链接:https://juejin.cn/post/7248424501813067813
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 快速复习GCD

iOS
多线程-串行、并行队列,同步、异步任务 1、创建串行队列和并行队列 //并行队列 dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_C...
继续阅读 »

多线程-串行、并行队列,同步、异步任务


1、创建串行队列和并行队列

    //并行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
//串行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);

  • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务),并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

2、同步异步任务

//同步
dispatch_sync(queue, ^{
        NSLog(@"1");
    });
//异步
dispatch_async(queue, ^{
        NSLog(@"1");
    });

同步执行:

  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行:

  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力。

异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

默认全局并发队列:dispatch_get_global_queue

第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT

第二个参数暂时没用,用 0 即可。


信号量 dispatch_semaphore_t



GCD中的信号量dispatch_semaphore_t中主要有三个函数:

  • dispatch_semaphore_create:创建信号
  • dispatch_semaphore_wait:等待信号
  • dispatch_semaphore_signal:释放信号

1、dispatch_semaphore_create
参数为int,表示信号量初始值,需大于等于0,否则创建失败,返回一个dispatch_semaphore_t


2、dispatch_semaphore_wait
参数1:

需传递一个 dispatch_semaphore_t 类型对象,对信号进行减1,然后判断信号量大小

参数2:

传递一个超时时间:dispatch_time_t 对象

  • 减1后信号量小于0,则阻塞当前线程,直到超时时间到达或者信号量大于等于0后继续执行后面代码
  • 减1后信号量大于等于0,对dispatch_semaphore_t 进行赋值,并返回dispatch_semaphore_t对象,继续执行后面代码

3、dispatch_semaphore_signal
参数:dispatch_semaphore_t

进行信号量加1操作,如果加1后结果大于等于0,则继续执行,否则继续等待。


用法:

- (void)startAsync{
//创建信号量 值为0
    self.sem = dispatch_semaphore_create(0);
//开启异步并发线程执行
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_semaphore 2\n");
        sleep(5);
//发送信号,信号量值+1
        dispatch_semaphore_signal(self.sem);
        NSLog(@"dispatch_semaphore 3\n");
    });
    NSLog(@"dispatch_semaphore 0\n");
//信号量 值-1 小于0 等待信号。。。
    dispatch_semaphore_wait(self.sem, DISPATCH_TIME_FOREVER);
    NSLog(@"dispatch_semaphore 1\n");

}
执行顺序0 2 1 3 1和3不确定顺序
如果初始化创建是信号量值为1
执行顺序0 1 2 3

常用总结:

1、异步并发线程顺序执行

2、异步并发线程控制最大并发数,比如下载功能控制最大下载数


调度组 dispatch_group_t


主要API:

  • dispatch_group_create:创建组

  • dispatch_group_async:进组任务

  • dispatch_group_notify:组任务执行完毕的通知

  • dispatch_group_enter:进组

  • dispatch_group_leave:出组

  • dispatch_group_wait:等待组任务时间


组合用法1:

- (void)dispatchGroupAsync{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//开启异步线程
    dispatch_group_async(group, queue, ^{
        sleep(2);
        NSLog(@"11");
    });
    dispatch_group_async(group, queue, ^{
        sleep(1);
        NSLog(@"12");
    });
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"13");
    });
    NSLog(@"14");
    dispatch_group_notify(group, queue, ^{
//收到执行完成的通知后执行
        NSLog(@"15");
    });
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
调度组执行完成后执行
    NSLog(@"16");
}

用法2:

- (void)dispatchSyncEnterGroup{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//进入调度组
    dispatch_group_enter(group);
//执行异步任务
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"21");
//执行完成后立刻调度组
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"22");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(3);
        NSLog(@"23");
        dispatch_group_leave(group);
    });
    NSLog(@"24");
    dispatch_group_notify(group, queue, ^{
//执行完后回调
        NSLog(@"25");
    });
    NSLog(@"26");
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"27");
}

总结:

1、dispatch_group_async 是对dispatch_group_enter和dispatch_group_leave的封装

2、dispatch_group_enter和dispatch_group_leave的须成双成对的出现


事件源 dispatch_source_t


主要API:

  • dispatch_source_create :创建源

  • dispatch_source_set_event_handler: 设置源的回调

  • dispatch_source_merge_data: 源事件设置数据

  • dispatch_source_get_data: 获取源事件的数据

  • dispatch_resume:恢复继续

  • dispatch_suspend:挂起

  • uintptr_t dispatch_source_get_handle(dispatch_source_t source) //得到dispatch源创建,即调用dispatch_source_create的第二个参数

  • unsignedlong dispatch_source_get_mask(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第三个参数


源的类型dispatch_source_type_t

1. DISPATCH_SOURCE_TYPE_DATA_ADD:用于ADD合并数据
2. DISPATCH_SOURCE_TYPE_DATA_OR:用于按位或合并数据
3.DISPATCH_SOURCE_TYPE_DATA_REPLACE:跟踪通过调用dispatch_source_merge_data获得的数据的分派源,新获得的数据值将替换 尚未交付给源处理程序 的现有数据值
4. DISPATCH_SOURCE_TYPE_MACH_SEND:用于监视Mach端口的无效名称通知的调度源,只能发送没有接收权限
5. DISPATCH_SOURCE_TYPE_MACH_RECV:用于监视Mach端口的挂起消息
6. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:用于监控系统内存压力变化
7.DISPATCH_SOURCE_TYPE_PROC:用于监视外部进程的事件
8. DISPATCH_SOURCE_TYPE_READ:监视文件描述符以获取可读取的挂起字节的分派源
9. DISPATCH_SOURCE_TYPE_SIGNAL:监控当前进程以获取信号的调度源
10. DISPATCH_SOURCE_TYPE_TIMER:基于计时器提交事件处理程序块的分派源
11. DISPATCH_SOURCE_TYPE_VNODE:用于监视文件描述符中定义的事件的分派源
12. DISPATCH_SOURCE_TYPE_WRITE:监视文件描述符以获取可写入字节的可用缓冲区空间的分派源。

1、dispatch_source_create 参数:

  • dispatch_source_type_t 要创建的源类型
  • uintptr_t 句柄 用于和其他事件并定,很少用,通常为0
  • uintptr_t mask 很少用,通常为0
  • dispatch_queue_t 事件处理的调度队列

用法:

self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

2、dispatch_source_set_event_handler 设置回调函数,当触发源事件时执行

//需要注意循环引用
dispatch_source_set_event_handler(self.sourceAdd, ^{
需要执行的代码
});
//启动
dispatch_resume(self.sourceAdd);
//挂起,即暂停
dispatch_suspend(self.sourceAdd);
这两个API需要成对使用,不可多次挂起或者多次恢复

3、dispatch_source_cancel 取消事件源,取消后不可再恢复或挂起,需要再次创建
4、dispatch_source_set_timer 当事件源类型为定时器类型(DISPATCH_SOURCE_TYPE_TIMER)时,设置开始时间、重复时间、允许时间误差


定时器实现比较简单容易,网上教程也多,这里主要介绍一下:DISPATCH_SOURCE_TYPE_DATA_ADD、DISPATCH_SOURCE_TYPE_DATA_OR、DISPATCH_SOURCE_TYPE_DATA_REPLACE。


先说下结果:

  • DISPATCH_SOURCE_TYPE_DATA_ADD 会把事件源累加 可以记录总共发送多少次事件进行合并
  • DISPATCH_SOURCE_TYPE_DATA_OR 会把事件源合并,最终得到的数据源数为1
  • DISPATCH_SOURCE_TYPE_DATA_REPLACE 会用最新事件源替换旧有未处理事件,最终得到的数据源数为1
  • 循环10000次实际跑处理回调事件次数 add315 or275 replace 284

从结果上来看,当需要把快速频繁的重复事件进行合并,最好的选择是DISPATCH_SOURCE_TYPE_DATA_OR,使用场景,监听消息时,多消息频繁下发需要刷新UI,如果不进行合并处理,会导致UI太过频繁的刷新,影响最终效果,且对性能开销过大。


当然,类似的场景也可使用其他方式处理,比如建立消息池,接收消息后标记消息池状态及变化,然后定时从消息池中取消息。诸如此类的方法较多,如果只是简单的处理,上面的DISPATCH_SOURCE_TYPE_DATA_OR模式应该满足使用。


代码:

//创建源
self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
//弱引用
__weak typeof(self) weakifySelf = self;
//设置回调事件
dispatch_source_set_event_handler(self.sourceAdd, ^{
//强引用
__strong typeof(self) strongSelf = weakifySelf;
//获取接收到的源数据
strongSelf.handleData = dispatch_source_get_data(strongSelf.sourceAdd);
NSLog(@"dispatch_source1 %ld\n",strongSelf.handleData);
//需要执行的代码
[strongSelf sourceHandle];

        });
//开启源
dispatch_resume(self.sourceAdd);
for (int i = 0; i<10000; i ++) {

[self dispatchSource];
}
- (void)dispatchSource{

    NSLog(@"dispatch_source2 %ld\n",self.handleData);
//发送源信号
    dispatch_source_merge_data(self.sourceAdd, 1);
}

作者:WhiteMonkey
链接:https://juejin.cn/post/7261866427119222844
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

App本地配置持久化方案

iOS
概述 在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。 UserDefaults Apple提供了UserDefault框...
继续阅读 »

概述


在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。


UserDefaults


Apple提供了UserDefault框架来帮助我们存储离散的配置,UserDefaults将以plist文件的形式存储在沙盒环境中。在不引入NoSql数据库的情况下,这是首推的方案。


注意事项


为了提升读取速度,App在启动时会将UserDefaults Standard对应的plist加载到内存中,如果文件过大就会增加App在启动时的加载时间,同时提高一定的内存消耗。


所以在Standard中,我们应该存放需要在App启动阶段立即获取的信息,比如用户最近登录的ID,App远程配置缓存的版本。


我们可以通过分表来缩减Standard的数据量。使用UserDefaults的suiteName模式创建不同的配置表,这样配置项将存储到各自的plist文件中,这些独立的plist不会在启动时被自动加载。


配置管理的常见问题

  1. 使用硬编码的String Key将配置存储到UserDefaults中,通过复制粘贴Key的字符串来存取数据。

  2. 零散的使用UserDefaults,缺少中心化管理方案。比如需要存储“开启通知功能”的配置,Key通常会直接被放在业务相关代码中维护。


方案 1.0


管理UserDefaults


创建一个UserDefault的管理类,主要用途是对UserDefault框架使用的收口,统一使用策略。

public class UserDefaultsManager {
public static let shared = UserDefaultsManager()
private init() {}
public var suiteName:String? {
didSet {
/**
根据传入的 suiteName的不同会产生四种情况:
传入 nil:跟使用UserDefaults.standard效果相同;
传入 bundle id:无效,返回 nil;
传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的 plist 文件。
*/
userDefault = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
}
}
public var userDefault = UserDefaults.standard
}


创建常量表

  1. 对配置项的Key进行中心化的注册与维护
struct UserDefaultsKey {
static let appLanguageCode = "appLanguageCode"
static let lastLaunchSaleDate = "resetLastLaunchSaleDate"
static let lastSaleDate = "lastSaleDate"
static let lastSaveRateDate = "lastSaveRateDate"
static let lastVibrateTime = "lastVibrateTime"
static let exportedImageSaveCount = "exportedImageSaveCount"

static let onceFirstLaunchDate = "onceFirstLaunchDate"
static let onceServerUserIdStr = "onceServerUserIdStr"
static let onceDidClickCanvasButton = "onceDidClickCanvasButton"
static let onceDidClickCanvasTips = "onceDidClickCanvasTips"
static let onceDidClickEditBarGuide = "onceDidClickEditBarGuide"
static let onceDidClickEditFreestyleGuide = "onceDidClickEditFreestyleGuide"
static let onceDidClickManualCutoutGuide = "onceDidClickManualCutoutGuide"
static let onceDidClickBackgroundBlurGuide = "onceDidClickBackgroundBlurGuide"
static let onceDidTapCustomStickerBubble = "onceDidTapCustomStickerBubble"
static let onceDidRequestHomeTemplatesFromAPI = "onceDidRequestHomeTemplatesFromAPI"

static let firSaveExportTemplateKey = "firSaveExportTemplateKey"
static let firSaveTemplateDateKey = "firSaveTemplateDateKey"
static let firShareExportTemplateKey = "firShareExportTemplateKey"
static let firShareTemplateDateKey = "firShareTemplateDateKey"
}

2. 提供CURD API
private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.config").userDefaults

var exportedImageSaveCount: Int {
return appConfigUserDefaults.integer(forKey: key)
}

func increaseExportedImageSaveCount() {
let key = UserDefaultsKey.exportedImageSaveCount
var count = appConfigUserDefaults.integer(forKey: key)
count += 1
appConfigUserDefaults.setValue(count, forKey: key)
}

我们对UserDefaults数据源进行了封装,String Key的注册也统一到常量文件中。当我们要查找或修改时,可以从配置表方便的查到String Key。


随着业务的膨胀,配置项会越来越多,我们会需要根据业务功能的分类,重新整理出多个分表。


随后我们会发现一些问题:

  1. String Key的注册虽然不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。

  2. CURD API的数量会膨胀的更快,需要更多的维护成本。那么能不能将配置的管理更加面向对象,实现类似ORM的方式来管理呢?


方案2.0


根据上述的问题,来演化下方案2.0,我们来创建一个协议,用来规范UserDefaults的使用类。


它将包含CURD API的默认实现,初始化关联UserDefaults,自动生成String Key。

/// UserDefaults存储协议,建议用String类型的枚举去实现该协议
public protocol UserDefaultPreference {

var userDefaults: UserDefaults { get }
var key: String { get }

var bool: Bool { get }
var int: Int { get }
var float: Float { get }
var double: Double { get }

var string: String? { get }
var stringValue: String { get }

var dictionary: [String: Any]? { get }
var dictionaryValue: [String: Any] { get }

var array: [Any]? { get }
var arrayValue: [Any] { get }

var stringArray: [String]? { get }
var stringArrayValue: [String] { get }

var data: Data? { get }
var dataValue: Data { get }

var object: Any? { get }
var url: URL? { get }

func codableObject<T: Decodable>(_ as:T.Type) -> T?

func save<T: Encodable>(codableObject: T) -> Bool

func save(string: String)
func save(object: Any?)
func save(int: Int)
func save(float: Float)
func save(double: Double)
func save(bool: Bool)
func save(url: URL?)
func remove()
}

定义完协议后,我们再添加一些默认实现,降低使用成本。

// 生成默认的String Key
public extension UserDefaultPreference where Self: RawRepresentable, Self.RawValue == String {
var key: String { return "\(type(of: self)).\(rawValue)" }
}

public extension UserDefaultPreference {
// 默认使用 standard UserDefaults,可以在实现类中配置
var userDefaults: UserDefaults { return UserDefaultsManager.shared.userDefaults }

func codableObject<T: Decodable>(_ as:T.Type) -> T? {
return UserDefaultsManager.codableObject(`as`, key: key, userDefaults: userDefaults)
}

@discardableResult
func save<T: Encodable>(codableObject: T) -> Bool {
return UserDefaultsManager.save(codableObject: codableObject, key: key, userDefaults: userDefaults)
}

var object: Any? { return userDefaults.object(forKey: key) }

func hasKey() -> Bool { return userDefaults.object(forKey: key) != nil }

var url: URL? { return userDefaults.url(forKey: key) }

var string: String? { return userDefaults.string(forKey: key) }
var stringValue: String { return string ?? "" }

var dictionary: [String: Any]? { return userDefaults.dictionary(forKey: key) }
var dictionaryValue: [String: Any] { return dictionary ?? [String: Any]() }

var array: [Any]? { return userDefaults.array(forKey: key) }
var arrayValue: [Any] { return array ?? [Any]() }

var stringArray: [String]? { return userDefaults.stringArray(forKey: key) }
var stringArrayValue: [String] { return stringArray ?? [String]() }

var data: Data? { return userDefaults.data(forKey: key) }
var dataValue: Data { return userDefaults.data(forKey: key) ?? Data() }

var bool: Bool { return userDefaults.bool(forKey: key) }
var boolValue: Bool? {
guard hasKey() else { return nil }
return bool
}

var int: Int { return userDefaults.integer(forKey: key) }
var intValue: Int? {
guard hasKey() else { return nil }
return int
}

var float: Float { return userDefaults.float(forKey: key) }
var floatValue: Float? {
guard hasKey() else { return nil }
return float
}

var double: Double { return userDefaults.double(forKey: key) }
var doubleValue: Double? {
guard hasKey() else { return nil }
return double
}

func save(object: Any?) { userDefaults.set(object, forKey: key) }
func save(string: String) { userDefaults.set(string, forKey: key) }
func save(int: Int) { userDefaults.set(int, forKey: key) }
func save(float: Float) { userDefaults.set(float, forKey: key) }
func save(double: Double) { userDefaults.set(double, forKey: key) }
func save(bool: Bool) { userDefaults.set(bool, forKey: key) }
func save(url: URL?) { userDefaults.set(url, forKey: key) }

func remove() { userDefaults.removeObject(forKey: key) }
}

OK,我们来看下使用的案例

// MARK: - Launch
enum LaunchEventKey: String {
case didShowLaunchGuideOnThisLaunch
case launchGuideIsAlreadyShow
}
extension LaunchEventKey: UserDefaultPreference { }

func checkIfNeedLaunchGuide() -> Bool {
return !LaunchEventKey.launchGuideIsAlreadyShow.bool
}
func launchContentView() {
LaunchEventKey.launchGuideIsAlreadyShow.save(bool: true)
}

// MARK: - Language
enum LanguageEventKey: String {
case appLanguageCode
}
extension LanguageEventKey: UserDefaultPreference { }

static var appLanguageCode: String {
get {
let code = LanguageEventKey.appLanguageCode.string ?? ""
return code
}
set {
LanguageEventKey.appLanguageCode.save(codableObject: newValue)
}
}

// MARK: - Purchase
enum PurchaseStatusKey: String {
case iapSubscribeExpireDate
}
extension PurchaseStatusKey: UserDefaultPreference { }

func handle() {
let expirationDate: Date = Entitlement.expirationDate
PurchaseStatusKey.iapSubscribeExpireDate.save(object: expirationDate)
}

func getValues() {
let subscribeExpireDate = PurchaseStatusKey.iapSubscribeExpireDate.object as? Date
}

// MARK: - GlobalConfig
enum AppConfig: String {
case globalConfig
}

private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.AppConfig").userDefaults

extension AppConfig: UserDefaultPreference {
var userDefaults: UserDefaults { return appConfigUserDefaults }
}

// 自定义类型
public class GlobalConfig: Codable {
/// 配置版本号
let configVersion: Int
/// 用户初始试用次数
let userInitialTrialCount: Int
/// 生成时间 如:2022-09-19T02:58:31Z
let createDate: String

enum CodingKeys: String, CodingKey {
case configVersion = "version"
case userInitialTrialCount = "user_initial_trial_count"
case createDate = "create_date"
}
...
}

lazy var globalConfig: GlobalConfig = {
guard let config = AppConfig.globalConfig.codableObject(GlobalConfig.self) else {
return GlobalConfig()
}
return config
}() {
didSet { AppConfig.globalConfig.save(codableObject: globalConfig) }
}

从上述案例可以看出,在配置项的注册和维护成本相对方案1.0有了大幅度的降低,对UserDefaults的使用进行了规范性的约束,提供了更方便的CURD API,使用方式也更加符合面向对象的习惯。


同时为了满足复杂结构体的存储需求,我们可以扩展实现Codable对象的存取逻辑。


总结


本方案的目的是解决乱象丛生的UserDefaults的使用情况,分析后向两个方向进行了优化:

  1. 提供中心化的配置方式,关联UserDefaults、维护String Key。
  2. 提供类ORM的管理方式,减少业务的接入成本。


针对更复杂的、类缓存集合的,或者有查询需求的配置项管理,请尽快用NoSQL替换,避免数据量上升带来的效率下降。


作者:Ginhoor大帅
链接:https://juejin.cn/post/7200945340122906681
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 拖拽式控件:QiDragView

iOS
首先,我们先看一下QiDragView的效果图:  一、QiDragView整体架构设计 话不多说,上架构图~ QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。 二、如何自定义...
继续阅读 »

首先,我们先看一下QiDragView的效果图: 


一、QiDragView整体架构设计


话不多说,上架构图~



QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。


二、如何自定义使用QiDragView?


在上Demo之前,先介绍几个可以自定义的UI配置属性:



以及一些逻辑配置属性:



使用起来也很方便:

  • 直接设置titles即可创建出对应title的Buttons。
  • 通过dragSortEnded的block方法回调,处理拖拽后的业务逻辑:按钮的排序、按钮是否选择等属性

默认配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];

dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

自定义配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];
dragSortView.rowHeight = 50.0;
dragSortView.rowMargin = 30.0;
dragSortView.rowPadding = 20.0;
dragSortView.columnCount = 3;
dragSortView.columnMargin = 30.0;
dragSortView.columnPadding = 20.0;
dragSortView.normalColor = [UIColor redColor];
dragSortView.selectedColor = [UIColor purpleColor];
dragSortView.enabledTitles = @[@"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 可以点击选择的Buttons(选填,默认全选)
dragSortView.selectedTitles = @[@"首页推荐", @"HULK一线杂谈", @"Qtest之道"];//!< 初始选择的Buttons(选填,默认全选)
dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

三、QiDragView的技术点


3.1 长按手势:


长按手势分别对应三种状态:UIGestureRecognizerStateBeganUIGestureRecognizerStateChangedUIGestureRecognizerStateEnded

//! 长按手势
- (void)longPress:(UILongPressGestureRecognizer *)gesture {

UIButton *currentButton = (UIButton *)gesture.view;

if (gesture.state == UIGestureRecognizerStateBegan) {

[self bringSubviewToFront:currentButton];

[UIView animateWithDuration:.25 animations:^{
self.originButtonCenter = currentButton.center;
self.originGesturePoint = [gesture locationInView:currentButton];
currentButton.transform = CGAffineTransformScale(currentButton.transform, 1.2, 1.2);
}];
}
else if (gesture.state == UIGestureRecognizerStateEnded) {

[UIView animateWithDuration:.25 animations:^{
currentButton.center = self.originButtonCenter;
currentButton.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
if (self.dragSortEnded) {
self.dragSortEnded(self.buttons);
}
}];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {

CGPoint gesturePoint = [gesture locationInView:currentButton];
CGFloat deltaX = gesturePoint.x - _originGesturePoint.x;
CGFloat deltaY = gesturePoint.y - _originGesturePoint.y;
currentButton.center = CGPointMake(currentButton.center.x + deltaX, currentButton.center.y + deltaY);

NSInteger fromIndex = currentButton.tag;
NSInteger toIndex = [self toIndexWithCurrentButton:currentButton];

if (toIndex >= 0) {
currentButton.tag = toIndex;

if (toIndex > fromIndex) {
for (NSInteger i = fromIndex; i < toIndex; i++) {
UIButton *nextButton = _buttons[i + 1];
CGPoint tempPoint = nextButton.center;
[UIView animateWithDuration:.5 animations:^{
nextButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
nextButton.tag = i;
}
}
else if (toIndex < fromIndex) {
for (NSInteger i = fromIndex; i > toIndex; i--) {
UIButton *previousButton = self.buttons[i - 1];
CGPoint tempPoint = previousButton.center;
[UIView animateWithDuration:.5 animations:^{
previousButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
previousButton.tag = i;
}
}
[_buttons sortUsingComparator:^NSComparisonResult(UIButton *obj1, UIButton *obj2) {
return obj1.tag > obj2.tag;
}];
}
}
}

3.2 配置按钮:


设计思路:在属性titles的setter方法中,初始化并配置好各个Buttons。

- (void)setTitles:(NSArray<NSString *> *)titles {

_titles = titles;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger differCount = titles.count - self.buttons.count;

if (differCount > 0) {
for (NSInteger i = self.buttons.count; i < titles.count; i++) {
[self.buttons addObject:[self buttonWithTag:i]];
}
}
else if (differCount < 0) {
NSArray *extraButtons = [self.buttons subarrayWithRange:(NSRange){titles.count, self.buttons.count - titles.count}];
[self.buttons removeObjectsInArray:extraButtons];
for (UIButton *button in extraButtons) {
[button removeFromSuperview];
}
}

self.enabledTitles = self.enabledTitles ?: titles;//!< 如果有,就传入,否则传入titles
self.selectedTitles = self.selectedTitles ?: titles;

for (NSInteger i = 0; i < self.buttons.count; i++) {
[self.buttons[i] setTitle:titles[i] forState:UIControlStateNormal];
[self.buttons[i] addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]];//!< 长按手势
[self selectButton:self.buttons[i] forStatus:[self.selectedTitles containsObject:titles[i]]];
if ([self.enabledTitles containsObject:titles[i]]) {
[self.buttons[i] addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
}

for (NSInteger i = 0; i < self.buttons.count; i++) {
NSInteger rowIndex = i / self.columnCount;
NSInteger columnIndex = i % self.columnCount;
CGFloat buttonWidth = (self.bounds.size.width - self.columnMargin * 2 - self.columnPadding * (self.columnCount - 1)) / self.columnCount;
CGFloat buttonX = self.columnMargin + columnIndex * (buttonWidth + self.columnPadding);
CGFloat buttonY = self.rowMargin + rowIndex * (self.rowHeight + self.rowPadding);
self.buttons[i].frame = CGRectMake(buttonX, buttonY, buttonWidth, self.rowHeight);
}

CGRect frame = self.frame;
NSInteger rowCount = ceilf((CGFloat)self.buttons.count / (CGFloat)self.columnCount);
frame.size.height = self.rowMargin * 2 + self.rowHeight * rowCount + self.rowPadding * (rowCount - 1);
self.frame = frame;
});
}

源码地址:QiDragView


作者:齐舞647
链接:https://juejin.cn/post/7255334045076701239
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

第三方库并不是必须的

iOS
前言 我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。 虽然这些都是合理的考量,但使用第三方库的风险和...
继续阅读 »

前言


我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。


虽然这些都是合理的考量,但使用第三方库的风险和相关成本往往被忽视或误解。在某些情况下,风险是值得的,但是在决定冒险之前,首先要能够明确的定义风险。为了使风险评估更加的透明和一致,我们制定了一个流程来衡量我们将其集成到app有多大的风险。


风险


大多数大型组织,包括我们,都有某种形式的代码审查,作为开发实践的一部分。对这些团队来说,添加一个第三方库就相当于添加了一堆由不属于团队成员开发,未经审查的代码。这破坏了团队一直坚持的代码审查原则,交付了质量未知的代码。这给app的运行方式以及长期开发带来了风险,对于大型团队而言,更是对整体业务带来了风险。


运行时风险


库代码通常来说,对于系统资源,和app拥有相同级别的访问权限,但它们不一定应用团队为管理这些资源而制定的最佳实践。这意味着它们可以在没有限制的情况下访问磁盘,网络,内存,CPU等等,因此,它们可以(过度)将文件写入磁盘,使用未优化的代码占用内存或CPU,导致死锁或主线程延迟,下载(和上传!)大量数据等等。更糟糕的是他们会导致崩溃,甚至崩溃循环


其中许多情况直到 app 已经上架才被发现,在这种情况下,修复它需要创建一个新版本,并通过审核,这通常需要大量时间和成本。这种风险可以通过一个变量控制是否调用来进行一定程度的控制,但是这种方法也并非万无一失(看下文)。


开发风险


引用一个同事的话:“每一行代码都是一种负担”,对不是你自己写的代码而言,这句话更甚。库在适配新技术或API时可能很慢,这阻碍了代码开发,或者太快,导致开发的版本过高。


库在采用新技术或API时可能很慢,阻碍了代码库,或者太快,导致部署目标太高。每当 Apple 和 Google 每年发布一个新 OS 版本时,他们通常要求开发人员根据SDK的变化更新代码,库开发人员也必须这样做。这需要协调一致的努力、优先事项的一致性以及及时完成工作的能力。


随着移动平台的不断变化,以及团队(成员)也不是一成不变,这将会成为一个持续不断的风险。当被集成的库不存在了,而库又需要更新时,会花很多时间来决定谁来做。事实证明一旦一个库存在,就很少也很难被移除,因此我们将其视为长期维护成本。


商业风险


如同我上面所说,现代的操作系统并没有对 app 代码和库代码进行区分,因此除了系统资源之外,它们还可以访问用户信息。作为 app 的开发者,我们负责恰当的使用这部分信息,也需要为任何第三方库负责。


如果用户给了 Lyft app 地理位置授权,任何第三方库也将自动得获得授权。他们可以将那些(地理位置)数据上传到自己服务器,竞对服务器,或者谁知道还有什么地方。当一个库需要我们没有的权限时,那问题就更大了。


同样,一个系统的安全取决于其最薄弱的环节,但如果其中包含未经审核的代码,那么你就不知道它到底有多安全。你精心设计的安全编码实践可能会被一个行为不当的库所破坏。苹果和谷歌实施的任何政策都是如此,例如“你不得对用户追踪”。


减少风险


当对一个库(是否)进行使用评估时,我们首先要问几个问题,以了解对库的需求。


我们内部能做么?


有时候我们只需要简单的粘贴复制真正需要的部分。在更复杂的场景中,库与自定义后端通信,我们对该API进行了逆向,并自己构建了一个迷你SDK(同样,只构建了我们需要的部分)。在90%的情况下,这是首选,但在与非常特定的供应商或需求集成时并不总是可行。


有多少用户从该库中受益?


在一种情况下,我们正在考虑添加一个风险很大的库(根据下面的标准),旨在为一小部分用户提供服务,同时将我们的所有用户都暴露在该库中。 对于我们认为会从中受益的一小部分客户,我们冒了为我们所有用户带来问题的风险。


这个库有什么传递依赖?


我们还需要评估库的所有依赖项的以下标准。


退出标准是什么?


如果集成成功,是否有办法将其转移到内部? 如果不成功,是否有办法删除?


评价标准


如果此时团队仍然希望集成库,我们要求他们根据一组标准对库进行“评分”。下面的列表并不全面,但应该能很好地说明我们希望看到的。


阻断标准


这些标准将阻止我们从技术上或者公司政策上集成此库,在进行下一步之前,我们必须解决:


过高的 deployment target/target SDKs。 我们支持过去4年主流的操作系统(版本),所以第三方库至少也需要支持一样多。


许可证不正确/缺失。 我们将许可文件与应用捆绑在一起,以确保我们可以合法使用代码并将其归属于许可持有人。


没有冲突的传递依赖关系。 一个库不能有一个我们已经包含但版本不同的传递依赖项。


不显示它自己的 UI 。 我们非常小心地使我们的产品看起来尽可能统一,定制用户界面对此不利。


它不使用私有 API 。 我们不愿意冒 app 因使用私有 API 而被拒绝的风险。


主要关注点


闭源。 访问源代码意味着我们可以选择我们想要包含的库的哪些部分,以及如何将该源代码与应用程序的其余部分捆绑在一起。 对于我们来说,一个封闭源代码的二进制发行版更难集成。


编译时有警告。 我们启用了“警告视为错误”,具有编译警告的库是库整体质量(下降)的良好指示。


糟糕的文档。 我们希望有高质量的内联文档,外部”如何使用“文档,以及有意义的更新日志。


二进制体积。 这个库有多大?一些库提供了很多功能,而我们只需要其中的一小部分。尤其是在没有访问源码权限的情况下,这通常是一个全有或全无的情况。


外部的网络流量。 与我们无法控制的上游服务器/端点通信的库可能会在服务器关闭、错误数据被发回等时关闭整个应用程序。这也与我上面提到的隐私问题相同。


技术支持。 当事情不能正常工作时,我们需要能够报告/上报问题,并在合理的时间内解决问题。开源项目通常由志愿者维护,也很难有一个时间线,但至少我们可以自己进行修改。这在闭源项目是不可能的。


无法禁用。 虽然大多数库特别要求我们初始化它,但有些库在实例化时更“主动”,并且在我们不调用它的情况下可以自己执行工作。这意味着当库导致问题时,我们无法通过功能变量或其他机制将其关闭。


我们为所有这些(和其他一些)标准分配了点数,并要求工程师为他们想要集成的库汇总这些点数。虽然默认情况下,低分数并不难被拒绝,但我们通常会要求更多的理由来继续前进。


最后


虽然这个过程看起来非常严格,在许多情况下,潜在风险是假设的,但我们有我在这篇博文中描述的每个场景的实际例子。将评估记录下来并公开,也有助于将相对风险传达给不熟悉移动平台工作方式的人,并证明我们没有随意评估风险。


此外,我不想声称每一个第三方库本质上都是坏的。事实上,我们在Lyft使用了很多:RxSwiftRxJavaBugsnagSDKGoogle MapsTensorflow,以及一些较小的用于非常特定的用例。但所有这些要么都经过了充分审查,要么我们已经决定风险值得收益,同时对这些风险和收益的真正含义有了清晰的认识。


最后,作为一个专业开发人员提示:始终在库的API之上创建自己的抽象,不要直接调用它们的API。这使得将来替换(或删除)底层库更加容易,再次减轻了与长期开发相关的一些风险。


作者:Swift社区
链接:https://juejin.cn/post/7145368397096681485
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

所有开发者注意,苹果审核策略有变

iOS
这里每天分享一个 iOS 的新知识,快来关注我吧 访问敏感数据的 App 新规 苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。 这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过...
继续阅读 »


这里每天分享一个 iOS 的新知识,快来关注我吧


访问敏感数据的 App 新规


苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。


这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。


早在今年 6 月的 WWDC23 上苹果就宣布,开发人员需要在其应用程序的隐私清单中声明使用某些 API 的原因,目前正式放出了这份需要声明的 API 列表。


新规详情


从今年(2023年)秋天开始,大概是 9 月中旬左右,如果你将你的 App 上传到 App Store Connect,你的应用程序使用到了需要声明原因的 API(也包括你引入的第三方 SDK),但是你没有在隐私清单文件中添加原因,那么 Apple 会给你发送一封警告性的邮件。


从 2024 年春季开始,大概是 3 月左右,没有在隐私清单文件中说明使用原因的 App 将会被拒审核。


需要声明原因的 API 有哪些?


1、NSUserdefaults 相关 API


这个 API 是被讨论最多争议最大的,因为几乎每个 App 都会用到,而且因为有沙盒保护,每个 app 的存储空间是隔离的,这都要申报理由,的确十分荒谬。


2、获取文件时间戳相关的 API

  • creationDate

  • modificationDate

  • fileModificationDate

  • contentModificationDateKey

  • creationDateKey

  • getattrlist(::::_:)

  • getattrlistbulk(::::_:)

  • fgetattrlist(::::_:)

  • stat

  • fstat(::)

  • fstatat(::::)

  • lstat(::)

  • getattrlistat(::::::)


3、获取系统启动时间的 API


大多数衡量 App 启动时间的 APM 库会用到这个 API。

  • systemUptime

  • mach_absolute_time()


4、磁盘空间 API

  • volumeAvailableCapacityKey

  • volumeAvailableCapacityForImportantUsageKey

  • volumeAvailableCapacityForOpportunisticUsageKey

  • volumeTotalCapacityKey

  • systemFreeSize

  • systemSize

  • statfs(::)

  • statvfs(::)

  • fstatfs(::)

  • fstatvfs(::)

  • getattrlist(::::_:)

  • fgetattrlist(::::_:)

  • getattrlistat(::::::)


5、活动键盘 API


这个 API 可以来确定当前用户文本输入的主要语言,有些 App 可能会用来标记用户。

  • activeInputModes

如何在 Xcode 中配置


由于目前 Xcode 15 正式版还没有发布,下边的操作是在 Beta 版本进行的。


在 Xcode 15 中隐私部分全部归类到了一个后缀为 .xcprivacy 的文件中,创建项目时默认没有生成这个文件,我们先来创建一下。


打开项目后,按快捷键 Command + N 新建文件,在面板中搜索 privacy,选择 App Pirvacy 点击下一步创建这个文件。



这个文件是个 plist 格式的面板,默认情况下长这样:



然后点击加号,创建一个 Privacy Accessed API TypesKey,这是一个数组,用来包含所有你 App 使用到需要申明原因的 API。



在这个数组下继续点击加号,创建一个 Item,会看到两个选项:

  • Privacy Accessed API Type:用到的 API 类型

  • Privacy Accessed API Reasons:使用这个 API 的原因(也是个数组,因为可能包含多个原因)



这两个 Key 都创建出来,然后在 Privacy Accessed API Type 一栏点击右侧的菜单,菜单中会列出上边提到的所有 API,选择你需要申报的 API,我这里就拿 UserDefault 来举例:



然后在 Privacy Accessed API Reasons 一览中点击加号,在右侧的选项框中选择对应的原因,每个 API 对应的原因都会列出来,可以到苹果的官方文档上查看这个 API 的原因对应的是哪个,比如 UserDefault 对应的是 CA92.1,我这里就选择这个:



到此,申报原因就完成了,原因不需要自己填写,直接使用苹果给出的选项就可以了,还是蛮简单的。


参考资料


[1]


公告原文: developer.apple.com/news/?id=z6…


[2]


需要在 App 内声明的 API 列表: developer.apple.com/documentati…


[3]


API 列表对应的原因: developer.apple.com/documentati…


作者:iOS新知
链接:https://juejin.cn/post/7267091810379759676
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 陀螺仪技术的应用探究

iOS
本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢。 前言 陀螺仪是一种硬件传感器,能够感知设备的旋转和方向变化。...
继续阅读 »

本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢。



前言


陀螺仪是一种硬件传感器,能够感知设备的旋转和方向变化。它通常通过MEMS(微机电系统)技术来实现,内部包含了微小但高精度的陀螺仪器件、加速度计和磁力计等传感器,可以实时地感知设备在空间中的旋转角度和方向。


在iOS系统中,可以通过CoreMotion框架来访问陀螺仪的数据。在开发iOS应用程序时,可以使用CoreMotion框架提供了一个CMMotionManager类,该类可以用来获取设备的运动数据,包括陀螺仪数据、加速度计数据等。


iOS陀螺仪的精度和灵敏度通常比较高,可以实现比加速度计更加准确的姿态估计和方向识别,也可以帮助开发者实现更加真实的虚拟现实和增强现实应用。同时,iOS陀螺仪的实时响应和低功耗特性,也使得它在移动应用程序开发中得到了广泛的应用和认可。


基础知识


在开发前,有几个基础的知识点,我们需要事先了解,这对我们后期开发会有更好的帮助


三轴方向


在 iOS 中,陀螺仪传感器的三轴方向通常遵循右手系的规则。具体来说:

  • x 轴:表示设备绕着横轴旋转。当设备的屏幕朝上时,x 轴指向设备的右侧;当设备的屏幕朝下时,x 轴指向设备的左侧
  • y 轴:表示设备绕着纵轴旋转。当设备的屏幕朝上时,y 轴指向设备的顶部;当设备的屏幕朝下时,y 轴指向设备的底部
  • z 轴:表示设备绕着竖轴旋转。当设备的屏幕朝上时,z 轴指向设备的正面;当设备的屏幕朝下时,z 轴指向设备的背面



姿态信息


陀螺仪用于侦测设备沿三个轴为中线所旋转时的角速度,故有了三个姿态信息,分别为 pitch (纵倾), roll (横倾) 和 yaw (横摆)

  • pitch(俯仰角):表示设备绕着 x 轴旋转的角度,也称为纵倾角。当设备正面朝上时,俯仰角为 0°;当设备向上仰起时,俯仰角为正值;当设备向下倾斜时,俯仰角为负值
  • roll(横滚角):表示设备绕着 y 轴旋转的角度,也称为横倾角。当设备正面朝上时,横滚角为 0°;当设备向右侧倾斜时,横滚角为正值;当设备向左侧倾斜时,横滚角为负值
  • yaw(偏航角):表示设备绕着 z 轴旋转的角度,也称为横摆角。当设备正面朝北时,偏航角为 0°;当设备逆时针旋转时,偏航角为正值;当设备顺时针旋转时,偏航角为负值
  • CMRotationMatrix 结构体表示设备绕X、Y、Z轴的旋转矩阵,可用于描述设备在三维空间中的方向和旋转状态, 这里再细讲该结构体中9个元素所代表的含义


陀螺仪的使用

import CoreMotion

let motionManager = CMMotionManager()
if motionManager.isGyroAvailable {
motionManager.gyroUpdateInterval = 0.1
motionManager.startGyroUpdates(to: OperationQueue.main) { (data, error) in
if let gyroData = data {
let rotationRateX = gyroData.rotationRate.x
let rotationRateY = gyroData.rotationRate.y
let rotationRateZ = gyroData.rotationRate.z

print("Rotation Rate X: \(rotationRateX)")
print("Rotation Rate Y: \(rotationRateY)")
print("Rotation Rate Z: \(rotationRateZ)")
}
}
} else {
print("Gyroscope is not available.")
}

关键类解析


CMDeviceMotion


CMDeviceMotion 是一个 Core Motion 框架中的类,用于表示设备的运动和姿态信息。通过 CMDeviceMotion 类,可以获取到设备在三维空间中的加速度、旋转速度、重力加速度、旋转矩阵以及设备的姿态信息等,以便进一步进行处理和计算。


下面是 CMDeviceMotion 类中常用的属性和方法:

  • attitude 属性:表示设备的姿态信息,包括俯仰角(pitch)、横滚角(roll)和偏航角(yaw)等信息。
  • userAcceleration 属性:表示设备在三维空间中的加速度,即不包括重力加速度的加速度
  • rotationRate 属性:表示设备在三维空间中的旋转速度
  • gravity 属性:表示设备在三维空间中的重力加速度,即不包括设备加速度的重力加速度

需要注意的是,在使用 CMDeviceMotion 类时,需要首先创建一个 CMMotionManager 对象,并设置其属性和回调函数,以便获取设备的运动和姿态信息。此外,由于设备运动和姿态信息的获取涉及到多个传感器的协同工作,因此在使用时需要考虑传感器的准确性和稳定性,以避免误差和不良体验。


CMAttitude


CMAttitude 表示设备在三维空间中的姿态信息,包括设备的旋转、倾斜、方向等信息。在 iOS 开发中,可以通过 CMMotionManager 获取设备的姿态信息,然后将其保存为 CMAttitude 对象,并使用其中的各个属性来进行相应的处理和计算。


CMAttitude 类中的主要属性如下:

  • pitch:设备绕 x 轴的旋转角度,单位为弧度
  • roll:设备绕 y 轴的旋转角度,单位为弧度
  • yaw:设备绕 z 轴的旋转角度,单位为弧度
  • quaternion:设备的四元数表示,用于表示设备的旋转状态,包括旋转角度和旋转轴等信息
  • rotationMatrix:设备的旋转矩阵表示,用于表示设备在三维空间中的旋转状态

其中,pitchroll 和 yaw 属性是最基本的属性,用于表示设备绕 x、y、z 轴的旋转角度。一般来说,可以通过这三个属性来进行设备的姿态检测和相应的处理。其余的属性包括四元数、旋转矩阵,都可以用于更加精确和复杂的姿态检测和处理。


应用场景

  • 姿态估计和方向识别:通过陀螺仪获取设备旋转的角度和方向,可以实现设备的姿态估计和方向识别,广泛应用于游戏、导航、运动感知等领域
  • 图像校正和稳定:通过将陀螺仪中的旋转信息应用于图像处理,可以实现图像校正和稳定,提高图像质量和用户体验
  • 虚拟现实和增强现实:通过与其他传感器的结合和数据处理,可以实现更加真实的虚拟现实和增强现实应用,如3D游戏、AR导航、AR应用等
  • 运动检测和姿态跟踪:通过结合加速度计和地磁计等传感器的信息,可以实现设备的运动检测和姿态跟踪,如步数统计、运动轨迹记录、体感游戏等
  • 安全防护和权限控制:通过使用陀螺仪的数据,可以实现设备的安全防护和权限控制,如设备锁定、用户身份验证、数据加密等

作者:万链飞空
链接:https://juejin.cn/post/7239715294230183993
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »