iOS开发之Xcode中版本号&Build版本号自增

1. 背景

在iOS项目打包时,有两个版本号,一个是Version,即显示在App Store中的版本号,其key为CFBundleShortVersionString;另一个是Build,即编译版本号,其key为CFBundleVersion

2. 问题发现

当App准备上架时,需要打包提交至App Store审核,在这个过程中我们可能会多次上传ipa包。在Version相同的情况下,若Build还相同,则上传到App Store Connect时会提示已有该版本,不能再次上传。

3. 问题解决

为了避免这种情况,我们可以在每次打包前去手动修改Build,以保证不会重复。或者我们可以通过脚本让Build在每次打包时自动加一,省去每次手动修改的麻烦。

2023.07 功能增强:版本号和Build版本号都可自动+1(版本号中的最小数字版本号自动+1)。

  • 假如当前Version版本号为1.0.0,则+1后为1.0.1
  • 假如当前Build版本号为10,则+1后为11

1、可通过设置脚本中的VERSION_INCREASE_WHEN_BUILDBUILD_INCREASE_WHEN_BUILD两个变量来控制Version或Build是否自动+1。
2、可到Xcode项目的.xcodeproj/project.pbxproj配置文件中获取Release XCBuildConfiguration节点 和 Debug XCBuildConfiguration节点的UUID,分别赋值给该脚本中的变量VERSION_UUID_ReleaseVERSION_UUID_Debug

  • 查找XCBuildConfiguration节点UUID的方法:先搜索“isa = XCBuildConfiguration”节点,再查看该节点是否包含MARKETING_VERSIONCURRENT_PROJECT_VERSION,若包含,则取该节点的UUID。
添加脚本的流程:
  1. Xcode切换到 Build Phases 选项卡;
  2. 点击左上角"+"号来增加一项"New Run Script Phase";
  3. 添加如下脚本代码:
Shell #!/bin/sh

代码:

# ******************************************************
#
# Copyright 2015-2023 BTStudio. All rights reserved.
#
# Name          : Version Increase
# Author        : Wangzhi
# Last modified : 2023-07-03
# Email         : wzqsyk@126.com
# Description   : 每次Build或Archive后,版本号中的最小数字版本号自动+1。
#
#
# 1. 最小数字版本号:版本号中最后一个.后的数字。无.时,与版本号中的数字相同。
#
# 2. 版本号格式:(注意:版本号设置为以下格式时,该脚本才能实现其功能。)
# - 1.0.0       规范的版本号 (推荐用于上架版本号)
# - 1.0.0_Beta  规范的版本号加上以下划线分割的后缀 (推荐用于开发阶段的版本号)
# - 10          纯数字版本号 (推荐用于Build版本号)
# - 10_Beta     纯数字版本号加上以下划线分割的后缀 (推荐用于Build版本号)
#
# 3. 版本号自动+1:
# - 若当前版本号为1.0.0,则+1后为1.0.1
# - 若当前版本号为1.0.0_Beta,则+1后为1.0.1_Beta
# - 若当前版本号为1,则+1后为2
# - 若当前版本号为1_Beta,则+1后为2_Beta
# - 若当前版本号为1.0.0.00,则+1后为1.0.0.01
#
# 4. 软件版本号规范
# 软件版本号由4部分组成:主版本号.子版本号.阶段版本号.日期版本号加希腊字母版本号。
# 示例:1.0.0_Alpha、1.0.0.230701_Alpha
#
# - 主版本号:当功能模块有较大的变动(比如增加多个模块或整体架构发生变化)时进行修改。
# - 子版本号:当功能有一定的增加或变化时进行修改。
# - 阶段版本号:一般是Bug修复或是一些小的变动,要经常发布修订版时进行修改。
# - 日期版本号:用于记录修改项目的当前日期,每天对项目的修改都需要修改。
# - 希腊字母版本号:用于标注当前版本的软件处于哪个开发阶段,当软件进入到另一个阶段时需要修改。
#   (开发阶段:Base、Alpha、Beta、RC、Release)
#
# ******************************************************


# 每次编译后版本号是否+1 (0-不执行+1,1-每次Build后+1,2-每次Archive后+1)
VERSION_INCREASE_WHEN_BUILD=2
# 每次编译后编译版本号是否+1 (0-不执行+1,1-每次Build后+1,2-每次Archive后+1)
BUILD_INCREASE_WHEN_BUILD=2

# 项目配置文件project.pbxproj的路径
ProjectPbxprojPath="${PROJECT_NAME}.xcodeproj/project.pbxproj"

# 配置文件中 MARKETING_VERSION 和 CURRENT_PROJECT_VERSION 所属节点(对象)的UUID
# UUID示例:336C88122A4D5E2C003DA11D,UUID需要手动到project.pbxproj中查找确定。
# 方法:先搜索isa = XCBuildConfiguration节点,再查看该节点是否包含
# MARKETING_VERSION 和 CURRENT_PROJECT_VERSION,若包含,则取该节点的UUID。
VERSION_UUID_Release=""
VERSION_UUID_Debug=""
    
# 注意:项目设置有不同Target时,需要根据Target名称确定UUID
if [[ "${TARGET_NAME}" == "XXX" ]]; then
    VERSION_UUID_Release=""
    VERSION_UUID_Debug=""
elif [[ "${TARGET_NAME}" == "XXX_Beta" ]]; then
    VERSION_UUID_Release=""
    VERSION_UUID_Debug=""
fi


ReleaseVersion="1.0.0" # 版本号默认值
BuildVersion="1" # 编译版本号默认值

# Info.plist中的版本号(CFBundleShortVersionString)
VersionOfInfo=""
# Info.plist中的编译版本号(CFBundleVersion)
BuildVersionOfInfo=""
# 项目配置文件project.pbxproj中的版本号(MARKETING_VERSION)
VersionOfConfig=""
# 项目配置文件project.pbxproj中的编译版本号(CURRENT_PROJECT_VERSION)
BuildVersionOfConfig=""

# Build或Archive的标志,默认为空
ConfigFlagForVersion=""
ConfigFlagForBuild=""


echo "VI_当前Xcode的 Build Configuration: $CONFIGURATION"

if [ $VERSION_INCREASE_WHEN_BUILD -eq 1 ]; then
    ConfigFlagForVersion="Debug"
elif [ $VERSION_INCREASE_WHEN_BUILD -eq 2 ]; then
    ConfigFlagForVersion="Release"
fi

if [ $BUILD_INCREASE_WHEN_BUILD -eq 1 ]; then
    ConfigFlagForBuild="Debug"
elif [ $BUILD_INCREASE_WHEN_BUILD -eq 2 ]; then
    ConfigFlagForBuild="Release"
fi


# 数字版本号自动+1函数
# - 参数: version
function version_auto_increment() {
    version=$1
    version_new=$1
    
    if [ -n "$version" ]; then
        # =~     : 用于判断左边的字符串和右边的正则表达式pattern是否匹配。
        # #*"."  : 从字符串第一次出现.的位置开始,截取.右边的所有字符。
        # ##*"." : 从字符串最后一次出现.的位置开始,截取.右边的所有字符。
        # %"."*  : 从字符串最后一次出现.的位置开始,截取.左边的所有字符。
        # %%"."* : 从字符串第一次出现.的位置开始,截取.左边的所有字符。
                        
        major_version="" # 主版本号
        minor_version="" # 小版本号
        suffix_version="" # 版本号后缀
        
        # 符合格式:10
        if [[ "$version" =~ ^[0-9]+$ ]]; then
            # 示例版本号:12345
            #
            # 主版本号:无
            # 小版本号:12345
            # 版本号后缀:无
            
            minor_version=$version
            #printf "VI_版本号: ${version}=${major_version}+${minor_version}+${suffix_version}"
                                
        # 符合格式:1.0.0
        elif [[ "$version" =~ ^[0-9][0-9\.]*[0-9]$ ]]; then
            # 示例版本号:1.23.45
            #
            # 主版本号:1.23
            # 小版本号:45
            # 版本号后缀:无
            
            major_version=${version%"."*}
            
            minor_version=${version##*"."}
            minor_version=${minor_version%%"_"*}
            #printf "VI_版本号:: ${version}=${major_version}+${minor_version}+${suffix_version}"
                        
        # 符合格式:10_Beta
        elif [[ "$version" =~ ^[0-9]+_[0-9A-z_]*$ ]]; then
            # 示例版本号:12345_Beta
            #
            # 主版本号:无
            # 小版本号:12345
            # 版本号后缀:Beta
            
            minor_version=${version%%"_"*}
            suffix_version=${version#*"_"}
            #printf "VI_版本号::: ${version}=${major_version}+${minor_version}+${suffix_version}"
                        
        # 符合格式:1.0.0_Beta
        elif [[ "$version" =~ ^[0-9][0-9\.]*[0-9]_[0-9A-z_]*$ ]]; then
            # 示例版本号:1.23.45_Beta
            #
            # 主版本号:1.23
            # 小版本号:45
            # 版本号后缀:Beta
            
            major_version=${version%"."*}
            
            minor_version=${version##*"."}
            minor_version=${minor_version%%"_"*}
                        
            suffix_version=${version#*"_"}
            #printf "VI_版本号:::: ${version}=${major_version}+${minor_version}+${suffix_version}"
            
        else
            version_new=$version
            #printf "VI_版本号: ${version} 不符合格式,不做+1处理"
        fi
        
        # 小版本号不为空,再去处理小版本号+1
        if [ -n "$minor_version" ]; then
            minor_length=${#minor_version}
            # 小版本号+1 (10#表示让Shell强制将"00009"当成十进制来解释,而不是八进制)
            minor_version=$((10#$minor_version + 1))
            # 小版本号格式化
            if [ $minor_length -eq 2 ]; then
                minor_version=$(printf "%02d" $minor_version)
            elif [ $minor_length -eq 3 ]; then
                minor_version=$(printf "%03d" $minor_version)
            elif [ $minor_length -eq 4 ]; then
                minor_version=$(printf "%04d" $minor_version)
            elif [ $minor_length -eq 5 ]; then
                minor_version=$(printf "%05d" $minor_version)
            else
                minor_version=$(printf "%d" $minor_version)
            fi
                     
            # 拼接新版本号   
            # 追加 . 和 _
            if [ -n "$major_version" ]; then
                major_version="${major_version}."
            fi
            if [ -n "$suffix_version" ]; then
                suffix_version="_${suffix_version}"
            fi
            
            version_new=${major_version}${minor_version}${suffix_version}
            #printf "VI_小版本号+1后: ${version_new}"
        fi
    fi
    
    # return只能返回1-255的整数,通常只是用来供其他地方调用获取状态,一般用0表示成功,1表示失败
    #return 1
     
    # echo用于返回字符串结果
    echo ${version_new}
}


# 版本号的处理
#if [ -z "$VERSION_UUID_Release" ]; then
#    echo "VI_请到项目.xcodeproj/project.pbxproj中获取Release XCBuildConfiguration节点的UUID,并赋值给该脚本中的变量VERSION_UUID_Release"
#fi
if [[ "${ConfigFlagForVersion}" == "${CONFIGURATION}" ]]; then
    # 用于处理的版本号
    VersionOfHandle=""

    # 1. 读取版本号
    VersionOfInfo=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${INFOPLIST_FILE}")
    VersionOfConfig=${MARKETING_VERSION}
    : '
    # 1.1 版本号的确定
    # 判断读取出来的VersionOfInfo是否为空字符串:
    # - 是:不做处理
    # - 否:再判断是否为字符串"$(MARKETING_VERSION)"
    # -- 是:则需从MARKETING_VERSION读取版本号
    # -- 否:则读取出来的VersionOfInfo即是版本号
    if [ -n "$VersionOfInfo" ]; then
        if [[ "$VersionOfInfo" == "\$(MARKETING_VERSION)" ]]; then
            VersionOfHandle=$VersionOfConfig
            echo "VI_Version(Info.plist): \$(MARKETING_VERSION)"
        else
            VersionOfHandle=$VersionOfInfo
            echo "VI_Version(Info.plist): ${VersionOfHandle}"
        fi
    else 
        echo "VI_Version(Info.plist): is empty"
    fi
        
    # VersionOfInfo和VersionOfConfig中以后者为优先,所以再判断
    # 读取出来的VersionOfConfig是否为空字符串:
    # - 是:不做处理
    # - 否:则读取出来的VersionOfConfig即是版本号
    if [ -n "$VersionOfConfig" ]; then
        VersionOfHandle=$VersionOfConfig
        echo "VI_Version(project.pbxproj): ${VersionOfHandle}"
    else
        echo "VI_Version(project.pbxproj): is empty"
    fi
    '
    if [ -n "$VersionOfInfo" ]; then
        if [[ "$VersionOfInfo" == "\$(MARKETING_VERSION)" ]]; then
            VersionOfHandle=$VersionOfConfig
            echo "VI_Version(Info.plist): \$(MARKETING_VERSION)=${VersionOfHandle}"
        else
            VersionOfHandle=$VersionOfInfo
            echo "VI_Version(Info.plist): ${VersionOfHandle}"
        fi
    else 
        VersionOfHandle=$VersionOfConfig
        echo "VI_Version(project.pbxproj): ${VersionOfHandle}"
    fi

    # 2. 版本号+1处理
    ReleaseVersion=$(version_auto_increment $VersionOfHandle)
    echo "VI_Version小版本号+1后: ${ReleaseVersion}"
    
    # 3. 写入新版本号
    # 3.1 新版本号写入Info.plist
    #if [ -n "$VersionOfInfo" ] && [[ "$VersionOfInfo" != "\$(MARKETING_VERSION)" ]]; then
    if [ -n "$VersionOfInfo" ]; then
        echo "VI_New Version写入: Info.plist"
        /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $ReleaseVersion" "${INFOPLIST_FILE}"
    fi
    : '
    # 3.2 新版本号写入项目配置文件project.pbxproj
    if [ -n "$VersionOfConfig" ]; then
        echo "VI_New Version写入: project.pbxproj"
        # Debug
        /usr/libexec/PlistBuddy -c "Set :objects:${VERSION_UUID_Debug}:buildSettings:MARKETING_VERSION ${ReleaseVersion}" "${ProjectPbxprojPath}"
        # Release
        /usr/libexec/PlistBuddy -c "Set :objects:${VERSION_UUID_Release}:buildSettings:MARKETING_VERSION ${ReleaseVersion}" "${ProjectPbxprojPath}"
    fi
    '
    echo "VI_Release version need increase."
else
    echo "VI_Release version don't need increase."
fi


# Build版本号的处理
#if [ -z "$VERSION_UUID_Debug" ]; then
#    echo "VI_请到项目.xcodeproj/project.pbxproj中获取Debug XCBuildConfiguration节点的UUID,并赋值给该脚本中的变量VERSION_UUID_Debug"
#fi
if [[ "${ConfigFlagForBuild}" == "${CONFIGURATION}" ]]; then
    # 用于处理的编译版本号
    BuildVersionOfHandle=""
    
    # 1. 读取Build版本号
    BuildVersionOfInfo=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "${INFOPLIST_FILE}")
    BuildVersionOfConfig=${CURRENT_PROJECT_VERSION}
    : '
    # 1.1 编译版本号的确定(与版本号的确定逻辑相同)
    if [ -n "$BuildVersionOfInfo" ]; then
        if [[ "$BuildVersionOfInfo" == "\$(CURRENT_PROJECT_VERSION)" ]]; then
            BuildVersionOfHandle=$BuildVersionOfConfig
            echo "VI_Build(Info.plist): \$(CURRENT_PROJECT_VERSION)"
        else
            BuildVersionOfHandle=$BuildVersionOfInfo
            echo "VI_Build(Info.plist): ${BuildVersionOfHandle}"
        fi
    else 
        echo "VI_Build(Info.plist): is empty"
    fi
    
    if [ -n "$BuildVersionOfConfig" ]; then
        BuildVersionOfHandle=$BuildVersionOfConfig
        echo "VI_Build(project.pbxproj): ${BuildVersionOfHandle}"
    else
        echo "VI_Build(project.pbxproj): is empty"
    fi
    '
    if [ -n "$BuildVersionOfInfo" ]; then
        if [[ "$BuildVersionOfInfo" == "\$(CURRENT_PROJECT_VERSION)" ]]; then
            BuildVersionOfHandle=$BuildVersionOfConfig
            echo "VI_Build(Info.plist): \$(CURRENT_PROJECT_VERSION)=${BuildVersionOfHandle}"
        else
            BuildVersionOfHandle=$BuildVersionOfInfo
            echo "VI_Build(Info.plist): ${BuildVersionOfHandle}"
        fi
    else
        BuildVersionOfHandle=$BuildVersionOfConfig
        echo "VI_Build(project.pbxproj): ${BuildVersionOfHandle}"
    fi

    # 2. 版本号+1处理
    BuildVersion=$(version_auto_increment $BuildVersionOfHandle)
    echo "VI_Build小版本号+1后: ${BuildVersion}"
    
    # 3. 写入新版本号
    # 3.1 新版本号写入Info.plist
    #if [ -n "$BuildVersionOfInfo" ] && [[ "$BuildVersionOfInfo" != "\$(CURRENT_PROJECT_VERSION)" ]]; then
    if [ -n "$BuildVersionOfInfo" ]; then
        echo "VI_New Build写入: Info.plist"
        /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BuildVersion" "${INFOPLIST_FILE}"
    fi
    : '
    # 3.2 新版本号写入项目配置文件project.pbxproj
    if [ -n "$BuildVersionOfConfig" ]; then
        echo "VI_New Build写入: project.pbxproj"
        # Debug
        /usr/libexec/PlistBuddy -c "Set :objects:${VERSION_UUID_Debug}:buildSettings:CURRENT_PROJECT_VERSION ${BuildVersion}" "${ProjectPbxprojPath}"
        # Release
        /usr/libexec/PlistBuddy -c "Set :objects:${VERSION_UUID_Release}:buildSettings:CURRENT_PROJECT_VERSION ${BuildVersion}" "${ProjectPbxprojPath}"
    fi
    '
    echo "VI_Build version need increase."
else
    echo "VI_Build version don't need increase."
fi

效果如下:


添加Build自动+1的脚本代码
  1. 之后每次进行打包时,Version或Build会自动+1,避免了Version或Build重复的问题。

提示:

  1. 可以双击 Version Increase 进行名称的修改;
  2. 增加一项 New Run Script Phase 后,Xcode默认名称为Run Script
  3. 每次编译或打包后,可以查看脚本的执行结果,如下图所示:
    右上角搜索框中输入VI_即可过滤显示脚本的执行结果。
    脚本的执行结果

小知识

PlistBuddy是Mac自带的专门解析plist的小工具,由于PlistBuddy并不在Mac默认的Path里,所以我们得通过绝对路径(/usr/libexec/PlistBuddy)来引用这个工具。

Shell基本语法:

  1. 定义变量时,变量名不加美元符号$ (PHP语言中变量需要)。如:
    your_name="runoob.com" (注意:变量名和等号之间不能有空格)
  2. 使用一个定义过的变量,只要在变量名前面加美元符号即可。如:
    echo $your_name
  3. 读取plist中某个字段的值,并赋给变量var:
    var=$(/usr/libexec/PlistBuddy -c "Print :字段名" plist文件路径)
  4. 修改plist中某个字段相应的值:
    /usr/libexec/PlistBuddy -c "Set :字段名 值" plist文件路径
  5. Shell的echo指令与PHP的echo指令类似,都是用于字符串的输出。
    命令格式:echo string
  6. Shell脚本没有{}括号,所以用fi表示if语句块的结束。
  7. #! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种Shell。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342