准备工作

设备

苹果电脑安装Xcode,苹果手机或者ipad

Unity

安装IOS模块

其他

证书:development证书或者distribute证书和对应描述文件(.mobileprovision)

打包苹果流程

注意:不要有中文路径!!!

我使用的是windows电脑发布工程,然后使用Mac电脑发布,为了方便,我在window磁盘添加了一个共享文件夹,使用Mac访问共享文件夹,Xcode直接打开工程,这样就不用传来传去了. 当然也可以直接使用MacUnity发布

  • Win中Unity中发布到共享文件夹
  • Mac中XCode直接打开共享文件夹中工程

Unity中发布工程

设置Icon,最好用一个1024*1024的图,直接设置为DefaultIcon就行了

image-20220917152425621

设置Bundle Identifier,一定要和证书保持一致(也就是和你苹果后台app包名一致)

image-20220917152741643

然后直接Build就可以了,打出的是一个Xcode工程,要求发布的文件夹是一个空文件,所以重复打包同一个文件夹,最好删除原来的或者把里面的文件都删了

image-20220917154844374

XCode打包

一定要注意磁盘空间够用,不然有很多问题,确保有10G以上

我使用的windows系统build xcode工程到一个共享文件夹,然后mac通过局域网直接访问windows 里面的工程

image-20220917154641798

image-20220917154807198

image-20220917154926468

image-20220917155247395

可以使用自动签名登录一下开发者账号,也可以使用证书配置文件

点击Provisioning Profile,选择对应配置文件就可以

然后点击Product,如果想直接跑在手机上,点击run,如果需要发布ipa或者上传App StoreConnect就点击Archive

image-20220917155726924

image-20220917161010874

  • App Store Connetct :导出的ipa 包可以发布到App Store或者在越狱的iOS设备。

  • Ad Hoc :安装测试用的,有udid限制。该ipa包不能提交到AppStore.

  • Enterprise :导出的ipa 包是用于企业应用账号的,个人账号是无法选择的。而且没有任何udid设备数量限制。

  • Development :导出的ipa包是dev证书编译的,其实这个和第二Ad Hoc很类似,唯一区别这个用dev证书编译的,而Ad Hoc用的是dis证书编译的。

image-20220917161050016

image-20220917161601509

image-20220917161647336

image-20220917161715583

最后导出到目标文件夹就可以了

image-20220917161818948

然后就可以安装了

安装方式

  • 第一方式是直接手机连接上Mac,XCode->Wondow->Diveces and Simulators

image-20220917162105757

  • 第二种通过蒲公英平台上传下载,不会的自行搜索

修改代码重新打包

unity重新build如果又重新把项目拷贝到mac,那么之前的设置都没了很麻烦, 大部分我们只要把新生成的copy,然后覆盖Classes Data Library 文件夹

Classes 是程序文件(il2cpp,生成的cpp)

Data 跟其他差不多

Library 是一些插件包 一般也不会修改这个

Unity与IOS交互

扩展名 说明
.h 头文件,它包含类名,类继承的父类,还有方法和变量的声明。它定义的类的成员变量以及方法等等是公开的,外部是可以访问的。
.m 是纯Object-C 文件(.m只能调用纯Object-C的类,不能调用混合的). 实现文件,可以包含Objective-C和C代码。同时,它是对.h文件中方法的实现外部不能访问,
.mm 是Object-C和C++混合(.mm可以调用Object-C的,也可以调用C++的). 实现文件,和.m文件类似,唯一的不同点就是,除了可以包含Objective-C和C代码以外,还可以包含C++代码。仅在你的Objective-C代码中确实需要使用C++类或者特性的时候才用这种扩展名。

Objective-c中.m、.h、.mm文件 - 何人之名 - 博客园 (cnblogs.com)

C#可以调用C和C++代码,而C和C++可以调用Objective-C代码,这里我们将.m后缀改为.mm,这样,就可以在里面编写C和C++代码.

Unity做插件就是使用.mm文件,调用的都是c和c++代码

什么是extern "C"

extern “C” 是C++ 指令,是告诉编译器 ,这里面的代码用C规则来编译

extern “C“的作用及理解_米碎师兄的博客

Unity调用IOS

Unity 中是直接调用IOS声明的方法,不用找到类

1
2
3
4
5
//固定写法
[DllImport("__Internal")]
//在mm或者.h中声明的方法名字 必须保持一致
//c# extern关键字 表示这个方法由外部实现,简单说这个方法是外部某个文件某个dll实现的 自己不需要实现
private static extern void UnityCallIOS();

OC代码基本都是面向对象的,即需要new一个对象才能调用,但是C不用,Unity直接调用C代码,然后再对应代码 newOC对象.

调用代码中写的方法,需要使用符合C代码写法.

IOS调用Unity

UnitySendMessage(const char *obj, const char *method, const char *msg)

第一个参数是物体名字 第二个参数是物体上面的方法(private也可以) 第三个参数

示例(使用Scheme url 打开其他app)

  1. 在xcode中声明一个.h文件和.mm文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "UnityAppController.h"
#import <Foundation/Foundation.h> //引入库文

#ifdef __cplusplus
//使用C语言编译
extern "C" {
#endif


//全局固定字段声明
const char * UnityGoName="NativeManager"; //unity中接收信息物体
const char * UnityMethodName="NativeToUnity"; //物体上面的方法名

//方法声明
void ShowToast(NSString *message,float duration); //显示toast
BOOL OpenScheme(NSString *scheme); //打开某个app 带有返回值
void UnityCallIOS(const char* key,const char* args); //unity调用ios
void IOS2Unity(NSString *msg); //ios发消息给unity

#ifdef __cplusplus
}
#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#import "MyIOSBridge.h" //引入头文件

//方法实现
void UnityCallIOS(const char* cmd,const char* args)
{
//得到指令
NSString *strcmd=[NSString stringWithUTF8String:cmd];
//把参数按照 | 切分
NSArray *strArgs=[[NSString stringWithUTF8String:args]componentsSeparatedByString:@"|"];
//NSLog(@"指令->%@",strcmd);
//如果指令是Toast
if([strcmd isEqualToString:@"Toast"])
{
//参数1: 消息
//参数2: 持续时间
NSString *msg=strArgs[0];
float duration=[strArgs[1] floatValue];
ShowToast(msg, duration);
}
//如果指令是OpenScheme
if([strcmd isEqualToString:@"OpenScheme"])
{
//参数1:scheme url
//参数2:app 名字
NSString *msg=strArgs[0];
NSString *appName=strArgs[1];
//NSLog(@"scheme指令->%@ 程序=%@",msg,appName);
BOOL result= OpenScheme(msg);
if(result==NO)
{
ShowToast([NSString stringWithFormat:@"没有安装该App:%@",appName], 2.0f);
//发送一个消息到unity
IOS2Unity([NSString stringWithFormat:@"noapp|%@",appName]);
}
}
}

//发送消息到unity
void IOS2Unity(NSString *msg)
{
//会有一帧的延迟
//IPhonePlayer表示是IPhonePlayer平台
msg=[NSString stringWithFormat:@"IPhonePlayer|%@",msg];
UnitySendMessage(UnityGoName, UnityMethodName, msg.UTF8String);
}

//Toast
inline void ShowToast(NSString *message, float toastLengthInSeconds)
{
// NSString *message = [NSString stringWithUTF8String:toastText];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIViewController *rootViewController = UnityGetGLViewController();
[rootViewController presentViewController:alert animated:YES completion:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, toastLengthInSeconds * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[alert dismissViewControllerAnimated:YES completion:nil];
});
}

//打开某个app 使用的是scheme url
BOOL OpenScheme(NSString *scheme)
{
NSURL *url = [NSURL URLWithString:scheme];
if([[UIApplication sharedApplication] canOpenURL:url]){
[[UIApplication sharedApplication] openURL:url];
return YES;
}
else return NO;
}


  1. 将两个文件导入Unity->Plugins->IOS目录

  2. 在Unity脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    using System.Collections;
    using System.Runtime.InteropServices;

    /// <summary>
    /// ____DESC: Unity IOS 交互
    /// </summary>
    public class Native_IOS
    {
    //固定写法
    [DllImport("__Internal")]
    //在mm或者.h中声明的方法名字 必须保持一致
    //c# extern 表示这个方法由外部实现,简单说这个方法是外部某个文件某个dll实现的 自己不需要实现
    /// <summary>
    /// 调用IOS args用 "|"分割
    /// </summary>
    private static extern void UnityCallIOS(string key,string args);

    public void UnityToIOS(string key, string[] args)
    {
    var v = string.Join('|', args);
    UnityCallIOS(key, v);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    /// <summary>
    /// ____DESC: 与原生平台交互管理器
    /// </summary>

    public class NativeManager : MonoSingleton<NativeManager>, IManager
    {
    public event System.Action<RuntimePlatform, string[]> OnReceiveNativeMsg;

    private Native_IOS native_IOS;

    public bool IsInit { get; set; } = false;

    private RuntimePlatform currRuntimePlatform;
    public void Init()
    {
    this.gameObject.name = nameof(NativeManager);
    currRuntimePlatform = Application.platform;
    else if (currRuntimePlatform == RuntimePlatform.IPhonePlayer)
    native_IOS = new Native_IOS();
    SetDontDestoryOnload();
    IsInit = true;
    }

    /// <summary>
    /// 接收到其他平台发来的消息,可以是私有方法
    /// </summary>
    /// <param name="msg">msg包含一个|</param>
    private void NativeToUnity(string msg)
    {
    //用|切分 第0个是平台
    var msgs = msg.Split('|');
    if (System.Enum.TryParse<RuntimePlatform>(msgs[0], ignoreCase: true, out RuntimePlatform runtimePlatform))
    {
    //其他参数
    List<string> args = new List<string>();
    for (int i = 1; i < msgs.Length; i++)
    args.Add(msgs[i]);

    OnReceiveNativeMsg?.Invoke(runtimePlatform, args.ToArray());
    }
    else
    MyLog.LogError($"平台错误:{msgs[0]}");
    }

    /// <summary>
    /// Unity调用原生平台
    /// </summary>
    /// <param name="runtimePlatform">平台</param>
    /// <param name="key">key</param>
    /// <param name="args">参数</param>
    public void UnityCallNative(RuntimePlatform runtimePlatform, string key, params string[] args)
    {
    switch (runtimePlatform)
    {
    case RuntimePlatform.IPhonePlayer:
    if (currRuntimePlatform == RuntimePlatform.IPhonePlayer)
    {
    native_IOS.UnityToIOS(key,args);
    }
    break;
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    using UnityEngine;
    using UnityEngine.UI;

    public class Test : MonoBehaviour
    {
    public Button btnQQ, btnWeibo, btnLofter, btnBilibili, btnNone;

    //QQ
    string schemeQQGroup = "mqqapi://card/show_pslcard?src_type=internal&version=1&card_type=group&uin=1020819114";
    string schemeQQGroup2 = "tim://card/show_pslcard?src_type=internal&version=1&uin=1020819114";

    //Lofter
    string schemeLofter = "lofter://qiyunyimeng.lofter.com/";
    string httpLofter = "https://qiyunyimeng.lofter.com/";

    //B站
    string schemeBilibili = "bilibili://space/1346329729";
    string httpBilibili = "https://m.bilibili.com/space/1346329729";

    //抖音
    string schemeDouyin = "https://v.douyin.com/2XR7GQ2/";

    //微博
    string schemeWeibo = "sinaweibo://userinfo?uid=5904919311";
    string schemeWeibo2 = "weibointernational://userinfo?uid=5904919311";
    string httpWeibo = "https://weibo.com/u/5904919311";

    private void Start()
    {
    NativeManager.Ins.Init();
    NativeManager.Ins.OnReceiveNativeMsg += Ins_OnReceiveNativeMsg;
    btnQQ.onClick.AddListener(() => NativeManager.Ins.UnityCallNative(RuntimePlatform.IPhonePlayer, "OpenScheme", schemeQQGroup,"QQ"));
    btnWeibo.onClick.AddListener(() => NativeManager.Ins.UnityCallNative(RuntimePlatform.IPhonePlayer, "OpenScheme", schemeWeibo2,"weibo"));
    btnLofter.onClick.AddListener(() => NativeManager.Ins.UnityCallNative(RuntimePlatform.IPhonePlayer, "OpenScheme", schemeLofter,"lofter"));
    btnBilibili.onClick.AddListener(() => NativeManager.Ins.UnityCallNative(RuntimePlatform.IPhonePlayer, "OpenScheme", schemeBilibili, "bilibili"));
    btnNone.onClick.AddListener(() => NativeManager.Ins.UnityCallNative(RuntimePlatform.IPhonePlayer, "OpenScheme", "tt://sdfsdfdss","tt"));
    }
    //接收到消息
    private void Ins_OnReceiveNativeMsg(RuntimePlatform platform, string[] args)
    {
    Debug.Log("接收到消息:" + platform.ToString() + " " + args[0]);
    if (platform == RuntimePlatform.IPhonePlayer)
    {
    //提示没有安装该app
    if (args[0] == "noapp")
    {
    //如果是qq
    if (args[1] == "QQ")
    {

    }
    //如果没有微博 就直接打开微博uri
    else if (args[1] == "weibo")
    {
    Application.OpenURL(httpWeibo);
    }
    else if (args[1] == "lofter")
    {
    Application.OpenURL(httpWeibo);
    }
    else if (args[1] == "bilibili")
    {
    Application.OpenURL(httpBilibili);
    }
    else if (args[1] == "tt")
    {
    Application.OpenURL("http://www.baidu.com");
    }
    }
    }
    }
    }

    注意点

    IOS使用scheme的时候需要添加白名单,不然打不开.

    添加scheme的时候不用加://

    第一种添加方式

    image-20220926193759959

    第二种添加方式

    image-20220926193846672

    参考链接

问题合集

证书不受信任

如果是别人给的证书那么就是以.p12结尾,双击就可以导入,然后再钥匙串中就可以看到

image-20220917160230943

如果证书显示不受信任

image-20220917160415447

那么需要重新下载一个 证书

地址:Apple PKI - Apple 下载之后双击按照就可以

image-20220917160718076

Distribution APP过程出现问题

出现The data couldn’t be read because it isn’t in the correct format或者zip faild什么的,都先检查是否存贮空间不够了

打出的IPA文件名是ProductName

ProductName 就是BundleName 游戏名字是Bundle Diaplay Name

image-20220917163355081

在Unity中设置了Productname 如果是英文名 好像不会出现这个错误,如果是中文,那么就有可能出现这个名字

  • 可以在Xcode工程中改
  • 也可以在Unity导出的时候改 Info.plist文件

这个时候我们可以在打包出IOS的时候自动设置一下,在unity 新建一个Editor脚本,加上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[PostProcessBuild]
private static void OnPostProcessBuild(BuildTarget buildTarget, string pathToBuiltProject)
{
if (buildTarget == BuildTarget.iOS)
{
string pbxProjectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
//xcode对象读取的就是xxxx.xcodeproj下面的project.pbxproj
//PBXProject pbxProject = new PBXProject();
//pbxProject.ReadFromFile(pbxProjectPath);
//string targetGUID = pbxProject.GetUnityMainTargetGuid();

//var configNames = pbxProject.BuildConfigNames();
//foreach (var configName in configNames)
//{
// var config = pbxProject.BuildConfigByName(targetGUID, configName);
// pbxProject.SetBuildProperty(config, "PRODUCT_NAME_APP", "qqym222");
//}
//pbxProject.WriteToFile(pbxProjectPath);

var identifiers = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.iOS).Split('.');
var bundleName = identifiers.Length > 1 ? identifiers[identifiers.Length - 1] : identifiers[0];
/*
*修改ProductName 打包发现BundleName如果是中文显示的就是"ProductName" 而不是app名字
*设置称为包名最后一个名字 com.aaaa.bbb 那么就是bbb
*/
//直接替换所有
string pbxProjectContent = File.ReadAllText(pbxProjectPath);
pbxProjectContent = pbxProjectContent.Replace("PRODUCT_NAME_APP = ProductName", $"PRODUCT_NAME_APP = {bundleName}");
File.WriteAllText(pbxProjectPath, pbxProjectContent);

//读取修改Info.plist 是以键值对存在的
//string plistPath = pathToBuiltProject + "/" + "Info.plist";
//PlistDocument plist = new PlistDocument();
//plist.ReadFromFile(plistPath);
//plist.root.SetString("CFBundleName", bundleName);
//plist.WriteToFile(plistPath);
}
}

#Unity# 在编辑器扩展中用PostProcessBuildAttribute来修改XCode工程的Product Name (xmanyou.com)

使用www(UnityWebRequest)无法读取SteamingAssets中的文件

流媒体资源 - Unity 手册

Application.streamingAssetsPath 返回的位置因平台而异:

  • 大多数平台(Unity Editor、Windows、Linux 播放器、PS4、Xbox One、Switch)使用 Application.dataPath + "/StreamingAssets"
  • macOS 播放器使用 Application.dataPath + "/Resources/Data/StreamingAssets"
  • iOS 使用 Application.dataPath + "/Raw"
  • Android 使用经过压缩的 APK/JAR 文件中的文件:"jar:file://" + Application.dataPath + "!/assets"

要在无法直接访问流媒体资源文件的平台(如 Android 和 WebGL)上读取流媒体资源,请使用 UnityWebRequest。有关示例,请参阅 Application.streamingAssetsPath

在许多平台上,流媒体资源文件夹位置是只读的;您不能在运行时在这些位置修改或写入新文件。请使用 Application.persistentDataPath 来获取可写的文件夹位置。

即使有些平台可以用IO在SteamingAssets进行写入操作,但是最好不要这么做

IOS与Android不同,IOS需要在前面加一个 file:// Android是经过压缩的 所以不能直接IO读取 要使用www 加上"jar:file://"协议

用WWW类加载本地,要注意各个平台路径需要加的访问名称,例如Android平台的路径前要加"jar:file://",其他平台使用"file://"

Unity安卓、iOS、PC、Mac读写目录 - 简书 (jianshu.com)

The App Store Icon in the asset catalog in ‘qyym. app’ can’t be transparent nor contain an alpha channel

这个报错原因是图标有透明或者有透明通道

  • 方法一:

    直接把unity icon 图标搞成jpg然后再打包

  • 方法二:

    image-20221011170141146

    把每个图片用windows自带画图编辑保存(画图保存自然会丢失a通道)

The bundle at ‘my.app/Frameworks/UnityFramework.framework’ contains disallowed file ‘Frameworks’.

就是不能嵌套包含,这个问题不知出在哪,我自己打包的时候没啥问题,用火星人工具打包出现了问题

新的解决方案:将ks广告设置为动态库

屏幕截图 2023-10-22 121246

屏幕截图 2023-10-22 121415

最后点击Add Files重新添加相应的Framework

解决方案

  1. XCode在上传testflight前会先Archive,首先先在Achieve成功后的项目中右键Show in Finder

image-20221011170555053

  1. 在.xcarchive文件点右键显示包内容

image-20221011170618609

  1. 在Products/Applications/xx.app文件再点右键显示包内容

image-20221011170632329

  1. 删除Frameworks/UnityFramework.framework/Frameworks文件夹

image-20221011170643670

  1. 再重新上传testflight就可以成功上传了!

以上这些步骤可以用shell命令来处理,步骤如下:

在Build Phases下新增Run Script,并添加以下代码:

1
2
3
cd            
"${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Frameworks/UnityFramework.framework/"
if [[ -d "Frameworks" ]]; then

转自:https://www.weixiuzhan.cn/news/show-29533.html

Command Ld failed with a nonzero exit code

这个问题多种多样,我这里记录一下

1:是我用mac连接window共享文件夹问题(很奇怪之前没这个问题)

DerivedData/Unity-iPhone-fazbdskwtnuvvqcdjsitsmtzxowo/Build/Products/ReleaseForRunning-iphoneos/UnityFramework.framework/UnityFramework’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target.

在xcode中 build setting->bitcode 设置为NO

添加动态库设置

例如添加AVPRO

1

32

4