// TODO: -仿写一个Twitter App

iOS开发系列集合
// TODO: 开篇
extension Notification {
public var keyboardSize: CGSize? {
return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size
}
public var keyboardAnimationDuration: TimeInterval? {
return userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval
}
}
public protocol KeyboardObserving: AnyObject {
func startObservingKeyboard()
func stopObservingKeyboard()
func keyboardWillShow(notification: Notification)
func keyboardWillHide(notification: Notification)
}
UIViewController有生命周期,需要在特定的阶段开启或取消对通知的监听,在willAppear
和willDisappear
阶段调用func startObservingKeyboard()
及func stopObservingKeyboard()
是比较方便的。尤其是present出另一个viewController的情况下,如果没有取消对键盘的监听,那么第二个ViewController触发键盘可能会影响第一个控制器执行升起键盘后的动画。
这个协议希望被需要监听键盘即将升降的对象来遵守,通常是UIViewController
,所以我们在默认实现中也通过where Self: UIViewController
限制为UIViewController
。
public extension KeyboardObserving where Self: UIViewController {
func startObservingKeyboard() {
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] (notification) in
self?.keyboardWillShow(notification: notification)
}
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] (notification) in
self?.keyboardWillHide(notification: notification)
}
}
func stopObservingKeyboard() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
}
class MediaBrowseViewController: UIViewController, KeyboardObserving {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 在生命周期的即将显示阶段 调用协议中的开启监听方法的默认实现
self.startObservingKeyboard()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 在生命周期的即将消失阶段 调用协议中的结束监听方法的默认实现
self.stopObservingKeyboard()
}
// MARK: - KeyboardObserving
func keyboardWillShow(notification: Notification) {
self.replyInputView.showMaskIfPossible()
if let height = notification.keyboardSize?.height, let duration = notification.keyboardAnimationDuration {
// 这里执行键盘即将升起时UI变化的任务
UIView.animate(withDuration: duration) {
self.replyInputView.willAscend()
self.replyInputView.snp.updateConstraints { make in
make.bottom.equalToSuperview().offset(-height)
}
self.view.layoutSubviews()
}
}
}
func keyboardWillHide(notification: Notification) {
if let duration = notification.keyboardAnimationDuration {
// 这里执行键盘即将降下时UI变化的任务
UIView.animate(withDuration: duration) {
self.replyInputView.willDescend()
self.replyInputView.snp.updateConstraints { make in
make.bottom.equalToSuperview().offset(-tabbarHeight)
}
self.view.layoutIfNeeded()
}
}
}
}
4、关键词:
生命周期
protocol
extension
// TODO: -开篇
public protocol MaskableProtocol: AnyObject {
var maskableHelper: MaskableHelper { get set }
func showMaskIfPossible()
func hideMaskIfPossible()
}
private var gestureMaskViewKey: UInt8 = 0
private var maskableHelperKey: UInt8 = 0
public extension MaskableProtocol where Self: UIView {
private var gestureMaskView: UIView? {
get {
return objc_getAssociatedObject(self, &gestureMaskViewKey) as? UIView
}
set {
objc_setAssociatedObject(self, &gestureMaskViewKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var maskableHelper: MaskableHelper {
get {
return objc_getAssociatedObject(self, &maskableHelperKey) as? MaskableHelper ?? MaskableHelper()
}
set {
objc_setAssociatedObject(self, &maskableHelperKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func showMaskIfPossible() {
guard gestureMaskView == nil, let superview = self.superview else { return }
UIView.performWithoutAnimation {
gestureMaskView = UIView()
gestureMaskView?.backgroundColor = UIColor(white: 0, alpha: 0.5)
let tap = UITapGestureRecognizer(target: maskableHelper, action: #selector(maskableHelper.executeMaskAction))
gestureMaskView?.addGestureRecognizer(tap)
superview.insertSubview(gestureMaskView!, belowSubview: self)
guard let maskView = gestureMaskView else { return }
maskView.frame = superview.bounds
}
}
func hideMaskIfPossible() {
self.gestureMaskView?.removeFromSuperview()
self.gestureMaskView = nil
}
}
这里在extension
中给UIView设置了两个关联对象
,分别是:
gestureMaskView: UIView
maskableHelper: MaskableHelper
public class MaskableHelper: NSObject {
var maskAction: (() -> Void)?
init(action: (() -> Void)? = nil) {
self.maskAction = action
super.init()
}
@objc func executeMaskAction() {
maskAction?()
}
}
让需要作用的UIView遵守MaskableProtocol
,并初始化一个MaskableHelper
class ContentInputView, MaskableProtocol {
// MARK: - Lazy Loading
internal lazy var maskableHelper: MaskableHelper = {
let helper = MaskableHelper()
helper.maskAction = { [weak self] in
guard let `self` = self else { return }
self.hideMaskIfPossible()
self.endEditing(true)
}
return helper
}()
}
下需要显示Mask的时候调用协议中的func showMaskIfPossible( )
即可
// MARK: - KeyboardObserving
func keyboardWillShow(notification: Notification) {
self.replyInputView.showMaskIfPossible()
if let height = notification.keyboardSize?.height, let duration = notification.keyboardAnimationDuration {
UIView.animate(withDuration: duration) {
self.replyInputView.willAscend()
self.replyInputView.snp.updateConstraints { make in
make.bottom.equalToSuperview().offset(-height)
}
self.view.layoutSubviews()
}
}
}
关联对象
协议
协议的默认实现
组件:强调物理拆分,以便复用。例如:图片库
,网络库
等。
模块:强调逻辑拆分,以便解耦。例如:订单模块
,账户模块
等。
介于业界习惯称之为组件化
,所以我们继续使用这个术语。【组件化 = 功能组件
+ 业务模块
】
模块化
,职责分离,提高代码安全性。组件化
,提高复用率及完整性。协议编程
具体化,形成独立子系统,可灵活配置。// TODO
服务中介:联系服务提供者
和服务消费者
的桥梁。
服务提供者:将自己提供的服务地址注册
到服务中介。
服务消费者:从服务中介那里查找自己想要的服务的地址,然后享受这个服务。
iOS客户端
是一个单工程设计,简单说就是APP作为一个独立个体,所有的业务生命周期与APP是一致的。并不能真正实现微服务中的“每个服务完全独立生存、管理、销毁”。为此,组件化更多是解决职责分离、高复用、独立发布 (非独立运行)问题。
iOS中服务提供者通常指的是业务提供方,比如:行情模块需要调用交易模块的交易记录接口,此时交易模块即作为服务提供者。而行情模块作为服务需求方,即为服务消费者。由于模块与模块之间可能处于同一级,也可以不同 级。按照《软件工程设计:分层概述》原则,同一级模块间不能相互依赖,也就是不能相互直接调用。此时就需要 一个服务中介来解决模块间通信问题。
概念
统跳路由是⻚面解耦的最常⻅方式,大量应用于前端⻚面
。
通过把一个 URL 与一个⻚面绑定,需要时通过 URL 可以方便的打开相应⻚面。 比如
bebull://quote/stockDetail?stock_id=00700 /**个股报价⻚**/
优点
多端一致
,H5、iOS、Android、Weex/RN/Cameron 、PC、Mac等。缺点
概念
此类型适合动态语言,借助反射技术进行动态化调用,比如OC
的Runtime,可以采用以下方式调用:
Class quoteManager = NSClassFromString(@"QuoteManager");
NSArray *stockList = [quoteManager performSelector:@selector(getStockList)];
优点
缺点
Swift
静态语言不支持Runtime概念
参考后端Dubbo服务框架,通过服务注册的方式来实现远程接口调用的。 即每个模块提供自己对外服务的协议声明(Protocol),然后将此声明注册到服务中介。调用方能从服务中介看到 存在哪些服务接口,然后直接调用即可。
@protocol QuoteModuleService
- (NSArray * _Nullable)getStockList;
@end
@interface QuoteModule : NSObject<QuoteModuleService>
@end
@implementation QuoteModule
+ (void)load {
[ServiceManager registerService:@protocol(QuoteModuleService)
withModule:self.class];
}
- (NSArray * _Nullable)getStockList {
return nil;
}
@end
id<QuoteModuleService> module = [ServiceManager
objByService:@protocol(QuoteModuleService)];
NSArray *stockList = [module getStockList];
优点
缺点
概念
直接基于系统的 NSNotificationCenter。
优点
缺点
PAAS化
(可由服务端自主下发配置,灵活管理⻚面调用及组合)。c++
、python
等开发后引入。MVC
、MVVM
等,如何组织类关系等。采用 路由 URL 的 UI ⻚面统跳管理方案
,实现跨端 + 端内任何场景可直接唤起指定⻚面能力。
采用 基于面向协议思想的服务注册方案
,通过 BNBee组件中心
的 UIService
进行服务调用,获得指定的 View、VC。
采用 基于面向协议思想的服务注册方案
,通过 BNBee组件中心
的 OpenService
进行服务调用,提供同步返回
(sync)和 异步返回(async)能力。
总体分为 业务层、公共层、基础框架层 三层。
业务层
VC
、View
、Model
、ViewModel
、DC
、Manager
、APIClient
等公共层职责
基础框架层职责
framework
静态库方式,基于pod
引入。行情模块
:包括行情一级tab下的自选列表、市场列表、个股报价⻚等。 交易模块
:包括交易一级tab下的开户、资金账户、持仓、买入卖出、交易记录、入金出金等。 资讯模块
:包括资讯一级tab下的资讯列表、资讯详情⻚等。账户模块
:包括我的一级tab下的用户登录、注册、个人资料等。公共基础平台
:主要涵盖与业务无关的部分,包括暂时找不到基础框架层可以下沉的暂留组件,比如:WebView、 tabbar等。业务间通信API
:主要负责上层业务模块的组件间通信API协议定义、传输model定义、业务通知key等。 皮肤资源中心:存放所有必牛项目的图片、lottie动画等资源。framework
,实现复用性、稳定性、安全性目标。 c++
、python
等开发更多底层框架。socket⻓连接
管理、HTTPS短链接
生命周期、连接、安全、可靠性管理。组件间通信
,提供基于URL注册、协议绑定等方案管理。info
、warning
、error
、file
及日志收集、存取和上报管理。 工具类
、系统类
、分类
等。曝光时⻓
、曝光量
等统计接口。QQ
、微信
、Twitter
等。 Scrollview
,tabbar
等。 内存级缓存
及磁盘级缓存
的接口,后端对接沙盒、SQLite
数据库等。UIButton
、UIView
的多肤管理。 crash
兜底,启动连续crash绕行
等方案。 券商APP的安全性不容小觑,对于第三方依赖库的引入需持有谨慎的态度;因此后期需要增加第三方依赖库的准入 机制,形成选型
、评审
、安全鉴定
和引入
的原则。同时,尽量剔除一些功能重复、失去维护、功能滞后的三方库, 保证APP包大小
、启动性能
等各项指标。
如:选新不选旧 or 稳定大于能力?Swift实现的优先?轻量化还是重量级?是否跨端配套⻬全?等等问题,需要有 明确的标准,组件化工作落地后,需要再明确这块,保证快捷接入的同时,可以更稳定、更一致。
以公司上架App到苹果的App Store为例子,需要先申请DUNS编号,然后注册开发者账号,才有资格上架
1、法人实体
2、实体地址
3、联系方式
4、税务信息
5、银行账户
6、DUNS
聊天应用在我们的日常生活中变得越来越重要,从社交媒体到客户服务,他们都成为我们生活的一部分。因此,如何设计和实现一个高效,可扩展的聊天系统构成了 iOS 开发者面临的一项重要挑战。
在设计聊天系统时,我们需要考虑的设计因素有:一对一的私人聊天、群聊功能、发送文本和媒体消息、实时消息传输、以及消息的持久化存储。而作为 iOS 开发者,我们会希望这个系统对设备资源的占用最小,且具有出色的性能表现。
在 Swift 中,我们可以利用一些设计模式进行良好的架构设计。其中 MVC(模型-视图-控制器)是最常用的模式。但是根据项目具体需求,也可以考虑使用 MVVM(模型-视图-视图模型)或 VIPER(视图-互动器-呈现器-实体-路由器)来架构你的 iOS 应用。
对于消息的处理,分发和同步,使用 Socket.IO 客户端库进行实时通信。Socket.IO 使你能够在客户端和服务器之间进行全双工通讯,实现了信息的推送。利用 Firebase Cloud Messaging,我们可以实现对未在线用户的消息推送。
我们可以借助一些第三方框架来实现漂亮并具有流畅性能的 UI,例如使用 JSQMessagesViewController 或者 MessageKit 来快速构建漂亮的 UI。
Alamofire 是 Swift 中处理 HTTP 网络请求的优秀库,功能强大且易用。它能简化许多网络操作的过程,从发出请求到处理响应。
在收到服务器的响应之后,我们通常需要处理返回的 JSON 数据。在 Swift 中,我们可以使用 SwiftyJSON 库来让 JSON 解析变得简单并错不可及。
在客户端,我们需要有效地管理和存储用户数据和消息。这时,CoreData 和 Realm 数据库成为最佳选择。这两个框架为 iOS 开发者提供了强大的数据管理能力。
在聊天应用中,确保消息的有序性是一项重要任务。我们将每条消息装配一个时间戳或者全局唯一的序列号,根据它们进行排序以保证正确的顺序。
任何设计中,用户的私密性和数据安全都是第一位的。应用内部应该使用 HTTPS 进行数据传输,同时聊天消息可采取端对端加密的方式以确保通信安全。另外,登陆界面应含有 OAuth 或其他安全认证机制,保障用户账号的安全。
设计和实现聊天应用是代码、设计以及版权等多种深度结合的结果。本文介绍的架构和设计的宝典并不是固定不变的,我们还需要根据自身的应用需求不断去创新和挑战。我希望上述的讨论和建议对你在开发 iOS 聊天应用的路上有所启发和帮助。
参考链接:来源
本篇文章从iOS开发者
的角度,介绍证券交易App
的一些技术选型、功能设计等方面的内容。涉及到与server的交互数据格式及定义、接口设计、功能设计等。
iOS
端采用原生开发,编程语言是Swift
。如果某些功能需要在双端同时实现可使用Flutter
。最近也参与了Flutter的跨平台开发,这篇文章简单介绍。
部分第三方库:
Alamofire
、Starscream
、SocketIO
HandyJSON
、SwiftProtobuf
、SwiftyJSON
SnapKit
Kingfisher
IQKeyboardManagerSwift
Track
Toast-Swift
WCDB.swift
常见的应用层通信协议有HTTP
、SMTP
、Web Socket
等,这里介绍当前项目使用到的两种:
HTTP(HyperText Transfer Protocol,超文本传输协议):
请求-响应
协议,客户端发送一个请求给服务器,然后服务器回送一个响应回来。这种通讯方式限制了只能有客户端发起通信,服务器不能主动给客户端发送数据。Web Socket:
网络通信中传递的是数据,那么如何组织这些数据编称作“编码”,常见的有XML和JSON这两种格式,在app开发中也几乎不会用XML这种格式了。另外一种是Google推出的Protocol Buffers(简称ProtoBuf或PB)。
JSON
(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。JSON 使用完全独立于语言的文本格式,采用 Key-Value 键值对的方式来存储和表示数据,常用于服务器和 Web 应用间的数据交换。
ProtoBuf
,全称 Protocol Buffers
,是 Google 公开的一种数据序列化协议
,用于数据结构的序列化,可以用于数据存储、通信协议等方面。相较于 JSON、XML 这些数据格式,ProtoBuf 更小、更快、也更简单。它使用二进制格式
,可以定义复杂的数据结构,还提供了众多语言的 API 接口,可用于网络通讯和数据存储。约定的数据结构能够被跨语言使用,比如 Server 使用 C++,Client 使用 Java,两者之间的通信,可以使用 ProtoBuf 来约定通信协议。
总体上,JSON
更简单易用,并且在WebView、Web Browser 这样的环境中使用更加便利。而 ProtoBuf
则适合于大规模的系统中,需要更高效或者更清晰的协议定义的地方。
我们当前讨论的证券交易App在查看行情的过程中需要接受大量的行情数据,同时对时效性有较高的要求。PB体积小(二进制格式)、效率高(编解码),也支持跨语言,同时Google也提供了完整的工具链(如将.proto格式的文件编译成目标语言如Swift文件),特别适合证券类高频率、大数据量的场景。
// TODO: –
WebSocket
是一个网络通信协议,提供了全双工(Full-duplex)通讯机制。在 WebSocket API
中,客户端(可以是web浏览器或app)和服务器只需要建立一次连接,就可以进行实时双向数据传输。这种协议的主要目标是在客户端和服务器之间实现实时通讯,同时也兼容了现有的 HTTP 协议。
建立WebSocket
连接需要知道服务器的ip地址
和端口号port
,其URI以ws://
或wss://
开头,如:"ws://172.16.10.207:11516"
,其中wss://
多了一层security layer
实现加密。
通常我们会将行情服务器信息ServerInfo
保存到本地,并提供更新接口。这样可以加速与行情服务器建立WebSocket连接的过程。如果本地未读取到则需要调用接口获取行情服务器信息,再建立连接、同时保存到本地。
消息的格式: 容器 消息类型 + 消息体,
数据的发送
数据接收后的解析,传递给业务层
检查登录状态
检查refreshtoken是否过期
刷新refreshtoken
单点登录
多点登录
心跳
踢下线
1、网络方面
网络状态检测 如果有网络的话,要建立websocket的连接 还要检查登录状态,refreshtoken等,同步用户数据
在项目使用私有库,能带来以下好处:
代码复用
:可以很方便地在多个项目中使用一份代码模块化
:比如可以按照业务模块
/公共模块
/基础模块
将代码分类到不同的库中,使项目整体结构清晰、同时可以减少代码间的耦合,提高代码质量易于维护
:当业务变化时,只需在私有库中更新,然后就能轻松推送到使用这个私有库的所有项目中。提升开发效率
:一次编写,就能在多个项目重化工复用,提高开发效率,节省时间。持续集成
:可以与CI/CD
工具集成。私有库更新后可以自动地被推送到所有相关的项目中,让项目都能使用最新版本的库。CocoaPods
假设我们的库的名称就叫XPod
,只需要在目标文件夹中,使用创建指令:
pod lib create XPod
需要设置用户名
及邮箱
Cloning `https://github.com/CocoaPods/pod-template.git` into `XPod`.
Configuring XPod template.
! Before you can create a new library we need to setup your git credentials.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
What is your name?
> XW
! Setting your name in git to XW
git config user.name "XW"
What is your email?
> admin@xiaowen.com
! Setting your email in git to admin@xiaowen.com
git config user.email "admin@xiaowen.com"
------------------------------
然后根据提示配置
lib,如platform
、language
等等,即可完成默认的创建过程
To get you started we need to ask a few questions, this should only take a minute.
2024-03-06 15:45:36.289 defaults[9529:76159]
The domain/default pair of (org.cocoapods.pod-template, HasRunBefore) does not exist
If this is your first time we recommend running through with the guide:
- https://guides.cocoapods.org/making/using-pod-lib-create.html
( hold cmd and click links to open in a browser. )
Press return to continue.
What platform do you want to use?? [ iOS / macOS ]
> iOS
What language do you want to use?? [ Swift / ObjC ]
> ObjC
Would you like to include a demo application with your library? [ Yes / No ]
> No
Which testing frameworks will you use? [ Specta / Kiwi / None ]
> None
Would you like to do view based testing? [ Yes / No ]
> No
What is your class prefix?
> X
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
Running pod install on your new library.
Analyzing dependencies
[!] CocoaPods could not find compatible versions for pod "XPod":
In Podfile:
XPod (from `../`)
Specs satisfying the `XPod (from `../`)` dependency were found, but they required a higher minimum deployment target.
[!] Automatically assigning platform `iOS` with version `9.3` on target `XPod_Tests` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.
Ace! you're ready to go!
We will start you off by opening your project in Xcode
open 'XPod/Example/XPod.xcworkspace'
The file /Users/rayvision/Desktop/Code/XPod/Example/XPod.xcworkspace does not exist.
To learn more about the template see `https://github.com/CocoaPods/pod-template.git`.
To learn more about creating a new pod, see `https://guides.cocoapods.org/making/making-a-cocoapod`.
➜ Code
创建完成后的目录是这样的:
podspec
是文件库的描述文件
,默认是这样的:
source_files
通常用于指定源代码文
件,而不是资源文件(如图片、音频等),如果想包含资源文件,可以使用s.resource_bundles
或s.resources
来指定资源文件的路径。
默认情况下是只包含了Classes目录下的文件
s.source_files = 'XPod/Classes/**/*.{h,m}'
如果我们还想用子文件夹
开区分不同模块,比如这里新建了一个叫做category
的文件夹来放各种分类,UIColor
的分类放在UIColor+
这个文件夹中,类似地还可以创建一个叫NSString+
的文件夹来存放各种NSString
的分类
s.source_files = 'XPod/**/*/.{h,m}'
这样配置后,XPod及其子目录下的所有.h和.m文件都会被包含在Pod中。下面绿色部分是我们新增的文件夹,红色部分是默认的文件夹。
使用s.resources
可以将指定的资源文件直接复制到生成的Framework
或静态库
中。这意味着这些资源文件会被直接暴露在Bundle的根目录下,可以通过[NSBundle mainBundle] pathForResource:ofType:
等方法访问这些资源文件。
使用s.resource_bundles
可以将资源文件打包成一个独立的Bundle
,并将这个Bundle作为一个整体引入到Pod中。这样做的好处是可以更好地组织资源文件
,避免资源文件之间的命名冲突
,也可以更方便地加载和管理资源文件。
s.resource_bundles = {
'XPod' => ['XPod/Assets/**/*.{png, pdf}']
}
因此,如果你希望将资源文件直接暴露在Bundle的根目录下,可以使用s.resources
;如果你希望将资源文件打包成一个独立的Bundle引入到Pod中,可以使用s.resource_bundles
。根据你的需求选择合适的方式来包含资源文件。
在我们的项目中要使用XPod
也很简单只要在Podfile
中新增:
pod 'XPod', :path => '../XPod'
注意这里使用的是相对路径
,然后再执行pod install
:
通常情况下,在开发过程中我们都直接使用本地的私有库。当阶段性地完成开发后,可以将私有库放到git上托管。以下是两种不同的引用库的方式:
本地:
pod 'XPod', :path => '../XPod'
git:
pod 'XPod', :git => 'https://git.xiaowen.org/ios/XPod.git', :branch => '0.1.0'
升级Xcode之后创建项目提示需要下载iOS 17.2
版本的模拟器
下载中⌛️
等待很久之后报下载失败
原因是失去了网络连接
重试很多次依然如此,但是可以排除网络问题。在开发者论坛看到类似的讨论:same issue:https://forums.developer.apple.com/forums/thread/740040
跳转到苹果官方给出的方案:https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#
最终解决方案是,手动下载
然后通过命令行安装
。
模拟器下载地址:https://developer.apple.com/download/all/ 进入这里需要登录开发者账号。
搜索模拟器iOS Simulator
:
出现很多版本的模拟器,包括iOS17.4 beta,这里我们只需要iOS 17.2
,所以我们搜索指定版本
找到指定版本,点击View Details
即可看到下载runtime.dmg
的地址
点击即可下载⏬。
这里直接给出具体的下载链接🔗 👇https://download.developer.apple.com/Developer_Tools/iOS_17.2_Simulator_Runtime/iOS_17.2_Simulator_Runtime.dmg
安装方法:
sudo xcode-select -s /Applications/Xcode.app
需要输入密码
,然后
xcodebuild -runFirstLaunch
接着:
xcrun simctl runtime add "/Users/rayvision/Downloads/iOS_17.2_Simulator_Runtime.dmg"
这里引号内的地址
,可以直接找到下载的文件拖拽过来,会验证runtime。
最后验证完成出现👇就完成了。
D: F56EFCE4-115C-490B-881C-C86B00B23EEB iOS (17.2 - 21C62) (Ready)