Support On Demand Rules (#9)

Fixes #9
This commit is contained in:
Kenta Kubo 2020-12-21 00:20:57 +09:00
parent 5b28682cc7
commit 99c9d7cbd3
12 changed files with 452 additions and 14 deletions

View file

@ -9,6 +9,13 @@
/* Begin PBXBuildFile section */
890B80D5251DC3A20046BAA0 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890B80D4251DC3A20046BAA0 /* DetailView.swift */; };
890B80DF251DC6B50046BAA0 /* Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890B80DE251DC6B50046BAA0 /* Presets.swift */; };
893AA853258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA852258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift */; };
893AA858258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA857258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift */; };
893AA85D258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA85C258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift */; };
893AA862258F998C0060B022 /* NEOnDemandRuleInterfaceType+ssidIsUsed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA861258F998C0060B022 /* NEOnDemandRuleInterfaceType+ssidIsUsed.swift */; };
893AA867258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA866258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift */; };
893AA86C258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA86B258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift */; };
893AA871258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA870258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift */; };
8940023C24ACBD2700EBE74B /* DNSecureApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940023B24ACBD2700EBE74B /* DNSecureApp.swift */; };
8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940023D24ACBD2700EBE74B /* ContentView.swift */; };
8940024024ACBD2800EBE74B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8940023F24ACBD2800EBE74B /* Assets.xcassets */; };
@ -16,6 +23,7 @@
8940024E24ACBD2800EBE74B /* DNSecureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940024D24ACBD2800EBE74B /* DNSecureTests.swift */; };
8940025924ACBD2800EBE74B /* DNSecureUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940025824ACBD2800EBE74B /* DNSecureUITests.swift */; };
8940026924ACBE4900EBE74B /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8940026824ACBE4900EBE74B /* NetworkExtension.framework */; };
894958AD2548405E009691D5 /* RuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894958AC2548405E009691D5 /* RuleView.swift */; };
8963FDFB251DF1BC00E3DFE7 /* Bundle+displayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */; };
8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8986CDCE251D9B3400D947CD /* Resolver.swift */; };
89CB922125209DD100B6983C /* HowToActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CB922025209DD100B6983C /* HowToActivateView.swift */; };
@ -42,6 +50,13 @@
890B80D4251DC3A20046BAA0 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = "<group>"; };
890B80DE251DC6B50046BAA0 /* Presets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presets.swift; sourceTree = "<group>"; };
8924EDF324C9CDF1004AF871 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
893AA852258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+CaseIterable.swift"; sourceTree = "<group>"; };
893AA857258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+CustomStringConvertible.swift"; sourceTree = "<group>"; };
893AA85C258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+Codable.swift"; sourceTree = "<group>"; };
893AA861258F998C0060B022 /* NEOnDemandRuleInterfaceType+ssidIsUsed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+ssidIsUsed.swift"; sourceTree = "<group>"; };
893AA866258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleAction+CaseIterable.swift"; sourceTree = "<group>"; };
893AA86B258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleAction+CustomStringConvertible.swift"; sourceTree = "<group>"; };
893AA870258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleAction+Codable.swift"; sourceTree = "<group>"; };
8940023824ACBD2700EBE74B /* DNSecure.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DNSecure.app; sourceTree = BUILT_PRODUCTS_DIR; };
8940023B24ACBD2700EBE74B /* DNSecureApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSecureApp.swift; sourceTree = "<group>"; };
8940023D24ACBD2700EBE74B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -56,6 +71,7 @@
8940025A24ACBD2800EBE74B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8940026624ACBE4900EBE74B /* DNSecure.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DNSecure.entitlements; sourceTree = "<group>"; };
8940026824ACBE4900EBE74B /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
894958AC2548405E009691D5 /* RuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleView.swift; sourceTree = "<group>"; };
8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+displayName.swift"; sourceTree = "<group>"; };
8986CDCE251D9B3400D947CD /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
89CB922025209DD100B6983C /* HowToActivateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowToActivateView.swift; sourceTree = "<group>"; };
@ -93,6 +109,7 @@
8940023D24ACBD2700EBE74B /* ContentView.swift */,
89CB922025209DD100B6983C /* HowToActivateView.swift */,
890B80D4251DC3A20046BAA0 /* DetailView.swift */,
894958AC2548405E009691D5 /* RuleView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -110,6 +127,13 @@
isa = PBXGroup;
children = (
8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */,
893AA852258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift */,
893AA857258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift */,
893AA85C258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift */,
893AA861258F998C0060B022 /* NEOnDemandRuleInterfaceType+ssidIsUsed.swift */,
893AA866258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift */,
893AA86B258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift */,
893AA870258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -317,9 +341,17 @@
8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */,
89CB922125209DD100B6983C /* HowToActivateView.swift in Sources */,
890B80D5251DC3A20046BAA0 /* DetailView.swift in Sources */,
893AA853258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift in Sources */,
893AA871258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift in Sources */,
893AA85D258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift in Sources */,
8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */,
894958AD2548405E009691D5 /* RuleView.swift in Sources */,
890B80DF251DC6B50046BAA0 /* Presets.swift in Sources */,
893AA858258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift in Sources */,
8940023C24ACBD2700EBE74B /* DNSecureApp.swift in Sources */,
893AA862258F998C0060B022 /* NEOnDemandRuleInterfaceType+ssidIsUsed.swift in Sources */,
893AA867258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift in Sources */,
893AA86C258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift in Sources */,
8963FDFB251DF1BC00E3DFE7 /* Bundle+displayName.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -0,0 +1,14 @@
//
// NEOnDemandRuleAction+CaseIterable.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleAction: CaseIterable {
public static var allCases: [Self] {
[.connect, .disconnect, .evaluateConnection, .ignore]
}
}

View file

@ -0,0 +1,10 @@
//
// NEOnDemandRuleAction+Codable.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleAction: Codable {}

View file

@ -0,0 +1,25 @@
//
// NEOnDemandRuleAction+CustomStringConvertible.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleAction: CustomStringConvertible {
public var description: String {
switch self {
case .connect:
return "Apply settings"
case .disconnect:
return "Do not apply settings"
case .evaluateConnection:
return "Apply with excluded domains"
case .ignore:
return "As is"
default:
return "Unknown"
}
}
}

View file

@ -0,0 +1,18 @@
//
// NEOnDemandRuleInterfaceType+CaseIterable.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleInterfaceType: CaseIterable {
public static var allCases: [Self] {
#if os(macOS)
return [.any, .ethernet, .wiFi]
#else
return [.any, .wiFi, .cellular]
#endif
}
}

View file

@ -0,0 +1,10 @@
//
// NEOnDemandRuleInterfaceType+Codable.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleInterfaceType: Codable {}

View file

@ -0,0 +1,29 @@
//
// NEOnDemandRuleInterfaceType+CustomStringConvertible.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleInterfaceType: CustomStringConvertible {
public var description: String {
switch self {
case .any:
return "Any"
#if os(macOS)
case .ethernet:
return "Ethernet"
#endif
case .wiFi:
return "Wi-Fi"
#if os(iOS)
case .cellular:
return "Cellular"
#endif
default:
return "Unknown"
}
}
}

View file

@ -0,0 +1,19 @@
//
// NEOnDemandRuleInterfaceType+ssidIsUsed.swift
// DNSecure
//
// Created by Kenta Kubo on 12/20/20.
//
import NetworkExtension
extension NEOnDemandRuleInterfaceType {
var ssidIsUsed: Bool {
switch self {
case .any, .wiFi:
return true
default:
return false
}
}
}

View file

@ -19,6 +19,10 @@ struct DoTConfiguration {
}
}
extension DoTConfiguration: Equatable {}
extension DoTConfiguration: Hashable {}
extension DoTConfiguration: Codable {}
struct DoHConfiguration {
@ -32,6 +36,10 @@ struct DoHConfiguration {
}
}
extension DoHConfiguration: Equatable {}
extension DoHConfiguration: Hashable {}
extension DoHConfiguration: Codable {}
enum Configuration {
@ -48,6 +56,10 @@ enum Configuration {
}
}
extension Configuration: Equatable {}
extension Configuration: Hashable {}
extension Configuration: Codable {
enum CodingKeys: String, CodingKey {
case base, dotConfiguration, dohConfiguration
@ -94,15 +106,106 @@ extension Configuration: CustomStringConvertible {
}
}
struct OnDemandRule {
var id = UUID()
var name: String
var action: NEOnDemandRuleAction = .ignore
var interfaceType: NEOnDemandRuleInterfaceType = .any
var ssidMatch: [String] = []
var dnsSearchDomainMatch: [String] = []
var dnsServerAddressMatch: [String] = []
var probeURL: URL?
}
extension OnDemandRule: Identifiable {}
extension OnDemandRule: Equatable {}
extension OnDemandRule: Hashable {}
extension OnDemandRule: Codable {}
extension Array where Self.Element == OnDemandRule {
func toNEOnDemandRules() -> [NEOnDemandRule] {
self.lazy
.map { rule in
switch rule.action {
case .connect:
let newRule = NEOnDemandRuleConnect()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.ssidIsUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .disconnect:
let newRule = NEOnDemandRuleDisconnect()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.ssidIsUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .evaluateConnection:
let newRule = NEOnDemandRuleEvaluateConnection()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.ssidIsUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
case .ignore:
let newRule = NEOnDemandRuleIgnore()
newRule.interfaceTypeMatch = rule.interfaceType
if rule.interfaceType.ssidIsUsed {
newRule.ssidMatch = rule.ssidMatch
}
newRule.dnsSearchDomainMatch = rule.dnsSearchDomainMatch
newRule.dnsServerAddressMatch = rule.dnsServerAddressMatch
newRule.probeURL = rule.probeURL
return newRule
default:
preconditionFailure("Unexpected NEOnDemandRuleAction")
}
}
}
}
struct Resolver {
var id = UUID()
var name: String
var configuration: Configuration
var onDemandRules: [OnDemandRule] = []
}
extension Resolver: Identifiable {}
extension Resolver: Codable {}
extension Resolver: Equatable {}
extension Resolver: Hashable {}
extension Resolver: Codable {
enum CodingKeys: String, CodingKey {
case id, name, configuration, onDemandRules
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UUID.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.configuration = try container.decode(Configuration.self, forKey: .configuration)
self.onDemandRules = try container.decodeIfPresent(
[OnDemandRule].self,
forKey: .onDemandRules
) ?? []
}
}
typealias Resolvers = [Resolver]

View file

@ -41,12 +41,17 @@ struct ContentView {
func removeServers(at indexSet: IndexSet) {
if let current = self.selection, indexSet.contains(where: { $0 <= current }) {
self.selection = nil
// FIXME: This is a workaround not to crash on deletion.
self.selection = -1
}
if indexSet.map({ self.servers[$0].id.uuidString }).contains(self.usedID) {
self.removeSettings()
}
self.servers.remove(atOffsets: indexSet)
// FIXME: This is a workaround not to crash on deletion.
// Wait for closing DetailView.
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
self.servers.remove(atOffsets: indexSet)
}
}
func moveServers(from src: IndexSet, to dst: Int) {
@ -76,6 +81,7 @@ struct ContentView {
#if !targetEnvironment(simulator)
let manager = NEDNSSettingsManager.shared()
manager.dnsSettings = server.configuration.toDNSSettings()
manager.onDemandRules = server.onDemandRules.toNEOnDemandRules()
manager.saveToPreferences { saveError in
if let saveError = saveError as NSError? {
guard saveError.domain != "NEConfigurationErrorDomain"
@ -125,21 +131,17 @@ extension ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(
"Instructions",
destination: HowToActivateView(isSheet: false),
tag: -1,
selection: self.$selection
)
Section(header: Text("Servers")) {
ForEach(0..<self.servers.count, id: \.self) { i in
NavigationLink(
destination: DetailView(
server: .init(
get: { self.servers[i] },
set: {
self.servers[i] = $0
let server = self.servers[i]
if server.id.uuidString == self.usedID {
self.saveSettings(of: server)
}
}
),
server: self.$servers[i],
isOn: .init(
get: {
self.usedID == self.servers[i].id.uuidString
@ -221,6 +223,15 @@ extension ContentView: View {
.onChange(of: self.scenePhase) { phase in
if phase == .active {
self.updateStatus()
} else {
// FIXME: This is a workaround for self.$severs[i].
// That cannot save settings as soon as it is modified.
guard let id = self.usedID,
let uuid = UUID(uuidString: id),
let server = self.servers.find(by: uuid) else {
return
}
self.saveSettings(of: server)
}
}
}

View file

@ -10,6 +10,13 @@ import SwiftUI
struct DetailView {
@Binding var server: Resolver
@Binding var isOn: Bool
func binding(for rule: OnDemandRule) -> Binding<OnDemandRule> {
guard let index = self.server.onDemandRules.firstIndex(of: rule) else {
preconditionFailure("Can't find rule in array")
}
return self.$server.onDemandRules[index]
}
}
extension DetailView: View {
@ -157,6 +164,25 @@ extension DetailView: View {
}
}
}
Section(
header: EditButton()
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("On Demand Rules"), alignment: .leading)
) {
ForEach(self.server.onDemandRules) { rule in
NavigationLink(
rule.name,
destination: RuleView(rule: self.binding(for: rule))
)
}
.onDelete { self.server.onDemandRules.remove(atOffsets: $0) }
.onMove { self.server.onDemandRules.move(fromOffsets: $0, toOffset: $1) }
Button("Add New Rule") {
self.server.onDemandRules
.append(OnDemandRule(name: "New Rule"))
}
}
}
.navigationTitle(self.server.name)
}

View file

@ -0,0 +1,141 @@
//
// RuleView.swift
// DNSecure
//
// Created by Kenta Kubo on 10/27/20.
//
import NetworkExtension
import SwiftUI
struct RuleView {
@Binding var rule: OnDemandRule
}
extension RuleView: View {
var body: some View {
Form {
Section {
HStack {
Text("Name")
TextField("Name", text: self.$rule.name)
.multilineTextAlignment(.trailing)
}
Picker("Action", selection: self.$rule.action) {
ForEach(NEOnDemandRuleAction.allCases, id: \.self) {
Text($0.description)
}
}
}
Section(
header: Text("Interface Type Match"),
footer: Text("If the current primary network interface is of this type and all of the other conditions in the rule match, then the rule matches.")
) {
Picker("Interface Type", selection: self.$rule.interfaceType) {
ForEach(NEOnDemandRuleInterfaceType.allCases, id: \.self) {
Text($0.description)
}
}
}
if self.rule.interfaceType.ssidIsUsed {
Section(
header: EditButton()
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("SSID Match"), alignment: .leading),
footer: Text("If the Service Set Identifier (SSID) of the current primary connected network matches one of the strings in this array and all of the other conditions in the rule match, then the rule matches.")
) {
ForEach(0..<self.rule.ssidMatch.count, id: \.self) { i in
TextField(
"SSID",
// self.$rule.ssidMatch[i] causes crash on deletion
text: .init(
get: { self.rule.ssidMatch[i] },
set: { self.rule.ssidMatch[i] = $0 }
)
)
}
.onDelete { self.rule.ssidMatch.remove(atOffsets: $0) }
.onMove { self.rule.ssidMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add SSID") {
self.rule.ssidMatch.append("")
}
}
}
Section(
header: EditButton()
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("DNS Search Domain Match"), alignment: .leading),
footer: Text("If the current default search domain is equal to one of the strings in this array and all of the other conditions in the rule match, then the rule matches.")
) {
ForEach(0..<self.rule.dnsSearchDomainMatch.count, id: \.self) { i in
TextField(
"Search Domain",
// self.$rule.dnsSearchDomainMatch[i] causes crash on deletion
text: .init(
get: { self.rule.dnsSearchDomainMatch[i] },
set: { self.rule.dnsSearchDomainMatch[i] = $0 }
)
)
}
.onDelete { self.rule.dnsSearchDomainMatch.remove(atOffsets: $0) }
.onMove { self.rule.dnsSearchDomainMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add DNS Search Domain") {
self.rule.dnsSearchDomainMatch.append("")
}
}
Section(
header: EditButton()
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("DNS Server Address Match"), alignment: .leading),
footer: Text("If each of the current default DNS servers is equal to one of the strings in this array and all of the other conditions in the rule match, then the rule matches.")
) {
ForEach(0..<self.rule.dnsServerAddressMatch.count, id: \.self) { i in
TextField(
"IP Address",
// self.$rule.dnsServerAddressMatch[i] causes crash on deletion
text: .init(
get: { self.rule.dnsServerAddressMatch[i] },
set: { self.rule.dnsServerAddressMatch[i] = $0 }
)
)
}
.onDelete { self.rule.dnsServerAddressMatch.remove(atOffsets: $0) }
.onMove { self.rule.dnsServerAddressMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add DNS Server Address") {
self.rule.dnsServerAddressMatch.append("")
}
}
Section(
header: Text("Probe URL Match"),
footer: Text("If a request sent to this URL results in a HTTP 200 OK response and all of the other conditions in the rule match, then the rule matches. If you don't want to use this rule, leave it empty.")
) {
HStack {
Text("Probe URL")
TextField(
"URL",
text: .init(
get: { self.rule.probeURL?.absoluteString ?? "" },
set: { self.rule.probeURL = URL(string: $0) }
)
)
.multilineTextAlignment(.trailing)
}
}
}
.navigationTitle(self.rule.name)
}
}
struct RuleView_Previews: PreviewProvider {
static var previews: some View {
RuleView(rule: .constant(OnDemandRule(name: "Preview Rule")))
}
}