WearEngineHostApiImpl.swift 4.89 KB
import Foundation
import PhotosUI
import UIKit
import Vision

enum ImageProcessError: Error{
    case invalidatePath
}

final class WearEngineHostApiImpl: WearEngineHostApi {
    func hasInstalledWatchSurface(completion: @escaping (Result<Bool, any Error>) -> Void) {
        // iOS does not expose the user's active watch face. The closest
        // supported equivalent is a paired Apple Watch with this Watch App installed.
        completion(.success(watchService.hasAvailableDevices()))
    }

  private let watchService: WatchConnectivityService

  init(watchService: WatchConnectivityService = .shared) {
    self.watchService = watchService
  }

  func hasAvailableDevices() throws -> Bool {
    watchService.hasAvailableDevices()
  }

  func checkConnectedDevice() throws -> WearDeviceInfo? {
    watchService.currentDeviceInfo()
  }

  func registerMessageReceiver() throws -> Bool {
    watchService.activate()
  }

  func sendTextMessage(message: String) throws -> Bool {
    watchService.sendTextMessage(message)
  }

  func sendWatchSyncPayload(jsonPayload: String) throws -> Bool {
      WatchConnectivityService.shared.sendWatchThemeChangedMessage(json: jsonPayload)
  }

    func removeBackground(originImagePath: String, completion: @escaping (Result<String?, any Error>) -> Void) {
        // 抠图
        guard let image = UIImage(contentsOfFile: originImagePath) else{
            completion(.failure(ImageProcessError.invalidatePath))
            return
        }
        Task{
            let processed = await WatchThemeImageProcessor.removeBackground(from: image)
            let path = WatchThemeImageProcessor.savePNG(processed)
            completion(.success(path))
        }
    }
}


private enum WatchThemeImageProcessor {
  static func removeBackground(from image: UIImage) async -> UIImage {
    guard #available(iOS 17.0, *),
          let cgImage = image.normalizedCGImage else {
      return image
    }

    return await Task.detached(priority: .userInitiated) {
      let request = VNGenerateForegroundInstanceMaskRequest()
      let handler = VNImageRequestHandler(cgImage: cgImage)

      do {
        try handler.perform([request])
        guard let observation = request.results?.first else {
          return image
        }

        let mask = try observation.generateScaledMaskForImage(
          forInstances: observation.allInstances,
          from: handler
        )
        return composite(image: cgImage, mask: mask) ?? image
      } catch {
        return image
      }
    }.value
  }

  static func savePNG(_ image: UIImage) -> String? {
    guard let data = image.pngData() else { return nil }
    let directory = FileManager.default.temporaryDirectory
      .appendingPathComponent("watch_theme", isDirectory: true)

    do {
      try FileManager.default.createDirectory(
        at: directory,
        withIntermediateDirectories: true
      )
      let file = directory.appendingPathComponent("\(UUID().uuidString).png")
      try data.write(to: file, options: .atomic)
      return file.path
    } catch {
      return nil
    }
  }

  private static func composite(image: CGImage, mask: CVPixelBuffer) -> UIImage? {
    let ciImage = CIImage(cgImage: image)
    let ciMask = CIImage(cvPixelBuffer: mask)
    guard let filter = CIFilter(name: "CIBlendWithMask") else {
      return nil
    }
    filter.setValue(ciImage, forKey: kCIInputImageKey)
    filter.setValue(ciMask, forKey: kCIInputMaskImageKey)
    filter.setValue(
      CIImage(color: .clear).cropped(to: ciImage.extent),
      forKey: kCIInputBackgroundImageKey
    )

    guard let output = filter.outputImage,
          let cgOutput = CIContext().createCGImage(output, from: ciImage.extent) else {
      return nil
    }
    return UIImage(cgImage: cgOutput, scale: 1, orientation: .up)
  }
}

private extension UIImage {
  var normalizedCGImage: CGImage? {
    if imageOrientation == .up, let cgImage {
      return cgImage
    }

    let format = UIGraphicsImageRendererFormat.default()
    format.scale = scale
    let renderer = UIGraphicsImageRenderer(size: size, format: format)
    return renderer.image { _ in
      draw(in: CGRect(origin: .zero, size: size))
    }.cgImage
  }
}

private extension UIApplication {
  var topMostViewController: UIViewController? {
    connectedScenes
      .compactMap { $0 as? UIWindowScene }
      .flatMap(\.windows)
      .first { $0.isKeyWindow }?
      .rootViewController?
      .topMostPresented
  }
}

private extension UIViewController {
  var topMostPresented: UIViewController {
    if let presentedViewController {
      return presentedViewController.topMostPresented
    }
    if let navigationController = self as? UINavigationController {
      return navigationController.visibleViewController?.topMostPresented ?? navigationController
    }
    if let tabBarController = self as? UITabBarController {
      return tabBarController.selectedViewController?.topMostPresented ?? tabBarController
    }
    return self
  }
}