跳到主要内容

iTunes Connect 构建版本不显示

· 阅读需 1 分钟
BY

前言

今天新项目上架,在Xcode打包上传到App Store后,在iTunes Connect构建版本中居然找不到上传的App...

解决

从iOS10开始,苹果更加注重对用于隐私的保护,App 里边如果需要访问用户隐私,必须要做描述,所以要在 plist 文件中添加描述。

而这三个基础描述是必须添加的:

  • 麦克风权限Privacy - Microphone Usage Description 是否允许此App使用你的麦克风?

  • 相机权限Privacy - Camera Usage Description 是否允许此App使用你的相机?

  • 相册权限Privacy - Photo Library Usage Description 是否允许此App访问你的媒体资料库?

其他的权限可以根据自己 APP 的情况来添加。

添加完权限之后然后继续提交 App 就可以了。

若还是找不到,返回 plist 文件中,删除之前的权限,重新添加一下,有可能你哪不小心添加的权限末尾有空格,或者字段不对。

End

Swift的HMAC和SHA1加密

· 阅读需 2 分钟
BY

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)。 HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。也就是说HMAC通过将哈希算法(SHA1, MD5)与密钥进行计算生成摘要。

Objectice-C

在上个 Objectice-C 项目中,使用的 HMAC 和 SHA1 进行加密。如下代码:

+ (NSString *)hmacsha1:(NSString *)text key:(NSString *)secret {

NSData *secretData = [secret dataUsingEncoding:NSUTF8StringEncoding];
NSData *clearTextData = [text dataUsingEncoding:NSUTF8StringEncoding];
unsigned char result[20];
// SHA1加密
CCHmac(kCCHmacAlgSHA1, [secretData bytes], [secretData length], [clearTextData bytes], [clearTextData length], result);
char base64Result[32];
size_t theResultLength = 32;
// 转为Base64
Base64EncodeData(result, 20, base64Result, &theResultLength,YES);
NSData *theData = [NSData dataWithBytes:base64Result length:theResultLength];
NSString *base64EncodedResult = [[NSString alloc] initWithData:theData encoding:NSUTF8StringEncoding];
return base64EncodedResult;
}

swift

最近用 swift 重构项目,用 Swift 重写了 HMAC 的 SHA1 加密方式。

使用

// 使用HMAC和SHA加密
let hmacResult:String = "myStringToHMAC".hmac(HMACAlgorithm.SHA1, key: "myKey")

代码

使用下面代码时,需要在 OC 桥接文件xxx-Bridging-Header#import <CommonCrypto/CommonHMAC.h>

extension String {
func hmac(algorithm: HMACAlgorithm, key: String) -> String {
let cKey = key.cStringUsingEncoding(NSUTF8StringEncoding)
let cData = self.cStringUsingEncoding(NSUTF8StringEncoding)
var result = [CUnsignedChar](count: Int(algorithm.digestLength()), repeatedValue: 0)
CCHmac(algorithm.toCCHmacAlgorithm(), cKey!, strlen(cKey!), cData!, strlen(cData!), &result)
var hmacData:NSData = NSData(bytes: result, length: (Int(algorithm.digestLength())))
var hmacBase64 = hmacData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding76CharacterLineLength)
return String(hmacBase64)
}
}

enum HMACAlgorithm {
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512

func toCCHmacAlgorithm() -> CCHmacAlgorithm {
var result: Int = 0
switch self {
case .MD5:
result = kCCHmacAlgMD5
case .SHA1:
result = kCCHmacAlgSHA1
case .SHA224:
result = kCCHmacAlgSHA224
case .SHA256:
result = kCCHmacAlgSHA256
case .SHA384:
result = kCCHmacAlgSHA384
case .SHA512:
result = kCCHmacAlgSHA512
}
return CCHmacAlgorithm(result)
}

func digestLength() -> Int {
var result: CInt = 0
switch self {
case .MD5:
result = CC_MD5_DIGEST_LENGTH
case .SHA1:
result = CC_SHA1_DIGEST_LENGTH
case .SHA224:
result = CC_SHA224_DIGEST_LENGTH
case .SHA256:
result = CC_SHA256_DIGEST_LENGTH
case .SHA384:
result = CC_SHA384_DIGEST_LENGTH
case .SHA512:
result = CC_SHA512_DIGEST_LENGTH
}
return Int(result)
}
}


Xcode9 无线调试功能

· 阅读需 1 分钟
BY

支持:Xcode 9 及 iOS 11

使用数据线连接 iPhone 到电 Mac,Mac 和 iPhone 必须在同一个局域网

1. 打开设备列表

使用快捷键盘 ⇧⌘2 或 在 Xcode 菜单栏选择 Window > Devices and Simulators,打开设备列表

2. 勾选在线调试按钮

3. 拔掉数据线

这时就可以无线调试了。

快速配置zsh

· 阅读需 1 分钟
BY

比较水的 Personal Notes

查看你的系统有几种shell

cat /etc/shells

显示

/bin/bash /bin/csh /bin/ksh /bin/sh /bin/tcsh /bin/zsh

安装 oh my zsh

git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

重新打开终端,输入

zsh

即可切换终端,并且发现 oh my zsh 已经帮我们配置好 zsh 了

修改主题

open ~/.zshrc

修改 ZSH_THEME=”robbyrussell”,主题在 ~/.oh-my-zsh/themes 目录下。 修改为

ZSH_THEME="kolo"

可以参照这里进行选择.

设置为默认shell

chsh -s /bin/zsh

添加自定义命令

open ~/.zshrc 添加显示隐藏文件的快捷命令

alias fd='defaults write com.apple.finder AppleShowAllFiles -boolean true ; killall Finder' alias fh='defaults write com.apple.finder AppleShowAllFiles -boolean false ; killall Finder'

How does SW-Precache works?

· 阅读需 10 分钟
Hux

SW-Precache is a great Service Worker tool from Google. It is a node module designed to be integrated into your build process and to generate a service worker for you. Though you can use sw-precache out of the box, you might still wonder what happens under the hood. There you go, this article is written for you!

This post was first published at Medium

Overview

The core files involving in sw-precache are mainly three:

service-worker.tmpl  
lib/
├ sw-precache.js
└ functions.js

sw-precache.js is the main entry of the module. It reads the configuration, processes parameters, populates the service-worker.tmpl template and writes the result into specified file. Andfunctions.js is just a module containing bunch of external functions which would be all injected into the generated service worker file as helpers.

Since the end effect of sw-precache is performed by the generated service worker file in the runtime, a easy way to get an idea of what happens is by checking out source code inside service-worker.tmpl . It’s not hard to understand the essentials and I will help you.

Initialization

The generated service worker file (let’s call it sw.js for instance) get configuration by text interpolation when sw-precache.js populating service-worker.tmpl .

// service-worker.tmpl  
var precacheConfig = <%= precacheConfig %>;

// sw.js
var precacheConfig = [
["js/a.js", "3cb4f0"],
["css/b.css", "c5a951"]
]

It’s not difficult to see that it’s a list of relative urls and MD5 hashes. In fact, one thing that sw-precache.js do in the build time is to calculate hash of each file that it asked to “precache” from staticFileGlobs parameter.

In sw.js, precacheConfig would be transformed into a ES6 Map with structure Map {absoluteUrl => cacheKey} as below. Noticed that I omit the origin part (e.g. http://localhost) for short.

> urlToCacheKeys  
< Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}

Instead of using raw URL as the cache key, sw-precache append a _sw-precache=[hash] to the end of each URL when populating, updating its cache and even fetching these subresouces. Those _sw-precache=[hash] are what we called cache-busting parameter*. It can prevent service worker from responding and caching out-of-date responses found in browsers’ HTTP cache indefinitely.

Because each build would re-calculate hashes and re-generate a new sw.js with new precacheConfig containing those new hashes, sw.js can now determine the version of each subresources thus decide what part of its cache needs a update. This is pretty similar with what we commonly do when realizing long-term caching with webpack or gulp-rev, to do a byte-diff ahead of runtime.

*: Developer can opt out this behaviour with dontCacheBustUrlsMatching option if they set HTTP caching headers right. More details on Jake’s Post.

On Install

ServiceWorker gives you an install event. You can use this to get stuff ready, stuff that must be ready before you handle other events.

During the install lifecycle, sw.js open the cache and get started to populate its cache. One cool thing that it does for you is its incremental update mechanism.

Sw-precache would search each cache key (the values of urlsToCacheKeys) in the cachedUrls, a ES6 Set containing URLs of all requests indexed from current version of cache, and only fetch and cache.put resources couldn’t be found in cache, i.e, never be cached before, thus reuse cached resources as much as possible.

If you can not fully understand it, don’t worry. We will recap it later, now let’s move on.

On Activate

Once a new ServiceWorker has installed & a previous version isn’t being used, the new one activates, and you get an activate event. Because the old version is out of the way, it's a good time to handle schema migrations in IndexedDB and also delete unused caches.

During activation phase, sw.js would compare all existing requests in the cache, named existingRequests (noticed that it now contains resources just cached on installation phase) with setOfExpectedUrls, a ES6 Set from the values of urlsToCacheKeys. And delete any requests not matching from cache.

// sw.js
existingRequests.map(function(existingRequest) {
if (!setOfExpectedUrls.has(existingRequest.url)) {
return cache.delete(existingRequest);
}
})

On Fetch

Although the comments in source code have elaborated everything well, I wanna highlight some points during the request intercepting duration.

Should Respond?

Firstly, we need to determine whether this request was included in our “pre-caching list”. If it was, this request should have been pre-fetched and pre-cached thus we can respond it directly from cache.

// sw.js*  
var url = event.request.url
shouldRespond = urlsToCacheKeys.has(url);

Noticed that we are matching raw URLs (e.g. http://localhost/js/a.js) instead of the hashed ones. It prevent us from calculating hashes at runtime, which would have a significant cost. And since we have kept the relationship in urlToCacheKeys it’s easy to index the hashed one out.

* In real cases, sw-precache would take ignoreUrlParametersMatching and directoryIndex options into consideration.

One interesting feature that sw-precache provided is navigationFallback(previously defaultRoute), which detect navigation request and respond a preset fallback HTML document when the URL of navigation request did not exist in urlsToCacheKeys.

It is presented for SPA using History API based routing, allowing responding arbitrary URLs with one single HTML entry defined in navigationFallback, kinda reimplementing a Nginx rewrite in service worker*. Do noticed that service worker only intercept document (navigation request) inside its scope (and any resources referenced in those documents of course). So navigation towards outside scope would not be effected.

* navigateFallbackWhitelist can be provided to limit the “rewrite” scope.

Respond from Cache

Finally, we get the appropriate cache key (the hashed URL) by raw URL with urlsToCacheKeys and invoke event.respondWith() to respond requests from cache directly. Done!

// sw.js*
event.respondWith(
caches.open(cacheName).then(cache => {
return cache.match(urlsToCacheKeys.get(url))
.then(response => {
if (response) return response;
});
})
);

* The code was “ES6-fied” with error handling part removed.

Cache Management Recap

That’s recap the cache management part with a full lifecycle simulation.

The first build

Supposed we are in the very first load, the cachedUrls would be a empty set thus all subresources listed to be pre-cached would be fetched and put into cache on SW install time.

// cachedUrls  
Set(0) {}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}

// SW Network Logs
[sw] GET a.js?_sw-precache=3cb4f0
[sw] GET b.css?_sw-precache=c5a951

After that, it will start to control the page immediately because the sw.js would call clients.claim() by default. It means the sw.js will start to intercept and try to serve future fetches from caches, so it’s good for performance.

In the second load, all subresouces have been cached and will be served directly from cache. So none requests are sent from sw.js.

// cachedUrls  
Set(2) {
"http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.css? _sw-precache=c5a951"
}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css? _sw-precache=c5a951"
}

// SW Network Logs
// Empty

The second build

Once we create a byte-diff of our subresouces (e.g., we modify a.js to a new version with hash value d6420f) and re-run the build process, a new version of sw.js would be also generated.

The new sw.js would run alongside with the existing one, and start its own installation phase.

// cachedUrls  
Set(2) {
"http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.css? _sw-precache=c5a951"
}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js? _sw-precache=d6420f",
"http.../css/b.js" => "http.../css/b.css? _sw-precache=c5a951"
}

// SW Network Logs
[sw] GET a.js?_sw-precache=d6420f

This time, sw.js see that there is a new version of a.js requested, so it fetch /js/a.js?_sw-precache=d6420f and put the response into cache. In fact, we have two versions of a.js in cache at the same time in this moment.

// what's in cache?
http.../js/a.js?_sw-precache=3cb4f0
http.../js/a.js?_sw-precache=d6420f
http.../css/b.css?_sw-precache=c5a951

By default, sw.js generated by sw-precache would call self.skipWaiting so it would take over the page and move onto activating phase immediately.

// existingRequests
http.../js/a.js?_sw-precache=3cb4f0
http.../js/a.js?_sw-precache=d6420f
http.../css/b.css?_sw-precache=c5a951

// setOfExpectedUrls
Set(2) {
"http.../js/a.js?_sw-precache=d6420f",
"http.../css/b.css?_sw-precache=c5a951"
}

// the one deleted
http.../js/a.js?_sw-precache=3cb4f0

By comparing existing requests in the cache with set of expected ones, the old version of a.js would be deleted from cache. This ensure there is only one version of our site’s resources each time.

That’s it! We finish the simulation successfully.

Conclusions

As its name implied, sw-precache is designed specifically for the needs of precaching some critical static resources. It only does one thing but does it well. I’d love to give you some opinionated suggestions but you decide whether your requirements suit it or not.

Precaching is NOT free

So don’t precached everything. Sw-precache use a “On Install — as a dependency” strategy for your precache configs. A huge list of requests would delay the time service worker finishing installing and, in addition, wastes users’ bandwidth and disk space.

For instance, if you wanna build a offline-capable blogs. You had better not include things like 'posts/*.html in staticFileGlobs. It would be a huge disaster to data-sensitive people if you have hundreds of posts. Use a Runtime Caching instead.

“App Shell”

A helpful analogy is to think of your App Shell as the code and resources that would be published to an app store for a native iOS or Android application.

Though I always consider that the term “App Shell” is too narrow to cover its actual usages now, It is widely used and commonly known. I personally prefer calling them “Web Installation Package” straightforward because they can be truly installed into users’ disks and our web app can boot up directly from them in any network environments. The only difference between “Web Installation Package” and iOS/Android App is that we need strive to limit it within a reasonable size.

Precaching is perfect for this kinda resources such as entry html, visual placeholders, offline pages etc., because they can be static in one version, small-sized, and most importantly, part of critical rendering path. We wanna put first meaningful paint ASAP to our user thus we precache them to eliminate HTTP roundtrip time.

BTW, if you are using HTML5 Application Cache before, sw-precache is really a perfect replacement because it can cover nearly all use cases the App Cache provide.

This is not the end

Sw-precache is just one of awesome tools that can help you build service worker. If you are planing to add some service worker power into your website, Don’t hesitate to checkout sw-toolbox, sw-helper (a new tool Google is working on) and many more from communities.

That’s all. Wish you enjoy!

在 Swift 中使用 IBInspectable

· 阅读需 3 分钟
BY

本文首次发布于 BY Blog, 作者 @柏荧(BY) ,转载请保留原文链接.

通过 IB 设置 控件 的属性非常的方便。

但是缺点也很明显,那就是有一些属性没有暴露在 IB 的设置面板中。这时候就要使用 @IBInspectable 在 IB 面板中添加这些没有的属性。

关于在 OC 中使用 IBInspectable 可以看一下我的 这篇文章

正文

在项目中最常遇到的情况是为 view 设置圆角、描边,以及为 文本控件 添加本地化字符串。

圆角、描边

先来看看设置圆角、描边

extension UIView {
@IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}

set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}

@IBInspectable var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue > 0 ? newValue : 0
}
}

@IBInspectable var borderColor: UIColor {
get {
return UIColor(cgColor: layer.borderColor!)
}
set {
layer.borderColor = newValue.cgColor
}
}

}

添加完成就可以在 IB 中设置 view 的这些属性了

运行效果

利用 @IBDesignable 在 IB 中实时显示 @IBInspectable 的样式

创建一个新的 class 继承 UIView ,并且使用 @IBDesignable 声明

import UIKit

@IBDesignable class IBDesignableView: UIView {

}

在 IB 中,选择 view 的 class 为 我们新建的 IBDesignableView

这样在 IB 调整属性时,这些属性的变化就会实时显示在 IB 中。

本地化字符串

本地化字符串的解决方法和上面的添加圆角一样

extension UILabel {
@IBInspectable var localizedKey: String? {
set {
guard let newValue = newValue else { return }
text = NSLocalizedString(newValue, comment: "")
}
get { return text }
}
}

extension UIButton {
@IBInspectable var localizedKey: String? {
set {
guard let newValue = newValue else { return }
setTitle(NSLocalizedString(newValue, comment: ""), for: .normal)
}
get { return titleLabel?.text }
}
}

extension UITextField {
@IBInspectable var localizedKey: String? {
set {
guard let newValue = newValue else { return }
placeholder = NSLocalizedString(newValue, comment: "")
}
get { return placeholder }
}
}

这样,在 IB 中我们就可以利用对应类型的 Localized Key 来直接设置本地化字符串了:

结语

IBInspectable 可以使用这些的类型

  • Int
  • CGFloat
  • Double
  • String
  • Bool
  • CGPoint
  • CGSize
  • CGRect
  • UIColor
  • UIImage

合理的使用@IBInspectable 能减少很多的模板代码,提高我们的开发效率。

参考

R.swift 的使用

· 阅读需 2 分钟
BY

本文首次发布于 BY Blog, 作者 @柏荧(BY) ,转载请保留原文链接.

介绍 R.swift 前,我们先看看 R.swift 能做什么

通常,我们是基于 字符串 来获取资源,例如:图片、xib、或者是 segue

let myImage = UIImage(named: "myImage")
let myViewController = R.storyboard.main.myViewController()

使用 R.swfit,我们可以这样写

let myImage = R.image.myImage()
let viewController = R.storyboard.main.myViewController()

R.swift 通过扫描你的各种基于字符串命名的资源,创建一个使用类型来获取资源。

在保证类型安全的同时,也在自动补全的帮助下节省了大量的时间。

导入 R.swift

R.swift 开源在 github 上。

这里是导入的视频教程

使用 CocoaPods 导入项目中

  1. 添加 pod 'R.swift'到 Podfile 文件,然后运行 pod install

  2. 添加一个 New Run Script Phase

  3. Run Script 拖动到 Check Pods Manifest.lock 的下面,并且添加脚本 "$PODS_ROOT/R.swift/rswift" "$SRCROOT/项目名称"

  4. Command+B 编译项目,在项目代码目录下,会生成一个 R.generated.swift 的文件,将它拖如项目中

    注意:不要勾选 Copy items if needed 选项,因为每次编译都会生成新的 R.generated.swift 文件,copy 的话,旧的 R.generated.swift 将不会被覆盖。

tip: 可以在添加 .gitignore 添加一行 *.generated.swift 忽略该文件,避免造成冲突

用法

导入完成后,就可以在使用 R.swift 了

关于 R.swift 的更多用法,可以 看这里

Swift 的懒加载和计算型属性

· 阅读需 2 分钟
BY

本文首次发布于 BY Blog, 作者 @柏荧(BY) ,转载请保留原文链接.

懒加载

常规(简化)写法

懒加载的属性用 var 声明

lazy var name: String = {
return "BY"
}()

完整写法

lazy var name: String = { () -> String i
return "BY"
}()

本质是一个创建一个闭包 {} 并且在调用该属性时执行闭包 ()

如OC的懒加载不同的是 swift 懒加载闭包只调用一次,再次调用该属性时因为属性已经创建,不再执行闭包。

计算型属性

常规写法

var name: string {
return "BY"
}

完整写法

var name: string {
get {
return "BY"
}
}

计算型属性本质是重写了 get 方法,其类似一个无参有返回值函数,每次调用该属性都会执行 return

通常这样使用

struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// Prints "the volume of fourByFiveByTwo is 40.0"

两者对比

相同点

  • 使用方法完全一致
  • 都是用 var 声明

不同点

  • 实现原理不同

    懒加载是第一次调用属性时执行闭包进行赋值

    计算型属性是重写 get 方法

  • 调用 {}的次数不同

    懒加载的闭包只在属性第一次调用时执行 计算型属性每次调用都要进入 {} 中,return 新的值

RVM 使用指南

· 阅读需 1 分钟
BY

RVM 常用的命令整理

RVM 是一个命令行工具,可以提供一个便捷的多版本 Ruby 环境的管理和切换。<https://rvm.io/>

我相信做为iOS开发者,对ruby的使用都是从安装 CocoaPods 开始的吧~

Note:这里所有的命令都是再用户权限下操作的,任何命令最好都不要用 sudo.

RVM 安装

	$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
$ \curl -sSL https://get.rvm.io | bash -s stable
$ source ~/.bashrc
$ source ~/.bash_profile

修改 RVM 的 Ruby 安装源到 Ruby China 的 Ruby 镜像服务器,这样能提高安装速度

	$ echo "ruby_url=https://cache.ruby-china.org/pub/ruby" > ~/.rvm/user/db

Ruby版本的安装与切换

列出已知的 Ruby 版本

	rvm list known

安装一个 Ruby 版本

	rvm install 2.2.0 --disable-binary

切换 Ruby 版本

	rvm use 2.2.0

如果想设置为默认版本,这样一来以后新打开的控制台默认的 Ruby 就是这个版本

	rvm use 2.2.0 --default 

查询已经安装的ruby

	rvm list

卸载一个已安装版本

	rvm remove 1.8.7

参考:<https://ruby-china.org/wiki/rvm-guide>

iOS自动打包

· 阅读需 3 分钟
BY

利用xcode的命令行工具 xcdeobulid 进行项目的编译打包,生成ipa包,并上传到fir

现在网上的自动打包教程几乎都还是xcodebuild + xcrun的方式先生成.app包 再生成.ipa包,结果弄了一整天硬是没成功~

后来发现PackageApplication is deprecated,悲剧。然后手动压缩的 .ipa包因为签名问题无法装到手机上。

后来用了archive + -exportArchive终于可以了~

正文

Xcodebuild

xcodebuild 的使用可以用 man xcodebuild查看。

查看项目详情

	# cd 项目主目录
xcodebuild -list

输出项目的信息

	Information about project "StackGameSceneKit":
Targets:
StackGameSceneKit
StackGameSceneKitTests

Build Configurations:
Debug
Release

If no build configuration is specified and -scheme is not passed then "Release" is used.

Schemes:
StackGameSceneKit

要留意 ConfigurationsSchemes这两个属性。

自动打包流程

生成 archive

生成archive的命令是 xcodebuild archive

	xcodebuild archive -workspace ${project_name}.xcworkspace \
-scheme ${scheme_name} \
-configuration ${build_configuration} \
-archivePath ${export_archive_path}
  • 参数一:项目类型,,如果是混合项目 workspace 就用 -workspace,如果是 project 就用 -project

  • -scheme:项目名,上面xcodebuild -list中的 Schemes

  • -configuration :编译类型,在configuration选择, Debug 或者 Release

  • -archivePath:生成 archive 包的路径,需要精确到 xx/xx.archive

首先需要创建一个AdHocExportOptions.plist文件

导出ipa包

导出.ipa包经常会出现错误,在ruby2.4.0版本中会报错,所以请使用其他版本的ruby,最初的原因是使用了 ruby2.4.0 进行编译时出现的错误。

解决方法是低版本的 ruby 进行编译,如使用系统版本:rvm use system。后面升级macOS系统(10.12.5)后发现 ruby2.4.0 能成功 导出ipa包了。

导出ipa包使用命令:xcodebuild -exportArchive

	xcodebuild  -exportArchive \
-archivePath ${export_archive_path} \
-exportPath ${export_ipa_path} \
-exportOptionsPlist ${ExportOptionsPlistPath}
  • archivePath:上面生成 archive 的路径
  • -exportPath:导出 ipa包 的路径
  • exportOptionsPlist:导出 ipa包 类型,需要指定格式的plist文件,分别是AppStroeAdHocEnterprise,如下图

选择这三个类别需要分别创建三个plist文件:

  • AdHocExportOptionsPlist.plist

  • AppStoreExportOptionsPlist.plist

  • EnterpriseExportOptionsPlist.plist

上传到 Fir

将项目上传到 Fir

下载 fir 命令行工具

	gem install fir-cli

获取 fir 的 API Token(右上角)

上传

	fir publish "ipa_Path" -T "firApiToken"

自动打包脚本

再次提醒,请不要使用 ruby 2.4.0 运行该脚本!,若在 ruby 2.4.0 下编译失败,请切换低版本的ruby。

切换完毕记得重新安装 fir 命令行工具。

脚本我fork了 jkpang 的脚本进行修改,添加了自动上传到 fir 的功能。

使用方法在Github上有详细介绍。

GitHub:<https://github.com/qiubaiying/iOSAutoArchiveScript>

利用 自定义终端指令 简化打包过程

以zsh为例:

	open ~/.zshrc

添加自定义命令 cd + sh

	alias mybuild='cd 项目地址/iOSAutoArchiveScript/ &&  sh 项目地址/iOSAutoArchiveScript/iOSAutoArchiveScript.sh'

这样打开终端输入mybuild,就可以轻松实现一键打包上传了

本文首次发布于 BY Blog, 作者 @柏荧(BY) ,转载请保留原文链接.