iOS新手用swift寫一個macos打包工具 一鍵打包到指定位置

使用dmg安裝macos app

打包出的app運行以下圖,使用磁盤壓縮成dmg,直接打開package.dmg便可git

圖片描述

配置完畢後點擊start運行打包腳本,生成ipa到指定目錄
該項目用swift開發,項目和dmg保存在
https://github.com/gwh111/tes...github


流程解析

概述整個流程就是,經過recoverAndSet()函數恢復以前保存數據,start()檢查路徑後會替換內部package.sh的動態路徑,而後起一個線程建立Process(),經過Pipe()監控腳本執行輸出,捕獲異常macos

1.recoverAndSet()

經過UserDefaults簡單地記住上次打包的路徑,下次寫了新代碼後便可點擊start當即打包
恢復時把值傳給控件swift

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:"/")將路徑分割自動取工程名xcode

@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裏回到主線程更新UIbash

@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 經常使用文件內容格式

compileBitcodeapp

  • For non-App Store exports, should Xcode re-compile the app from bitcode? Defaults to YES

embedOnDemandResourcesAssetPacksInBundleless

  • 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

methodasync

  • 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
相關文章
相關標籤/搜索