Update KVO usage for Swift 5

Better compiler checks of KVO, and reduced code.

PiperOrigin-RevId: 316974406
diff --git a/src/Tulsi/ConfigEditorSourceFilterViewController.swift b/src/Tulsi/ConfigEditorSourceFilterViewController.swift
index ed5aab4..5447781 100644
--- a/src/Tulsi/ConfigEditorSourceFilterViewController.swift
+++ b/src/Tulsi/ConfigEditorSourceFilterViewController.swift
@@ -44,20 +44,20 @@
 
       guard let entry = entry as? UISourcePath else { return }
       let enabled = newValue == NSControl.StateValue.on.rawValue
-      willChangeValue(forKey: "explicitlyRecursive")
+      willChangeValue(for: \.explicitlyRecursive)
       entry.recursive = enabled
-      didChangeValue(forKey: "explicitlyRecursive")
+      didChangeValue(for: \.explicitlyRecursive)
 
       // If this node is newly recursive, force hasRecursiveEnabledParent, otherwise have children
       // inherit this node's status.
       setChildrenHaveRecursiveParent(enabled || hasRecursiveEnabledParent)
 
       // Notify KVO that this node's ancestors have also changed state.
-      var ancestor = parent
-      while ancestor != nil {
-        ancestor!.willChangeValue(forKey: "recursive")
-        ancestor!.didChangeValue(forKey: "recursive")
-        ancestor = ancestor!.parent
+      var child: SourcePathNode? = self
+      while let parent = child?.parent as? SourcePathNode {
+        parent.willChangeValue(for: \.recursive)
+        parent.didChangeValue(for: \.recursive)
+        child = parent
       }
     }
   }
diff --git a/src/Tulsi/HeadlessTulsiProjectCreator.swift b/src/Tulsi/HeadlessTulsiProjectCreator.swift
index 1d802ad..77c7f70 100644
--- a/src/Tulsi/HeadlessTulsiProjectCreator.swift
+++ b/src/Tulsi/HeadlessTulsiProjectCreator.swift
@@ -19,36 +19,8 @@
 /// Provides functionality to generate a Tulsiproj bundle.
 struct HeadlessTulsiProjectCreator {
 
-  /// Provides functionality to signal a semaphore when the "processing" key on some object is set
-  /// to false.
-  private class ProcessingCompletedObserver: NSObject {
-    let semaphore: DispatchSemaphore
-
-    init(semaphore: DispatchSemaphore) {
-      self.semaphore = semaphore
-    }
-
-    override func observeValue(forKeyPath keyPath: String?,
-                                of object: Any?,
-                                change: [NSKeyValueChangeKey : Any]?,
-                                context: UnsafeMutableRawPointer?) {
-      if context != &HeadlessTulsiProjectCreator.KVOContext {
-        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
-        return
-      }
-
-      if keyPath == "processing", let newValue = change?[NSKeyValueChangeKey.newKey] as? Bool {
-        if (!newValue) {
-          semaphore.signal()
-        }
-      }
-    }
-  }
-
   let arguments: TulsiCommandlineParser.Arguments
 
-  private static var KVOContext: Int = 0
-
   init(arguments: TulsiCommandlineParser.Arguments) {
     self.arguments = arguments
   }
@@ -149,18 +121,16 @@
     // Updating the project's bazelPackages will cause it to go into processing, observe the
     // processing key and block further execution until it is completed.
     let semaphore = DispatchSemaphore(value: 0)
-    let observer = ProcessingCompletedObserver(semaphore: semaphore)
-    document.addObserver(observer,
-                         forKeyPath: "processing",
-                         options: .new,
-                         context: &HeadlessTulsiProjectCreator.KVOContext)
-
+    let observer = document.observe(\.processing, options: .new) { _, change in
+      guard change.newValue == false else { return }
+      semaphore.signal()
+    }
+    defer { observer.invalidate() }
     document.bazelPackages = Array(bazelPackages)
 
     // Wait until processing completes.
     _ = semaphore.wait(timeout: DispatchTime.distantFuture)
 
-    document.removeObserver(observer, forKeyPath: "processing")
     return bazelPackages
   }
 
diff --git a/src/Tulsi/TulsiGeneratorConfigDocument.swift b/src/Tulsi/TulsiGeneratorConfigDocument.swift
index 490cd5a..7f6e685 100644
--- a/src/Tulsi/TulsiGeneratorConfigDocument.swift
+++ b/src/Tulsi/TulsiGeneratorConfigDocument.swift
@@ -88,16 +88,24 @@
     }
   }
 
+  private var uiRuleInfoObservers: [NSKeyValueObservation] = []
+
   /// The UIRuleEntry instances that are acted on by the associated UI.
   @objc dynamic var uiRuleInfos = [UIRuleInfo]() {
     willSet {
       stopObservingRuleEntries()
 
-      for entry in newValue {
-        entry.addObserver(self,
-                          forKeyPath: "selected",
-                          options: .new,
-                          context: &TulsiGeneratorConfigDocument.KVOContext)
+      uiRuleInfoObservers = newValue.map { entry in
+        entry.observe(
+          \.selected, options: .new
+        ) { [unowned self] _, change in
+          guard let value = change.newValue else { return }
+          if value {
+            self.selectedRuleInfoCount += 1
+          } else {
+            self.selectedRuleInfoCount -= 1
+          }
+        }
       }
     }
   }
@@ -159,8 +167,6 @@
   // Closure to be invoked when a save operation completes.
   private var saveCompletionHandler: ((_ canceled: Bool, _ error: Error?) -> Void)? = nil
 
-  private static var KVOContext: Int = 0
-
   static func isGeneratorConfigFilename(_ filename: String) -> Bool {
     return (filename as NSString).pathExtension == TulsiGeneratorConfig.FileExtension
   }
@@ -424,23 +430,6 @@
     return false
   }
 
-  override func observeValue(forKeyPath keyPath: String?,
-                              of object: Any?,
-                              change: [NSKeyValueChangeKey : Any]?,
-                              context: UnsafeMutableRawPointer?) {
-    if context != &TulsiGeneratorConfigDocument.KVOContext {
-      super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
-      return
-    }
-    if keyPath == "selected", let newValue = change?[NSKeyValueChangeKey.newKey] as? Bool {
-      if (newValue) {
-        selectedRuleInfoCount += 1
-      } else {
-        selectedRuleInfoCount -= 1
-      }
-    }
-  }
-
   private func enabledFeatures(options: TulsiOptionSet) -> Set<BazelSettingFeature> {
     return BazelBuildSettingsFeatures.enabledFeatures(options: options)
   }
@@ -724,9 +713,8 @@
   // MARK: - Private methods
 
   private func stopObservingRuleEntries() {
-    for entry in uiRuleInfos {
-      entry.removeObserver(self, forKeyPath: "selected", context: &TulsiGeneratorConfigDocument.KVOContext)
-    }
+    uiRuleInfoObservers.forEach { $0.invalidate() }
+    uiRuleInfoObservers = []
   }
 
   private func makeConfig(withFullyResolvedOptions resolve: Bool = false) -> TulsiGeneratorConfig? {
diff --git a/src/Tulsi/TulsiProjectDocument.swift b/src/Tulsi/TulsiProjectDocument.swift
index 0853886..1cf8014 100644
--- a/src/Tulsi/TulsiProjectDocument.swift
+++ b/src/Tulsi/TulsiProjectDocument.swift
@@ -190,9 +190,12 @@
   }
 
   func createNewProject(_ projectName: String, workspaceFileURL: URL) {
-    willChangeValue(forKey: "bazelURL")
-    willChangeValue(forKey: "bazelPackages")
-    willChangeValue(forKey: "workspaceRootURL")
+    willChangeValue(for: \.bazelURL)
+    defer { didChangeValue(for: \.bazelURL) }
+    willChangeValue(for: \.bazelPackages)
+    defer { didChangeValue(for: \.bazelPackages) }
+    willChangeValue(for: \.workspaceRootURL)
+    defer { didChangeValue(for: \.workspaceRootURL) }
 
     // Default the bundleURL to a sibling of the selected workspace file.
     let bundleName = "\(projectName).\(bundleExtension)"
@@ -205,10 +208,6 @@
     updateChangeCount(.changeDone)
 
     LogMessage.postSyslog("Create project: \(projectName)")
-
-    didChangeValue(forKey: "bazelURL")
-    didChangeValue(forKey: "bazelPackages")
-    didChangeValue(forKey: "workspaceRootURL")
   }
 
   override func writeSafely(to url: URL,
diff --git a/src/Tulsi/UISelectableOutlineViewNode.swift b/src/Tulsi/UISelectableOutlineViewNode.swift
index 601b244..4639a34 100644
--- a/src/Tulsi/UISelectableOutlineViewNode.swift
+++ b/src/Tulsi/UISelectableOutlineViewNode.swift
@@ -72,7 +72,7 @@
         return
       }
 
-      willChangeValue(forKey: "state")
+      willChangeValue(for: \.state)
       if let entry = entry {
         entry.selected = newSelectionState
       }
@@ -80,13 +80,13 @@
       for node in children {
         node.state = newValue
       }
-      didChangeValue(forKey: "state")
+      didChangeValue(for: \.state)
 
       // Notify KVO that this node's ancestors have also changed state.
       var ancestor = parent
       while ancestor != nil {
-        ancestor!.willChangeValue(forKey: "state")
-        ancestor!.didChangeValue(forKey: "state")
+        ancestor!.willChangeValue(for: \.state)
+        ancestor!.didChangeValue(for: \.state)
         ancestor = ancestor!.parent
       }
     }
diff --git a/src/TulsiGenerator/ProcessRunner.swift b/src/TulsiGenerator/ProcessRunner.swift
index 9607ad1..9315a2e 100644
--- a/src/TulsiGenerator/ProcessRunner.swift
+++ b/src/TulsiGenerator/ProcessRunner.swift
@@ -38,8 +38,8 @@
 
   /// Coordinates logging with Process lifetime to accurately report when a given process started.
   final class TimedProcessRunnerObserver: NSObject {
-    /// Context for KVO
-    private static var KVOContext: Int = 0
+    /// Observer for KVO on the process.
+    private var processObserver: NSKeyValueObservation?
 
     /// Mapping between Processes and LogSessionHandles created for each.
     ///
@@ -78,44 +78,25 @@
       accessPendingLogHandles { pendingLogHandles in
         pendingLogHandles[process] = logSessionHandle
       }
-      process.addObserver(self,
-                          forKeyPath: #keyPath(Process.isRunning),
-                          options: .new,
-                          context: &TimedProcessRunnerObserver.KVOContext)
+      processObserver = process.observe(\.isRunning, options: .new) {
+        [unowned self] process, change in
+        guard change.newValue == true else { return }
+        self.accessPendingLogHandles { pendingLogHandles in
+          pendingLogHandles[process]?.resetStartTime()
+        }
+      }
     }
 
     /// Report the time this process has taken, and cleanup its logging handle and KVO observer.
     fileprivate func stopLogging(process: Process, messageLogger: LocalizedMessageLogger) {
       if let logHandle = self.pendingLogHandles[process] {
         messageLogger.logProfilingEnd(logHandle)
-        process.removeObserver(self,
-                               forKeyPath: #keyPath(Process.isRunning),
-                               context: &TimedProcessRunnerObserver.KVOContext)
+        processObserver?.invalidate()
         accessPendingLogHandles { pendingLogHandles in
           _ = pendingLogHandles.removeValue(forKey: process)
         }
       }
     }
-
-    /// KVO to set the logger start time to the moment when the Process indicates that it's running.
-    override public func observeValue(forKeyPath keyPath: String?,
-                                      of object: Any?,
-                                      change: [NSKeyValueChangeKey : Any]?,
-                                      context: UnsafeMutableRawPointer?) {
-      if context != &TimedProcessRunnerObserver.KVOContext {
-        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
-        return
-      }
-
-      if keyPath == #keyPath(Process.isRunning),
-          let newValue = change?[NSKeyValueChangeKey.newKey] as? NSNumber,
-          newValue.boolValue,
-          let process = object as? Process {
-        accessPendingLogHandles { pendingLogHandles in
-          pendingLogHandles[process]?.resetStartTime()
-        }
-      }
-    }
   }