iOS KeyboardObserving(beta)

// TODO: 开篇

  • 常见问题(本文要解决的)
  • 思路(用protocol个extension)

1、在Notification的extension中方便快捷地获取键盘的动画时长和尺寸

extension Notification {

    public var keyboardSize: CGSize? {
        return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size
    }

    public var keyboardAnimationDuration: TimeInterval? {
        return userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval
    }

}

2、定义协议,协议中需要暴露两方面的接口,一是开启和结束键盘状态的监听,二是监听到键盘升降之后的action

public protocol KeyboardObserving: AnyObject {
    func startObservingKeyboard()
    func stopObservingKeyboard()

    func keyboardWillShow(notification: Notification)
    func keyboardWillHide(notification: Notification)
}

UIViewController有生命周期,需要在特定的阶段开启或取消对通知的监听,在willAppearwillDisappear阶段调用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)
    }

}

3、用例

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、关键词:

  • UIViewController的生命周期
  • protocol
  • extension

iOS 一个易用的MaskableProtocol(beta)

// 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
    • 作用:添加到目标view的superview上,用于响应手势事件
  • maskableHelper: MaskableHelper
    • 作用:由于swift的extension中不支持运行时,无法直接使用Selector,需要借助一个类来中转Action

借助一个辅助类

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()
        }
    }
}

关键词

  • 关联对象
  • 协议
  • 协议的默认实现

iOS证券交易App组件化方案

一、前言

1.1 什么是组件化

组件:强调物理拆分,以便复用。例如:图片库网络库等。

模块:强调逻辑拆分,以便解耦。例如:订单模块账户模块等。

介于业界习惯称之为组件化,所以我们继续使用这个术语。【组件化 = 功能组件 + 业务模块

1.2 为什么要组件化

  • 解决代码(工程)臃肿问题,提高编译速度。
  • 核心业务模块化,职责分离,提高代码安全性。
  • 通用功能组件化,提高复用率及完整性。
  • 协议编程具体化,形成独立子系统,可灵活配置。
  • 提高模块自治力,独立升级部署,相互间不形成绝对依赖。
  • 支持异构,提升系统技术多元化能力。

二、业界方案

2.1 组件化演进之路

// TODO

2.2 微服务概念

2.2.1 三个⻆色

服务中介:联系服务提供者服务消费者的桥梁。

服务提供者:将自己提供的服务地址注册到服务中介。

服务消费者:从服务中介那里查找自己想要的服务的地址,然后享受这个服务。

2.2.2 三个⻆色在iOS组件化中的体现

iOS客户端是一个单工程设计,简单说就是APP作为一个独立个体,所有的业务生命周期与APP是一致的。并不能真正实现微服务中的“每个服务完全独立生存、管理、销毁”。为此,组件化更多是解决职责分离高复用独立发布 (非独立运行)问题。

iOS中服务提供者通常指的是业务提供方,比如:行情模块需要调用交易模块交易记录接口,此时交易模块即作为服务提供者。而行情模块作为服务需求方,即为服务消费者。由于模块与模块之间可能处于同一级,也可以不同 级。按照《软件工程设计:分层概述》原则,同一级模块间不能相互依赖,也就是不能相互直接调用。此时就需要 一个服务中介来解决模块间通信问题

2.3 组件间通信常⻅方案

2.3.1 基于路由 URL UI ⻚面统跳管理

概念

统跳路由是⻚面解耦的最常⻅方式,大量应用于前端⻚面
通过把一个 URL 与一个⻚面绑定,需要时通过 URL 可以方便的打开相应⻚面。 比如

bebull://quote/stockDetail?stock_id=00700 /**个股报价⻚**/ 

优点

  • 多端一致,H5、iOS、Android、Weex/RN/Cameron 、PC、Mac等。
  • 浏览器、其他APP可以通过openURL直接唤起APP指定⻚面。
  • 可结合服务端下发配置表,动态绑定/解绑指定URL实现⻚面灰度。

缺点

  • 无法支撑较为复杂操作及数据传输模型,比如跨模块⻓调用链场景。
2.3.2 基于反射的远程接口调用封装

概念

此类型适合动态语言,借助反射技术进行动态化调用,比如OCRuntime,可以采用以下方式调用:

Class quoteManager = NSClassFromString(@"QuoteManager"); 
NSArray *stockList = [quoteManager performSelector:@selector(getStockList)];

优点

  • 调用较为方便
  • 代码自动补全
  • 编译检查有效

缺点

  • 存在hardcode问题
  • Swift静态语言不支持Runtime
2.3.3 基于面向协议思想的服务注册方案

概念

参考后端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];

优点

  • 调用方便
  • 代码自动补全
  • 无需hardcode,面向协议,减少对接错误⻛险

缺点

  • 高度依赖协议,协议变更时容易导致所有依赖方编译失败。
  • 存在服务注册过程,有一定性能开销。
2.3.4 基于通知的广播方案

概念

直接基于系统的 NSNotificationCenter

优点

  • 实现简单。
  • 适合一对多场景。

缺点

  • 不适合复杂数据传输场景。
  • 同步调用不方便。

三、架构设计思路

3.1 目标

  • 业务模块分离,业务职责单一,划清边界。
  • 业务模块间通信灵活且统一化。
  • 业务模块间通信协议严格标准化,具备准入原则。
  • 通用逻辑独立化,构建稳定后采用二进制形式链接。
  • ⻚面多端统一性,方便后续PAAS化(可由服务端自主下发配置,灵活管理⻚面调用及组合)。
  • 代码安全,只开放必要的模块代码权限。
  • 服务异构化,后续部分模块可以采用更多语言如c++python等开发后引入。
  • 设计模式规范化,明确各场景采用MVCMVVM等,如何组织类关系等。

3.2 组件间通信场景

3.2.1 场景一:UI独立⻚面

采用 路由 URL UI ⻚面统跳管理方案 ,实现跨端 + 端内任何场景可直接唤起指定⻚面能力。

3.2.2 场景二:跨模块UI嵌套

采用 基于面向协议思想的服务注册方案 ,通过 BNBee组件中心UIService 进行服务调用,获得指定的 View、VC。

3.2.3 场景三:跨模块数据传输

采用 基于面向协议思想的服务注册方案 ,通过 BNBee组件中心OpenService 进行服务调用,提供同步返回

(sync)和 异步返回(async)能力。

四、架构分层

4.1 图示

4.2 各层职责

总体分为 业务层公共层基础框架层 三层。

业务层

  • 承担业务层面相关工作
  • 主要包括VCViewModelViewModelDCManagerAPIClient
  • 目前阶段分为行情交易资讯账户公共主体五类

公共层职责

  • 承担整体性非业务相关的相关工作。
  • 主要包括公共相关事务(如BNURLHelper),组件间通信API、Model,以及皮肤资源整合。
  • 目前阶段分为公共基础平台、业务间通信API、皮肤资源中心。

基础框架层职责

  • 包含一些日常使用,趋于稳定的库。
  • 主要分为自研和第三方依赖两大类。
  • 自研部分采用framework静态库方式,基于pod引入。

4.3 各层间依赖关系

  • 同层之间禁止直接依赖,通信方式通过BNBee组件服务中心。提供方提供具体实现,将协议定义于BNUAPI工程中;消费方于BNUAPI中获得对应API及model。
  • 上层可以直接引用下层,也可以跨层下级引用
  • 下层禁止引用上层。

4.4 模块说明

  • 行情模块:包括行情一级tab下的自选列表、市场列表、个股报价⻚等。
  • 交易模块:包括交易一级tab下的开户、资金账户、持仓、买入卖出、交易记录、入金出金等。
  • 资讯模块:包括资讯一级tab下的资讯列表、资讯详情⻚等。
  • 账户模块:包括我的一级tab下的用户登录、注册、个人资料等。
  • 公共基础平台:主要涵盖与业务无关的部分,包括暂时找不到基础框架层可以下沉的暂留组件,比如:WebView、 tabbar等。
  • 业务间通信API:主要负责上层业务模块的组件间通信API协议定义、传输model定义、业务通知key等。 皮肤资源中心:存放所有必牛项目的图片、lottie动画等资源。

4.5 模块内结构

五、BNBee(待补充)

5.1 基本能力

5.2 URL注册绑定

5.3 面向协议服务注册

六、自研框架库

6.1 目标

  • 将重要且需要稳定的模块独立出来,抽离成framework,实现复用性稳定性安全性目标。
  • 标准化一些常用逻辑,做到多业务一致。
  • 代码安全,只开放必要的代码权限。
  • 服务异构化,后续可以采用更多语言如c++python等开发更多底层框架。

6.2 组件库

  • 网络组件库:负责socket⻓连接管理、HTTPS短链接生命周期、连接、安全、可靠性管理。
  • 组件服务中心:负责管理组件间通信,提供基于URL注册、协议绑定等方案管理。
  • 日志组件库:分级日志系统,对infowarningerrorfile及日志收集存取上报管理。
  • 开发组件库:日常开发常用的工具类系统类分类等。
  • 数据上报组件库:直接接驳三方数据上报平台,提供与上报平台无关的通用接口,包括曝光时⻓曝光量统计接口
  • 分享组件库:主要提供通用分享组件,包括分享到QQ微信Twitter等。
  • UI组件库:主要沉淀通用UI组件,如嵌套Scrollviewtabbar等。
  • 数据管理组件库:主要包装内存级缓存磁盘级缓存的接口,后端对接沙盒、SQLite数据库等。
  • 皮肤组件库:主要提供style定义系统,用于约定常用UI组件如UIButtonUIView的多肤管理。
  • 健康防护系统:防范常⻅类型crash兜底,启动连续crash绕行等方案。
  • k线组件库:k线组件作为核心性能指标,沉淀出来方便后续各业务使用。

七、第三方依赖库

7.1 设定准入标准

券商APP的安全性不容小觑,对于第三方依赖库的引入需持有谨慎的态度;因此后期需要增加第三方依赖库的准入 机制,形成选型评审安全鉴定引入的原则。同时,尽量剔除一些功能重复、失去维护、功能滞后的三方库, 保证APP包大小启动性能等各项指标。

7.2 选型需明确

如:选新不选旧 or 稳定大于能力?Swift实现的优先?轻量化还是重量级?是否跨端配套⻬全?等等问题,需要有 明确的标准,组件化工作落地后,需要再明确这块,保证快捷接入的同时,可以更稳定、更一致。


App上架前的准备

前言

以公司上架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 实现

我们可以借助一些第三方框架来实现漂亮并具有流畅性能的 UI,例如使用 JSQMessagesViewController 或者 MessageKit 来快速构建漂亮的 UI。

网络请求

Alamofire 是 Swift 中处理 HTTP 网络请求的优秀库,功能强大且易用。它能简化许多网络操作的过程,从发出请求到处理响应。

数据解析

在收到服务器的响应之后,我们通常需要处理返回的 JSON 数据。在 Swift 中,我们可以使用 SwiftyJSON 库来让 JSON 解析变得简单并错不可及。

数据存储

在客户端,我们需要有效地管理和存储用户数据和消息。这时,CoreData 和 Realm 数据库成为最佳选择。这两个框架为 iOS 开发者提供了强大的数据管理能力。

消息有序性

在聊天应用中,确保消息的有序性是一项重要任务。我们将每条消息装配一个时间戳或者全局唯一的序列号,根据它们进行排序以保证正确的顺序。

隐私和安全

任何设计中,用户的私密性和数据安全都是第一位的。应用内部应该使用 HTTPS 进行数据传输,同时聊天消息可采取端对端加密的方式以确保通信安全。另外,登陆界面应含有 OAuth 或其他安全认证机制,保障用户账号的安全。

总结

设计和实现聊天应用是代码、设计以及版权等多种深度结合的结果。本文介绍的架构和设计的宝典并不是固定不变的,我们还需要根据自身的应用需求不断去创新和挑战。我希望上述的讨论和建议对你在开发 iOS 聊天应用的路上有所启发和帮助。

参考链接:来源

设计一个证券交易App

前言:

本篇文章从iOS开发者的角度,介绍证券交易App的一些技术选型功能设计等方面的内容。涉及到与server的交互数据格式及定义、接口设计、功能设计等。

一、App编程语言及三方库

iOS端采用原生开发,编程语言是Swift。如果某些功能需要在双端同时实现可使用Flutter。最近也参与了Flutter的跨平台开发,这篇文章简单介绍。

部分第三方库:

  • 网络:AlamofireStarscreamSocketIO
  • 数据解析:HandyJSONSwiftProtobufSwiftyJSON
  • 布局:SnapKit
  • 网络图片:Kingfisher
  • 键盘输入:IQKeyboardManagerSwift
  • 缓存:Track
  • 弹框:Toast-Swift
  • 数据库:WCDB.swift

二、网络部分

1、网络通信协议

常见的应用层通信协议HTTPSMTPWeb Socket等,这里介绍当前项目使用到的两种:

HTTP(HyperText Transfer Protocol,超文本传输协议):

  • 是一种网页(HTML)传输协议,适用于从万维网服务器传输超文本到本地浏览器的传送协议。
  • 是一种请求-响应协议,客户端发送一个请求给服务器,然后服务器回送一个响应回来。这种通讯方式限制了只能有客户端发起通信,服务器不能主动给客户端发送数据。

Web Socket:

  • 是一种在单个 TCP 连接上进行全双工通信的协议。目标就是在网页和服务器之间建立持久连接,然后进行双向数据传输。
  • 比起 HTTP(HTTP 协议中,服务器不能主动向客户端推送信息),WebSocket 更适合于实时性要求较高的场景,例如网页聊天,股票行情,游戏等。

2、数据编码格式

网络通信中传递的是数据,那么如何组织这些数据编称作“编码”,常见的有XMLJSON这两种格式,在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文件),特别适合证券类高频率、大数据量的场景。

3、安全性

// TODO: –

三、主要的业务逻辑

1、与行情服务器建立WebSocket连接

WebSocket 是一个网络通信协议,提供了全双工(Full-duplex)通讯机制。在 WebSocket API 中,客户端(可以是web浏览器或app)和服务器只需要建立一次连接,就可以进行实时双向数据传输。这种协议的主要目标是在客户端和服务器之间实现实时通讯,同时也兼容了现有的 HTTP 协议。

建立WebSocket连接需要知道服务器ip地址端口号port,其URIws://wss://开头,如:"ws://172.16.10.207:11516",其中wss://多了一层security layer实现加密

通常我们会将行情服务器信息ServerInfo保存到本地,并提供更新接口。这样可以加速与行情服务器建立WebSocket连接的过程。如果本地未读取到则需要调用接口获取行情服务器信息,再建立连接、同时保存到本地

2、数据的发送与接收

消息的格式: 容器 消息类型 + 消息体,

数据的发送

数据接收后的解析,传递给业务层

3、登录

检查登录状态

检查refreshtoken是否过期

刷新refreshtoken

单点登录

多点登录

4、在线状态

心跳

踢下线

四、启动后要做的一些事情

1、网络方面

网络状态检测 如果有网络的话,要建立websocket的连接 还要检查登录状态,refreshtoken等,同步用户数据

私有库

前言:

在项目使用私有库,能带来以下好处:

  • 代码复用:可以很方便地在多个项目中使用一份代码
  • 模块化:比如可以按照业务模块/公共模块/基础模块将代码分类到不同的库中,使项目整体结构清晰、同时可以减少代码间的耦合,提高代码质量
  • 易于维护:当业务变化时,只需在私有库中更新,然后就能轻松推送到使用这个私有库的所有项目中。
  • 提升开发效率:一次编写,就能在多个项目重化工复用,提高开发效率,节省时间。
  • 持续集成:可以与CI/CD工具集成。私有库更新后可以自动地被推送到所有相关的项目中,让项目都能使用最新版本的库。

1、前期准备

  • 安装并使用CocoaPods

2、创建过程

假设我们的库的名称就叫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,如platformlanguage等等,即可完成默认的创建过程

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

创建完成后的目录是这样的:

XPod

3、自定义配置

podspec是文件库的描述文件,默认是这样的:

XPod.podspec default

s.source_files

source_files通常用于指定源代码文件,而不是资源文件(如图片、音频等),如果想包含资源文件,可以使用s.resource_bundless.resources来指定资源文件的路径。

默认情况下是只包含了Classes目录下的文件

s.source_files = 'XPod/Classes/**/*.{h,m}'

如果我们还想用子文件夹开区分不同模块,比如这里新建了一个叫做category的文件夹来放各种分类,UIColor的分类放在UIColor+这个文件夹中,类似地还可以创建一个叫NSString+的文件夹来存放各种NSString的分类

s.source_files = 'XPod/**/*/.{h,m}'
create pod sub directory

这样配置后,XPod及其子目录下的所有.h和.m文件都会被包含在Pod中。下面绿色部分是我们新增的文件夹,红色部分是默认的文件夹。

xcode pod directory

s.resources

使用s.resources可以将指定的资源文件直接复制到生成的Framework静态库中。这意味着这些资源文件会被直接暴露在Bundle的根目录下,可以通过[NSBundle mainBundle] pathForResource:ofType:等方法访问这些资源文件。

s.resource_bundles

使用s.resource_bundles可以将资源文件打包成一个独立的Bundle,并将这个Bundle作为一个整体引入到Pod中。这样做的好处是可以更好地组织资源文件,避免资源文件之间的命名冲突,也可以更方便地加载和管理资源文件。

s.resource_bundles = {
    'XPod' => ['XPod/Assets/**/*.{png, pdf}']
  }

因此,如果你希望将资源文件直接暴露在Bundle的根目录下,可以使用s.resources;如果你希望将资源文件打包成一个独立的Bundle引入到Pod中,可以使用s.resource_bundles。根据你的需求选择合适的方式来包含资源文件。

4、使用

在我们的项目中要使用XPod也很简单只要在Podfile中新增:

 pod 'XPod', :path => '../XPod'

注意这里使用的是相对路径,然后再执行pod install

XPod install

通常情况下,在开发过程中我们都直接使用本地的私有库。当阶段性地完成开发后,可以将私有库放到git上托管。以下是两种不同的引用库的方式:

本地:

  pod 'XPod', :path => '../XPod'

git:

  pod 'XPod', :git => 'https://git.xiaowen.org/ios/XPod.git', :branch => '0.1.0'

Xcode Simulator

升级Xcode之后创建项目提示需要下载iOS 17.2版本的模拟器

需要下载iOS 17.2 Simulator

下载中⌛️

下载中⌛️

等待很久之后报下载失败

下载失败💔

原因是失去了网络连接

失败原因:lost connection

重试很多次依然如此,但是可以排除网络问题。在开发者论坛看到类似的讨论:same issuehttps://forums.developer.apple.com/forums/thread/740040

跳转到苹果官方给出的方案:https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#

install and manager simulator runtimes

最终解决方案是,手动下载然后通过命令行安装

模拟器下载地址:https://developer.apple.com/download/all/ 进入这里需要登录开发者账号。

Apple download all

搜索模拟器iOS Simulator

Apple downloading search simulator

出现很多版本的模拟器,包括iOS17.4 beta,这里我们只需要iOS 17.2,所以我们搜索指定版本

iOS 17.2 Simulator search

找到指定版本,点击View Details即可看到下载runtime.dmg的地址

iOS 17.2 Simulator downloading

点击即可下载⏬。

simulator手动下载中⌛️

这里直接给出具体的下载链接🔗 👇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。

simulator runtime validate

最后验证完成出现👇就完成了。

D: F56EFCE4-115C-490B-881C-C86B00B23EEB iOS (17.2 - 21C62) (Ready)

simulator add