Swift制作Framework,提供给OC项目和Swift项目通用

Swift制作framework

公司的需要需要制作sdk给其他团队用,其实就是framework
简直炸裂!踩了一个又一个的坑!
遍体鳞伤之后,决定一定要记录下来,方便以后自己和有需要的人查阅,能有一点点帮助也是好的

进入正题

官方提供的是.framework.a两种方式制作SDK的方式。
分别对应创建工程时下方的Cocoa Touch FrameworkCocoa Touch Static Library
.framework.a两者区别自行百度吧,简书上制作Framework相关的文章基本都有,就懒得copy了。
.framework其实是分动态静态的,所以.framework即可满足我的要求,想要制作动态或者静态创建framework后可以修改,往后面看。

ps: 
在 iOS 8 之前,
iOS 平台不支持开发者使用用户自己的动态 Framework,这种限制可能是出于安全的考虑。
换一个角度讲,因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,
同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,
实际上动态库也就没有存在的必要了。

但是,iOS 8/Xcode 6 推出之后,
iOS添加了对动态库的支持,为什么 iOS 8 要添加动态库的支持?唯一的理由大概就是Extension 的出现。
Extension 和 App 是两个分开的可执行文件,同时需要共享代码,这种情况下动态库的支持就是必不可少的了。
但是这种动态 Framework 和系统的UIKit.Framework 还是有很大区别。
系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 Framework 哪怕是动态的,
最后也还是要拷贝到 App 中(App 和Extension 的 Bundle 是共享的),
因此苹果又把这种 Framework 称为 Embedded Framework

手洗干净,挽起袖子干 0_0

第一步:创建Framework工程

运行 XCode -> Cocoa Touch Framework -> 取个名, 语言选择 Swift -> 创建成功

第二步:基本设置

创建完不急着编写代码,先做一些设置:

  1. 修改最低的系统要求,建议当然低一些好

  2. mach -0 type ,即选择动态库or静态库(甚至Object File)
    想知道这几种type的区别可以移步
    参考浅谈 SDK 开发(一)五种 Mach-O 类型的凛冬之战
    这里我选择默认的Dynamic Library即动态库

  3. Architectures 该编译选项指定了工程将被编译成支持哪些指令集,支持指令集是通过编译生成对应的二进制数据包实现的,如果支持的指令集数目有多个,就会编译出包含多个指令集代码的数据包,造成最终编译的包很大。
    那么指令集是什么呢:

    **iPhone指令集**,苹果处理器支持两个不同的指令集:
    32位ARM指令集(armv6|armv7|armv7s)和
    64位ARM指令集(arm64),i386|x86_64 是Mac处理器的指令集,
    i386是针对intel通用微处理器32架构的。
    x86_64是针对x86架构的64位处理器。
    当使用iOS模拟器的时候会遇到i386|x86_64,iOS模拟器没有arm指令集。
    

    所以我们看看我们需要什么指令集

    1、debug环境下
    设备:arm64(测试机型有限: 6P、5、7 )
    模拟器:iPhone7-Plus:x86_64、iPhone4s:i386
    2、release环境下
    设备:armv7、arm64
    模拟器:i386、x86_64
    

    以上所生成的framework均不包含armv7s, 在 Building Setting 中设置一下 Architectures,在原有基础上添加一行 armv7s ,如下:



    在原有基础上增加 armv7s


  4. Build Active Architecture Only 意思是: 该编译项用于设置是否只编译当前使用的设备对应的arm指令集
    当该选项设置成YES时,你连上一个armv7指令集的设备,就算你的Valid Architectures和Architectures都设置成armv7/armv7s/arm64,还是依然只会生成一个armv7指令集的二进制包
    Release模式为发布模式,需要支持各种设备指令集,所以设置为NO


  5. Valid Architectures 设置的支持arm指令集。指令集的版本有:armv7/armv7s/arm64。
    假设Architectures设置的支持arm指令集版本只有:arm64时,这时Xcode只会生成一个arm64指令集的二进制包
    所以这里我们都不用改,都包含进来就好了

  6. Dead Code Stripping, 设置为 NO 关闭对代码中“dead”,“unreachable”代码过滤

  7. Link With Standard Libraries 设置为 NO 避免重复链接

  8. Build 环境 设置build环境为release环境下


第三步:编写代码

在此之前,我们先command+B 看看是否成功


build success 并看到Products 下的 文件 XPKit.framework 由红变黑,说明制作成功,右键show in finder

这就是我们的framework,只是里面啥都还没写- -。
好了,咱们抓紧写几句,饥渴难耐了吧,但是还是先跟着我来吧,弄明白了再去写自己的代码,少踩好多坑
创建一个Manager类继承自 NSObject

写上这么个 func

@objc public class XPManager: NSObject {
    
    @objc public func sayHello(){
        print("XPKit-->: hello")
    }
    
    public func sayWorld() {
        print("XPKit-->: world")
    }
    
    @objc func saySwift() {
        print("XPKit-->: Swift")
    }
}

完毕, command + B ,报错了!来看看为啥:

ld: symbol dyld_stub_binder not found (normally in libSystem.dylib).  Needed to perform lazy binding to function __T0s23_ContiguousArrayStorageCMa for architecture i386
clang: error: linker command failed with exit code 1 (use -v to see invocation)

这里提示我们少了一个系统库libSystem.dylib,我也不知道为啥,那我们就给加上呗,来到PROJECT->Build Phases->Link Binary With Libraries->+ 加上libSystem.tbd


这时候再回来编译,发现Build Success。目的达到了 0-0!

第四步:测试写好的Framework

1. 先来测试Swift项目调用swift的framework

先到Products 下的 XPKit.framework 右键 show in finder找到 XPKit.framework

紧接着 create 一个 swift 工程app single view app,并将👆的XPKit.framework拖到工程中,记得勾选 copy if needed

import XPKit

然后 command + 左键 进入,可以看到暴露出来能用的方法

image.png

这里我们能发现我们写了public 修饰的 都暴露出来供app调用

import UIKit
import XPKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let manager = XPManager()
        manager.sayHello()
        manager.sayWorld()
    }
}

command + R Run一下发现报错了

dyld: Library not loaded: @rpath/XPKit.framework/XPKit
  Referenced from: /Users/midoo/Library/Developer/CoreSimulator/Devices/8F8ADF9B-D5DD-45A1-B925-03BCBB11D9FF/data/Containers/Bundle/Application/C7D5F85A-DDC8-4BC7-964A-FDC4B5154C5A/SwiftInvokDemo.app/SwiftInvokDemo
  Reason: image not found
(lldb) 

这里因为我们制作的frameworkdynamic library动态的,所以我们到Project->General->滑到最下方


选中 linked frameworks and libraries 中的 XPKit.framework-号 删除,再在上方 Embedded Binaries+号XPKit.framework 加回来,发现上下都有了,如:

这时候再command + R Run,就不报错了,并且成功打印:

2. 再来测试Swift项目调用swift的framework

同理 create 一个 OC 的 app,拖进我们的 XPKit.framework 并在 Project->General -> Embedded Binaries 下添加进去
这个时候就要导入#import <XPKit/XPKit-Swift.h> 而不是 #import <XPKit/XPKit.h>
#import <XPKit/XPKit-Swift.h>OC 项目导入 Swift Framework 时自动产生的文件, 给我们展示可以用哪些接口
command + 左键 点到这个文件里去可以看到

image.png

这时候我们就会发现,XPManager 我们的类暴露出来了,但是方法只有一个sayHello()

所以敲黑板,划重点

我们制作 swift framework 的时候,一定要注意可用性,因为难免会遇到让OC调用的时候。
所以要在暴露在外给人家用的话,一定要写上修饰词 `@objc` 与 `public` 缺一不可

而我们的类,即 Class ,继承了 NSObject 那么即使不写 @objc , 也是OK的,但是属性func一定要写

可以做个测试,在framework工程中写:

public class XPTest: NSObject{
    public func sayWorld() {
        print("XPKit-->: world")
    }
    @objc public func sayHello(){
        print("XPKit-->: hello")
    }
}

重新 build , 注: (每次重新build才会更新framework)
并删除 OC-App 下的 framework 。重新拖到项目中并添加到Embedded Binaries
发现,XPKit-Swift.h下暴露的是这样的:

@interface XPTest : NSObject
- (void)sayHello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

所以如果是 Class 继承了 NSObject 那么即使不写 @objc , 也是OK的,但是属性func一定要写
这里就是简单介绍,还有些细节你们可以慢慢测试

第五步:创建可以直接调试的工程

如果你都跟着步骤写了以后,会发现一直 Build, 一直拖,一直添加,太繁琐。所以咱们可以这样创建一个 workspace 将两个项目都包含进去

  1. 新建项目或者我们把原先的添加了的framework先丢掉,左上角 File -> save as workspace

  2. 关闭这个.xcodeproj 文件重新打开这个 .xcworkspace

  3. 将我们的 XPKit 项目 拖到 workspace 中,与 Demo-App 并行


    这个时候就会发现,我们就有两个项目了可以分别 build

  4. Demo工程文件 -> General ->embedded binaries 中将 XPKit 下的 .framework 加进来

  5. 试一试,scheme 选择 Demobuild 一下是success
    我们再来到我们的 XPManager.h ,添加几行代码(当测试)

public class XPHomeViewController: UIViewController{
    @objc public let size = CGSize(width: 40, height: 40)
    @objc public var point: CGPoint = CGPoint(x: 20, y: 20)
    public var content: String?
    @objc public var textColor: UIColor?
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.red
    }
    @objc public func sayHello(){
        print("XPKit-->: hello")
    }
    
    public func sayWorld() {
        print("XPKit-->: world")
    }
}

当然还是先build一下我们的XPKit,否则Demo里的framework不更新呐,再到OC项目中import并点进去看看。
注:我这里试了几次总是没有自动补全,那就自己手写 import地址吧

#import <XPKit/XPKit-Swift.h>

点进去发现

@interface XPHomeViewController : UIViewController
@property (nonatomic, readonly) CGSize size;
@property (nonatomic) CGPoint point;
@property (nonatomic, strong) UIColor * _Nullable textColor;
- (void)viewDidLoad;
- (void)sayHello;
- (nonnull instancetype)initWithNibName:(NSString * _Nullable)nibNameOrNil bundle:(NSBundle * _Nullable)nibBundleOrNil OBJC_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtC5XPKit9XPManager")
@interface XPManager : NSObject
- (void)sayHello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtC5XPKit6XPTest")
@interface XPTest : NSObject
- (void)sayHello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

哇,完美,并且,可以直接在项目里用。那么边写边测,还能设断点,完美~跟平时写app没两样,其实还差最后一步 >_<.
当你调用并且运行,发现又报错了

dyld: Library not loaded: @rpath/libswiftCore.dylib
  Referenced from: /Users/midoo/Library/Developer/Xcode/DerivedData/OCInvokDemo-fvmdsbbzuqqnjnbswwgpcidptord/Build/Products/Debug-iphonesimulator/XPKit.framework/XPKit
  Reason: image not found

并且这次我们已经加到 Embedded Binaries 中了,原因是,如果我们在OC项目中引用swift framework,还需要到 build setting 中设置如下:

image.png

到此为止才是真正完美 ~

第五步:创建通用包

之前说过iphone指令集, 真机与模拟器是不同的,所以编译出来的包也是不通用的,现在我们要做通用的包提供别人使用。

  • 切换到XPKit, 选择任意一个模拟器build一下,
  • 再选择Generic iOS Device ,build 一下
  • show in finder,我们只看release包的,不要关闭finder
  • 我们需要用到终端来创建通用包,打开我们的终端
  • cd 到一个文件目录下,为了存放我们制作的包
cd /Users/midoo/Desktop/测试文件 
  • 输入lipo -create空格以后,分别到 release-iphoneosrelease-iphonesimulator 下的 XPKit.framework -> 选择XPKit二进制文件,拖动到 终端下

  • 紧接在后面写上 -output + 你的包名 ,回车
    注:我选择的文件夹下面有 XPKit 文件名,重复了 - -。创建失败...所以换了个文件夹
  • OK,这个时候还没完,只是创建了一个通用的二进制文件,还得把其他东西给他加上,回到release-iphoneosrelease-iphonesimulator,选择任意一个文件夹,整个复制出来
  • 并将制作好的二进制文件拖出来 替换掉


    image.png
  • 还没完!!!再到另一个文件夹,将指令集文件copy到这个,我们这个新的文件夹下



    保证这些都包含在内,那么这个framework包才算制作完成


  • 到此为止我们得到了我们想要的通用包,按照相同的方法,拖到工程中,引入到Embedded Binaries,可以调试了

第六步:用Shell脚本创建通用包

创建通用包用到的次数不多,上面的方法够用了,但是如果你还是觉得不方便、很繁琐。那你可以跟我这样做

  • 选择XPKit 工程点击左下角 +
  • 创建一个 Aggregate。去个名字,类似 CommonBuilder
    image.png
  • 选中 CommonBuilder -> Build Phases -> 添加New Run Script Phase
  • 在编辑器内输入我们的脚本代码,请全部复制,黏贴,记得修改第二步引号内的内容为你的framework name
# Merge Script

# 1
# Set bash script to exit immediately if any commands fail.
set -e

# 2
# Setup some constants for use later on.
FRAMEWORK_NAME="Your framework name" 

# 3
# If remnants from a previous build exist, delete them.
if [ -d "${SRCROOT}/build" ]; then
rm -rf "${SRCROOT}/build"
fi

# 4
# Build the framework for device and for simulator (using
# all needed architectures).
xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch arm64 -arch armv7 -arch armv7s only_active_arch=no defines_module=yes -sdk "iphoneos"
xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch x86_64 -arch i386 only_active_arch=no defines_module=yes -sdk "iphonesimulator"

# 5
# Remove .framework file if exists on Desktop from previous run.
if [ -d "${HOME}/Desktop/${FRAMEWORK_NAME}.framework" ]; then
rm -rf "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"
fi

# 6
# Copy the device version of framework to Desktop.
cp -r "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"

# 7
# Replace the framework executable within the framework with
# a new version created by merging the device and simulator
# frameworks' executables with lipo.
lipo -create -output "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}"

# 8
# Copy the Swift module mappings for the simulator into the
# framework.  The device mappings already exist from step 6.
cp -r "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule/" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule"

# 9
# Delete the most recent build.
if [ -d "${SRCROOT}/build" ]; then
rm -rf "${SRCROOT}/build"
fi

如图:


  • scheme 选择 CommonBuilder,任意模拟器,编译,报错了看看报了什么错
Command /bin/sh failed with exit code 65

你们以后看到这些不用慌,网上看,信息都在上面

=== BUILD TARGET XPKit OF PROJECT XPKit WITH CONFIGURATION Release ===

Check dependencies
No architectures to compile for (ARCHS=x86_64 i386, VALID_ARCHS=arm64 armv7 armv7s).

** BUILD FAILED **

分析一下,这里都是我们提到过的指令集。 VALID_ARCHS=arm64 armv7 armv7s 这就是我们开始在 Build Setting ->Valid Architectures 中设置的内容,很明显,意思是脚本里,要制作包含 x86_64i386的包,但是我们的Valid Architectures 中没有。
那么解决问题就方便了,分别添加x86_64i386


编译成功~
来到桌面我们发现XPKit.framework,已经静悄悄的在桌面上了,这就是我们的通用包

总结一下

好了,我们总结一下,本篇简单的介绍了一下

  1. 如何用Swift编写,OC项目Swift项目 都能用的 dynamic framework
  2. 如何正确的调试我们的framework
  3. 如何制作通用的 framework 包
为了能让OC项目也能调用,你还记得 @objc 和 public 吗 ^_^

我们的 framework 如果需要导入其他第三方库,该怎么做
本来也想写篇文章,有点懒,大家先可以看看
Swift + framework 的制作(基于pod管理的workspace)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342