iOS新手用swift写一个macos打包工具 一键打包到指定位置
使用dmg安装macos app
打包出的app运行如下图,使用磁盘压缩成dmg,直接打开package.dmg即可
配置完毕后点击start运行打包脚本,生成ipa到指定目录
该项目用swift开发,项目和dmg保存在
https://github.com/gwh111/tes...
流程解析
概述整个流程就是,通过recoverAndSet()函数恢复之前保存数据,start()检查路径后会替换内部package.sh的动态路径,然后起一个线程创建Process(),通过Pipe()监控脚本执行输出,捕获异常
1.recoverAndSet()
通过UserDefaults简单地记住上次打包的路径,下次写了新代码后即可点击start立即打包
恢复时把值传给控件
func recoverAndSet() { let objs:[Any]=[projectPath,projectName,exportOptionsPath,ipaPath] let names:[NSString]=["projectPath","projectName","exportOptionsPath","ipaPath"] for i in 0...3{ print(i) let key=names[i] let obj=objs[i] as! NSTextField let v=UserDefaults.standard.value(forKey: key as String) if (v == nil){ continue } obj.stringValue=(v as? String)! } let ps=UserDefaults.standard.value(forKey: "projectName" as String) if (ps==nil){ }else{ projectName.stringValue=(ps as? String)!; } let dr=UserDefaults.standard.value(forKey: "debugRelease") if (dr==nil){ }else{ debugRelease.selectedSegment=dr as! Int; } debugRelease.action = #selector(segmentControlChanged(segmentControl:)) }
2.selectPath()
通过NSOpenPanel()创建打开文档面板对象,选择文件目录,而不是手动输入
通常项目路径名和项目名称是一致的,这里使用了path.components(separatedBy:"/")将路径分割自动取工程名
@IBAction func selectPath(_ sender: NSButton) { let tag=sender.tag print(tag) // 1. 创建打开文档面板对象 let openPanel = NSOpenPanel() // 2. 设置确认按钮文字 openPanel.prompt = "Select" // 3. 设置禁止选择文件 openPanel.canChooseFiles = true if tag==0||tag==2 { openPanel.canChooseFiles = false } // 4. 设置可以选择目录 openPanel.canChooseDirectories = true if tag==1 { openPanel.canChooseDirectories = false openPanel.allowedFileTypes=["plist"] } // 5. 弹出面板框 openPanel.beginSheetModal(for: self.view.window!) { (result) in // 6. 选择确认按钮 if result == NSApplication.ModalResponse.OK { // 7. 获取选择的路径 let path=openPanel.urls[0].absoluteString.removingPercentEncoding! if tag==0 { self.projectPath.stringValue=path let array=path.components(separatedBy:"/") if array.count>1{ let name=array[array.count-2] print(array) print(name as Any) self.projectName.stringValue=name } }else if tag==1 { self.exportOptionsPath.stringValue=path }else{ self.ipaPath.stringValue=path } let names:[NSString]=["projectPath","exportOptionsPath","ipaPath"] UserDefaults.standard.setValue(openPanel.url?.path, forKey: names[tag] as String) UserDefaults.standard.setValue(self.projectName.stringValue, forKey: "projectName") UserDefaults.standard.synchronize() // self.savePath.stringValue = (openPanel.directoryURL?.path)! // // 8. 保存用户选择路径(为了可以在其他地方有权限访问这个路径,需要对用户选择的路径进行保存) // UserDefaults.standard.setValue(openPanel.url?.path, forKey: kSelectedFilePath) // UserDefaults.standard.synchronize() } // 9. 恢复按钮状态 // sender.state = NSOffState } }
3.start()
通过str.replacingOccurrences(of: "file://", with: "")将路径和sh里的路径替换
通过DispatchQueue.global(qos: .default).async获取Concurrent Dispatch Queue并开启Process()
在处理完的terminationHandler里回到主线程更新UI
@IBAction func start(_ sender: Any) { guard projectPath.stringValue != "" else { self.logTextField.stringValue="工程目录不能为空"; return } guard projectName.stringValue != "" else { self.logTextField.stringValue="工程名不能为空"; return } guard exportOptionsPath.stringValue != "" else { self.logTextField.stringValue="exportOptions不能为空 xcode生成ipa文件夹中包含"; return } guard ipaPath.stringValue != "" else { self.logTextField.stringValue="输出ipa目录不能为空"; return } var str1="abc" let str2="abc" if str1==str2{ print("same") } //save let objs:[Any]=[projectPath,exportOptionsPath,ipaPath] let names:[NSString]=["projectPath","exportOptionsPath","ipaPath"] for i in 0...2{ let obj=objs[i] as! NSTextField UserDefaults.standard.setValue(obj.stringValue, forKey: names[i] as String) } UserDefaults.standard.setValue(self.projectName.stringValue, forKey: "projectName") UserDefaults.standard.setValue(self.debugRelease.selectedSegment, forKey: "debugRelease") UserDefaults.standard.synchronize() // self.showInfoTextView.string="abc"; if isLoadingRepo { self.logTextField.stringValue="正在执行上一个任务"; return }// 如果正在执行,则返回 isLoadingRepo = true // 设置正在执行标记 let projectStr=self.projectPath.stringValue let nameStr=self.projectName.stringValue let plistStr=self.exportOptionsPath.stringValue let ipaStr=self.ipaPath.stringValue let returnData = Bundle.main.path(forResource: "package", ofType: "sh") let data = NSData.init(contentsOfFile: returnData!) var str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String if debugRelease.selectedSegment==0 { str = str.replacingOccurrences(of: "DEBUG_RELEASE", with: "debug") }else{ str = str.replacingOccurrences(of: "DEBUG_RELEASE", with: "release") } str = str.replacingOccurrences(of: "NAME_PROJECT", with: nameStr) str = str.replacingOccurrences(of: "PATH_PROJECT", with: projectStr) str = str.replacingOccurrences(of: "PATH_PLIST", with: plistStr) str = str.replacingOccurrences(of: "PATH_IPA", with: ipaStr) str = str.replacingOccurrences(of: "file://", with: "") print("返回的数据:\(str)"); self.logTextField.stringValue="执行中。。。"; DispatchQueue.global(qos: .default).async { // str="aaaabc" // str = str.replacingOccurrences(of: "ab", with: "dd") // print(self.projectPath.stringValue) // print(self.exportOptionsPath.stringValue) // print(self.ipaPath.stringValue) let task = Process() // 创建NSTask对象 // 设置task task.launchPath = "/bin/bash" // 执行路径(这里是需要执行命令的绝对路径) // 设置执行的具体命令 task.arguments = ["-c",str] task.terminationHandler = { proce in // 执行结束的闭包(回调) self.isLoadingRepo = false // 恢复执行标记 //5. 在主线程处理UI DispatchQueue.main.async(execute: { self.logTextField.stringValue="执行完毕"; }) } self.captureStandardOutputAndRouteToTextView(task) task.launch() // 开启执行 task.waitUntilExit() // 阻塞直到执行完毕 } }
4.captureStandardOutputAndRouteToTextView()
对执行脚本的日志监控
为了看到脚本报错或执行成功提示,使用Pipe()监控 NSPipe一般是两个线程之间进行通信使用的
在osx 系统中 ,沙盒有个规则:在App运行期间通过NSOpenPanel用户手动打开的任意位置的文件,把这个这个路径保存下来,后面都是可以直接用这个路径继续访问文件,但当App退出后再次运行,这个路径默认是不可以访问的
fileprivate func captureStandardOutputAndRouteToTextView(_ task:Process) { //1. 设置标准输出管道 outputPipe = Pipe() task.standardOutput = outputPipe //2. 在后台线程等待数据和通知 outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() //3. 接受到通知消息 observe=NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in //4. 获取管道数据 转为字符串 let output = self.outputPipe.fileHandleForReading.availableData let outputString = String(data: output, encoding: String.Encoding.utf8) ?? "" if outputString != ""{ //5. 在主线程处理UI DispatchQueue.main.async { if self.isLoadingRepo == false { let previousOutput = self.showInfoTextView.string let nextOutput = previousOutput + "\n" + outputString self.showInfoTextView.string = nextOutput // 滚动到可视位置 let range = NSRange(location:nextOutput.utf8CString.count,length:0) self.showInfoTextView.scrollRangeToVisible(range) if self.observe==nil { return } NotificationCenter.default.removeObserver(self.observe!) return }else{ let previousOutput = self.showInfoTextView.string var nextOutput = previousOutput + "\n" + outputString as String if nextOutput.count>5000 { nextOutput=String(nextOutput.suffix(1000)); } // 滚动到可视位置 let range = NSRange(location:nextOutput.utf8CString.count,length:0) self.showInfoTextView.scrollRangeToVisible(range) self.showInfoTextView.string = nextOutput } } } if self.isLoadingRepo == false { return } //6. 继续等待新数据和通知 self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() } }
-exportOptions.Plist 常用文件内容格式
compileBitcode
- For non-App Store exports, should Xcode re-compile the app from bitcode? Defaults to YES
embedOnDemandResourcesAssetPacksInBundle
- For non-App Store exports, if the app uses On Demand Resources and this is YES, asset packs are embedded in the app bundle so that the app can be tested without a server to host asset packs. Defaults to YES unless onDemandResourcesAssetPacksBaseURL is specified
method
- Describes how Xcode should export the archive. Available options: app-store, ad-hoc, package, enterprise, development, and developer-id. The list of options varies based on the type of archive. Defaults to development
teamID
- The Developer Portal team to use for this export. Defaults to the team used to build the archive
thinning
- For non-App Store exports, should Xcode thin the package for one or more device variants? Available options: <none> (Xcode produces a non-thinned universal app), <thin-for-all-variants> (Xcode produces a universal app and all available thinned variants), or a model identifier for a specific device (e.g. "iPhone7,1"). Defaults to <none>
uploadBitcode
- For App Store exports, should the package include bitcode? Defaults to YES
uploadSymbols
- For App Store exports, should the package include symbols? Defaults to YES