简体   繁体   中英

Spawning a process in an app built with UIKit for macOS (Catalyst)

I'm building an application that shares most of the code between macOS and iOS versions (targeting macOS 11 and iOS 14). UIKit for Mac seems like a natural choice to help with this. Unfortunately, one of the libraries uses the Process type under the hood. Building it produces "Cannot find type Process in scope" error when a dependency on it is added and when targeting macOS. I'm fine with excluding this library for iOS, but I still need to link with it on macOS while keeping the ability to use UIKit on all platforms.

在此处输入图像描述

I've selected this library to be linked only for macOS in Xcode, but this has no effect and the same build error persists. Also, I'm getting this error without adding a single import SwiftLSPClient statement in the app, so I don't think conditional imports would help in this case.

Xcode 中的框架、库和嵌入式内容设置

What would be the best way to resolve this issue within the constraints listed above?

I created a LSPCatalyst class in my Mac Catalyst app to replace the MacOS LanguageServerProcessHost. To make that work, I replaced the process property with a processProxy that accesses the process instance in a MacOS bundle using the FoundationApp protocol as explained below.

Following @Adam's suggestion, I created a MacOS bundle to proxy for the process instance. You follow the same idea as he pointed to for AppKit access from Catalyst apps, but you just need Foundation to get access to Process. I called the bundle FoundationGlue and put everything in a FoundationGlue folder in my Xcode project. The bundle needs an Info.plist that identifies the principal class as "FoundationGlue.MacApp", and the MacApp.swift looks like:

    import Foundation

    class MacApp: NSObject, FoundationApp {
    var process: Process!
    var terminationObserver: NSObjectProtocol!
    
    func initProcess(_ launchPath: String!, _ arguments: [String]?, _ environment: [String : String]?) {
        process = Process()
        process.launchPath = launchPath
        process.arguments = arguments
        process.environment = environment
    }
    
    func setTerminationCompletion(_ completion: (()->Void)!) {
        let terminationCompletion = {
            NotificationCenter.default.removeObserver(self.terminationObserver!)
            completion?()
        }
        terminationObserver =
            NotificationCenter.default.addObserver(
                forName: Process.didTerminateNotification,
                object: process,
                queue: nil) { notification -> Void in
                terminationCompletion()
            }
    }
    
    func setupProcessPipes(_ stdin: Pipe!, _ stdout: Pipe!, _ stderr: Pipe!) {
        process.standardInput = stdin
        process.standardOutput = stdout
        process.standardError = stderr
    }
    
    func launchProcess() {
        process.launch()
        print("Launched process \(process.processIdentifier)")
    }

    func terminateProcess() {
        process.terminate()
    }
    
    func isRunningProcess() -> Bool {
        return process.isRunning
    }

    
}

The corresponding header I called FoundationApp.h looks like:

#import <Foundation/Foundation.h>

@protocol FoundationApp <NSObject>

typedef void (^terminationCompletion) ();
- (void)initProcess: (NSString *) launchPath :(NSArray<NSString *> *) arguments :(NSDictionary<NSString *, NSString *> *) environment;
- (void)setTerminationCompletion: (terminationCompletion) completion;
- (void)setupProcessPipes: (NSPipe *) stdin :(NSPipe *) stdout :(NSPipe *) stderr;
- (void)launchProcess;
- (void)terminateProcess;
- (bool)isRunningProcess;

@end

And the FoundationAppGlue-Bridging-Header.h just contains:

#import "FoundationApp.h"

Once you have the bundle built for MacOS, add it as a framework to your Mac Catalyst project. I created a Catalyst.swift in that project for access to the FoundationGlue bundle functionality::

import Foundation

@available(macCatalyst 13, *)
struct Catalyst {

    /// Catalyst.foundation gives access to the Foundation functionality identified in FoundationApp.h and implemented in FoundationGlue/MacApp.swift
    static var foundation: FoundationApp! {
        let url = Bundle.main.builtInPlugInsURL?.appendingPathComponent("FoundationGlue.bundle")
        let bundle = Bundle(path: url!.path)!
        bundle.load()
        let cls = bundle.principalClass as! NSObject.Type
        return cls.init() as? FoundationApp
    }
    
}

Then, you use it from your app like:

let foundationApp = Catalyst.foundation!
foundationApp.initProcess("/bin/sh", ["-c", "echo 1\nsleep 1\necho 2\nsleep 1\necho 3\nsleep 1\necho 4\nsleep 1\nexit\n"], nil)
foundationApp.setTerminationCompletion({print("terminated")})
foundationApp.launchProcess()

This is a messy solution but I know it works: Add a “Mac bundle” to your Catalyst app and import the MacOS-only framework with that.

Here's a guide to creating and loading a Mac bundle: https://medium.com/better-programming/how-to-access-the-appkit-api-from-mac-catalyst-apps-2184527020b5

Once you have the bundle, you can add Mac-only libraries and frameworks to it. You'll have to bridge data and method calls between the bundle and your iOS app, but it's manageable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM