注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Vision pro,当一切“眼见为实”

iOS
关于 Vision pro,留存一点感想,或许十年后再来回顾。缺点肯定不少,但是这个产品带来了很有趣的新维度 WWDC直播时,最大的疑问是眼动追踪交互足够准确吗?能即时反馈吗?看过各位媒体的文字或口述体验之后,才知道苹果竟然将这种交互方式做得像来自未来一样,...
继续阅读 »

关于 Vision pro,留存一点感想,或许十年后再来回顾。缺点肯定不少,但是这个产品带来了很有趣的新维度




WWDC直播时,最大的疑问是眼动追踪交互足够准确吗?能即时反馈吗?看过各位媒体的文字或口述体验之后,才知道苹果竟然将这种交互方式做得像来自未来一样,通过你的目光,精准得可以定位到一个小小的字母,随时随地随心而动,简直不可思议。


直播中所展示的三维交互效果,让我想起人类对信息的记录和显示方式。文字、图画刻在石头上,刻在竹简上,纸墨印刷成书册;照片、视频,呈现在手机或者电脑的二维屏幕上。而三维信息,可能从未被如此精确真实地呈现在我们的面前。诚然 3D 电影和许多别的 VR、AR 生产厂商也做了诸多努力和探索,但是那些效果都还不足以 “以假乱真”。从现在开始,消费级的信息记录方式,或许又能上升一个维度。


而直播中最大的震撼其实是迪士尼宣传片的一连串 What if...What if all the things we thought impossible were suddenly possible🤯一连串电影和想象中的角色和景观展现在我的眼前,和现实仿若融为一体。这让我感受到 Vision pro 或许能用一种全新的交互体验,让观众真正地身临其境,沉浸其中,甚至忘记真实和虚幻的界限。


直播接近尾声时,BGM 反复唱着 Be a Dreamer,苹果是个实干的梦想家,他们有足够的底气和积淀去梦想,更用努力和科技,将不可能变成可能,将许许多多科幻片中的梦想带来到 2023 年,用 Vision pro 为所有人铺就了无限大的画布,哦,不是二维的画布,是无限大的梦想空间!


当然,这个空间,目前似乎还只有基础的系统应用,像个刚通水电的毛坯房。他究竟能有怎样的表现,还是得看这些内容生产者开发出怎样的内容。有人诟病苹果在六月份拿出来这样的宣传,却要在明年年初才能售卖。可我相信,过去 APP Store 的成功很可能会在 Vision OS 中再现。WWDC,是苹果开发者大会,即主要面向开发者等专业人士的会议。Apple 召集起这些媒体,摄影师,导演,应用和游戏开发者率先开始了解 Vision pro。这些内容生产者,有他们,就有了 dream maker,造梦人,为普通用户编织光怪陆离的绚烂梦境。


看完各位博主的真机体验,Vision pro 并不是一个取代现有的手机、电脑的产品,这是一个全新的,开创新的体验维度,开创人类新需求的产品。


作为多年的哈迷,感觉现在 Vision pro 的语音输入,手势识别和 3D 交互完全可以让我们拿着魔杖释放咒语,让我们和神奇动物面对面,让我们就像骑着飞天扫帚一样去追踪金色飞贼。因为有了如此先进的科技,魔法世界不再是幻想🥺🤩


更可以想象,无论是工业设计,照片、视频、电视电影还是游戏,都可能会因为这种全新的沉浸式的三维交互体验,而被改写。


你可以和同事一起在虚拟空间中建造模型,模拟生产制造流程。


你可以把与亲朋好友、猫猫狗狗共度的美好时光定格在一片似真似幻的空间。无论何时再回首,他们好像永远在你身边,永不褪色。


电影制作人未来可以使用专门的摄像机制作沉浸式三维电影,在家就能有 100 英尺,接近 30 米的巨幕享受。 篮球比赛你可以选择不同的机位跟踪你喜欢的球星和精彩瞬间。 演唱会你可以在任何地方躺下享受最佳视角和空间音效。


而游戏,新增的交互体验更是给了游戏制作人们无限的想象空间。 操控赛车从北极的冰川到热带的雨林;在枪林弹雨中和队友并肩对抗敌人;在球赛场上面对面激情碰撞。配合上 AI 和语言模型,喜欢的二次元角色仿佛搭着你的肩膀和你耳语;所有的一切,开始“眼见为实”。


Vision pro 的眼部追踪、手势交互和 3D 显示混合现实的完成度,带来了像当年 iPhone 实现多点触控的革命性质变。从技术的进步来说,我个人认为这次的质变可能更加惊艳,更加了不起。但是 3499 美元,加上税可能 3 万人民币。说实话,这不是一个大众消费者能够接受的价格。即使有 air 版本,我感觉可能也需要上万人民币。所以,个人估计,它受欢迎的程度应该会大于等于 mac 小于 iPhone 和 AirPods。


当年 Apple Macintosh 开创了精美的高完成度的计算机图形界面 GUI,让电脑走入消费者群体,可价格太贵,真正让个人电脑普及的是微软;现在,iOS 将手机变成了一个功能强大的多媒体设备,可价格不便宜,真正让千家万户享受到智能手机的是 Android。未来,相比 Vision pro,或许有其他品牌的廉价替代品,更开放的开源混合现实系统,Vision 系列或许都不能获得最大的市场份额。可是由 Vision 所真正掀开的新维度不会被关闭,这个被精心打造出来的梦想空间只会无限延伸,满载着人类的创意和梦想…最后的最后,正如 WWDC 中 Apple 所言:


Be a dreamer. This is just the START.


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

SwiftUI 入门教程 - 基础控件

iOS
SwiftUI 是 Apple 新推出的一款能快速搭建页面的 framework。它采用的是声明式语法,简洁明了。 而且它是所见即所得的,你写的代码都能通过 Preview 实时的看到效果,这可以很大的节省开发者开发时间。当你开发一个复杂的项目,需要等待几分钟...
继续阅读 »

SwiftUI 是 Apple 新推出的一款能快速搭建页面的 framework。它采用的是声明式语法,简洁明了。


而且它是所见即所得的,你写的代码都能通过 Preview 实时的看到效果,这可以很大的节省开发者开发时间。当你开发一个复杂的项目,需要等待几分钟的时间去编译运行代码,只为了看一个 UILabel 字体大小或者颜色是否改变时,你就能体会到所见即所得的快乐了。


基础控件


当我们新建一个项目,选择 Interface 选择 SwiftUI 时,建好的项目会自带一个 ContentView,这是下面的默认代码:

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

ContentView 是需要我们根据需求修改代码的部分,下面的 ContentView_Previews 则是为了实时预览的。


Tips:如果注释ContentView_Previews,你会发现预览页面也会消失。


ContentView 代码说明


首先,可以看到 ContentView 有一个 body 的计算属性,该属性代表当前视图的内容。当你实现一个自定义 view 的时候,必须要实现该属性,否则代码会报错。


VStack 代表的是一个垂直布局。里面包含 Image 和 Text,两个控件垂直布局。padding 则代表当前视图外边距的间距。


Text 对应 UILabel


在 SwiftUI 中,用 Text 控件来展示静态文本。下面是它的代码示例:

Text("我是一个文本")
.font(.title)
.foregroundColor(.red)
.frame(width: 100, alignment: .center)
.lineLimit(1)
.background(.yellow)

常用的属性基本就这几个:


  • font:字体。如果想更加细致化的指定字体,可以用 system,.font(.system(size: 16, weight: .light))
  • foregroundColor:字体颜色。
  • frame:控制文本的大小和对齐位置。这个不写的话默认是自适应宽高。如果仅指定宽度就是高度自适应,仅指定高度就是宽度自适应。
  • lineLimit:指定行数,默认为 0,不限制行数。
  • background:用来设置背景。比如背景形状、背景颜色等等。

Tips:SwiftUI 的布局简化了自动布局和弱化了 frame 指定具体数值的布局方式。默认都是自适应的,这一点和 Flutter 类似,大大提高了开发效率。


Image 对应 UIImageView


在 SwiftUI 中,Image 用来展示图像资源。下面是它的示例代码:

Image(systemName: "globe")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.background(.red)

常用属性:


  • resizable:可调整大小以适应当前布局。
  • aspectRatio:调整缩放比。
  • foregroundColor、background:参见 Text。

Button 对应 UIButton


在 SwiftUI 中,用 Button 来表示一个按钮。下面是它的示例代码:

Button {
print("点击了按钮")
} label: {
Text("按钮文本")
Image(systemName: "globe")
}
.cornerRadius(10)
.background(.red)
.font(.body)
.border(.black, width: 2)

常用属性:


  • font、foregroundColor、background 等属性与 Text 使用一致。
  • label:用来自定义按钮的文本和图标。
  • cornerRadius:设置圆角。
  • border:设置边框。

总结


本文主要讲解了 SwiftUI 的三个基本控件 Text:用来展示静态文本;Image:用来加载图像资源;Button:用来展示按钮。以及三个控件的基本使用。希望通过此文大家可以对 SwiftUI 的语法有个基本的了解。


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

iOS 电商倒计时

iOS
背景 最近项目中,需要做一个如图所示的倒计时控件,上网搜了一圈,发现大家的方法大同小异,都是把倒计时的秒,转换成时分秒然后拼接字符串,见下图 网上大部分采用的方法 juejin.cn/post/684490…  在我的项目中,期望这个倒计时控件的f...
继续阅读 »

背景


最近项目中,需要做一个如图所示的倒计时控件,上网搜了一圈,发现大家的方法大同小异,都是把倒计时的秒,转换成时分秒然后拼接字符串,见下图




网上大部分采用的方法
juejin.cn/post/684490… 



在我的项目中,期望这个倒计时控件的format是可以自定义的,所以计算时分秒这样的方式,对于我的需求是不太灵活的


既然format需要自定义,那么很容易想到一个时间格式处理的类:DateFormatter


思路


后端返回的字段

init_time // 需要倒计时的时长,单位ms
format // 展示的倒计时格式

我们的需求其实非常明确,就是完成一个可以自定义format的倒计时label


那我们拆解一下整个需求:

  • 自定formatlabel
    • Date自定义format显示
    • 指定Date自定义format显示
  • 可以进行倒计时功能
  • 那么我们怎么才能把要倒计时的时长,转换为时分秒呢?

    • 直接计算后端给的init_time,算出是多少小时,多少分钟,多少秒
    • 如果我从每天的零点开始计时,然后把init_time作为偏移量不就是我要倒计时的时间吗,而且这个可以完美解决需要自定义format的问题,Date可以直接通过 DateFormatter转化成字符串 



Date自定义format显示

let df = DateFormatter()

df.dateFormat = "hh:mm:ss"

print("🍀", df.string(from: Date()), "🍀\n\n")

输出:🍀 03:56:28 🍀

指定Date自定义format显示

let df = DateFormatter()

var calendar = Calendar(identifier: .gregorian)

let startOfDate = calendar.startOfDay(for: Date())

df.dateFormat = "hh:mm:ss"

print("🍀", df.string(from: startOfDate), "🍀\n\n")

输出:🍀 12:00:00 🍀

完整功能

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        initCountdownTimer()

        return true

    }

private var timer: DispatchSourceTimer?

private var second = 0

// 单位ms
var delayTime = 0

// 单位ms
var interval = 1000
var initSecound = 10

var format = "hh:mm:ss"

private lazy var startDate: Date = {
var calendar = Calendar(identifier: .gregorian)
        let startOfDate = calendar.startOfDay(for: Date())
        return Date(timeInterval: TimeInterval(initSecound), since: startOfDate)
  }()

  private lazy var df: DateFormatter = {
let df = DateFormatter()
        df.dateFormat = format
        return df
  }()

  func initCountdownTimer() {
        timer = DispatchSource.makeTimerSource(queue: .main)
        timer?.schedule(deadline: .now() + .milliseconds(delayTime), repeating: .milliseconds(interval), leeway: .milliseconds(1))
        timer?.setEventHandler { [weak self] in
            self?.updateText()
            self?.second += 1
        }

        timer?.resume()
    }

    func deinitTimer() {
        timer?.cancel()
        timer = nil
    }

    func updateText() {
        if second == initSecound && second != 0 {
            deinitTimer()
        }
        if second == initSecound {
            return
        }
        let date = Date(timeInterval: -TimeInterval(second + 1), since: startDate)
        let text = df.string(from: date)

        print(text)
    }

输出:
12:00:09
12:00:08
12:00:07
12:00:06
12:00:05
12:00:04
12:00:03
12:00:02
12:00:01
12:00:00

以上整个功能基本完成,但是细心的同学肯定发现了,按道理小时部分应该是00,但是实际是12,这是为什么呢,为什么呢?


我在这里研究了好久,上网查了很多资料


最后去研究了foramt每个字母的意思才知道:

  • h 代表 12小时制

  • H 代表 24小时制,如果想要显示00,把"hh:mm:ss"改成"HH:mm:ss"即可


时间格式符号字段详见


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

Metal每日分享,不同色彩空间转换滤镜效果

iOS
本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像; Demo HarbethDemo地址iDay每日分享文档地址 实操代码// 色彩空间转换滤镜 let filter = C7ColorSpace.init(with:...
继续阅读 »

本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像;




Demo



实操代码

// 色彩空间转换滤镜
let filter = C7ColorSpace.init(with: .rgb_to_yuv)

// 方案1:
ImageView.image = try? BoxxIO(element: originImage, filters: [filter, filter2, filter3]).output()

// 方案2:
ImageView.image = originImage.filtering(filter, filter2, filter3)

// 方案3:
ImageView.image = originImage ->> filter ->> filter2 ->> filter3

效果对比图


  • 不同参数下效果

    实现原理


  • 过滤器


这款滤镜采用并行计算编码器设计.compute(kernel: type.rawValue)

/// 色彩空间转换
public struct C7ColorSpace: C7FilterProtocol {

public enum SwapType: String, CaseIterable {
case rgb_to_yiq = "C7ColorSpaceRGB2YIQ"
case yiq_to_rgb = "C7ColorSpaceYIQ2RGB"
case rgb_to_yuv = "C7ColorSpaceRGB2YUV"
case yuv_to_rgb = "C7ColorSpaceYUV2RGB"
}

private let type: SwapType

public var modifier: Modifier {
return .compute(kernel: type.rawValue)
}

public init(with type: SwapType) {
self.type = type
}
}

  • 着色器

每条通道乘以各自偏移求和得到Y,用Y作为新的像素rgb;

kernel void C7ColorSpaceRGB2Y(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half Y = half((0.299 * inColor.r) + (0.587 * inColor.g) + (0.114 * inColor.b));
const half4 outColor = half4(Y, Y, Y, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YIQ
kernel void C7ColorSpaceRGB2YIQ(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYIQ = half3x3({0.299, 0.587, 0.114}, {0.596, -0.274, -0.322}, {0.212, -0.523, 0.311});
const half3 yiq = RGBtoYIQ * inColor.rgb;
const half4 outColor = half4(yiq, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYIQ2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 YIQtoRGB = half3x3({1.0, 0.956, 0.621}, {1.0, -0.272, -0.647}, {1.0, -1.105, 1.702});
const half3 rgb = YIQtoRGB * inColor.rgb;
const half4 outColor = half4(rgb, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YUV
kernel void C7ColorSpaceRGB2YUV(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYUV = half3x3({0.299, 0.587, 0.114}, {-0.299, -0.587, 0.886}, {0.701, -0.587, -0.114});
const half3 yuv = RGBtoYUV * inColor.rgb;
const half4 outColor = half4(yuv, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYUV2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
                                texture2d<half, access::read> inputTexture [[texture(1)]],
                                uint2 grid [[thread_position_in_grid]]) {
    const half4 inColor = inputTexture.read(grid);

    const half3x3 YUVtoRGB = half3x3({1.0, 0.0, 1.28033}, {1.0, -0.21482, -0.38059}, {1.0, 2.21798, 0.0});
    const half3 rgb = YUVtoRGB * inColor.rgb;
    const half4 outColor = half4(rgb, inColor.a);

    outputTexture.write(outColor, grid);
}

色彩空间


  • YIQ

在YIQ系统中,是NTSC(National Television Standards Committee)电视系统标准;


  • Y是提供黑白电视及彩色电视的亮度信号Luminance,即亮度Brightness;
  • I代表In-phase,色彩从橙色到青色;
  • Q代表Quadrature-phase,色彩从紫色到黄绿色;



转换公式如下:




  • YUV

YUV是在工程师想要在黑白基础设施中使用彩色电视时发明的。他们需要一种信号传输方法,既能与黑白 (B&W) 电视兼容,又能添加颜色。亮度分量已经作为黑白信号存在;他们将紫外线信号作为解决方案添加到其中。

由于 U 和 V 是色差信号,因此在直接 R 和 B 信号上选择色度的 UV 表示。换句话说,U 和 V 信号告诉电视在不改变亮度的情况下改变某个点的颜色。
或者 U 和 V 信号告诉显示器以牺牲另一种颜色为代价使一种颜色更亮,以及它应该移动多少。
U 和 V 值越高(或负值越低),斑点的饱和度(色彩)就越高。
U 值和 V 值越接近零,颜色偏移越小,这意味着红、绿和蓝光的亮度会更均匀,从而产生更灰的点。
这是使用色差信号的好处,即不是告诉颜色有多少红色,而是告诉红色比绿色或蓝色多多少。
反过来,这意味着当 U 和 V 信号为零或不存在时,它只会显示灰度图像。
如果使用 R 和 B,即使在黑白场景中,它们也将具有非零值,需要所有三个数据承载信号。
这在早期的彩色电视中很重要,因为旧的黑白电视信号没有 U 和 V 信号,这意味着彩色电视开箱后只会显示为黑白电视。
此外,黑白接收器可以接收 Y' 信号并忽略 U 和 V 颜色信号,使 YUV 向后兼容所有现有的黑白设备、输入和输出。
如果彩色电视标准不使用色差信号,这可能意味着彩色电视会从 B& 中产生有趣的颜色 W 广播,否则需要额外的电路将黑白信号转换为彩色。
有必要为色度通道分配较窄的带宽,因为没有可用的额外带宽。
如果某些亮度信息是通过色度通道到达的(如果使用 RB 信号而不是差分 UV 信号,就会出现这种情况),黑白分辨率就会受到影响。

YUV 模型定义了一个亮度分量 (Y),表示物理线性空间亮度,以及两个色度分量,分别称为 U(蓝色投影)和 V(红色投影)。它可用于在 RGB 模型之间进行转换,并具有不同的颜色空间




转换公式如下:




最后


  • 慢慢再补充其他相关滤镜,喜欢就给我点个星🌟吧。

✌️.


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

Swift - LeetCode - 二叉树的所有路径

iOS
题目 给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1: 输入:root = [1,2,3,null,5]输出:["1->2->5","1->3"] 示例 2:...
继续阅读 »

题目


给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。


叶子节点 是指没有子节点的节点。


示例 1:



  • 输入:root = [1,2,3,null,5]
  • 输出:["1->2->5","1->3"]


示例 2:



  • 输入:root = [1]
  • 输出:["1"]


提示:


  • 树中节点的数目在范围 [1, 100] 内
  • -100 <= Node.val <= 100

方法一:深度优先搜索


思路及解法


最直观的方法是使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。


  • 如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。
  • 如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。

如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。当然,深度优先搜索也可以使用非递归的方式实现,这里不再赘述。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
constructPaths(root, "", &paths)
return paths
}

func constructPaths(_ root: TreeNode?, _ path: String, _ paths: inout [String]) {
if nil != root {
var path = path
path += String(root!.val)
if nil == root?.left && nil == root?.right {
paths.append(path)
} else {
path += "->"
constructPaths(root?.left, path, &paths)
constructPaths(root?.right, path, &paths)
}
}
}
}

复杂度分析

  • 时间复杂度:(2),其中  表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对  变量进行拷贝构造,时间代价为 (),故时间复杂度为 (2)

  • 空间复杂度:(2),其中  表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 ,此时每一层的  变量的空间代价的总和为 (=1)=(2) 空间复杂度为 (2)。最好情况下,当二叉树为平衡二叉树时,它的高度为 log,此时空间复杂度为 ((log)2)


方法二:广度优先搜索


思路及解法


我们也可以用广度优先搜索来实现。我们维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,我们取出队列中的首节点,如果它是叶子节点,则将它对应的路径加入到答案中。如果它不是叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时广度优先搜索结束,我们即能得到答案。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
if nil == root {
return paths
}
var node_queue: [TreeNode] = []
var path_queue: [String] = []

node_queue.append(root!)
path_queue.append(String(root!.val))

while !node_queue.isEmpty {
let node: TreeNode? = node_queue.removeFirst()
let path: String = path_queue.removeFirst()

if nil == node?.left && nil == node?.right {
paths.append(path)
} else {
if nil != node?.left {
node_queue.append(node!.left!)
path_queue.append(path + "->" + String(node!.left!.val))
}

if nil != node?.right {
node_queue.append(node!.right!)
path_queue.append(path + "->" + String(node!.right!.val))
}
}
}
return paths
}
}

复杂度分析


  • 时间复杂度:(2),其中  表示节点数目。分析同方法一。

  • 空间复杂度:(2),其中  表示节点数目。在最坏情况下,队列中会存在  个节点,保存字符串的队列中每个节点的最大长度为 ,故空间复杂度为 (2)


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

交互小组件 — iOS 17

iOS
作为一名 iOS 开发人员,该平台有一些令人兴奋的特性和功能值得探索。 其中,小部件是我的最爱。 小部件已成为 iOS 和 macOS 体验中不可或缺的一部分,并且随着 SwiftUI 中引入的最新功能,它们现在变得更加强大。 在本文中,我们将探讨如何通过交互...
继续阅读 »

作为一名 iOS 开发人员,该平台有一些令人兴奋的特性和功能值得探索。 其中,小部件是我的最爱。 小部件已成为 iOS 和 macOS 体验中不可或缺的一部分,并且随着 SwiftUI 中引入的最新功能,它们现在变得更加强大。 在本文中,我们将探讨如何通过交互性和动画使小组件变得栩栩如生,使它们更具吸引力和视觉吸引力。 我们将深入探讨动画如何与小组件配合使用的细节,并展示新的 Xcode Preview API,它可以实现快速迭代和自定义。 此外,我们将探索如何使用熟悉的控件(如 Button 和 Toggle)向小部件添加交互性,并利用 App Intents 的强大功能。 那么让我们开始吧!


小部件中的交互性
小部件在单独的进程中呈现,它们的视图代码仅在归档期间运行。 为了使小组件具有交互性,我们可以使用 Button 和 Toggle 等控件。 但是,由于 SwiftUI 不会在应用程序的进程空间中执行闭包或改变绑定,因此我们需要一种方法来表示可由小部件扩展执行的操作。 App Intents 为此提供了一个解决方案,允许我们定义可由系统调用的操作。 通过导入 SwiftUI 和 AppIntents,我们可以使用接受 AppIntent 作为参数的 Button 和 Toggle 初始值设定项来执行所需的操作。


现在我们要为现有项目创建小组件。




相应地命名它。 请注意,禁用两个复选框




现在我将使用清单和按钮重写现有代码。

struct Provider: TimelineProvider {  
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry( checkList: Array(ModelData.shared.items.prefix(3)))
}

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(checkList: Array(ModelData.shared.items.prefix(3)))
completion(entry)
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//var entries: [SimpleEntry] = []

// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let data = Array(ModelData.shared.items.prefix(3))
let entries = [SimpleEntry(checkList: data)]

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}

struct SimpleEntry: TimelineEntry {
var date: Date = .now

var checkList: [ProvisionModel]
}

struct InteractiveWidgetEntryView : View {
var entry: Provider.Entry

var body: some View {
VStack(alignment: .leading, spacing: 5.0) {
Text("My List")
if entry.checkList.isEmpty{
Text("You've bought all🏆")
}else{
ForEach(entry.checkList) { item in
HStack(spacing: 5.0){

Image(systemName: item.isAdded ? "checkmark.circle.fill":"circle")
.foregroundColor(.green)


VStack(alignment: .leading, spacing: 5){
Text(item.itemName)
.textScale(.secondary)
.lineLimit(1)
Divider()
}
}
}
}
}
.containerBackground(.fill.tertiary, for: .widget)
}
}

struct InteractiveWidget: Widget {
let kind: String = "InteractiveWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
InteractiveWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}


提供的代码在 iOS 或 macOS 应用程序中使用 SwiftUI 定义小部件。 让我们分解代码并解释每个部分:

  1. Provider:该结构体符合TimelineProvider协议,负责向widget提供数据。 它包含三个功能:
    • placeholder(in:):此函数返回一个占位符条目,表示首次添加小部件时的外观。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry。
    • getSnapshot(in:completion:):此函数生成一个表示小部件当前状态的快照条目。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry。
    • getTimeline(in:completion:):此函数生成小部件的条目时间线。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry 实例的数组,并返回包含这些条目的时间线。
    1. SimpleEntry:此结构符合 TimelineEntry 协议,表示小部件时间线中的单个条目。 它包含一个表示条目日期的日期属性和一个 checkList 属性,后者是一个 ProvisionModel 项的数组。
    2. InteractiveWidgetEntryView:此结构定义用于显示小部件条目的视图层次结构。 它采用 Provider.Entry 类型的条目作为输入。 在 body 属性内部,它创建一个具有对齐和间距设置的 VStack。 它显示一个标题,并根据 checkList 数组是否为空,显示一条消息或迭代该数组以显示每个项目的信息。
    3. InteractiveWidget:该结构定义小部件本身。 它符合Widget协议并指定了Widget的种类。 它提供了一个 StaticConfiguration,其中包含一个 Provider 实例作为数据提供者,并提供一个 InteractiveWidgetEntryView 作为每个条目的视图。 它还设置小部件的显示名称和描述。
    4. Preview:此代码块用于在开发过程中预览小部件的外观。 它为 .systemSmall 大小的小部件创建预览,并提供 SimpleEntry 实例作为条目。 总的来说,此代码设置了一个使用 SwiftUI 框架显示清单的小部件。 小部件的数据由 Provider 结构提供,条目的视图由 InteractiveWidgetEntryView 结构定义。 InteractiveWidget 结构配置小部件并提供用于开发目的的预览。


还有按钮动作!


Apple 为此推出了 AppIntents!


我已经创建了视图模型和应用程序意图。

struct ProvisionModel: Identifiable{  
var id: String = UUID().uuidString
var itemName: String
var isAdded: Bool = false

}

class ModelData{
static let shared = ModelData()

var items: [ProvisionModel] = [.init(
itemName: "Orange"
), .init(
itemName: "Cheese"
), .init(
itemName: "Bread"
), .init(
itemName: "Rice"
), .init(
itemName: "Sugar"
), .init(
itemName: "Oil"
), .init(
itemName: "Chocolate"
), .init(
itemName: "Corn"
)]
}

提供的代码包括两个数据结构的定义:ProvisionModel 和 ModelData。 以下是每项的解释:


ProvisionModel:该结构表示清单中的一个供应项。 它符合可识别协议,该协议要求它具有唯一的标识符。 它具有以下属性:


id:一个字符串属性,保存使用 UUID 生成的唯一标识符。 每个 ProvisionModel 实例都会有一个不同的 id。


itemName:表示供应项目名称的字符串属性。


isAdded:一个布尔属性,指示该项目是否已添加到清单中。 它使用默认值 false 进行初始化。


ModelData:此类充当数据存储和单例,提供对供应项的共享访问。 它具有以下组件:
共享:ModelData 类型的静态属性,表示类的共享实例。 它遵循单例模式,允许跨应用程序访问同一实例。


items:一个数组属性,包含表示供应项的 ProvisionModel 实例。 该数组使用一组预定义的项目进行初始化,每个项目都使用特定的 itemName 进行初始化。 ModelData.shared 实例提供对此数组的访问。
总的来说,此代码为清单应用程序设置了数据模型。 ProvisionModel 结构定义每个供应项的属性,包括其唯一标识符以及是否已添加到清单中。 ModelData 类提供对供应项列表的共享访问,并遵循单例模式以确保访问和修改数据的一致性。


现在是 appIntent 的时候了!

struct MyActionIntent: AppIntent{  

static var title: LocalizedStringResource = "Toggle Task State"
@Parameter(title: "Task ID")
var id: String
init(){

}

init(id: String){
self.id = id
}

func perform() async throws -> some IntentResult {
if let index = ModelData.shared.items.firstIndex(where: { $0.id == id }) {
ModelData.shared.items[index].isAdded.toggle()

let itemToRemove = ModelData.shared.items[index]
ModelData.shared.items.remove(at: index)

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
ModelData.shared.items.removeAll(where: { $0.id == itemToRemove.id })
}

print("Updated")
}

return .result()
}
}

提供的代码定义了一个名为 MyActionIntent 的结构,该结构符合 AppIntent 协议。 此结构表示在清单应用程序中切换任务状态的意图。 以下是对其组成部分的解释:


title(静态属性):该属性表示操作意图的标题。 它的类型为 LocalizedStringResource,它是用于本地化目的的本地化字符串资源。


id(属性装饰器):该属性用@Parameter装饰,表示需要切换的任务的ID。


init():这是结构的默认初始化程序。 它不执行任何特定的初始化。


init(id: String):此初始化程序允许您使用特定任务 ID 创建 MyActionIntent 实例。


Perform()(方法):AppIntent 协议需要此方法,并执行与 Intent 相关的操作。
以下是其实施细目:
它检查 ModelData.shared.items 数组中是否存在与意图中提供的 ID 匹配的任务。


如果找到匹配项,它将使用toggle() 方法切换任务的isAdded 属性。 这会改变任务的状态。
然后,它创建一个局部变量 itemToRemove 来存储切换的任务。
使用remove(at:)方法和找到任务的索引从ModelData.shared.items数组中删除任务。
延迟 2 秒后,使用removeAll(where:) 和检查匹配 ID 的闭包从 ModelData.shared.items 数组中删除 itemToRemove。


最后,“Updated”被打印到控制台。
return .result():该语句返回一个IntentResult实例,表示intent的完成,没有任何具体的结果值。
总的来说,此代码定义了一个意图,用于执行切换清单中任务状态的操作。 它访问 ModelData 的共享实例,以根据提供的 ID 查找和修改任务。


现在是时候用 AppIntents 替换图像了

Button(intent: MyActionIntent(id: item.id)) {  
Image(systemName: item.isAdded ? "checkmark.circle.fill":"circle")
.foregroundColor(.green)
}
.buttonStyle(.plain)


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

iOS文件系统

iOS
沙盒机制 概念 iOS 沙盒机制是一种安全策略,它将每个应用程序的数据和资源隔离在一个专用目录中,限制了应用程序访问其他应用程序或系统文件的能力,从而保护了用户数据和系统安全. 目录结构 For security purposes, an iOS app’s...
继续阅读 »

沙盒机制


概念


iOS 沙盒机制是一种安全策略,它将每个应用程序的数据和资源隔离在一个专用目录中,限制了应用程序访问其他应用程序或系统文件的能力,从而保护了用户数据和系统安全.


目录结构



For security purposes, an iOS app’s interactions with the file system are limited to the directories inside the app’s sandbox directory. During installation of a new app, the installer creates a number of container directories for the app inside the sandbox directory. Each container directory has a specific role. The bundle container directory holds the app’s bundle, whereas the data container directory holds data for both the app and the user. The data container directory is further divided into a number of subdirectories that the app can use to sort and organize its data. The app may also request access to additional container directories—for example, the iCloud container—at runtime.



👆大概内容讲的是出于安全考虑,iOS应用只能在当前APP的沙盒目录下与文件系统进行交互(读取、创建、删除等).在APP安装时,系统会在当前APP的沙盒目录下创建不同类型不同功能的容器目录(Bundle、Data、iCloud).Data Container又进一步被划分为Documents、Library、temp、System Data.沙盒目录结构如下图所示:




各目录描述如下图所示:




下面介绍一些关于文件系统中常用的API.


创建NSFileManager

// 1.创建实例
NSFileManager *fileManager = [[NSFileManager alloc] init];

// 2.获取单例
NSFileManager *fileManager = [NSFileManager defaultManager];

// 3.自定义
自定义NSFileManager实现一些自定义功能.

NSFileManager有一个delegate(NSFileManagerDelegate)属性,实现该协议的对象能够对文件的拷贝、移动等操作做更多的逻辑处理,同时能在这些操作发生错误时做一些容错的处理.

// 该协议用于控制文件/文件夹,是否允许移动、删除、拷贝.以及允许这些操作发生错误时进行额外的处理.
@protocol NSFileManagerDelegate <NSObject>

// 控制是否允许该操作:
// 以下每种方法都有一个NSURL和NSString的版本,URL和Path同作用的方法只会调用一次,并且优先调用URL的方法.如果两个方法都没实现,则实现系统的默认值YES.
// 其中srcURL/srcPath分别代表原(文件/文件夹)路径URL(file://xxx)/路径(xxx). xxx为完整路径.
// 其中dstURL/dstPath分别代表目标(文件/文件夹)路径URL(file://xxx)/路径(xxx). xxx为完整路径.

// 错误处理:
// 以下每种方法都有一个NSURL和NSString的版本,URL和Path同作用的方法只会调用一次,并且优先调用URL的方法.如果两个方法都没实现,则不处理错误.

@optional

/// Moving an Item

// 在移动文件/文件夹前调用,控制是否允许移动操作
// 如果两个方法都没有实现,系统默认YES即允许移动.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldMoveItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldMoveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

// 移动失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error movingItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error movingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

/// Copy an Item

// 在拷贝文件/文件夹前调用,控制是否允许拷贝操作
// 如果两个方法都没有实现,系统默认YES即允许拷贝.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

// 拷贝失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;

/// Delete an Item

// 在拷贝文件/文件夹前调用,控制是否允许删除操作
// 如果两个方法都没有实现,系统默认YES即允许删除.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtPath:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtURL:(NSURL *)URL;

// 删除失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtPath:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtURL:(NSURL *)URL;

@end

考虑线程安全的问题,如果需要实现NSFileManagerDelegate协议,通常是新创建NSFileManager实例或是继承NSFileManager后新建实例,确保一个一个实例仅对应一个delegate.


创建文件/文件夹

/// 文件

// 创建文件,会覆盖已存在的文件内容.
// path: 文件路径.
// data: 文件内容.
// attr: 文件属性.例如:设置文件夹可读写权限、文件夹创建日期中.传入nil,则使用系统默认的配置.
// return: YES:文件存在或创建成功. NO:文件创建失败
- (BOOL)createFileAtPath:(NSString *)path
contents:(NSData *)data
attributes:(NSDictionary<NSFileAttributeKey, id> *)attr;

/// 文件夹

// url: 文件夹路径URL.
// createIntermediate: 是否自动创建目录中不存在的中间目录,如果设置为NO,仅仅会创建路径的最后一级目录,若任意中间路径不存在,该方法会返回NO.同时如果任意中间目录是文件也会失败.
// attributes: nil,则使用系统默认的配置.
// error: 错误信息.
// YES: 文件夹创建成功.YES: createIntermediates为YES且文件夹已存在. NO: 错误发生.
- (BOOL)createDirectoryAtURL:(NSURL *)url
withIntermediateDirectories:(BOOL)createIntermediates
attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
error:(NSError * _Nullable *)error;

// 同上
- (BOOL)createDirectoryAtPath:(NSString *)path
withIntermediateDirectories:(BOOL)createIntermediates
attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
error:(NSError * _Nullable *)error;


其他方式写入文件:NSData、NSString、All kinds Of Collections (写入plist).


删除文件/文件夹


删除操作是指将文件/文件夹从指定目录移除掉.

// 以file://xxx的形式删除文件/文件夹.
// srcURL: 原文件/文件夹目录URL
// dstURL: 目标目录URL.
// error: 错误信息.
// return: YES: 移动成功或URL为nil或delegate停止删除操作. NO: 错误发生.
- (BOOL)removeItemAtURL:(NSURL *)URL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)removeItemAtPath:(NSString *)path
error:(NSError * _Nullable *)error;

// 将文件/文件夹移入到废纸篓,适用于Macos.iOS上使用会失败.
- (BOOL)trashItemAtURL:(NSURL *)url
resultingItemURL:(NSURL * _Nullable *)outResultingURL
error:(NSError * _Nullable *)error;

// 注意:
// 1.如果删除的是文件夹,则会删除文件夹中所有的内容.
// 2.删除文件前会调用delegate的-[fileManager:shouldRemoveItemAtURL:]或-[fileManager:shouldRemoveItemAtPath:]方法,用于控制能否删除.如果都没有实现,则默认可以删除.
// 3.删除失败时会调用delegate的-[fileManager:shouldProceedAfterError:removingItemAtURL:]或-[fileManager:shouldProceedAfterError:removingItemAtPath:]方法,用于处理错误.
// 如果2个方法都没有实现则删除失败.并且删除方法会返回相应的error信息.
// 方法返回YES会认为删除成功,返回NO则删除失败,删除方法接收error信息.

文件/文件夹是否存在


// path: 文件/文件夹路径.
// isDirectory: YES: 当前为文件夹. NO: 当前为文件.
// return: YES: 文件/文件夹存在. NO: 文件/文件夹不存在.
- (BOOL)fileExistsAtPath:(NSString *)path
isDirectory:(BOOL *)isDirectory;

// 同上,不过不能判断当前是文件还是文件夹.
- (BOOL)fileExistsAtPath:(NSString *)path;

遍历文件夹


有时候我们并不知道文件的具体路径,此时就需要遍历文件夹去拼接完整路径.

/// 浅度遍历: 返回当前目录下的文件/文件夹(包括隐藏文件).
// 默认带上了options: NSDirectoryEnumerationSkipsSubdirectoryDescendants.
// NSDirectoryEnumerationIncludesDirectoriesPostOrder无效,因为不会遍历子目录.
- (nullable NSArray<NSString *> *)contentsOfDirectoryAtPath:(NSString *)path
error:(NSError **)error

// url: 文件路径.
// keys: 预请求每个文件属性的key数组.如果不想预请求则传入@[],传nil会有默认的预请求keys.
// options: 遍历时可选掩码.
// error: 错误信息.
- (nullable NSArray<NSURL *> *)contentsOfDirectoryAtURL:(NSURL *)url
includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys
options:(NSDirectoryEnumerationOptions)mask
error:(NSError **)error

/// 深度遍历(递归遍历): 返回当前目录下的所有文件/文件夹(包括隐藏文件).
- (nullable NSDirectoryEnumerator<NSString *> *)enumeratorAtPath:(NSString *)path
- (nullable NSDirectoryEnumerator<NSURL *> *)enumeratorAtURL:(NSURL *)url
includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys
options:(NSDirectoryEnumerationOptions)mask
errorHandler:(nullable BOOL (^)(NSURL *url, NSError *error))handler
- (NSArray<NSString *> *)subpathsOfDirectoryAtPath:(NSString *)path
error:(NSError * _Nullable *)error;
- (NSArray<NSString *> *)subpathsAtPath:(NSString *)path;

获取文件夹

// iOS基本上都是使用NSUserDomainMask.

// 获取指定目录类型和mask的文件夹.类似于NSSearchPathForDirectoriesInDomains方法.
// 常用的directory:
// NSApplicationSupportDirectory -> Library/Application Support.
// NSCachesDirectory -> /Library/Caches.
// NSLibraryDirectory -> /Library.
- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory
inDomains:(NSSearchPathDomainMask)domainMask

// 获取指定directory & domainMask下的文件夹.
// domain: 不能传入NSAllDomainsMask.
// url: 仅当domain = NSUserDomainMask & directory = NSItemReplacementDirectory时有效.
// shouldCreate: 文件不存在时 是否创建.
- (NSURL *)URLForDirectory:(NSSearchPathDirectory)directory
inDomain:(NSSearchPathDomainMask)domain
appropriateForURL:(NSURL *)url
create:(BOOL)shouldCreate
error:(NSError * _Nullable *)error;
// 同上
FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
// 获取/temp目录
FOUNDATION_EXPORT NSString *NSTemporaryDirectory(void);
// 获取沙盒根目录
FOUNDATION_EXPORT NSString *NSHomeDirectory(void);

设置和获取文件/文件夹属性

// 获取指定目录下文件/文件夹的属性[NSFileAttributeKey].(https://developer.apple.com/documentation/foundation/nsfileattributekey).
- (nullable NSDictionary<NSFileAttributeKey, id> *)attributesOfItemAtPath:(NSString *)path
error:(NSError **)error;

// 为指定目录文件/文件夹设置属性.
- (BOOL)setAttributes:(NSDictionary<NSFileAttributeKey, id> *)attributes ofItemAtPath:(NSString *)path error:(NSError * _Nullable *)error;

// 基于NSDictionary提供的便利方法 //
@interface NSDictionary<KeyType, ObjectType> (NSFileAttributes)

// 文件大小
- (unsigned long long)fileSize;

// 文件创建日期
- (nullable NSDate *)fileCreationDate;
// 文件修改日期
- (nullable NSDate *)fileModificationDate;

// 文件类型.NSFileAttributeType
- (nullable NSString *)fileType;

// 文件权限掩码
- (NSUInteger)filePosixPermissions; // 位掩码 可见文件 _s_ifmt.h eg:S_IRWXU
/* File mode */
/* Read, write, execute/search by owner */
#define S_IRWXU 0000700 /* [XSI] RWX mask for owner */
#define S_IRUSR 0000400 /* [XSI] R for owner */
#define S_IWUSR 0000200 /* [XSI] W for owner */
#define S_IXUSR 0000100 /* [XSI] X for owner */
/* Read, write, execute/search by group */
#define S_IRWXG 0000070 /* [XSI] RWX mask for group */
#define S_IRGRP 0000040 /* [XSI] R for group */
#define S_IWGRP 0000020 /* [XSI] W for group */
#define S_IXGRP 0000010 /* [XSI] X for group */
/* Read, write, execute/search by others */
#define S_IRWXO 0000007 /* [XSI] RWX mask for other */
#define S_IROTH 0000004 /* [XSI] R for other */
#define S_IWOTH 0000002 /* [XSI] W for other */
#define S_IXOTH 0000001 /* [XSI] X for other */

// 当前文件/文件夹所处的文件系统编号
- (NSInteger)fileSystemNumber;
这两个方法可以拼接文件的引用URL -> file:///.file/id=fileSystemNumber.fileSystemFileNumber
// 当前文件/文件夹在文件系统中的编号
- (NSUInteger)fileSystemFileNumber;

// 是否是隐藏文件
- (BOOL)fileExtensionHidden;
...

@end

移动文件/文件夹


移动操作是将文件从一个位置移动到另一个位置.

// 以file://xxx的形式移动文件/文件夹.
// srcURL: 原文件/文件夹位置URL.
// dstURL: 目标位置URL.
// error: 错误信息.
// return: YES: 移动成功或manager的delegate停止移动操作. NO: 错误发生.
- (BOOL)moveItemAtURL:(NSURL *)srcURL
toURL:(NSURL *)dstURL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)moveItemAtPath:(NSString *)srcPath
toPath:(NSString *)dstPath
error:(NSError * _Nullable *)error;

// 注意:
// 1.srcURL/srcPath、dstURL/dstPath任意为nil会crash.
// 2.如果目标目录文件已存在会被覆盖.
// 3.移动文件前会调用delegate的-[fileManager:shouldMoveItemAtURL:toURL:]或-[fileManager:shouldMoveItemAtPath:toPath:]方法,用于控制能否移动.如果都没有实现,则默认可以移动.
// 4.移动失败时会调用delegate的-[fileManager:shouldMoveItemAtURL:toURL:]或-[fileManager:shouldMoveItemAtPath:toPath:]方法,用于处理错误.
// 如果2个方法都没有实现则移动失败.并且移动方法会返回相应的error信息.
// 方法返回YES会认为移动成功,返回NO则移动失败,移动方法接收error信息.

拷贝文件/文件夹


拷贝操作是将原文件从一个位置copy到另一个位置,类似于复制粘贴.

// 以file://xxx的形式拷贝文件/文件夹.
// srcURL: 原文件/文件夹/位置URL.
// dstURL: 目标位置URL.
// error: 错误信息.
// return: YES: 拷贝成功或manager的delegate停止拷贝操作. NO: 错误发生.
- (BOOL)copyItemAtURL:(NSURL *)srcURL
toURL:(NSURL *)dstURL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)copyItemAtPath:(NSString *)srcPath
toPath:(NSString *)dstPath
error:(NSError * _Nullable *)error;

// 注意:
// 1.srcURL/srcPath、dstURL/dstPath任意为nil会crash.
// 2.如果目标目录已经存在则会发生错误.
// 3.拷贝文件前会调用delegate的-[fileManager:shouldCopyItemAtURL:toURL:]或-[fileManager:shouldCopyItemAtPath:toPath:]方法,用于控制能否拷贝.如果都没有实现,则默认可以拷贝.
// 4.拷贝失败时会调用delegate的-[fileManager:shouldProceedAfterError:copyingItemAtURL:toURL:]或-[fileManager:shouldProceedAfterError:copyingItemAtPath:toPath:]方法,用于处理错误.
// 如果2个方法都没有实现则拷贝失败.并且拷贝方法会返回相应的error信息.
// 方法返回YES会认为拷贝成功,返回NO则拷贝失败,拷贝方法接收error信息.

参考资料


  1. developer.apple.com/library/arc…
  2. developer.apple.com/documentati…

好物推荐


  1. OpenSim:用于快速定位模拟器中项目沙盒目录.

  2. Flex:用于真机或模拟器Debug环境下调试.


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

iOS 内存泄漏排查方法及原因分析

iOS
本文将从以下两个层面解决iOS内存泄漏问题:内存泄漏排查方法(工具)内存泄漏原因分析(解决方案) 在正式开始前,我们先区分两个基本概念: 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直...
继续阅读 »

本文将从以下两个层面解决iOS内存泄漏问题:

  • 内存泄漏排查方法(工具)
  • 内存泄漏原因分析(解决方案)


在正式开始前,我们先区分两个基本概念:


  • 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。(因此,开发中我们要尽量避免内存泄漏的出现)
  • 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用。 通俗理解就是内存不够用了,通常在运行大型应用或游戏时,应用或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。最终导致机器重启或者程序crash


简单来说:





一、排查方法



我们知道,iOS开发有“ARC机制”帮忙管理内存,但在实际开发中,如果处理不好堆空间上的内存还是会存在内存泄漏的问题。如果内存泄漏严重,最终会导致程序的崩溃。



首先,我们需要检查我们的App有没有内存泄漏,并且快速定位到内存泄漏的代码。目前比较常用的内存泄漏的排查方法有两种,都在Xcode中可以直接使用:


  • 第一种:静态分析方法(Analyze
  • 第二种:动态分析方法(Instrument工具库里的Leaks)。一般推荐使用第二种。

1.1 静态内存泄漏分析方法:


  • 第一步:通过Xcode打开项目,然后点击Product->Analyze,开始进入静态内存泄漏分析。 如下图所示:

  • 第二步:等待分析结果。

  • 第三步:根据分析的结果对可能造成内存泄漏的代码进行排查,如下图所示。



PS:静态内存泄漏分析能发现大部分问题,但只是静态分析,并且并不准确,只是有可能发生内存泄漏。一些动态内存分配的情形并没有分析。如果需要更精准一些,那就要用到下面要介绍的动态内存泄漏分析方法(Instruments工具中的Leaks方法)进行排查。



1.2 动态内存泄漏分析方法:



静态内存泄漏分析不能把所有的内存泄漏排查出来,因为有的内存泄漏发生在运行时,当用户做某些操作时才发生内存泄漏。这是就要使用动态内存泄漏检测方法了。



步骤如下:


  • 第一步:通过Xcode打开项目,然后点击Product->Profile,如下图所示:

    • 第二步:按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择Leaks选项,点击右下角的【choose】按钮。如下图:

    • 第三步:这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:


点击左上角的红色圆点,这时项目开始启动了,由于Leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。



选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。


举个例子:



PS:AFHTTPSessionManager内存泄漏是一个很常见的问题:解决方法有两种:点击这里





二、内存泄漏的原因分析


目前,在ARC环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致dealloc()方法无法被调用。主要原因大概有一下几种类型:


2.1 ViewController中存在NSTimer


如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用

[NSTimer scheduledTimerWithTimeInterval:1.0 
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];

  • 理由:这时 target: self,增加了ViewController的retain count, 即self强引用timertimer强引用self。造成循环引用。
  • 解决方案:在恰当时机调用[timer invalidate]即可。

2.2 ViewController中的代理delegate



代理在一般情况下,需要使用weak修饰。如果你这个VC需要外部传某个delegate进来,通过delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量使用weak修饰,否则你的VC会持续持有这个delegate,直到代理自身被释放。



  • 理由:如果代理用strong修饰,ViewController(self)会强引用ViewView强引用delegatedelegate内部强引用ViewController(self)。造成内存泄漏。
  • 解决方案:代理尽量使用weak修饰。

举个例子:代理一般用weak修饰,避免循环引用。

@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end


@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;

- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画

2.3 ViewController中Block



在我们日常开发中,如果block使用不当,很容易导致内存泄漏。



  • 理由:如果block被当前ViewController(self)持有,这时,如果block内部再持有ViewController(self),就会造成循环引用。
  • 解决方案:在block外部对弱化self,再在block内部强化已经弱化的weakSelf

For Example:

    __weak typeof(self) weakSelf = self;

[self.operationQueue addOperationWithBlock:^{

__strong typeof(weakSelf) strongSelf = weakSelf;

if (completionHandler) {

KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);

completionHandler([strongSelf serialReaderWithRequest:request]);
}
}];

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

如何判断设备是否越狱?

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »